@youtyan/code-viewer 0.6.9 → 0.7.0

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.
@@ -96,7 +96,7 @@ var init_sqlite_driver = __esm(() => {
96
96
 
97
97
  // web-src/server/json-store.ts
98
98
  import { randomBytes } from "node:crypto";
99
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
99
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
100
100
  import { dirname } from "node:path";
101
101
  function isEnoent(err) {
102
102
  return err?.code === "ENOENT";
@@ -140,8 +140,15 @@ function createJsonFileStore(options) {
140
140
  }
141
141
  await mkdir(dirname(file), { recursive: true });
142
142
  const tmp = tmpPath(file);
143
- await writeFile(tmp, content, "utf8");
144
- await rename(tmp, file);
143
+ try {
144
+ await writeFile(tmp, content, "utf8");
145
+ await rename(tmp, file);
146
+ } catch (err) {
147
+ await unlink(tmp).catch(() => {
148
+ return;
149
+ });
150
+ throw err;
151
+ }
145
152
  }
146
153
  async function load(root) {
147
154
  const pendingWrite = queues.get(options.filePath(root));
@@ -841,6 +848,7 @@ function runSync(args, cwd, options = {}) {
841
848
  encoding: "buffer",
842
849
  stdio: ["ignore", "pipe", "pipe"],
843
850
  timeout: options.timeout,
851
+ maxBuffer: options.maxBuffer ?? 64 * 1024 * 1024,
844
852
  killSignal: "SIGKILL"
845
853
  });
846
854
  return {
@@ -849,19 +857,87 @@ function runSync(args, cwd, options = {}) {
849
857
  stderr: appendProcessError(new TextDecoder().decode(proc.stderr || new Uint8Array), proc.error)
850
858
  };
851
859
  }
852
- function runBytesSync(args, cwd, options = {}) {
853
- const proc = spawnSync(args[0], args.slice(1), {
854
- cwd,
855
- encoding: "buffer",
856
- stdio: ["ignore", "pipe", "pipe"],
857
- timeout: options.timeout,
858
- killSignal: "SIGKILL"
860
+ function runAsync(args, cwd, options = {}) {
861
+ return runBytesAsync(args, cwd, options).then((proc) => ({
862
+ code: proc.code,
863
+ stdout: new TextDecoder().decode(proc.stdout),
864
+ stderr: proc.stderr
865
+ }));
866
+ }
867
+ function runBytesAsync(args, cwd, options = {}) {
868
+ const maxBuffer = options.maxBuffer ?? 64 * 1024 * 1024;
869
+ return new Promise((resolve) => {
870
+ const proc = spawn(args[0], args.slice(1), {
871
+ cwd,
872
+ stdio: ["ignore", "pipe", "pipe"]
873
+ });
874
+ const stdoutChunks = [];
875
+ const stderrChunks = [];
876
+ let stdoutBytes = 0;
877
+ let stderrBytes = 0;
878
+ let settled = false;
879
+ let timedOut = false;
880
+ let bufferExceeded = false;
881
+ let processError;
882
+ const killSignal = "SIGKILL";
883
+ const timer = options.timeout === undefined ? null : setTimeout(() => {
884
+ timedOut = true;
885
+ proc.kill(killSignal);
886
+ }, options.timeout);
887
+ const finish = (code) => {
888
+ if (settled)
889
+ return;
890
+ settled = true;
891
+ if (timer)
892
+ clearTimeout(timer);
893
+ let stderr = new TextDecoder().decode(concatBytes(stderrChunks));
894
+ if (timedOut) {
895
+ stderr = appendProcessError(stderr, new Error(`spawn ${args[0]} ETIMEDOUT`));
896
+ } else if (bufferExceeded) {
897
+ stderr = appendProcessError(stderr, new Error("stdout maxBuffer exceeded"));
898
+ } else {
899
+ stderr = appendProcessError(stderr, processError);
900
+ }
901
+ resolve({
902
+ code,
903
+ stdout: concatBytes(stdoutChunks),
904
+ stderr
905
+ });
906
+ };
907
+ const collect = (chunks, onBytes) => (chunk) => {
908
+ const bytes = new Uint8Array(chunk);
909
+ chunks.push(bytes);
910
+ onBytes(bytes.byteLength);
911
+ if (!bufferExceeded && (stdoutBytes > maxBuffer || stderrBytes > maxBuffer)) {
912
+ bufferExceeded = true;
913
+ proc.kill(killSignal);
914
+ }
915
+ };
916
+ proc.stdout?.on("data", collect(stdoutChunks, (length) => {
917
+ stdoutBytes += length;
918
+ }));
919
+ proc.stderr?.on("data", collect(stderrChunks, (length) => {
920
+ stderrBytes += length;
921
+ }));
922
+ proc.on("error", (err) => {
923
+ processError = err;
924
+ });
925
+ proc.on("close", (code) => {
926
+ finish(timedOut || bufferExceeded ? 1 : code ?? (processError ? 1 : 0));
927
+ });
859
928
  });
860
- return {
861
- code: proc.status ?? (proc.error ? 1 : 0),
862
- stdout: new Uint8Array(proc.stdout || new Uint8Array),
863
- stderr: appendProcessError(new TextDecoder().decode(proc.stderr || new Uint8Array), proc.error)
864
- };
929
+ }
930
+ function concatBytes(chunks) {
931
+ if (chunks.length === 0)
932
+ return new Uint8Array;
933
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
934
+ const out = new Uint8Array(total);
935
+ let offset = 0;
936
+ for (const chunk of chunks) {
937
+ out.set(chunk, offset);
938
+ offset += chunk.byteLength;
939
+ }
940
+ return out;
865
941
  }
866
942
  function spawnDetached(args) {
867
943
  const child = spawn(args[0], args.slice(1), {
@@ -1035,10 +1111,13 @@ var init_runtime = () => {};
1035
1111
 
1036
1112
  // web-src/server/git.ts
1037
1113
  import {
1114
+ closeSync,
1038
1115
  existsSync,
1039
1116
  lstatSync,
1117
+ openSync,
1040
1118
  readdirSync,
1041
1119
  readFileSync,
1120
+ readSync,
1042
1121
  statSync as statSync2
1043
1122
  } from "node:fs";
1044
1123
  import { join as join3 } from "node:path";
@@ -1061,8 +1140,8 @@ function run(args, cwd) {
1061
1140
  timeout: GIT_COMMAND_TIMEOUT_MS
1062
1141
  });
1063
1142
  }
1064
- function runBytes(args, cwd) {
1065
- return runBytesSync(resolveGitArgs(args), cwd, {
1143
+ function runGitAsync(args, cwd) {
1144
+ return runAsync(resolveGitArgs(args), cwd, {
1066
1145
  timeout: GIT_COMMAND_TIMEOUT_MS
1067
1146
  });
1068
1147
  }
@@ -1089,6 +1168,10 @@ function runGitRefLookup(args, cwd) {
1089
1168
  const res = run(args, cwd);
1090
1169
  return res.code === 0 ? res.stdout.trimEnd() : null;
1091
1170
  }
1171
+ async function runGitRefLookupAsync(args, cwd) {
1172
+ const res = await runGitAsync(args, cwd);
1173
+ return res.code === 0 ? res.stdout.trimEnd() : null;
1174
+ }
1092
1175
  function repoRoot(cwd) {
1093
1176
  return runGitRefLookup(["git", "rev-parse", "--show-toplevel"], cwd);
1094
1177
  }
@@ -1107,14 +1190,17 @@ function repoRootResult(cwd) {
1107
1190
  function currentBranch(cwd) {
1108
1191
  return runGitRefLookup(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd);
1109
1192
  }
1110
- function verifyCommit(ref, cwd) {
1111
- const res = run(["git", "rev-parse", "--verify", `${ref}^{commit}`], cwd);
1193
+ function currentBranchAsync(cwd) {
1194
+ return runGitRefLookupAsync(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd);
1195
+ }
1196
+ async function verifyCommitAsync(ref, cwd) {
1197
+ const res = await runGitAsync(["git", "rev-parse", "--verify", `${ref}^{commit}`], cwd);
1112
1198
  if (res.code === 0)
1113
1199
  return { ok: true, sha: res.stdout.trim() };
1114
1200
  return { ok: false, error: gitFailureMessage(res, "unknown ref") };
1115
1201
  }
1116
- function statusPorcelainForPath(path, cwd) {
1117
- const res = run([
1202
+ async function statusPorcelainForPathAsync(path, cwd) {
1203
+ const res = await runGitAsync([
1118
1204
  "git",
1119
1205
  "-c",
1120
1206
  "core.quotepath=false",
@@ -1135,66 +1221,63 @@ function statusPorcelainForPath(path, cwd) {
1135
1221
  function show(ref, path, cwd) {
1136
1222
  return run(["git", "show", `${ref}:${path}`], cwd);
1137
1223
  }
1138
- function showBytes(ref, path, cwd) {
1139
- return runBytes(["git", "show", `${ref}:${path}`], cwd);
1224
+ function showAsync(ref, path, cwd) {
1225
+ return runGitAsync(["git", "show", `${ref}:${path}`], cwd);
1140
1226
  }
1141
1227
  function catFileBlobStream(oid, cwd) {
1142
1228
  return spawnStream(resolveGitArgs(["git", "cat-file", "blob", oid]), cwd);
1143
1229
  }
1144
- function objectSize(ref, path, cwd) {
1145
- const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
1230
+ async function objectSizeAsync(ref, path, cwd) {
1231
+ const res = await runGitAsync(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
1146
1232
  return {
1147
1233
  code: res.code,
1148
1234
  size: Number(res.stdout.trim()) || 0,
1149
1235
  stderr: res.stderr
1150
1236
  };
1151
1237
  }
1152
- function objectByteSize(oid, cwd) {
1153
- const res = run(["git", "cat-file", "-s", oid], cwd);
1238
+ async function objectByteSizeAsync(oid, cwd) {
1239
+ const res = await runGitAsync(["git", "cat-file", "-s", oid], cwd);
1154
1240
  return {
1155
1241
  code: res.code,
1156
1242
  size: Number(res.stdout.trim()) || 0,
1157
1243
  stderr: res.stderr
1158
1244
  };
1159
1245
  }
1160
- function lastCommitDateForPath(ref, path, cwd) {
1246
+ async function lastCommitDateForPathAsync(ref, path, cwd) {
1161
1247
  const args = ["git", "log", "-1", "--format=%cI", ref, "--", path];
1162
- const res = run(args, cwd);
1248
+ const res = await runGitAsync(args, cwd);
1163
1249
  if (res.code !== 0)
1164
1250
  return null;
1165
1251
  return res.stdout.trim() || null;
1166
1252
  }
1167
- function objectId(ref, path, cwd) {
1168
- const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
1253
+ async function objectIdAsync(ref, path, cwd) {
1254
+ const res = await runGitAsync(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
1169
1255
  const oid = res.stdout.trim();
1170
1256
  if (res.code !== 0 || !oid)
1171
1257
  return { code: res.code || 1, oid: "", stderr: res.stderr };
1172
- const type = run(["git", "cat-file", "-t", oid], cwd);
1258
+ const type = await runGitAsync(["git", "cat-file", "-t", oid], cwd);
1173
1259
  if (type.code !== 0 || type.stdout.trim() !== "blob")
1174
1260
  return { code: 1, oid: "", stderr: type.stderr };
1175
1261
  return { code: 0, oid, stderr: "" };
1176
1262
  }
1177
- function verifyTreeRef(ref, cwd) {
1178
- return verifyTreeRefResult(ref, cwd).ok;
1179
- }
1180
- function verifyTreeRefResult(ref, cwd) {
1263
+ async function verifyTreeRefResultAsync(ref, cwd) {
1181
1264
  if (!ref || ref === "worktree")
1182
1265
  return { ok: false, error: "invalid target", status: 400 };
1183
1266
  if (ref.startsWith("-"))
1184
1267
  return { ok: false, error: "invalid target", status: 400 };
1185
- const res = run(["git", "rev-parse", "--verify", `${ref}^{tree}`], cwd);
1268
+ const res = await runGitAsync(["git", "rev-parse", "--verify", `${ref}^{tree}`], cwd);
1186
1269
  if (res.code === 0)
1187
1270
  return { ok: true };
1188
1271
  return { ok: false, ...gitFailureResult(res, "invalid target") };
1189
1272
  }
1190
- function refsResult(cwd) {
1273
+ async function refsResultAsync(cwd) {
1191
1274
  const out = {
1192
1275
  branches: [],
1193
1276
  tags: [],
1194
1277
  commits: [],
1195
1278
  current: ""
1196
1279
  };
1197
- const branches = run([
1280
+ const branches = await runGitAsync([
1198
1281
  "git",
1199
1282
  "for-each-ref",
1200
1283
  "--sort=-committerdate",
@@ -1214,7 +1297,7 @@ function refsResult(cwd) {
1214
1297
  out.branches.push({ name, when });
1215
1298
  }
1216
1299
  }
1217
- const tags = run([
1300
+ const tags = await runGitAsync([
1218
1301
  "git",
1219
1302
  "for-each-ref",
1220
1303
  "--sort=-creatordate",
@@ -1233,7 +1316,7 @@ function refsResult(cwd) {
1233
1316
  out.tags.push({ name, when });
1234
1317
  }
1235
1318
  }
1236
- const commits = refCommitPageResult(cwd, {
1319
+ const commits = await refCommitPageResultAsync(cwd, {
1237
1320
  query: "",
1238
1321
  max: DEFAULT_REF_COMMIT_LIMIT
1239
1322
  });
@@ -1241,7 +1324,7 @@ function refsResult(cwd) {
1241
1324
  return { refs: out, error: commits.error, status: commits.status };
1242
1325
  }
1243
1326
  out.commits = commits.commits;
1244
- out.current = currentBranch(cwd) || "";
1327
+ out.current = await currentBranchAsync(cwd) || "";
1245
1328
  return { refs: out };
1246
1329
  }
1247
1330
  function clampCommitLimit(max) {
@@ -1295,22 +1378,22 @@ function mergeCommitResults(limit, ...groups) {
1295
1378
  }
1296
1379
  return merged;
1297
1380
  }
1298
- function runCommitLogResult(cwd, args) {
1299
- const commits = run(args, cwd);
1381
+ async function runCommitLogResultAsync(cwd, args) {
1382
+ const commits = await runGitAsync(args, cwd);
1300
1383
  if (commits.code === 0)
1301
1384
  return { commits: parseCommitLog(commits.stdout) };
1302
1385
  if (isCommandNotFoundResult("git", commits))
1303
1386
  return { commits: [], ...gitFailureResult(commits, "git log failed") };
1304
1387
  return { commits: [] };
1305
1388
  }
1306
- function refCommitPageResult(cwd, options = {}) {
1389
+ async function refCommitPageResultAsync(cwd, options = {}) {
1307
1390
  const limit = clampCommitLimit(options.max ?? DEFAULT_REF_COMMIT_LIMIT);
1308
1391
  const skip = clampCommitSkip(options.skip ?? 0);
1309
1392
  const fetchLimit = limit + 1;
1310
1393
  const hashMatches = [];
1311
1394
  const trimmed = (options.query || "").trim().slice(0, 200).replace(/\0/g, "");
1312
1395
  if (skip === 0 && /^[0-9a-f]{4,40}$/i.test(trimmed)) {
1313
- const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
1396
+ const verified = await runGitAsync(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
1314
1397
  if (verified.code !== 0 && isCommandNotFoundResult("git", verified)) {
1315
1398
  return {
1316
1399
  commits: [],
@@ -1318,7 +1401,7 @@ function refCommitPageResult(cwd, options = {}) {
1318
1401
  ...gitFailureResult(verified, "unknown ref")
1319
1402
  };
1320
1403
  }
1321
- const single = run([
1404
+ const single = await runGitAsync([
1322
1405
  "git",
1323
1406
  "log",
1324
1407
  "-z",
@@ -1338,7 +1421,7 @@ function refCommitPageResult(cwd, options = {}) {
1338
1421
  }
1339
1422
  }
1340
1423
  if (!trimmed) {
1341
- const result = runCommitLogResult(cwd, commitLogArgs(fetchLimit, skip));
1424
+ const result = await runCommitLogResultAsync(cwd, commitLogArgs(fetchLimit, skip));
1342
1425
  if (result.error) {
1343
1426
  return {
1344
1427
  commits: [],
@@ -1353,11 +1436,19 @@ function refCommitPageResult(cwd, options = {}) {
1353
1436
  hasMore: commits.length > limit
1354
1437
  };
1355
1438
  }
1356
- const subjectMatches = runCommitLogResult(cwd, [
1357
- ...commitLogArgs(fetchLimit, skip),
1358
- "--regexp-ignore-case",
1359
- "--fixed-strings",
1360
- `--grep=${trimmed}`
1439
+ const [subjectMatches, authorMatches] = await Promise.all([
1440
+ runCommitLogResultAsync(cwd, [
1441
+ ...commitLogArgs(fetchLimit, skip),
1442
+ "--regexp-ignore-case",
1443
+ "--fixed-strings",
1444
+ `--grep=${trimmed}`
1445
+ ]),
1446
+ runCommitLogResultAsync(cwd, [
1447
+ ...commitLogArgs(fetchLimit, skip),
1448
+ "--regexp-ignore-case",
1449
+ "--fixed-strings",
1450
+ `--author=${trimmed}`
1451
+ ])
1361
1452
  ]);
1362
1453
  if (subjectMatches.error) {
1363
1454
  return {
@@ -1367,12 +1458,6 @@ function refCommitPageResult(cwd, options = {}) {
1367
1458
  status: subjectMatches.status
1368
1459
  };
1369
1460
  }
1370
- const authorMatches = runCommitLogResult(cwd, [
1371
- ...commitLogArgs(fetchLimit, skip),
1372
- "--regexp-ignore-case",
1373
- "--fixed-strings",
1374
- `--author=${trimmed}`
1375
- ]);
1376
1461
  if (authorMatches.error) {
1377
1462
  return {
1378
1463
  commits: [],
@@ -1408,6 +1493,12 @@ function remoteWebUrl(cwd) {
1408
1493
  return null;
1409
1494
  return parseRemoteWebUrl(res.stdout.trim());
1410
1495
  }
1496
+ async function remoteWebUrlAsync(cwd) {
1497
+ const res = await runGitAsync(["git", "remote", "get-url", "origin"], cwd);
1498
+ if (res.code !== 0)
1499
+ return null;
1500
+ return parseRemoteWebUrl(res.stdout.trim());
1501
+ }
1411
1502
  function parseHistoryLog(stdout) {
1412
1503
  const parts = stdout.split("\x00");
1413
1504
  const commits = [];
@@ -1524,6 +1615,60 @@ function commitHistory(cwd, options) {
1524
1615
  const hasMore = parsed.length > limit;
1525
1616
  return { commits: hasMore ? parsed.slice(0, limit) : parsed, hasMore };
1526
1617
  }
1618
+ async function commitHistoryAsync(cwd, options) {
1619
+ const ref = (options.ref || "HEAD").trim();
1620
+ if (!ref || ref.startsWith("-") || ref.includes("\x00"))
1621
+ return { commits: [], hasMore: false, error: "invalid ref" };
1622
+ const verified = await runGitAsync(["git", "rev-parse", "--verify", `${ref}^{commit}`], cwd);
1623
+ if (verified.code !== 0)
1624
+ return {
1625
+ commits: [],
1626
+ hasMore: false,
1627
+ ...gitFailureResult(verified, "unknown ref")
1628
+ };
1629
+ const skip = Math.max(0, Math.floor(options.skip) || 0);
1630
+ const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
1631
+ const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
1632
+ const pathFilter = (options.path || "").trim();
1633
+ const pathArgs = [];
1634
+ if (pathFilter && !pathFilter.includes("\x00") && !pathFilter.startsWith("-")) {
1635
+ if (!pathFilter.endsWith("/"))
1636
+ pathArgs.push("--follow");
1637
+ pathArgs.push("--", pathFilter);
1638
+ }
1639
+ const res = await runGitAsync([
1640
+ "git",
1641
+ "log",
1642
+ "-z",
1643
+ `--skip=${skip}`,
1644
+ `--max-count=${limit + 1}`,
1645
+ `--format=${HISTORY_FORMAT}`,
1646
+ ...filterArgs,
1647
+ verified.stdout.trim(),
1648
+ ...pathspec,
1649
+ ...pathArgs
1650
+ ], cwd);
1651
+ if (res.code !== 0)
1652
+ return {
1653
+ commits: [],
1654
+ hasMore: false,
1655
+ ...gitFailureResult(res, "git log failed")
1656
+ };
1657
+ let parsed = parseHistoryLog(res.stdout);
1658
+ if (shaTerm && skip === 0) {
1659
+ const bySha = await runGitAsync(["git", "rev-parse", "--verify", `${shaTerm}^{commit}`], cwd);
1660
+ const sha = bySha.code === 0 ? bySha.stdout.trim() : "";
1661
+ if (sha) {
1662
+ const single = await runGitAsync(["git", "log", "-z", "-1", `--format=${HISTORY_FORMAT}`, sha], cwd);
1663
+ if (single.code === 0) {
1664
+ const hit = parseHistoryLog(single.stdout);
1665
+ parsed = [...hit, ...parsed.filter((c) => c.sha !== sha)];
1666
+ }
1667
+ }
1668
+ }
1669
+ const hasMore = parsed.length > limit;
1670
+ return { commits: hasMore ? parsed.slice(0, limit) : parsed, hasMore };
1671
+ }
1527
1672
  function nameStatusResult(args, cwd) {
1528
1673
  const res = run([
1529
1674
  "git",
@@ -1568,6 +1713,50 @@ function nameStatusResult(args, cwd) {
1568
1713
  }
1569
1714
  return { files };
1570
1715
  }
1716
+ async function nameStatusResultAsync(args, cwd) {
1717
+ const res = await runGitAsync([
1718
+ "git",
1719
+ "-c",
1720
+ "core.quotepath=false",
1721
+ "diff",
1722
+ "--no-color",
1723
+ "--no-ext-diff",
1724
+ "--find-renames",
1725
+ "--name-status",
1726
+ "-z",
1727
+ ...args
1728
+ ], cwd);
1729
+ if (res.code !== 0) {
1730
+ return {
1731
+ files: [],
1732
+ error: gitFailureMessage(res, "git diff --name-status failed")
1733
+ };
1734
+ }
1735
+ const parts = res.stdout.split("\x00");
1736
+ const files = [];
1737
+ for (let i = 0;i < parts.length; ) {
1738
+ const status = parts[i++];
1739
+ if (!status)
1740
+ break;
1741
+ const kind = status[0];
1742
+ if (kind === "R" || kind === "C") {
1743
+ const oldPath = parts[i++] || "";
1744
+ const path = parts[i++] || "";
1745
+ if (path)
1746
+ files.push({
1747
+ status: kind,
1748
+ old_path: oldPath,
1749
+ path,
1750
+ similarity: Number(status.slice(1)) || undefined
1751
+ });
1752
+ } else {
1753
+ const path = parts[i++] || "";
1754
+ if (path)
1755
+ files.push({ status: kind, path });
1756
+ }
1757
+ }
1758
+ return { files };
1759
+ }
1571
1760
  function numstatZResult(args, cwd) {
1572
1761
  const res = run([
1573
1762
  "git",
@@ -1611,6 +1800,49 @@ function numstatZResult(args, cwd) {
1611
1800
  }
1612
1801
  return { files };
1613
1802
  }
1803
+ async function numstatZResultAsync(args, cwd) {
1804
+ const res = await runGitAsync([
1805
+ "git",
1806
+ "-c",
1807
+ "core.quotepath=false",
1808
+ "diff",
1809
+ "--no-color",
1810
+ "--no-ext-diff",
1811
+ "--find-renames",
1812
+ "--numstat",
1813
+ "-z",
1814
+ ...args
1815
+ ], cwd);
1816
+ if (res.code !== 0) {
1817
+ return {
1818
+ files: [],
1819
+ error: gitFailureMessage(res, "git diff --numstat failed")
1820
+ };
1821
+ }
1822
+ const parts = res.stdout.split("\x00");
1823
+ const files = [];
1824
+ for (let i = 0;i < parts.length; ) {
1825
+ const rec = parts[i++];
1826
+ if (!rec)
1827
+ break;
1828
+ const match = rec.match(/^(\S+)\t(\S+)\t(.*)$/);
1829
+ if (!match)
1830
+ break;
1831
+ const [, add, del, rest] = match;
1832
+ const binary = add === "-" && del === "-";
1833
+ const additions = binary ? 0 : Number(add) || 0;
1834
+ const deletions = binary ? 0 : Number(del) || 0;
1835
+ if (rest === "") {
1836
+ const oldPath = parts[i++] || "";
1837
+ const path = parts[i++] || "";
1838
+ if (path)
1839
+ files.push({ old_path: oldPath, path, additions, deletions, binary });
1840
+ } else {
1841
+ files.push({ path: rest, additions, deletions, binary });
1842
+ }
1843
+ }
1844
+ return { files };
1845
+ }
1614
1846
  function pathHasSegment(path, segment) {
1615
1847
  const target = segment.toLowerCase();
1616
1848
  return path.split(/[\\/]+/).some((part) => part.toLowerCase() === target);
@@ -1753,6 +1985,98 @@ function blame(cwd, options) {
1753
1985
  lines.sort((a, b) => a.lineNo - b.lineNo);
1754
1986
  return { lines, commits };
1755
1987
  }
1988
+ async function blameAsync(cwd, options) {
1989
+ const path = options.path;
1990
+ if (!path || path.includes("\x00") || path.startsWith("-")) {
1991
+ return { lines: [], commits: {}, error: "invalid path" };
1992
+ }
1993
+ const normalized = normalizeBlameRef(options.ref, options.base);
1994
+ const args = ["git", "blame", "--porcelain"];
1995
+ if (normalized.base === "HEAD") {
1996
+ if (normalized.ref.startsWith("-") || normalized.ref.includes("\x00"))
1997
+ return { lines: [], commits: {}, error: "invalid ref" };
1998
+ args.push(normalized.ref);
1999
+ }
2000
+ args.push("--", path);
2001
+ const res = await runGitAsync(args, cwd);
2002
+ if (res.code !== 0) {
2003
+ if (isCommandNotFoundResult("git", res)) {
2004
+ return {
2005
+ lines: [],
2006
+ commits: {},
2007
+ error: commandNotFoundDetail("git"),
2008
+ status: 503
2009
+ };
2010
+ }
2011
+ if (normalized.base === "worktree") {
2012
+ return syntheticUncommittedBlameFromWorktree(cwd, path);
2013
+ }
2014
+ return {
2015
+ lines: [],
2016
+ commits: {},
2017
+ error: res.stderr.trim() || "blame failed"
2018
+ };
2019
+ }
2020
+ const lines = [];
2021
+ const commits = {};
2022
+ const rawLines = res.stdout.split(`
2023
+ `);
2024
+ let i = 0;
2025
+ while (i < rawLines.length) {
2026
+ const headerLine = rawLines[i];
2027
+ if (!headerLine) {
2028
+ i++;
2029
+ continue;
2030
+ }
2031
+ const headerMatch = /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/.exec(headerLine);
2032
+ if (!headerMatch) {
2033
+ i++;
2034
+ continue;
2035
+ }
2036
+ const sha = headerMatch[1];
2037
+ const finalLine = Number(headerMatch[3]);
2038
+ i++;
2039
+ let commit = commits[sha];
2040
+ if (!commit) {
2041
+ commit = {
2042
+ sha,
2043
+ author: "",
2044
+ authorMail: "",
2045
+ authorTime: 0,
2046
+ summary: "",
2047
+ isUncommitted: sha === BLAME_ZERO_SHA
2048
+ };
2049
+ commits[sha] = commit;
2050
+ }
2051
+ while (i < rawLines.length && !rawLines[i].startsWith("\t")) {
2052
+ const metaLine = rawLines[i++];
2053
+ if (!metaLine)
2054
+ continue;
2055
+ const sp = metaLine.indexOf(" ");
2056
+ const key = sp >= 0 ? metaLine.slice(0, sp) : metaLine;
2057
+ const val = sp >= 0 ? metaLine.slice(sp + 1) : "";
2058
+ if (key === "author" && !commit.author)
2059
+ commit.author = val;
2060
+ else if (key === "author-mail" && !commit.authorMail)
2061
+ commit.authorMail = val.replace(/^</, "").replace(/>$/, "");
2062
+ else if (key === "author-time" && !commit.authorTime)
2063
+ commit.authorTime = Number(val) || 0;
2064
+ else if (key === "summary" && !commit.summary)
2065
+ commit.summary = val;
2066
+ }
2067
+ if (i < rawLines.length && rawLines[i].startsWith("\t"))
2068
+ i++;
2069
+ if (Number.isFinite(finalLine) && finalLine > 0) {
2070
+ lines.push({
2071
+ lineNo: finalLine,
2072
+ sha,
2073
+ isUncommitted: sha === BLAME_ZERO_SHA
2074
+ });
2075
+ }
2076
+ }
2077
+ lines.sort((a, b) => a.lineNo - b.lineNo);
2078
+ return { lines, commits };
2079
+ }
1756
2080
  function untracked(cwd, path = "") {
1757
2081
  const args = ["git", "ls-files", "--others", "--exclude-standard"];
1758
2082
  if (path)
@@ -1763,6 +2087,16 @@ function untracked(cwd, path = "") {
1763
2087
  return res.stdout.split(`
1764
2088
  `).filter(Boolean).filter((entry) => !isToolInternalPath(entry));
1765
2089
  }
2090
+ async function untrackedAsync(cwd, path = "") {
2091
+ const args = ["git", "ls-files", "--others", "--exclude-standard"];
2092
+ if (path)
2093
+ args.push("--", `${path}/`);
2094
+ const res = await runGitAsync(args, cwd);
2095
+ if (res.code !== 0)
2096
+ return [];
2097
+ return res.stdout.split(`
2098
+ `).filter(Boolean).filter((entry) => !isToolInternalPath(entry));
2099
+ }
1766
2100
  function normalizeTreePath(path) {
1767
2101
  return path.replace(/^\/+|\/+$/g, "");
1768
2102
  }
@@ -1790,6 +2124,18 @@ function worktreeSubmodulePaths(cwd) {
1790
2124
  return split >= 0 ? normalizeTreePath(line.slice(split + 1)) : "";
1791
2125
  }).filter(Boolean));
1792
2126
  }
2127
+ async function worktreeSubmodulePathsAsync(cwd) {
2128
+ if (!existsSync(join3(cwd, ".gitmodules")))
2129
+ return new Set;
2130
+ const res = await runGitAsync(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
2131
+ if (res.code !== 0)
2132
+ return new Set;
2133
+ return new Set(res.stdout.split(`
2134
+ `).map((line) => {
2135
+ const split = line.indexOf(" ");
2136
+ return split >= 0 ? normalizeTreePath(line.slice(split + 1)) : "";
2137
+ }).filter(Boolean));
2138
+ }
1793
2139
  function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames, submodulePaths) {
1794
2140
  if (excludeNames.has(name.toLowerCase()))
1795
2141
  return {
@@ -1887,6 +2233,93 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
1887
2233
  walk(root, base, 0);
1888
2234
  return combineDirectAndRecursiveFiles(directEntries, fileEntries.sort((a, b) => a.path.localeCompare(b.path)));
1889
2235
  }
2236
+ async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
2237
+ const base = normalizeTreePath(path);
2238
+ const root = join3(cwd, base);
2239
+ const omitDirNameSet = new Set(omitDirNames);
2240
+ const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
2241
+ const submodulePaths = await worktreeSubmodulePathsAsync(cwd);
2242
+ let directEntries;
2243
+ try {
2244
+ const dirents = readdirSync(root, { withFileTypes: true });
2245
+ directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
2246
+ } catch {
2247
+ return [];
2248
+ }
2249
+ if (!recursive)
2250
+ return directEntries;
2251
+ const fileEntries = [];
2252
+ let truncated = false;
2253
+ const pushRecursiveEntry = (entry) => {
2254
+ if (fileEntries.length >= WORKTREE_RECURSIVE_ENTRY_LIMIT) {
2255
+ if (!truncated) {
2256
+ fileEntries.push({
2257
+ name: "more...",
2258
+ path: "__code_viewer_truncated__",
2259
+ type: "tree",
2260
+ children_omitted: true,
2261
+ children_omitted_reason: "truncated"
2262
+ });
2263
+ truncated = true;
2264
+ }
2265
+ return false;
2266
+ }
2267
+ fileEntries.push(entry);
2268
+ return true;
2269
+ };
2270
+ let visitedDirs = 0;
2271
+ const yieldIfNeeded = async () => {
2272
+ visitedDirs++;
2273
+ if (visitedDirs % 25 === 0) {
2274
+ await new Promise((resolve) => setTimeout(resolve, 0));
2275
+ }
2276
+ };
2277
+ const walk = async (dir, prefix, depth) => {
2278
+ if (truncated)
2279
+ return;
2280
+ if (depth >= WORKTREE_RECURSIVE_DEPTH_LIMIT)
2281
+ return;
2282
+ await yieldIfNeeded();
2283
+ let entries;
2284
+ try {
2285
+ entries = readdirSync(dir, { withFileTypes: true });
2286
+ } catch {
2287
+ return;
2288
+ }
2289
+ for (const entry of entries) {
2290
+ if (excludeNameSet.has(entry.name.toLowerCase()))
2291
+ continue;
2292
+ const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2293
+ const full = join3(dir, entry.name);
2294
+ if (entry.isDirectory()) {
2295
+ const omittedReason = omittedWorktreeDirectoryReason(entry.name, omitDirNameSet);
2296
+ if (omittedReason) {
2297
+ if (!pushRecursiveEntry({
2298
+ name: entry.name,
2299
+ path: entryPath,
2300
+ type: "tree",
2301
+ children_omitted: true,
2302
+ children_omitted_reason: omittedReason
2303
+ }))
2304
+ return;
2305
+ continue;
2306
+ }
2307
+ if (hasDotGitEntry(full))
2308
+ continue;
2309
+ await walk(full, entryPath, depth + 1);
2310
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
2311
+ if (!pushRecursiveEntry({
2312
+ name: entry.name,
2313
+ path: entryPath,
2314
+ type: "blob"
2315
+ }))
2316
+ return;
2317
+ }
2318
+ }
2319
+ };
2320
+ await walk(root, base, 0);
2321
+ return combineDirectAndRecursiveFiles(directEntries, fileEntries.sort((a, b) => a.path.localeCompare(b.path)));
2322
+ }
1890
2323
  function hasDotGitEntry(dir) {
1891
2324
  try {
1892
2325
  lstatSync(join3(dir, ".git"));
@@ -1924,6 +2357,35 @@ function gitTreeEntries(ref, path, cwd, recursive) {
1924
2357
  entries = sortTreeEntries(entries);
1925
2358
  return { code: 0, entries, stderr: "" };
1926
2359
  }
2360
+ async function gitTreeEntriesAsync(ref, path, cwd, recursive) {
2361
+ const base = normalizeTreePath(path);
2362
+ const args = ["git", "-c", "core.quotepath=false", "ls-tree"];
2363
+ if (recursive)
2364
+ args.push("-r");
2365
+ args.push("-z", "--full-tree", ref, "--");
2366
+ if (base)
2367
+ args.push(`${base}/`);
2368
+ const res = await runGitAsync(args, cwd);
2369
+ if (res.code !== 0)
2370
+ return { code: res.code, entries: [], stderr: res.stderr };
2371
+ const allowedTypes = recursive ? "blob|commit" : "tree|blob|commit";
2372
+ let entries = res.stdout.split("\x00").filter(Boolean).map((rec) => {
2373
+ const match = rec.match(new RegExp(`^\\d+\\s+(${allowedTypes})\\s+[0-9a-fA-F]+\\t(.+)$`));
2374
+ if (!match)
2375
+ return null;
2376
+ const entryPath = match[2];
2377
+ return {
2378
+ name: entryPath.split("/").pop() || entryPath,
2379
+ path: entryPath,
2380
+ type: match[1]
2381
+ };
2382
+ }).filter((entry) => !!entry);
2383
+ if (recursive)
2384
+ entries.sort((a, b) => a.path.localeCompare(b.path));
2385
+ else
2386
+ entries = sortTreeEntries(entries);
2387
+ return { code: 0, entries, stderr: "" };
2388
+ }
1927
2389
  function combineDirectAndRecursiveFiles(directEntries, fileEntries) {
1928
2390
  const seen = new Set(directEntries.map((entry) => entry.path));
1929
2391
  return [
@@ -1952,8 +2414,29 @@ function listTree(ref, path, cwd, options = {}) {
1952
2414
  stderr: ""
1953
2415
  };
1954
2416
  }
1955
- function listTreeResult(ref, path, cwd, options = {}) {
1956
- const result = listTree(ref, path, cwd, options);
2417
+ async function listTreeAsync(ref, path, cwd, options = {}) {
2418
+ const base = normalizeTreePath(path);
2419
+ if (ref === "worktree") {
2420
+ return {
2421
+ code: 0,
2422
+ entries: await worktreeFilesystemEntriesAsync(cwd, base, !!options.recursive, options.omitDirNames, options.excludeNames),
2423
+ stderr: ""
2424
+ };
2425
+ }
2426
+ const direct = await gitTreeEntriesAsync(ref, base, cwd, false);
2427
+ if (direct.code !== 0 || !options.recursive)
2428
+ return direct;
2429
+ const recursive = await gitTreeEntriesAsync(ref, base, cwd, true);
2430
+ if (recursive.code !== 0)
2431
+ return recursive;
2432
+ return {
2433
+ code: 0,
2434
+ entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries),
2435
+ stderr: ""
2436
+ };
2437
+ }
2438
+ async function listTreeResultAsync(ref, path, cwd, options = {}) {
2439
+ const result = await listTreeAsync(ref, path, cwd, options);
1957
2440
  if (result.code === 0)
1958
2441
  return { entries: result.entries };
1959
2442
  return { entries: [], ...gitFailureResult(result, "git ls-tree failed") };
@@ -1961,21 +2444,51 @@ function listTreeResult(ref, path, cwd, options = {}) {
1961
2444
  function untrackedMeta(cwd) {
1962
2445
  return untracked(cwd).flatMap((path) => {
1963
2446
  const full = join3(cwd, path);
1964
- let binary = false;
1965
- let lines = 0;
1966
2447
  let fileExists = false;
1967
2448
  try {
1968
2449
  fileExists = existsSync(full) && statSync2(full).isFile();
1969
2450
  } catch {
1970
2451
  fileExists = false;
1971
2452
  }
2453
+ let scan;
2454
+ if (fileExists) {
2455
+ try {
2456
+ scan = scanFileBinaryAndNewlines(full);
2457
+ } catch {
2458
+ return [];
2459
+ }
2460
+ } else {
2461
+ return [];
2462
+ }
2463
+ return [
2464
+ {
2465
+ path,
2466
+ status: "A",
2467
+ additions: scan.binary ? 0 : scan.newlines,
2468
+ deletions: 0,
2469
+ binary: scan.binary,
2470
+ untracked: true
2471
+ }
2472
+ ];
2473
+ });
2474
+ }
2475
+ async function untrackedMetaAsync(cwd) {
2476
+ const paths = await untrackedAsync(cwd);
2477
+ return paths.flatMap((path) => {
2478
+ const full = join3(cwd, path);
2479
+ let fileExists = false;
2480
+ try {
2481
+ fileExists = existsSync(full) && statSync2(full).isFile();
2482
+ } catch {
2483
+ fileExists = false;
2484
+ }
2485
+ let scan;
1972
2486
  if (fileExists) {
1973
- const data = readFileSync(full);
1974
- const probe = data.subarray(0, 8192);
1975
- binary = probe.includes(0);
1976
- if (!binary)
1977
- lines = data.toString("utf8").split(`
1978
- `).length - 1;
2487
+ try {
2488
+ scan = scanFileBinaryAndNewlines(full);
2489
+ } catch {
2490
+ return [];
2491
+ }
1979
2492
  } else {
1980
2493
  return [];
1981
2494
  }
@@ -1983,14 +2496,40 @@ function untrackedMeta(cwd) {
1983
2496
  {
1984
2497
  path,
1985
2498
  status: "A",
1986
- additions: binary ? 0 : lines,
2499
+ additions: scan.binary ? 0 : scan.newlines,
1987
2500
  deletions: 0,
1988
- binary,
2501
+ binary: scan.binary,
1989
2502
  untracked: true
1990
2503
  }
1991
2504
  ];
1992
2505
  });
1993
2506
  }
2507
+ function scanFileBinaryAndNewlines(full) {
2508
+ const fd = openSync(full, "r");
2509
+ const buffer = Buffer.allocUnsafe(64 * 1024);
2510
+ let newlines = 0;
2511
+ let inspected = 0;
2512
+ try {
2513
+ while (true) {
2514
+ const read = readSync(fd, buffer, 0, buffer.length, null);
2515
+ if (read <= 0)
2516
+ break;
2517
+ const binaryProbeBytes = Math.min(read, Math.max(0, 8192 - inspected));
2518
+ for (let i = 0;i < binaryProbeBytes; i++) {
2519
+ if (buffer[i] === 0)
2520
+ return { binary: true, newlines: 0 };
2521
+ }
2522
+ inspected += read;
2523
+ for (let i = 0;i < read; i++) {
2524
+ if (buffer[i] === 10)
2525
+ newlines++;
2526
+ }
2527
+ }
2528
+ } finally {
2529
+ closeSync(fd);
2530
+ }
2531
+ return { binary: false, newlines };
2532
+ }
1994
2533
  function fileMetaResult(args, cwd, includeUntracked = false) {
1995
2534
  const ns = nameStatusResult(args, cwd);
1996
2535
  if (ns.error)
@@ -2012,6 +2551,27 @@ function fileMetaResult(args, cwd, includeUntracked = false) {
2012
2551
  files: includeUntracked ? files.concat(untrackedMeta(cwd)) : files
2013
2552
  };
2014
2553
  }
2554
+ async function fileMetaResultAsync(args, cwd, includeUntracked = false) {
2555
+ const ns = await nameStatusResultAsync(args, cwd);
2556
+ if (ns.error)
2557
+ return { files: [], error: ns.error };
2558
+ const nm = await numstatZResultAsync(args, cwd);
2559
+ if (nm.error)
2560
+ return { files: [], error: nm.error };
2561
+ const byPath = new Map(nm.files.map((file) => [file.path, file]));
2562
+ const files = ns.files.map((file) => {
2563
+ const stats = byPath.get(file.path);
2564
+ return {
2565
+ ...file,
2566
+ additions: stats?.additions || 0,
2567
+ deletions: stats?.deletions || 0,
2568
+ binary: stats?.binary || false
2569
+ };
2570
+ });
2571
+ return {
2572
+ files: includeUntracked ? files.concat(await untrackedMetaAsync(cwd)) : files
2573
+ };
2574
+ }
2015
2575
  function fileDiffText(args, path, cwd) {
2016
2576
  const paths = Array.isArray(path) ? path : [path];
2017
2577
  const res = run([
@@ -2031,6 +2591,25 @@ function fileDiffText(args, path, cwd) {
2031
2591
  }
2032
2592
  return res;
2033
2593
  }
2594
+ async function fileDiffTextAsync(args, path, cwd) {
2595
+ const paths = Array.isArray(path) ? path : [path];
2596
+ const res = await runGitAsync([
2597
+ "git",
2598
+ "-c",
2599
+ "core.quotepath=false",
2600
+ "diff",
2601
+ "--no-color",
2602
+ "--no-ext-diff",
2603
+ "--find-renames",
2604
+ ...args,
2605
+ "--",
2606
+ ...paths
2607
+ ], cwd);
2608
+ if (isCommandNotFoundResult("git", res)) {
2609
+ return { ...res, stderr: commandNotFoundDetail("git"), status: 503 };
2610
+ }
2611
+ return res;
2612
+ }
2034
2613
  function untrackedFileDiff(extras, path, cwd) {
2035
2614
  const res = run([
2036
2615
  "git",
@@ -2049,16 +2628,38 @@ function untrackedFileDiff(extras, path, cwd) {
2049
2628
  }
2050
2629
  return res;
2051
2630
  }
2631
+ async function untrackedFileDiffAsync(extras, path, cwd) {
2632
+ const res = await runGitAsync([
2633
+ "git",
2634
+ "-c",
2635
+ "core.quotepath=false",
2636
+ "diff",
2637
+ "--no-color",
2638
+ "--no-ext-diff",
2639
+ "--no-index",
2640
+ ...extras,
2641
+ "/dev/null",
2642
+ path
2643
+ ], cwd);
2644
+ if (isCommandNotFoundResult("git", res)) {
2645
+ return { ...res, stderr: commandNotFoundDetail("git"), status: 503 };
2646
+ }
2647
+ return res;
2648
+ }
2052
2649
  function splitHunks(diffText) {
2053
2650
  if (!diffText)
2054
2651
  return { header: "", hunks: [] };
2055
- const first = diffText.startsWith("@@") ? 0 : diffText.indexOf(`
2056
- @@`) + 1;
2057
- if (first <= 0)
2652
+ const startsWithHunk = diffText.startsWith("@@");
2653
+ const first = startsWithHunk ? 0 : diffText.indexOf(`
2654
+ @@`);
2655
+ if (first < 0)
2656
+ return { header: diffText, hunks: [] };
2657
+ const hunkStart = startsWithHunk ? 0 : first + 1;
2658
+ if (hunkStart >= diffText.length)
2058
2659
  return { header: diffText, hunks: [] };
2059
- const header = diffText.slice(0, first);
2660
+ const header = diffText.slice(0, hunkStart);
2060
2661
  const hunks = [];
2061
- let cur = first;
2662
+ let cur = hunkStart;
2062
2663
  while (cur < diffText.length) {
2063
2664
  const next = diffText.indexOf(`
2064
2665
  @@`, cur + 1);
@@ -3382,11 +3983,16 @@ __export(exports_file_cli, {
3382
3983
  sliceLines: () => sliceLines,
3383
3984
  safeWorktreePathFromRoot: () => safeWorktreePathFromRoot,
3384
3985
  runFileCli: () => runFileCli,
3986
+ readShowTextAsync: () => readShowTextAsync,
3385
3987
  readShowText: () => readShowText,
3386
3988
  parseFileArgs: () => parseFileArgs,
3989
+ buildFileShowReportAsync: () => buildFileShowReportAsync,
3387
3990
  buildFileShowReport: () => buildFileShowReport,
3991
+ buildFileHistoryReportAsync: () => buildFileHistoryReportAsync,
3388
3992
  buildFileHistoryReport: () => buildFileHistoryReport,
3993
+ buildFileDiffReportAsync: () => buildFileDiffReportAsync,
3389
3994
  buildFileDiffReport: () => buildFileDiffReport,
3995
+ buildFileBlameReportAsync: () => buildFileBlameReportAsync,
3390
3996
  buildFileBlameReport: () => buildFileBlameReport,
3391
3997
  FILE_HISTORY_HARD_CAP: () => FILE_HISTORY_HARD_CAP,
3392
3998
  FILE_HELP: () => FILE_HELP,
@@ -3706,6 +4312,19 @@ function buildFileBlameReport(root, command) {
3706
4312
  result
3707
4313
  };
3708
4314
  }
4315
+ async function buildFileBlameReportAsync(root, command) {
4316
+ const result = await blameAsync(root, {
4317
+ path: command.path,
4318
+ ref: command.ref,
4319
+ base: command.base
4320
+ });
4321
+ return {
4322
+ path: command.path,
4323
+ ref: command.ref,
4324
+ base: command.base,
4325
+ result
4326
+ };
4327
+ }
3709
4328
  function buildFileHistoryReport(root, command) {
3710
4329
  const result = commitHistory(root, {
3711
4330
  ref: command.ref,
@@ -3723,6 +4342,23 @@ function buildFileHistoryReport(root, command) {
3723
4342
  result
3724
4343
  };
3725
4344
  }
4345
+ async function buildFileHistoryReportAsync(root, command) {
4346
+ const result = await commitHistoryAsync(root, {
4347
+ ref: command.ref,
4348
+ skip: command.skip,
4349
+ limit: command.limit,
4350
+ query: command.query,
4351
+ path: command.path
4352
+ });
4353
+ return {
4354
+ path: command.path,
4355
+ ref: command.ref,
4356
+ limit: command.limit,
4357
+ skip: command.skip,
4358
+ ...command.query !== undefined ? { query: command.query } : {},
4359
+ result
4360
+ };
4361
+ }
3726
4362
  function runBlame(root, command) {
3727
4363
  const report = buildFileBlameReport(root, command);
3728
4364
  if (command.json) {
@@ -3811,6 +4447,28 @@ function readShowText(root, command) {
3811
4447
  return { code: 1, stdout: "", stderr: "file not readable" };
3812
4448
  }
3813
4449
  }
4450
+ async function readShowTextAsync(root, command) {
4451
+ if (command.ref !== "worktree" && command.ref !== "") {
4452
+ return showAsync(command.ref, command.path, root);
4453
+ }
4454
+ const full = safeWorktreePathFromRoot(root, command.path);
4455
+ if (!full) {
4456
+ return {
4457
+ code: 1,
4458
+ stdout: "",
4459
+ stderr: "file not found or forbidden"
4460
+ };
4461
+ }
4462
+ try {
4463
+ const stat = statSync3(full);
4464
+ if (!stat.isFile()) {
4465
+ return { code: 1, stdout: "", stderr: "not a file" };
4466
+ }
4467
+ return { code: 0, stdout: readFileSync4(full, "utf8"), stderr: "" };
4468
+ } catch {
4469
+ return { code: 1, stdout: "", stderr: "file not readable" };
4470
+ }
4471
+ }
3814
4472
  function buildFileShowReport(root, command) {
3815
4473
  const res = readShowText(root, command);
3816
4474
  if (res.code !== 0) {
@@ -3838,6 +4496,33 @@ function buildFileShowReport(root, command) {
3838
4496
  `)
3839
4497
  };
3840
4498
  }
4499
+ async function buildFileShowReportAsync(root, command) {
4500
+ const res = await readShowTextAsync(root, command);
4501
+ if (res.code !== 0) {
4502
+ const detail = res.stderr.trim() || `git show exited with code ${res.code}`;
4503
+ return {
4504
+ path: command.path,
4505
+ ref: command.ref,
4506
+ ...command.start !== undefined ? { start: command.start } : {},
4507
+ ...command.end !== undefined ? { end: command.end } : {},
4508
+ totalLines: 0,
4509
+ complete: false,
4510
+ text: "",
4511
+ error: detail
4512
+ };
4513
+ }
4514
+ const sliced = sliceLines(res.stdout, command.start, command.end);
4515
+ return {
4516
+ path: command.path,
4517
+ ref: command.ref,
4518
+ ...command.start !== undefined ? { start: command.start } : {},
4519
+ ...command.end !== undefined ? { end: command.end } : {},
4520
+ totalLines: sliced.total,
4521
+ complete: sliced.complete,
4522
+ text: sliced.lines.join(`
4523
+ `)
4524
+ };
4525
+ }
3841
4526
  function runShow(root, command) {
3842
4527
  const report = buildFileShowReport(root, command);
3843
4528
  if (report.error !== undefined) {
@@ -3920,6 +4605,64 @@ function buildFileDiffReport(root, command) {
3920
4605
  ...errText ? { error: errText } : {}
3921
4606
  };
3922
4607
  }
4608
+ async function buildFileDiffReportAsync(root, command) {
4609
+ const base = {
4610
+ path: command.path,
4611
+ ...command.oldPath !== undefined ? { old_path: command.oldPath } : {},
4612
+ from: command.untracked ? "/dev/null" : command.from,
4613
+ to: command.to,
4614
+ untracked: command.untracked,
4615
+ ignore_ws: command.ignoreWs,
4616
+ ignore_blank: command.ignoreBlank,
4617
+ mode: command.mode,
4618
+ max_hunks: command.mode === "preview" ? command.maxHunks : null,
4619
+ max_lines: command.mode === "preview" ? command.maxLines : null,
4620
+ diff: "",
4621
+ hunk_count: 0,
4622
+ rendered_hunk_count: 0,
4623
+ line_count: 0,
4624
+ truncated: false,
4625
+ binary: false
4626
+ };
4627
+ if (!command.untracked && isSameWorktreeRange({ from: command.from, to: command.to })) {
4628
+ return base;
4629
+ }
4630
+ const extras = [];
4631
+ if (command.ignoreWs)
4632
+ extras.push("-w");
4633
+ if (command.ignoreBlank)
4634
+ extras.push("--ignore-blank-lines");
4635
+ let diffText = "";
4636
+ let errText = "";
4637
+ if (command.untracked) {
4638
+ const res = await untrackedFileDiffAsync(extras, command.path, root);
4639
+ diffText = res.stdout || "";
4640
+ if (res.code !== 0)
4641
+ errText = res.stderr.trim();
4642
+ } else {
4643
+ const args = [
4644
+ ...extras,
4645
+ ...buildFileDiffRangeArgs(command.from, command.to)
4646
+ ];
4647
+ const paths = command.oldPath !== undefined ? [command.oldPath, command.path] : command.path;
4648
+ const res = await fileDiffTextAsync(args, paths, root);
4649
+ diffText = res.stdout || "";
4650
+ if (res.code !== 0)
4651
+ errText = res.stderr.trim();
4652
+ }
4653
+ const truncated = command.mode === "preview" ? truncateToNHunks(diffText, command.maxHunks, command.maxLines) : truncateToNHunks(diffText, 1e9);
4654
+ const previewTruncated = command.mode === "preview" && (truncated.totalHunks > truncated.renderedHunks || truncated.lineTruncated);
4655
+ return {
4656
+ ...base,
4657
+ diff: truncated.text,
4658
+ hunk_count: truncated.totalHunks,
4659
+ rendered_hunk_count: truncated.renderedHunks,
4660
+ line_count: truncated.lineCount,
4661
+ truncated: previewTruncated,
4662
+ binary: diffText.includes("Binary files"),
4663
+ ...errText ? { error: errText } : {}
4664
+ };
4665
+ }
3923
4666
  function runDiff(root, command) {
3924
4667
  const report = buildFileDiffReport(root, command);
3925
4668
  if (command.json) {
@@ -4406,8 +5149,8 @@ function buildGithubIssueViewArgs(options) {
4406
5149
  args.push("--repo", repo);
4407
5150
  return args;
4408
5151
  }
4409
- function readGithubIssueList(options) {
4410
- const proc = runSync(buildGithubIssueListArgs(options), options.cwd, {
5152
+ async function readGithubIssueListAsync(options) {
5153
+ const proc = await runAsync(buildGithubIssueListArgs(options), options.cwd, {
4411
5154
  timeout: 30000
4412
5155
  });
4413
5156
  if (proc.code !== 0) {
@@ -4420,8 +5163,8 @@ function readGithubIssueList(options) {
4420
5163
  throw new GithubIssueListError("failed to parse gh issue list output");
4421
5164
  }
4422
5165
  }
4423
- function readGithubIssue(options) {
4424
- const proc = runSync(buildGithubIssueViewArgs(options), options.cwd, {
5166
+ async function readGithubIssueAsync(options) {
5167
+ const proc = await runAsync(buildGithubIssueViewArgs(options), options.cwd, {
4425
5168
  timeout: 30000
4426
5169
  });
4427
5170
  if (proc.code !== 0) {
@@ -5075,7 +5818,7 @@ async function runJournalCli(argv) {
5075
5818
  console.error(commandConfig.error);
5076
5819
  process.exit(1);
5077
5820
  }
5078
- const issues = readGithubIssueList({
5821
+ const issues = await readGithubIssueListAsync({
5079
5822
  cwd: root,
5080
5823
  repo: command.repo,
5081
5824
  labels: command.ghLabels,
@@ -5099,7 +5842,7 @@ async function runJournalCli(argv) {
5099
5842
  console.error(commandConfig.error);
5100
5843
  process.exit(1);
5101
5844
  }
5102
- const issue = readGithubIssue({
5845
+ const issue = await readGithubIssueAsync({
5103
5846
  cwd: root,
5104
5847
  number: command.issueNumber,
5105
5848
  repo: command.repo
@@ -7839,11 +8582,106 @@ function computeFuzzyMatch(query, path) {
7839
8582
  tier
7840
8583
  };
7841
8584
  }
7842
- function rankFuzzyPaths(query, items) {
7843
- return items.map((item) => {
8585
+ function rankFuzzyPaths(query, items, limit) {
8586
+ const bounded = Number.isInteger(limit) && limit !== undefined && limit > 0 ? Math.floor(limit) : 0;
8587
+ const compare = (a, b) => b.tier - a.tier || b.score - a.score || a.item.path.localeCompare(b.item.path);
8588
+ if (!bounded) {
8589
+ return items.map((item) => {
8590
+ const match = computeFuzzyMatch(query, item.path);
8591
+ return match ? { item, score: match.score, ranges: match.ranges, tier: match.tier } : null;
8592
+ }).filter((item) => item !== null).sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
8593
+ }
8594
+ const top = [];
8595
+ for (const item of items) {
7844
8596
  const match = computeFuzzyMatch(query, item.path);
7845
- return match ? { item, score: match.score, ranges: match.ranges, tier: match.tier } : null;
7846
- }).filter((item) => item !== null).sort((a, b) => b.tier - a.tier || b.score - a.score || a.item.path.localeCompare(b.item.path)).map(({ item, score, ranges }) => ({ item, score, ranges }));
8597
+ if (!match)
8598
+ continue;
8599
+ const ranked = {
8600
+ item,
8601
+ score: match.score,
8602
+ ranges: match.ranges,
8603
+ tier: match.tier
8604
+ };
8605
+ pushBoundedTop(top, ranked, bounded, compare);
8606
+ }
8607
+ return top.sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
8608
+ }
8609
+ function pushBoundedTop(heap, item, limit, compareBestFirst) {
8610
+ const isWorse = (a, b) => compareBestFirst(a, b) > 0;
8611
+ const siftUp = (index) => {
8612
+ while (index > 0) {
8613
+ const parent = Math.floor((index - 1) / 2);
8614
+ if (!isWorse(heap[index], heap[parent]))
8615
+ break;
8616
+ [heap[index], heap[parent]] = [heap[parent], heap[index]];
8617
+ index = parent;
8618
+ }
8619
+ };
8620
+ const siftDown = (index) => {
8621
+ while (true) {
8622
+ const left = index * 2 + 1;
8623
+ const right = left + 1;
8624
+ let worst = index;
8625
+ if (left < heap.length && isWorse(heap[left], heap[worst]))
8626
+ worst = left;
8627
+ if (right < heap.length && isWorse(heap[right], heap[worst]))
8628
+ worst = right;
8629
+ if (worst === index)
8630
+ break;
8631
+ [heap[index], heap[worst]] = [heap[worst], heap[index]];
8632
+ index = worst;
8633
+ }
8634
+ };
8635
+ if (heap.length < limit) {
8636
+ heap.push(item);
8637
+ siftUp(heap.length - 1);
8638
+ return;
8639
+ }
8640
+ if (compareBestFirst(item, heap[0]) >= 0)
8641
+ return;
8642
+ heap[0] = item;
8643
+ siftDown(0);
8644
+ }
8645
+ function rankGlobPathMatches(query, items, limit) {
8646
+ const matchPath = createGlobPathMatcher(query);
8647
+ if (!matchPath)
8648
+ return [];
8649
+ const bounded = Number.isInteger(limit) && limit !== undefined && limit > 0 ? Math.floor(limit) : 0;
8650
+ const compare = (a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path);
8651
+ if (!bounded) {
8652
+ return items.map((item) => {
8653
+ const match = matchPath(item.path);
8654
+ return match ? {
8655
+ item,
8656
+ score: match.score,
8657
+ ranges: match.ranges,
8658
+ mode: "glob"
8659
+ } : null;
8660
+ }).filter((item) => item !== null).sort(compare);
8661
+ }
8662
+ const top = [];
8663
+ for (const item of items) {
8664
+ const match = matchPath(item.path);
8665
+ if (!match)
8666
+ continue;
8667
+ const ranked = {
8668
+ item,
8669
+ score: match.score,
8670
+ ranges: match.ranges,
8671
+ mode: "glob"
8672
+ };
8673
+ pushBoundedTop(top, ranked, bounded, compare);
8674
+ }
8675
+ return top.sort(compare);
8676
+ }
8677
+ function rankPathMatches(query, items, limit) {
8678
+ if (isGlobPathQuery(query)) {
8679
+ return rankGlobPathMatches(query, items, limit);
8680
+ }
8681
+ return rankFuzzyPaths(query, items, limit).map((item) => ({
8682
+ ...item,
8683
+ mode: "fuzzy"
8684
+ }));
7847
8685
  }
7848
8686
  function isGlobPathQuery(query) {
7849
8687
  return /[*?]/.test(query.trim());
@@ -7887,49 +8725,37 @@ function globToRegExp(query) {
7887
8725
  return null;
7888
8726
  }
7889
8727
  }
7890
- function globMatchPath(query, path) {
8728
+ function createGlobPathMatcher(query) {
7891
8729
  const regex = globToRegExp(query);
7892
- const baseStart = basenameStart(path);
7893
- const basename = path.slice(baseStart);
7894
- if (!regex || !regex.test(path) && (query.includes("/") || !regex.test(basename)))
8730
+ if (!regex)
7895
8731
  return null;
7896
8732
  const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
7897
- const ranges = [];
7898
- const lowerPath = path.toLowerCase();
7899
- for (const part of literal) {
7900
- const start = lowerPath.indexOf(part.toLowerCase());
7901
- if (start >= 0)
7902
- ranges.push({ start, end: start + part.length });
7903
- }
7904
- ranges.sort((a, b) => a.start - b.start || a.end - b.end);
7905
- const mergedRanges = [];
7906
- for (const range of ranges) {
7907
- const last = mergedRanges[mergedRanges.length - 1];
7908
- if (last && last.end >= range.start) {
7909
- last.end = Math.max(last.end, range.end);
7910
- } else {
7911
- mergedRanges.push({ ...range });
8733
+ const suffix = query.replace(/^\*+/, "").toLowerCase();
8734
+ return (path) => {
8735
+ const baseStart = basenameStart(path);
8736
+ const basename = path.slice(baseStart);
8737
+ if (!regex.test(path) && (query.includes("/") || !regex.test(basename)))
8738
+ return null;
8739
+ const ranges = [];
8740
+ const lowerPath = path.toLowerCase();
8741
+ for (const part of literal) {
8742
+ const start = lowerPath.indexOf(part.toLowerCase());
8743
+ if (start >= 0)
8744
+ ranges.push({ start, end: start + part.length });
8745
+ }
8746
+ ranges.sort((a, b) => a.start - b.start || a.end - b.end);
8747
+ const mergedRanges = [];
8748
+ for (const range of ranges) {
8749
+ const last = mergedRanges[mergedRanges.length - 1];
8750
+ if (last && last.end >= range.start) {
8751
+ last.end = Math.max(last.end, range.end);
8752
+ } else {
8753
+ mergedRanges.push({ ...range });
8754
+ }
7912
8755
  }
7913
- }
7914
- const score = 1000 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(query.replace(/^\*+/, "").toLowerCase()) ? 50 : 0);
7915
- return { score, ranges: mergedRanges };
7916
- }
7917
- function rankPathMatches(query, items) {
7918
- if (isGlobPathQuery(query)) {
7919
- return items.map((item) => {
7920
- const match = globMatchPath(query, item.path);
7921
- return match ? {
7922
- item,
7923
- score: match.score,
7924
- ranges: match.ranges,
7925
- mode: "glob"
7926
- } : null;
7927
- }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
7928
- }
7929
- return rankFuzzyPaths(query, items).map((item) => ({
7930
- ...item,
7931
- mode: "fuzzy"
7932
- }));
8756
+ const score = 1000 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(suffix) ? 50 : 0);
8757
+ return { score, ranges: mergedRanges };
8758
+ };
7933
8759
  }
7934
8760
 
7935
8761
  // web-src/server/search.ts
@@ -8020,9 +8846,11 @@ function parseRgOutput(stdout, max, omitDirNames = [], excludeNames = []) {
8020
8846
  const matches = [];
8021
8847
  for (const line of stdout.split(`
8022
8848
  `)) {
8023
- if (!line || matches.length >= max)
8849
+ if (!line)
8024
8850
  continue;
8025
- const parsed = /^(.*):(\d+):(\d+):(.*)$/.exec(line);
8851
+ if (matches.length >= max)
8852
+ break;
8853
+ const parsed = /^(.*?):(\d+):(\d+):(.*)$/.exec(line);
8026
8854
  if (!parsed)
8027
8855
  continue;
8028
8856
  const path = parsed[1];
@@ -13308,10 +14136,10 @@ function hasControlCharacter(value) {
13308
14136
 
13309
14137
  // web-src/server/database/discovery.ts
13310
14138
  import {
13311
- closeSync,
14139
+ closeSync as closeSync2,
13312
14140
  existsSync as existsSync6,
13313
- openSync,
13314
- readSync,
14141
+ openSync as openSync2,
14142
+ readSync as readSync2,
13315
14143
  realpathSync as realpathSync4,
13316
14144
  statSync as statSync4
13317
14145
  } from "node:fs";
@@ -13323,11 +14151,11 @@ function isSqliteFile(fullPath) {
13323
14151
  if (!stat2.isFile() || stat2.size < 16)
13324
14152
  return false;
13325
14153
  const buf = Buffer.alloc(16);
13326
- const fd = openSync(fullPath, "r");
14154
+ const fd = openSync2(fullPath, "r");
13327
14155
  try {
13328
- readSync(fd, buf, 0, 16, 0);
14156
+ readSync2(fd, buf, 0, 16, 0);
13329
14157
  } finally {
13330
- closeSync(fd);
14158
+ closeSync2(fd);
13331
14159
  }
13332
14160
  return buf.toString("utf8", 0, 16) === SQLITE_MAGIC;
13333
14161
  } catch {
@@ -13449,15 +14277,26 @@ function validateDbPath(cwd, dbPath) {
13449
14277
  return null;
13450
14278
  return realFull;
13451
14279
  }
13452
- function serviceListIncludesS3(value) {
14280
+ function serviceListIncludesAwsService(value, service) {
13453
14281
  if (value === undefined || value.trim() === "")
13454
14282
  return true;
13455
- return value.split(/[,\s]+/).map((part) => part.trim().toLowerCase()).filter(Boolean).includes("s3");
14283
+ return value.split(/[,\s]+/).map((part) => part.trim().toLowerCase()).filter(Boolean).includes(service);
14284
+ }
14285
+ function detectAwsKindsFromServices(services) {
14286
+ const kinds = [];
14287
+ if (serviceListIncludesAwsService(services, "s3"))
14288
+ kinds.push("s3");
14289
+ if (serviceListIncludesAwsService(services, "dynamodb"))
14290
+ kinds.push("dynamodb");
14291
+ return kinds;
14292
+ }
14293
+ function isLocalstackImage(image) {
14294
+ return !!image && image.toLowerCase().includes("localstack/localstack");
13456
14295
  }
13457
14296
  function imageLooksLikeMinio(image) {
13458
14297
  return /(^|\/)minio(?::|\/|$)/.test(image.toLowerCase());
13459
14298
  }
13460
- function detectDbKind(image, env = {}) {
14299
+ function detectDbKind(image, _env = {}) {
13461
14300
  const lower = image.toLowerCase();
13462
14301
  if (lower.includes("postgres"))
13463
14302
  return "postgresql";
@@ -13469,9 +14308,6 @@ function detectDbKind(image, env = {}) {
13469
14308
  return "elasticsearch";
13470
14309
  if (imageLooksLikeMinio(lower))
13471
14310
  return "s3";
13472
- if (lower.includes("localstack/localstack")) {
13473
- return serviceListIncludesS3(env.SERVICES) ? "s3" : null;
13474
- }
13475
14311
  return null;
13476
14312
  }
13477
14313
  function detectDbKindFromEnv(env) {
@@ -13484,10 +14320,33 @@ function detectDbKindFromEnv(env) {
13484
14320
  if (env.MINIO_ROOT_USER || env.MINIO_ROOT_PASSWORD || env.MINIO_ACCESS_KEY || env.MINIO_SECRET_KEY) {
13485
14321
  return "s3";
13486
14322
  }
13487
- if (env.SERVICES && serviceListIncludesS3(env.SERVICES))
13488
- return "s3";
13489
14323
  return null;
13490
14324
  }
14325
+ function detectDbKindsForService(image, env, containerPort, serviceName) {
14326
+ if (isLocalstackImage(image) || env.SERVICES !== undefined) {
14327
+ return detectAwsKindsFromServices(env.SERVICES);
14328
+ }
14329
+ const single = (image ? detectDbKind(image, env) : null) ?? detectDbKindFromEnv(env) ?? detectDbKindFromContainerPort(containerPort) ?? detectDbKindFromServiceName(serviceName);
14330
+ return single ? [single] : [];
14331
+ }
14332
+ function dbKindDisplayName(kind) {
14333
+ switch (kind) {
14334
+ case "s3":
14335
+ return "S3";
14336
+ case "dynamodb":
14337
+ return "DynamoDB";
14338
+ case "redis":
14339
+ return "Redis";
14340
+ case "elasticsearch":
14341
+ return "Elasticsearch";
14342
+ case "postgresql":
14343
+ return "PostgreSQL";
14344
+ case "mysql":
14345
+ return "MySQL";
14346
+ case "sqlite":
14347
+ return "SQLite";
14348
+ }
14349
+ }
13491
14350
  function detectDbKindFromContainerPort(port) {
13492
14351
  switch (port) {
13493
14352
  case "3306":
@@ -13532,8 +14391,10 @@ function defaultPortFor(kind, image, env = {}) {
13532
14391
  return "9200";
13533
14392
  case "s3": {
13534
14393
  const lower = image?.toLowerCase() || "";
13535
- return lower.includes("localstack/localstack") || env.SERVICES !== undefined && serviceListIncludesS3(env.SERVICES) ? "4566" : "9000";
14394
+ return lower.includes("localstack/localstack") || env.SERVICES !== undefined && serviceListIncludesAwsService(env.SERVICES, "s3") ? "4566" : "9000";
13536
14395
  }
14396
+ case "dynamodb":
14397
+ return "4566";
13537
14398
  default:
13538
14399
  return "";
13539
14400
  }
@@ -13620,7 +14481,7 @@ function parseComposePortMappings(serviceBlock, composeDirEnv = {}) {
13620
14481
  const trimmed = line.trim();
13621
14482
  if (!trimmed.startsWith("-"))
13622
14483
  continue;
13623
- const value = resolveEnvValue(trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim(), composeDirEnv);
14484
+ const value = resolveEnvValue(trimmed.slice(1).trim(), composeDirEnv).split("/")[0].trim();
13624
14485
  if (!value || value.includes("target:"))
13625
14486
  continue;
13626
14487
  const parts = value.split(":");
@@ -13705,40 +14566,45 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
13705
14566
  const env = parseComposeEnv(svcBlock, composeDirEnv);
13706
14567
  const containerPort = parseComposeContainerPort(svcBlock);
13707
14568
  const profiled = /^\s+profiles:/m.test(svcBlock);
13708
- const kind = (image ? detectDbKind(image, env) : null) ?? detectDbKindFromEnv(env) ?? detectDbKindFromContainerPort(containerPort) ?? detectDbKindFromServiceName(svc.name);
13709
- if (!kind)
13710
- continue;
13711
- const defaultPort = defaultPortFor(kind, image, env);
13712
- const serviceContainerPort = kind === "s3" ? defaultPort : containerPort || defaultPort;
13713
- const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort, composeDirEnv) || parseComposeHostPortForContainer(svcBlock, defaultPort, composeDirEnv) || parseComposePorts(svcBlock, composeDirEnv);
13714
- const hostPort = kind === "s3" ? publishedHostPort || undefined : publishedHostPort || defaultPort;
13715
- const imageLabel = image ?? `build:${kind}`;
13716
- const id = isRoot ? `docker:${svc.name}` : `docker:${svc.name}@${encodeURIComponent(relDirSlash)}`;
13717
- const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
13718
- let label;
13719
- if (kind === "redis" || kind === "elasticsearch" || kind === "s3") {
13720
- const endpointLabel = hostPort ? `localhost:${hostPort}` : `container:${serviceContainerPort}`;
13721
- label = `${svc.name} (${imageLabel}, ${endpointLabel}${labelPath})`;
13722
- } else {
13723
- const dbName = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || svc.name;
13724
- const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
13725
- label = `${svc.name} (${imageLabel}, ${user}@localhost:${hostPort}/${dbName}${labelPath})`;
14569
+ const kinds = detectDbKindsForService(image, env, containerPort, svc.name);
14570
+ const isMultiKind = kinds.length > 1;
14571
+ for (const kind of kinds) {
14572
+ if (results.length >= MAX_DOCKER_SERVICES)
14573
+ return;
14574
+ const defaultPort = defaultPortFor(kind, image, env);
14575
+ const serviceContainerPort = kind === "s3" || kind === "dynamodb" ? defaultPort : containerPort || defaultPort;
14576
+ const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort, composeDirEnv) || parseComposeHostPortForContainer(svcBlock, defaultPort, composeDirEnv) || parseComposePorts(svcBlock, composeDirEnv);
14577
+ const hostPort = kind === "s3" || kind === "dynamodb" ? publishedHostPort || undefined : publishedHostPort || defaultPort;
14578
+ const imageLabel = image ?? `build:${kind}`;
14579
+ const kindSuffix = isMultiKind ? `#${kind}` : "";
14580
+ const id = isRoot ? `docker:${svc.name}${kindSuffix}` : `docker:${svc.name}@${encodeURIComponent(relDirSlash)}${kindSuffix}`;
14581
+ const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
14582
+ let label;
14583
+ if (kind === "redis" || kind === "elasticsearch" || kind === "s3" || kind === "dynamodb") {
14584
+ const endpointLabel = hostPort ? `localhost:${hostPort}` : `container:${serviceContainerPort}`;
14585
+ const svcLabel = isMultiKind ? `${svc.name} / ${dbKindDisplayName(kind)}` : svc.name;
14586
+ label = `${svcLabel} (${imageLabel}, ${endpointLabel}${labelPath})`;
14587
+ } else {
14588
+ const dbName = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || svc.name;
14589
+ const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
14590
+ label = `${svc.name} (${imageLabel}, ${user}@localhost:${hostPort}/${dbName}${labelPath})`;
14591
+ }
14592
+ results.push({
14593
+ id,
14594
+ path: isRoot ? filename : `${relDirSlash}/${filename}`,
14595
+ name: label,
14596
+ sizeBytes: 0,
14597
+ kind,
14598
+ serviceName: svc.name,
14599
+ ...image ? { image } : {},
14600
+ env,
14601
+ composeDir,
14602
+ relDirSlash,
14603
+ ...hostPort ? { hostPort } : {},
14604
+ containerPort: serviceContainerPort,
14605
+ ...profiled ? { profiled: true } : {}
14606
+ });
13726
14607
  }
13727
- results.push({
13728
- id,
13729
- path: isRoot ? filename : `${relDirSlash}/${filename}`,
13730
- name: label,
13731
- sizeBytes: 0,
13732
- kind,
13733
- serviceName: svc.name,
13734
- ...image ? { image } : {},
13735
- env,
13736
- composeDir,
13737
- relDirSlash,
13738
- ...hostPort ? { hostPort } : {},
13739
- containerPort: serviceContainerPort,
13740
- ...profiled ? { profiled: true } : {}
13741
- });
13742
14608
  }
13743
14609
  }
13744
14610
  async function parseComposeFileAsync(filepath, composeDir, cwd, results) {
@@ -13836,12 +14702,24 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
13836
14702
  });
13837
14703
  return cloneDockerDiscoveryResult(results);
13838
14704
  }
14705
+ function isDbKind(value) {
14706
+ return DB_KIND_VALUES.has(value);
14707
+ }
13839
14708
  function parseDockerDbId(dbId) {
13840
14709
  if (!dbId.startsWith("docker:"))
13841
14710
  return null;
13842
14711
  let rest = dbId.slice(7);
13843
14712
  if (!rest)
13844
14713
  return null;
14714
+ let kind;
14715
+ const hashIdx = rest.indexOf("#");
14716
+ if (hashIdx >= 0) {
14717
+ const kindPart = rest.slice(hashIdx + 1);
14718
+ if (!isDbKind(kindPart))
14719
+ return null;
14720
+ kind = kindPart;
14721
+ rest = rest.slice(0, hashIdx);
14722
+ }
13845
14723
  let database;
13846
14724
  const atIdx = rest.indexOf("@");
13847
14725
  if (atIdx >= 0) {
@@ -13865,7 +14743,8 @@ function parseDockerDbId(dbId) {
13865
14743
  return {
13866
14744
  serviceName,
13867
14745
  relDir,
13868
- database
14746
+ database,
14747
+ kind
13869
14748
  };
13870
14749
  } catch {
13871
14750
  return null;
@@ -13880,23 +14759,27 @@ function parseDockerDbId(dbId) {
13880
14759
  return null;
13881
14760
  if (!isSafeDockerDatabaseName(database))
13882
14761
  return null;
13883
- return { serviceName: rest, relDir: "", database };
14762
+ return { serviceName: rest, relDir: "", database, kind };
13884
14763
  }
13885
14764
  function canonicalizeDockerDbId(dbId) {
13886
14765
  const parsed = parseDockerDbId(dbId);
13887
14766
  if (!parsed)
13888
14767
  return null;
13889
14768
  const database = parsed.database ? `:${parsed.database}` : "";
14769
+ const kindSuffix = parsed.kind ? `#${parsed.kind}` : "";
13890
14770
  if (!parsed.relDir)
13891
- return `docker:${parsed.serviceName}${database}`;
13892
- return `docker:${parsed.serviceName}@${encodeURIComponent(parsed.relDir)}${database}`;
14771
+ return `docker:${parsed.serviceName}${database}${kindSuffix}`;
14772
+ return `docker:${parsed.serviceName}@${encodeURIComponent(parsed.relDir)}${database}${kindSuffix}`;
13893
14773
  }
13894
14774
  async function findDockerServiceByDbIdAsync(cwd, dbId, kind, omitDirNames, signal) {
13895
14775
  const parsed = parseDockerDbId(dbId);
13896
14776
  if (!parsed)
13897
14777
  return null;
14778
+ if (kind && parsed.kind && parsed.kind !== kind)
14779
+ return null;
14780
+ const effectiveKind = parsed.kind ?? kind;
13898
14781
  const services = await discoverDockerDatabasesAsync(cwd, omitDirNames, signal);
13899
- return services.find((d) => d.serviceName === parsed.serviceName && d.relDirSlash === parsed.relDir && (!kind || d.kind === kind)) || null;
14782
+ return services.find((d) => d.serviceName === parsed.serviceName && d.relDirSlash === parsed.relDir && (!effectiveKind || d.kind === effectiveKind)) || null;
13900
14783
  }
13901
14784
  function isSafeDockerServiceName(value) {
13902
14785
  return /^[A-Za-z0-9_-]+$/.test(value);
@@ -14031,7 +14914,7 @@ async function findSupabaseCliProjectByDbIdAsync(cwd, dbId, omitDirNames, signal
14031
14914
  const projects = await discoverSupabaseCliProjectsAsync(cwd, omitDirNames, signal);
14032
14915
  return projects.find((p) => p.projectId === parsed.projectId && p.relDirSlash === parsed.relDir) || null;
14033
14916
  }
14034
- var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, DOCKER_DISCOVERY_TTL_MS = 5000, SQLITE_DISCOVERY_TTL_MS = 5000, sqliteDiscoveryCache, COMPOSE_FILENAMES, MAX_DOCKER_SERVICES = 30, dockerDiscoveryCache, MAX_SUPABASE_PROJECTS = 30, SUPABASE_DISCOVERY_TTL_MS = 5000, DEFAULT_SUPABASE_DB_PORT = "54322", supabaseDiscoveryCache;
14917
+ var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, DOCKER_DISCOVERY_TTL_MS = 5000, SQLITE_DISCOVERY_TTL_MS = 5000, sqliteDiscoveryCache, COMPOSE_FILENAMES, MAX_DOCKER_SERVICES = 30, dockerDiscoveryCache, DB_KIND_VALUES, MAX_SUPABASE_PROJECTS = 30, SUPABASE_DISCOVERY_TTL_MS = 5000, DEFAULT_SUPABASE_DB_PORT = "54322", supabaseDiscoveryCache;
14035
14918
  var init_discovery = __esm(() => {
14036
14919
  SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
14037
14920
  sqliteDiscoveryCache = new Map;
@@ -14042,6 +14925,15 @@ var init_discovery = __esm(() => {
14042
14925
  "compose.yaml"
14043
14926
  ];
14044
14927
  dockerDiscoveryCache = new Map;
14928
+ DB_KIND_VALUES = new Set([
14929
+ "sqlite",
14930
+ "postgresql",
14931
+ "mysql",
14932
+ "redis",
14933
+ "elasticsearch",
14934
+ "s3",
14935
+ "dynamodb"
14936
+ ]);
14045
14937
  supabaseDiscoveryCache = new Map;
14046
14938
  });
14047
14939
 
@@ -15024,19 +15916,481 @@ async function searchTableAsync(adapter, table, columns, term, maxHits, includeN
15024
15916
  rowPreview: serializeDbRow(row)
15025
15917
  });
15026
15918
  }
15027
- } catch (err) {
15028
- if (isAbortLikeError(err, signal))
15029
- throw err;
15030
- }
15919
+ } catch (err) {
15920
+ if (isAbortLikeError(err, signal))
15921
+ throw err;
15922
+ }
15923
+ }
15924
+ return hits;
15925
+ }
15926
+ function getPrimaryKeyColumnsFromColumns(columns) {
15927
+ return columns.filter((c) => c.primaryKey).map((c) => c.name);
15928
+ }
15929
+ var init_global_search = __esm(() => {
15930
+ init_serialize();
15931
+ init_sql_utils();
15932
+ });
15933
+
15934
+ // web-src/server/database/adapters/dynamodb.ts
15935
+ import { spawnSync as spawnSync5 } from "node:child_process";
15936
+ import { createHash as createHash5, createHmac as createHmac2 } from "node:crypto";
15937
+ function createDynamoDbRequestDeadline() {
15938
+ const timeoutMs = dynamoDbRequestTimeoutMs;
15939
+ return { expiresAt: Date.now() + timeoutMs, timeoutMs };
15940
+ }
15941
+ function createDynamoDbDockerCurlDeadline() {
15942
+ const timeoutMs = dynamoDbDockerCurlTimeoutMs;
15943
+ return { expiresAt: Date.now() + timeoutMs, timeoutMs };
15944
+ }
15945
+ function createDynamoDbTransportDeadline(config) {
15946
+ return config.dockerContainerName ? createDynamoDbDockerCurlDeadline() : createDynamoDbRequestDeadline();
15947
+ }
15948
+ function dynamoDbTimeoutError(deadline) {
15949
+ return new DynamoDbHttpError(503, `DynamoDB request timed out after ${deadline?.timeoutMs ?? dynamoDbRequestTimeoutMs}ms`);
15950
+ }
15951
+ function remainingDynamoDbTimeoutMs(deadline) {
15952
+ if (!deadline)
15953
+ return dynamoDbRequestTimeoutMs;
15954
+ return Math.max(0, deadline.expiresAt - Date.now());
15955
+ }
15956
+ function hmac2(key, value) {
15957
+ return createHmac2("sha256", key).update(value, "utf8").digest();
15958
+ }
15959
+ function sha2562(value) {
15960
+ return createHash5("sha256").update(value, "utf8").digest("hex");
15961
+ }
15962
+ function amzDate2(date = new Date) {
15963
+ const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
15964
+ return { dateStamp: iso.slice(0, 8), amzDate: iso };
15965
+ }
15966
+ function signingKey2(secret, dateStamp, region) {
15967
+ const kDate = hmac2(`AWS4${secret}`, dateStamp);
15968
+ const kRegion = hmac2(kDate, region);
15969
+ const kService = hmac2(kRegion, "dynamodb");
15970
+ return hmac2(kService, "aws4_request");
15971
+ }
15972
+ function compareCodeUnit2(a, b) {
15973
+ return a < b ? -1 : a > b ? 1 : 0;
15974
+ }
15975
+ function signedHeadersString2(headers) {
15976
+ return Object.keys(headers).map((key) => key.toLowerCase()).sort(compareCodeUnit2).join(";");
15977
+ }
15978
+ function canonicalHeaders2(headers) {
15979
+ return Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value.trim().replace(/\s+/g, " ")]).sort(([a], [b]) => compareCodeUnit2(a, b)).map(([key, value]) => `${key}:${value}
15980
+ `).join("");
15981
+ }
15982
+ function splitCurlHeadersAndBody2(bytes) {
15983
+ const buffer = Buffer.from(bytes);
15984
+ let idx = buffer.indexOf(`\r
15985
+ \r
15986
+ `);
15987
+ let sepLen = 4;
15988
+ if (idx < 0) {
15989
+ idx = buffer.indexOf(`
15990
+
15991
+ `);
15992
+ sepLen = 2;
15993
+ }
15994
+ if (idx < 0)
15995
+ return { headerText: "", body: bytes };
15996
+ return {
15997
+ headerText: buffer.subarray(0, idx).toString("utf8"),
15998
+ body: new Uint8Array(buffer.subarray(idx + sepLen))
15999
+ };
16000
+ }
16001
+ function responseFromCurlOutput2(bytes) {
16002
+ const { headerText, body } = splitCurlHeadersAndBody2(bytes);
16003
+ const headerBlocks = headerText.split(/\r?\n\r?\n/).map((block2) => block2.trim()).filter(Boolean);
16004
+ const block = headerBlocks[headerBlocks.length - 1] || "";
16005
+ const lines = block.split(/\r?\n/).filter(Boolean);
16006
+ const statusMatch = lines[0]?.match(/^HTTP\/\S+\s+(\d+)/i);
16007
+ const status = statusMatch ? Number(statusMatch[1]) : 502;
16008
+ const headers = new Headers;
16009
+ for (const line of lines.slice(1)) {
16010
+ const idx = line.indexOf(":");
16011
+ if (idx <= 0)
16012
+ continue;
16013
+ headers.append(line.slice(0, idx).trim(), line.slice(idx + 1).trim());
16014
+ }
16015
+ const arrayBuffer = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
16016
+ return new Response(arrayBuffer, { status, headers });
16017
+ }
16018
+ function curlConfigQuote2(value) {
16019
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ")}"`;
16020
+ }
16021
+ function curlHeaderConfig2(headers) {
16022
+ const lines = [];
16023
+ headers.forEach((value, key) => {
16024
+ lines.push(`header = ${curlConfigQuote2(`${key}: ${value}`)}`);
16025
+ });
16026
+ return `${lines.join(`
16027
+ `)}
16028
+ `;
16029
+ }
16030
+ function dockerCurlCommand2(opts) {
16031
+ const shellScript = [
16032
+ "set -eu",
16033
+ 'headers_file="$(mktemp)"',
16034
+ 'body_file="$(mktemp)"',
16035
+ `trap 'rm -f "$headers_file" "$body_file"' EXIT`,
16036
+ `while IFS= read -r line; do [ "$line" = "${DOCKER_CURL_BODY_MARKER}" ] && break; printf '%s\\n' "$line" >> "$headers_file"; done`,
16037
+ 'cat > "$body_file"',
16038
+ `curl -sS -X POST -D - -o - -K "$headers_file" --data-binary "@$body_file" ${curlConfigQuote2(opts.url)}`
16039
+ ].join(`
16040
+ `);
16041
+ return {
16042
+ args: ["exec", "-i", opts.containerName, "sh", "-c", shellScript],
16043
+ input: Buffer.from(`${curlHeaderConfig2(opts.headers)}${DOCKER_CURL_BODY_MARKER}
16044
+ ${opts.body}`, "utf8")
16045
+ };
16046
+ }
16047
+ function guardedDynamoDbTransport(signal, operation, deadline) {
16048
+ if (signal?.aborted) {
16049
+ return Promise.reject(new DynamoDbHttpError(503, "DynamoDB HTTP transport aborted"));
16050
+ }
16051
+ const timeoutMs = remainingDynamoDbTimeoutMs(deadline);
16052
+ if (timeoutMs <= 0)
16053
+ return Promise.reject(dynamoDbTimeoutError(deadline));
16054
+ const controller = new AbortController;
16055
+ let timedOut = false;
16056
+ let settled = false;
16057
+ let timer;
16058
+ let cleanupParent = () => {
16059
+ return;
16060
+ };
16061
+ const abort = (err, reject) => {
16062
+ if (settled)
16063
+ return;
16064
+ settled = true;
16065
+ controller.abort(err);
16066
+ reject(err);
16067
+ };
16068
+ const guarded = new Promise((resolve2, reject) => {
16069
+ const onParentAbort = () => abort(new DynamoDbHttpError(503, "DynamoDB HTTP transport aborted"), reject);
16070
+ if (signal) {
16071
+ signal.addEventListener("abort", onParentAbort, { once: true });
16072
+ cleanupParent = () => signal.removeEventListener("abort", onParentAbort);
16073
+ }
16074
+ timer = setTimeout(() => {
16075
+ timedOut = true;
16076
+ abort(dynamoDbTimeoutError(deadline), reject);
16077
+ }, timeoutMs);
16078
+ operation(controller.signal).then((value) => {
16079
+ if (settled)
16080
+ return;
16081
+ settled = true;
16082
+ resolve2(value);
16083
+ }, (err) => {
16084
+ if (settled)
16085
+ return;
16086
+ settled = true;
16087
+ if (timedOut) {
16088
+ reject(dynamoDbTimeoutError(deadline));
16089
+ } else if (signal?.aborted) {
16090
+ reject(new DynamoDbHttpError(503, "DynamoDB HTTP transport aborted"));
16091
+ } else {
16092
+ reject(err);
16093
+ }
16094
+ });
16095
+ });
16096
+ return guarded.finally(() => {
16097
+ if (timer)
16098
+ clearTimeout(timer);
16099
+ cleanupParent();
16100
+ });
16101
+ }
16102
+ async function readStreamChunkWithTimeout2(reader, signal, deadline) {
16103
+ return guardedDynamoDbTransport(signal, (transportSignal) => {
16104
+ const cancelRead = () => {
16105
+ reader.cancel(transportSignal.reason).catch(() => {
16106
+ return;
16107
+ });
16108
+ };
16109
+ if (transportSignal.aborted) {
16110
+ cancelRead();
16111
+ } else {
16112
+ transportSignal.addEventListener("abort", cancelRead, { once: true });
16113
+ }
16114
+ return reader.read().finally(() => {
16115
+ transportSignal.removeEventListener("abort", cancelRead);
16116
+ });
16117
+ }, deadline);
16118
+ }
16119
+ async function readResponseBytesWithTimeout2(res, signal, deadline) {
16120
+ if (!res.body)
16121
+ return new Uint8Array;
16122
+ const reader = res.body.getReader();
16123
+ const chunks = [];
16124
+ let total = 0;
16125
+ try {
16126
+ for (;; ) {
16127
+ const { done, value } = await readStreamChunkWithTimeout2(reader, signal, deadline);
16128
+ if (done)
16129
+ break;
16130
+ if (!value?.byteLength)
16131
+ continue;
16132
+ chunks.push(value);
16133
+ total += value.byteLength;
16134
+ }
16135
+ } finally {
16136
+ try {
16137
+ reader.releaseLock();
16138
+ } catch {}
16139
+ }
16140
+ if (chunks.length === 1)
16141
+ return chunks[0];
16142
+ const bytes = new Uint8Array(total);
16143
+ let offset = 0;
16144
+ for (const chunk of chunks) {
16145
+ bytes.set(chunk, offset);
16146
+ offset += chunk.byteLength;
16147
+ }
16148
+ return bytes;
16149
+ }
16150
+ async function readResponseTextWithTimeout2(res, signal, deadline) {
16151
+ return new TextDecoder("utf-8", { fatal: false }).decode(await readResponseBytesWithTimeout2(res, signal, deadline));
16152
+ }
16153
+ async function dockerCurlFetch2(opts) {
16154
+ const { args, input } = dockerCurlCommand2(opts);
16155
+ if (spawnSyncImplIsTestOverride2) {
16156
+ const proc2 = spawnSyncImpl4(dockerCommand(), args, {
16157
+ encoding: "buffer",
16158
+ input,
16159
+ timeout: dynamoDbDockerCurlTimeoutMs,
16160
+ stdio: ["pipe", "pipe", "pipe"]
16161
+ });
16162
+ if ((proc2.status ?? 1) !== 0) {
16163
+ const stderr = new TextDecoder().decode(proc2.stderr || new Uint8Array).concat(proc2.error ? `
16164
+ ${proc2.error.message}` : "").replace(/\s+/g, " ").trim();
16165
+ throwIfDockerCommandUnavailableResult({
16166
+ code: proc2.status ?? 1,
16167
+ stderr
16168
+ });
16169
+ throw new DynamoDbHttpError(503, `DynamoDB HTTP transport failed via docker exec${stderr ? `: ${stderr.slice(0, 240)}` : ""}`);
16170
+ }
16171
+ return responseFromCurlOutput2(new Uint8Array(proc2.stdout || new Uint8Array));
16172
+ }
16173
+ if (opts.signal?.aborted) {
16174
+ throw new DynamoDbHttpError(503, "DynamoDB HTTP transport aborted");
16175
+ }
16176
+ const proc = await spawnCollectAsync({
16177
+ command: dockerCommand(),
16178
+ args,
16179
+ input,
16180
+ timeoutMs: dynamoDbDockerCurlTimeoutMs,
16181
+ signal: opts.signal,
16182
+ abortMessage: "DynamoDB HTTP transport aborted",
16183
+ timeoutMessage: `docker exec curl timed out after ${dynamoDbDockerCurlTimeoutMs}ms`,
16184
+ rejectOnError: false
16185
+ });
16186
+ if (proc.code !== 0) {
16187
+ const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
16188
+ throwIfDockerCommandUnavailableResult({ code: proc.code, stderr });
16189
+ throw new DynamoDbHttpError(503, `DynamoDB HTTP transport failed via docker exec${stderr ? `: ${stderr.slice(0, 240)}` : ""}`);
16190
+ }
16191
+ return responseFromCurlOutput2(new Uint8Array(proc.stdout));
16192
+ }
16193
+ function sanitizeDynamoDbError(status, text) {
16194
+ let message = "";
16195
+ try {
16196
+ const parsed = JSON.parse(text);
16197
+ const type = typeof parsed.__type === "string" ? parsed.__type.split("#").pop() || parsed.__type : "";
16198
+ const bodyMessage = typeof parsed.message === "string" ? parsed.message : typeof parsed.Message === "string" ? parsed.Message : "";
16199
+ message = [type, bodyMessage].filter(Boolean).join(": ");
16200
+ } catch {
16201
+ message = text.replace(/\s+/g, " ").trim().slice(0, 240);
16202
+ }
16203
+ return new DynamoDbHttpError(status, `DynamoDB HTTP ${status}: ${message || "request failed"}`);
16204
+ }
16205
+ function assertTableName(tableName) {
16206
+ if (!/^[A-Za-z0-9_.-]{3,255}$/.test(tableName)) {
16207
+ throw new Error("invalid DynamoDB table name");
16208
+ }
16209
+ }
16210
+ function asObject(value) {
16211
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
16212
+ }
16213
+ function asStringArray(value) {
16214
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
16215
+ }
16216
+ function asNumber(value) {
16217
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
16218
+ }
16219
+ function createDynamoDbAdapter(config) {
16220
+ async function signedJsonRequest(action, body, signal, deadline = createDynamoDbTransportDeadline(config)) {
16221
+ const requestBody = JSON.stringify(body);
16222
+ const res = await guardedDynamoDbTransport(signal, (transportSignal) => {
16223
+ const endpoint = new URL(config.endpoint);
16224
+ const { dateStamp, amzDate: requestDate } = amzDate2();
16225
+ const payloadHash = requestBody ? sha2562(requestBody) : EMPTY_SHA2562;
16226
+ const headers = {
16227
+ "content-type": DYNAMODB_JSON_CONTENT_TYPE,
16228
+ host: endpoint.host,
16229
+ "x-amz-content-sha256": payloadHash,
16230
+ "x-amz-date": requestDate,
16231
+ "x-amz-target": `DynamoDB_20120810.${action}`,
16232
+ ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {}
16233
+ };
16234
+ const signedNames = signedHeadersString2(headers);
16235
+ const canonicalRequest = [
16236
+ "POST",
16237
+ "/",
16238
+ "",
16239
+ canonicalHeaders2(headers),
16240
+ signedNames,
16241
+ payloadHash
16242
+ ].join(`
16243
+ `);
16244
+ const scope = `${dateStamp}/${config.region}/dynamodb/aws4_request`;
16245
+ const stringToSign = [
16246
+ "AWS4-HMAC-SHA256",
16247
+ requestDate,
16248
+ scope,
16249
+ sha2562(canonicalRequest)
16250
+ ].join(`
16251
+ `);
16252
+ const signature = createHmac2("sha256", signingKey2(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
16253
+ const requestHeaders = new Headers;
16254
+ for (const [key, value] of Object.entries(headers)) {
16255
+ if (key !== "host")
16256
+ requestHeaders.set(key, value);
16257
+ }
16258
+ requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
16259
+ const url = `${config.endpoint.replace(/\/$/, "")}/`;
16260
+ if (config.dockerContainerName) {
16261
+ return dockerCurlFetch2({
16262
+ containerName: config.dockerContainerName,
16263
+ url,
16264
+ headers: requestHeaders,
16265
+ body: requestBody,
16266
+ signal: transportSignal
16267
+ });
16268
+ }
16269
+ return fetch(url, {
16270
+ method: "POST",
16271
+ headers: requestHeaders,
16272
+ body: requestBody,
16273
+ signal: transportSignal
16274
+ });
16275
+ }, deadline);
16276
+ const text = await readResponseTextWithTimeout2(res, signal, deadline);
16277
+ if (!res.ok)
16278
+ throw sanitizeDynamoDbError(res.status, text);
16279
+ return text ? JSON.parse(text) : {};
15031
16280
  }
15032
- return hits;
16281
+ async function listTablesAsync(opts) {
16282
+ const body = {
16283
+ ...opts?.limit ? { Limit: Math.min(100, Math.max(1, opts.limit)) } : {},
16284
+ ...opts?.exclusiveStartTableName ? { ExclusiveStartTableName: opts.exclusiveStartTableName } : {}
16285
+ };
16286
+ const raw = await signedJsonRequest("ListTables", body, opts?.signal);
16287
+ return {
16288
+ tableNames: asStringArray(raw.TableNames),
16289
+ ...typeof raw.LastEvaluatedTableName === "string" ? { lastEvaluatedTableName: raw.LastEvaluatedTableName } : {}
16290
+ };
16291
+ }
16292
+ async function describeTableAsync(tableName, signal) {
16293
+ assertTableName(tableName);
16294
+ const raw = await signedJsonRequest("DescribeTable", { TableName: tableName }, signal);
16295
+ return asObject(raw.Table);
16296
+ }
16297
+ async function scanAsync(opts) {
16298
+ assertTableName(opts.tableName);
16299
+ const raw = await signedJsonRequest("Scan", {
16300
+ TableName: opts.tableName,
16301
+ ...opts.limit ? { Limit: Math.min(1000, Math.max(1, opts.limit)) } : {},
16302
+ ...opts.exclusiveStartKey ? { ExclusiveStartKey: opts.exclusiveStartKey } : {},
16303
+ ...opts.indexName ? { IndexName: opts.indexName } : {},
16304
+ ...opts.projectionExpression ? { ProjectionExpression: opts.projectionExpression } : {},
16305
+ ...opts.filterExpression ? { FilterExpression: opts.filterExpression } : {},
16306
+ ...opts.expressionAttributeNames ? { ExpressionAttributeNames: opts.expressionAttributeNames } : {},
16307
+ ...opts.expressionAttributeValues ? { ExpressionAttributeValues: opts.expressionAttributeValues } : {}
16308
+ }, opts.signal);
16309
+ return {
16310
+ items: Array.isArray(raw.Items) ? raw.Items : [],
16311
+ count: asNumber(raw.Count),
16312
+ scannedCount: asNumber(raw.ScannedCount),
16313
+ ...raw.LastEvaluatedKey ? { lastEvaluatedKey: asObject(raw.LastEvaluatedKey) } : {}
16314
+ };
16315
+ }
16316
+ async function queryAsync(opts) {
16317
+ assertTableName(opts.tableName);
16318
+ const raw = await signedJsonRequest("Query", {
16319
+ TableName: opts.tableName,
16320
+ KeyConditionExpression: opts.keyConditionExpression,
16321
+ ...opts.limit ? { Limit: Math.min(1000, Math.max(1, opts.limit)) } : {},
16322
+ ...opts.exclusiveStartKey ? { ExclusiveStartKey: opts.exclusiveStartKey } : {},
16323
+ ...opts.indexName ? { IndexName: opts.indexName } : {},
16324
+ ...opts.projectionExpression ? { ProjectionExpression: opts.projectionExpression } : {},
16325
+ ...opts.filterExpression ? { FilterExpression: opts.filterExpression } : {},
16326
+ ...opts.expressionAttributeNames ? { ExpressionAttributeNames: opts.expressionAttributeNames } : {},
16327
+ ...opts.expressionAttributeValues ? { ExpressionAttributeValues: opts.expressionAttributeValues } : {},
16328
+ ...opts.scanIndexForward !== undefined ? { ScanIndexForward: opts.scanIndexForward } : {}
16329
+ }, opts.signal);
16330
+ return {
16331
+ items: Array.isArray(raw.Items) ? raw.Items : [],
16332
+ count: asNumber(raw.Count),
16333
+ scannedCount: asNumber(raw.ScannedCount),
16334
+ ...raw.LastEvaluatedKey ? { lastEvaluatedKey: asObject(raw.LastEvaluatedKey) } : {}
16335
+ };
16336
+ }
16337
+ async function getItemAsync(opts) {
16338
+ assertTableName(opts.tableName);
16339
+ const raw = await signedJsonRequest("GetItem", {
16340
+ TableName: opts.tableName,
16341
+ Key: opts.key,
16342
+ ...opts.projectionExpression ? { ProjectionExpression: opts.projectionExpression } : {},
16343
+ ...opts.expressionAttributeNames ? { ExpressionAttributeNames: opts.expressionAttributeNames } : {},
16344
+ ...opts.consistentRead !== undefined ? { ConsistentRead: opts.consistentRead } : {}
16345
+ }, opts.signal);
16346
+ return {
16347
+ ...raw.Item ? { item: asObject(raw.Item) } : {},
16348
+ ...raw.ConsumedCapacity ? { consumedCapacity: raw.ConsumedCapacity } : {}
16349
+ };
16350
+ }
16351
+ return {
16352
+ kind: "dynamodb",
16353
+ model: "document",
16354
+ close() {},
16355
+ listTablesAsync,
16356
+ describeTableAsync,
16357
+ scanAsync,
16358
+ queryAsync,
16359
+ getItemAsync
16360
+ };
15033
16361
  }
15034
- function getPrimaryKeyColumnsFromColumns(columns) {
15035
- return columns.filter((c) => c.primaryKey).map((c) => c.name);
16362
+ async function dynamoDbConfigFromDockerInfoAsync(info, signal) {
16363
+ const env = info.env;
16364
+ const dockerContainerName = info.hostPort ? undefined : await resolveRunningComposeContainerNameOrThrowAsync(info.serviceName, info.composeDir, signal);
16365
+ return {
16366
+ endpoint: info.hostPort ? `http://localhost:${info.hostPort}` : `http://127.0.0.1:${info.containerPort}`,
16367
+ ...dockerContainerName ? { dockerContainerName } : {},
16368
+ region: env.AWS_REGION || env.AWS_DEFAULT_REGION || "us-east-1",
16369
+ accessKeyId: env.AWS_ACCESS_KEY_ID || "test",
16370
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY || "test",
16371
+ ...env.AWS_SESSION_TOKEN ? { sessionToken: env.AWS_SESSION_TOKEN } : {}
16372
+ };
15036
16373
  }
15037
- var init_global_search = __esm(() => {
15038
- init_serialize();
15039
- init_sql_utils();
16374
+ async function openDynamoDbExplorerAsync(info, signal) {
16375
+ return createDynamoDbAdapter(await dynamoDbConfigFromDockerInfoAsync(info, signal));
16376
+ }
16377
+ function isDynamoDbHttpError(err) {
16378
+ return err instanceof DynamoDbHttpError;
16379
+ }
16380
+ var spawnSyncImpl4, spawnSyncImplIsTestOverride2 = false, DynamoDbHttpError, EMPTY_SHA2562 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", DEFAULT_DYNAMODB_REQUEST_TIMEOUT_MS = 5000, DEFAULT_DYNAMODB_DOCKER_CURL_TIMEOUT_MS = 30000, DYNAMODB_JSON_CONTENT_TYPE = "application/x-amz-json-1.0", DOCKER_CURL_BODY_MARKER = "__CODE_VIEWER_DYNAMODB_BODY__", dynamoDbRequestTimeoutMs, dynamoDbDockerCurlTimeoutMs;
16381
+ var init_dynamodb = __esm(() => {
16382
+ init_docker_utils();
16383
+ init_spawn_runner();
16384
+ spawnSyncImpl4 = spawnSync5;
16385
+ DynamoDbHttpError = class DynamoDbHttpError extends Error {
16386
+ status;
16387
+ constructor(status, message) {
16388
+ super(message);
16389
+ this.status = status;
16390
+ }
16391
+ };
16392
+ dynamoDbRequestTimeoutMs = DEFAULT_DYNAMODB_REQUEST_TIMEOUT_MS;
16393
+ dynamoDbDockerCurlTimeoutMs = DEFAULT_DYNAMODB_DOCKER_CURL_TIMEOUT_MS;
15040
16394
  });
15041
16395
 
15042
16396
  // web-src/server/database/handle-shared.ts
@@ -15339,6 +16693,247 @@ var init_handle_shared = __esm(() => {
15339
16693
  logQueue = Promise.resolve();
15340
16694
  });
15341
16695
 
16696
+ // web-src/server/database/handle-dynamodb.ts
16697
+ var exports_handle_dynamodb = {};
16698
+ __export(exports_handle_dynamodb, {
16699
+ handleDynamoDbRoute: () => handleDynamoDbRoute,
16700
+ closeDynamoDbAdapter: () => closeDynamoDbAdapter
16701
+ });
16702
+ function closeDynamoDbAdapter(dbId) {
16703
+ dynamoDbAdapterCache.close(dbId);
16704
+ }
16705
+ function resolveDynamoDb(cwd, dbParam, signal, omitDirNames) {
16706
+ return resolveDockerExplorerAsync(cwd, dbParam, "dynamodb", dynamoDbAdapterCache, (info) => openDynamoDbExplorerAsync(info), omitDirNames, signal);
16707
+ }
16708
+ function dynamoDbErrorResponse(err, action, signal) {
16709
+ if (isAbortLikeError(err, signal)) {
16710
+ return handleError("dynamodb", action, err, signal);
16711
+ }
16712
+ if (isDynamoDbHttpError(err))
16713
+ return textError(err.message, err.status);
16714
+ return handleError("dynamodb", action, err, signal);
16715
+ }
16716
+ function validateTableName(value) {
16717
+ if (!value)
16718
+ return textError("missing table parameter", 400);
16719
+ if (value.length < 3 || value.length > MAX_TABLE_NAME_LEN || hasControlCharacter(value) || !/^[A-Za-z0-9_.-]+$/.test(value)) {
16720
+ return textError("invalid table parameter", 400);
16721
+ }
16722
+ return value;
16723
+ }
16724
+ function validateOptionalText(value, name, maxLen = MAX_EXPRESSION_LEN) {
16725
+ if (value === null || value === "")
16726
+ return;
16727
+ if (value.length > maxLen || hasControlCharacter(value)) {
16728
+ return textError(`invalid ${name} parameter`, 400);
16729
+ }
16730
+ return value;
16731
+ }
16732
+ function parseLimit2(url, param, defaultValue) {
16733
+ const raw = Number(url.searchParams.get(param) || defaultValue);
16734
+ return Math.min(param === "limit" ? MAX_ITEMS_LIMIT : MAX_TABLES_LIMIT, Math.max(1, Number.isFinite(raw) ? raw : defaultValue));
16735
+ }
16736
+ function parseJsonParam(value, name) {
16737
+ if (value === null || value === "")
16738
+ return;
16739
+ if (value.length > MAX_JSON_PARAM_LEN || hasControlCharacter(value)) {
16740
+ return textError(`invalid ${name} parameter`, 400);
16741
+ }
16742
+ try {
16743
+ return JSON.parse(value);
16744
+ } catch {
16745
+ return textError(`invalid ${name} parameter`, 400);
16746
+ }
16747
+ }
16748
+ function parseKey(value, name) {
16749
+ const parsed = parseJsonParam(value, name);
16750
+ if (parsed instanceof Response)
16751
+ return parsed;
16752
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16753
+ return textError(`missing ${name} parameter`, 400);
16754
+ }
16755
+ return parsed;
16756
+ }
16757
+ function parseExpressionAttributeNames(url) {
16758
+ const parsed = parseJsonParam(url.searchParams.get("expressionAttributeNames"), "expressionAttributeNames");
16759
+ if (parsed instanceof Response || parsed === undefined)
16760
+ return parsed;
16761
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.values(parsed).some((value) => typeof value !== "string")) {
16762
+ return textError("invalid expressionAttributeNames parameter", 400);
16763
+ }
16764
+ return parsed;
16765
+ }
16766
+ function parseExpressionAttributeValues(url) {
16767
+ const parsed = parseJsonParam(url.searchParams.get("expressionAttributeValues"), "expressionAttributeValues");
16768
+ if (parsed instanceof Response || parsed === undefined)
16769
+ return parsed;
16770
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16771
+ return textError("invalid expressionAttributeValues parameter", 400);
16772
+ }
16773
+ return parsed;
16774
+ }
16775
+ async function handleTables(req, cwd, url, omitDirNames) {
16776
+ const r = await resolveDynamoDb(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
16777
+ if (r instanceof Response)
16778
+ return r;
16779
+ const exclusiveStartTableName = validateOptionalText(url.searchParams.get("exclusiveStartTableName"), "exclusiveStartTableName", MAX_TABLE_NAME_LEN);
16780
+ if (exclusiveStartTableName instanceof Response)
16781
+ return exclusiveStartTableName;
16782
+ try {
16783
+ const result = await r.explorer.listTablesAsync({
16784
+ limit: parseLimit2(url, "tablesLimit", MAX_TABLES_LIMIT),
16785
+ ...exclusiveStartTableName ? { exclusiveStartTableName } : {},
16786
+ signal: req.signal
16787
+ });
16788
+ const body = { dbId: r.dbId, ...result };
16789
+ return json(body);
16790
+ } catch (err) {
16791
+ return dynamoDbErrorResponse(err, "list dynamodb tables", req.signal);
16792
+ }
16793
+ }
16794
+ async function handleTable(req, cwd, url, omitDirNames) {
16795
+ const r = await resolveDynamoDb(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
16796
+ if (r instanceof Response)
16797
+ return r;
16798
+ const tableName = validateTableName(url.searchParams.get("table"));
16799
+ if (tableName instanceof Response)
16800
+ return tableName;
16801
+ try {
16802
+ const table = await r.explorer.describeTableAsync(tableName, req.signal);
16803
+ const body = { dbId: r.dbId, table };
16804
+ return json(body);
16805
+ } catch (err) {
16806
+ return dynamoDbErrorResponse(err, "describe dynamodb table", req.signal);
16807
+ }
16808
+ }
16809
+ async function handleItems(req, cwd, url, omitDirNames) {
16810
+ const r = await resolveDynamoDb(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
16811
+ if (r instanceof Response)
16812
+ return r;
16813
+ const tableName = validateTableName(url.searchParams.get("table"));
16814
+ if (tableName instanceof Response)
16815
+ return tableName;
16816
+ const mode = url.searchParams.get("mode") || "scan";
16817
+ if (mode !== "scan" && mode !== "query") {
16818
+ return textError("invalid mode parameter", 400);
16819
+ }
16820
+ const exclusiveStartKey = parseJsonParam(url.searchParams.get("exclusiveStartKey"), "exclusiveStartKey");
16821
+ if (exclusiveStartKey instanceof Response)
16822
+ return exclusiveStartKey;
16823
+ const expressionAttributeNames = parseExpressionAttributeNames(url);
16824
+ if (expressionAttributeNames instanceof Response)
16825
+ return expressionAttributeNames;
16826
+ const expressionAttributeValues = parseExpressionAttributeValues(url);
16827
+ if (expressionAttributeValues instanceof Response)
16828
+ return expressionAttributeValues;
16829
+ const indexName = validateOptionalText(url.searchParams.get("index"), "index");
16830
+ if (indexName instanceof Response)
16831
+ return indexName;
16832
+ const projectionExpression = validateOptionalText(url.searchParams.get("projectionExpression"), "projectionExpression");
16833
+ if (projectionExpression instanceof Response)
16834
+ return projectionExpression;
16835
+ const filterExpression = validateOptionalText(url.searchParams.get("filterExpression"), "filterExpression");
16836
+ if (filterExpression instanceof Response)
16837
+ return filterExpression;
16838
+ const keyConditionExpression = validateOptionalText(url.searchParams.get("keyConditionExpression"), "keyConditionExpression");
16839
+ if (keyConditionExpression instanceof Response)
16840
+ return keyConditionExpression;
16841
+ if (mode === "query" && !keyConditionExpression) {
16842
+ return textError("missing keyConditionExpression parameter", 400);
16843
+ }
16844
+ try {
16845
+ const common = {
16846
+ tableName,
16847
+ limit: parseLimit2(url, "limit", DEFAULT_ITEMS_LIMIT),
16848
+ ...exclusiveStartKey ? { exclusiveStartKey } : {},
16849
+ ...indexName ? { indexName } : {},
16850
+ ...projectionExpression ? { projectionExpression } : {},
16851
+ ...filterExpression ? { filterExpression } : {},
16852
+ ...expressionAttributeNames ? { expressionAttributeNames } : {},
16853
+ ...expressionAttributeValues ? { expressionAttributeValues } : {},
16854
+ signal: req.signal
16855
+ };
16856
+ const result = mode === "query" ? await r.explorer.queryAsync({
16857
+ ...common,
16858
+ keyConditionExpression,
16859
+ scanIndexForward: url.searchParams.get("scanIndexForward") !== "false"
16860
+ }) : await r.explorer.scanAsync(common);
16861
+ const body = {
16862
+ dbId: r.dbId,
16863
+ tableName,
16864
+ mode,
16865
+ ...result
16866
+ };
16867
+ return json(body);
16868
+ } catch (err) {
16869
+ return dynamoDbErrorResponse(err, "read dynamodb items", req.signal);
16870
+ }
16871
+ }
16872
+ async function handleItem(req, cwd, url, omitDirNames) {
16873
+ const r = await resolveDynamoDb(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
16874
+ if (r instanceof Response)
16875
+ return r;
16876
+ const tableName = validateTableName(url.searchParams.get("table"));
16877
+ if (tableName instanceof Response)
16878
+ return tableName;
16879
+ const key = parseKey(url.searchParams.get("key"), "key");
16880
+ if (key instanceof Response)
16881
+ return key;
16882
+ const expressionAttributeNames = parseExpressionAttributeNames(url);
16883
+ if (expressionAttributeNames instanceof Response)
16884
+ return expressionAttributeNames;
16885
+ const projectionExpression = validateOptionalText(url.searchParams.get("projectionExpression"), "projectionExpression");
16886
+ if (projectionExpression instanceof Response)
16887
+ return projectionExpression;
16888
+ try {
16889
+ const result = await r.explorer.getItemAsync({
16890
+ tableName,
16891
+ key,
16892
+ ...projectionExpression ? { projectionExpression } : {},
16893
+ ...expressionAttributeNames ? { expressionAttributeNames } : {},
16894
+ consistentRead: url.searchParams.get("consistentRead") === "true",
16895
+ signal: req.signal
16896
+ });
16897
+ const body = {
16898
+ dbId: r.dbId,
16899
+ tableName,
16900
+ key,
16901
+ ...result
16902
+ };
16903
+ return json(body);
16904
+ } catch (err) {
16905
+ return dynamoDbErrorResponse(err, "read dynamodb item", req.signal);
16906
+ }
16907
+ }
16908
+ async function handleDynamoDbRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
16909
+ const wrap = createQueryStrippedLogger("dynamodb", req, url);
16910
+ return dispatchRoutes(req, url, {
16911
+ "/_db/dynamodb/tables": {
16912
+ methods: ["GET"],
16913
+ handler: () => handleTables(req, cwd, url, omitDirNames)
16914
+ },
16915
+ "/_db/dynamodb/table": {
16916
+ methods: ["GET"],
16917
+ handler: () => handleTable(req, cwd, url, omitDirNames)
16918
+ },
16919
+ "/_db/dynamodb/items": {
16920
+ methods: ["GET"],
16921
+ handler: () => handleItems(req, cwd, url, omitDirNames)
16922
+ },
16923
+ "/_db/dynamodb/item": {
16924
+ methods: ["GET"],
16925
+ handler: () => handleItem(req, cwd, url, omitDirNames)
16926
+ }
16927
+ }, sideEffectAllowed, wrap, (err) => handleError("dynamodb", "handle dynamodb request", err, req.signal));
16928
+ }
16929
+ var dynamoDbAdapterCache, DEFAULT_ITEMS_LIMIT = 100, MAX_ITEMS_LIMIT = 1000, MAX_TABLES_LIMIT = 100, MAX_TABLE_NAME_LEN = 255, MAX_EXPRESSION_LEN = 4096, MAX_JSON_PARAM_LEN;
16930
+ var init_handle_dynamodb = __esm(() => {
16931
+ init_dynamodb();
16932
+ init_handle_shared();
16933
+ dynamoDbAdapterCache = createDockerAdapterCache();
16934
+ MAX_JSON_PARAM_LEN = 64 * 1024;
16935
+ });
16936
+
15342
16937
  // web-src/server/database/handle-elasticsearch.ts
15343
16938
  var exports_handle_elasticsearch = {};
15344
16939
  __export(exports_handle_elasticsearch, {
@@ -15804,7 +17399,7 @@ function validateKey(value) {
15804
17399
  }
15805
17400
  return value;
15806
17401
  }
15807
- function validateOptionalText(value, name, maxLen) {
17402
+ function validateOptionalText2(value, name, maxLen) {
15808
17403
  if (!value)
15809
17404
  return "";
15810
17405
  if (value.length > maxLen || hasControlCharacter(value)) {
@@ -15812,7 +17407,7 @@ function validateOptionalText(value, name, maxLen) {
15812
17407
  }
15813
17408
  return value;
15814
17409
  }
15815
- function parseLimit2(url) {
17410
+ function parseLimit3(url) {
15816
17411
  const raw = Number(url.searchParams.get("limit") || DEFAULT_OBJECT_LIMIT);
15817
17412
  return Math.min(MAX_OBJECT_LIMIT, Math.max(1, Number.isFinite(raw) ? raw : DEFAULT_OBJECT_LIMIT));
15818
17413
  }
@@ -15897,13 +17492,13 @@ async function handleObjects(cwd, req, url, omitDirNames) {
15897
17492
  const bucket = validateBucket(url.searchParams.get("bucket"));
15898
17493
  if (bucket instanceof Response)
15899
17494
  return bucket;
15900
- const prefix = validateOptionalText(url.searchParams.get("prefix"), "prefix", 2048);
17495
+ const prefix = validateOptionalText2(url.searchParams.get("prefix"), "prefix", 2048);
15901
17496
  if (prefix instanceof Response)
15902
17497
  return prefix;
15903
- const search = validateOptionalText(url.searchParams.get("q"), "q", 512);
17498
+ const search = validateOptionalText2(url.searchParams.get("q"), "q", 512);
15904
17499
  if (search instanceof Response)
15905
17500
  return search;
15906
- const token = validateOptionalText(url.searchParams.get("token"), "token", 4096);
17501
+ const token = validateOptionalText2(url.searchParams.get("token"), "token", 4096);
15907
17502
  if (token instanceof Response)
15908
17503
  return token;
15909
17504
  const mode = parseMode(url);
@@ -15912,7 +17507,7 @@ async function handleObjects(cwd, req, url, omitDirNames) {
15912
17507
  const sort = parseSort2(url);
15913
17508
  if (sort instanceof Response)
15914
17509
  return sort;
15915
- const limit = parseLimit2(url);
17510
+ const limit = parseLimit3(url);
15916
17511
  try {
15917
17512
  if (mode === "prefix") {
15918
17513
  const effectivePrefix = search || prefix;
@@ -16035,10 +17630,10 @@ async function handleFolder(cwd, req, url, omitDirNames) {
16035
17630
  const bucket = validateBucket(url.searchParams.get("bucket"));
16036
17631
  if (bucket instanceof Response)
16037
17632
  return bucket;
16038
- const prefix = validateOptionalText(url.searchParams.get("prefix"), "prefix", 2048);
17633
+ const prefix = validateOptionalText2(url.searchParams.get("prefix"), "prefix", 2048);
16039
17634
  if (prefix instanceof Response)
16040
17635
  return prefix;
16041
- const token = validateOptionalText(url.searchParams.get("token"), "token", 4096);
17636
+ const token = validateOptionalText2(url.searchParams.get("token"), "token", 4096);
16042
17637
  if (token instanceof Response)
16043
17638
  return token;
16044
17639
  try {
@@ -16386,7 +17981,7 @@ var init_query_history = __esm(() => {
16386
17981
  });
16387
17982
 
16388
17983
  // web-src/server/database/snapshot-store.ts
16389
- import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
17984
+ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
16390
17985
  import { mkdirSync as mkdirSync3 } from "node:fs";
16391
17986
  import { join as join12 } from "node:path";
16392
17987
  async function getStoreDb(cwd) {
@@ -16417,7 +18012,7 @@ function makeId2(prefix) {
16417
18012
  return `${prefix}-${randomBytes2(8).toString("hex")}`;
16418
18013
  }
16419
18014
  function hashPayload(payloadJson) {
16420
- return createHash5("sha256").update(payloadJson).digest("hex");
18015
+ return createHash6("sha256").update(payloadJson).digest("hex");
16421
18016
  }
16422
18017
  function hashLengthPrefixed(hasher, value) {
16423
18018
  hasher.update(`${Buffer.byteLength(value, "utf8")}:`);
@@ -16492,7 +18087,7 @@ async function addSnapshotTableRows(cwd, revisionId, rows) {
16492
18087
  db.exec("BEGIN");
16493
18088
  try {
16494
18089
  for (const row of rows) {
16495
- const rowKeyHash = createHash5("sha256").update(row.rowKeyJson).digest("hex");
18090
+ const rowKeyHash = createHash6("sha256").update(row.rowKeyJson).digest("hex");
16496
18091
  const payloadHash = hashPayload(row.payloadJson);
16497
18092
  insertRow.run(revisionId, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
16498
18093
  insertPayload.run(payloadHash, row.payloadJson);
@@ -16506,7 +18101,7 @@ async function addSnapshotTableRows(cwd, revisionId, rows) {
16506
18101
  }
16507
18102
  }
16508
18103
  function computeRevisionTableHash(db, revisionId) {
16509
- const hasher = createHash5("sha256");
18104
+ const hasher = createHash6("sha256");
16510
18105
  hashLengthPrefixed(hasher, `snapshot-table-v${SNAPSHOT_TABLE_HASH_VERSION}`);
16511
18106
  let rowCount = 0;
16512
18107
  let last;
@@ -17100,6 +18695,34 @@ function sanitizeS3(v) {
17100
18695
  return;
17101
18696
  return out;
17102
18697
  }
18698
+ function sanitizeDynamodb(v) {
18699
+ if (!v || typeof v !== "object")
18700
+ return;
18701
+ const r = v;
18702
+ const out = {};
18703
+ const table = sanitizeOptionalString(r.table, MAX_DYNAMODB_TABLE_LEN);
18704
+ if (table !== undefined)
18705
+ out.table = table;
18706
+ if (r.mode === "scan" || r.mode === "query")
18707
+ out.mode = r.mode;
18708
+ const keyConditionExpression = sanitizeOptionalString(r.keyConditionExpression, MAX_DYNAMODB_EXPRESSION_LEN);
18709
+ if (keyConditionExpression !== undefined)
18710
+ out.keyConditionExpression = keyConditionExpression;
18711
+ const filterExpression = sanitizeOptionalString(r.filterExpression, MAX_DYNAMODB_EXPRESSION_LEN);
18712
+ if (filterExpression !== undefined)
18713
+ out.filterExpression = filterExpression;
18714
+ const expressionAttributeValues = sanitizeOptionalString(r.expressionAttributeValues, MAX_DYNAMODB_ATTRIBUTE_VALUES_LEN);
18715
+ if (expressionAttributeValues !== undefined)
18716
+ out.expressionAttributeValues = expressionAttributeValues;
18717
+ if (typeof r.scanIndexForward === "boolean")
18718
+ out.scanIndexForward = r.scanIndexForward;
18719
+ const itemKey = sanitizeOptionalString(r.itemKey, MAX_DYNAMODB_ITEM_KEY_LEN);
18720
+ if (itemKey !== undefined)
18721
+ out.itemKey = itemKey;
18722
+ if (out.table === undefined && out.mode === undefined && out.keyConditionExpression === undefined && out.filterExpression === undefined && out.expressionAttributeValues === undefined && out.scanIndexForward === undefined && out.itemKey === undefined)
18723
+ return;
18724
+ return out;
18725
+ }
17103
18726
  function sanitize(input) {
17104
18727
  if (!input || typeof input !== "object")
17105
18728
  return emptyState2();
@@ -17125,7 +18748,7 @@ function sanitize(input) {
17125
18748
  if (isToolInternalDbId(dbId))
17126
18749
  continue;
17127
18750
  const schema = sanitizeOptionalString(tab.schema, MAX_SCHEMA_NAME_LEN);
17128
- const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
18751
+ const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN2) ?? null;
17129
18752
  const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
17130
18753
  const out = { id, dbId, table, view };
17131
18754
  if (schema !== undefined)
@@ -17156,6 +18779,9 @@ function sanitize(input) {
17156
18779
  const s3 = sanitizeS3(tab.s3);
17157
18780
  if (s3 !== undefined)
17158
18781
  out.s3 = s3;
18782
+ const dynamodb = sanitizeDynamodb(tab.dynamodb);
18783
+ if (dynamodb !== undefined)
18784
+ out.dynamodb = dynamodb;
17159
18785
  tabs.push(out);
17160
18786
  }
17161
18787
  let activeTabId = sanitizeOptionalString(obj.activeTabId, MAX_TAB_ID_LEN) ?? null;
@@ -17170,7 +18796,7 @@ async function loadTabsAsync(cwd) {
17170
18796
  async function saveTabsAsync(cwd, state) {
17171
18797
  return tabsStore.save(cwd, state);
17172
18798
  }
17173
- var CODE_VIEWER_DIR5 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES2 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN2 = 2048, MAX_SCHEMA_NAME_LEN = 512, MAX_TABLE_NAME_LEN = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, MAX_S3_BUCKET_LEN = 256, MAX_S3_KEY_LEN = 2048, MAX_S3_QUERY_LEN = 2048, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS, tabsStore;
18799
+ var CODE_VIEWER_DIR5 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES2 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN2 = 2048, MAX_SCHEMA_NAME_LEN = 512, MAX_TABLE_NAME_LEN2 = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, MAX_S3_BUCKET_LEN = 256, MAX_S3_KEY_LEN = 2048, MAX_S3_QUERY_LEN = 2048, MAX_DYNAMODB_TABLE_LEN = 255, MAX_DYNAMODB_EXPRESSION_LEN = 4096, MAX_DYNAMODB_ATTRIBUTE_VALUES_LEN = 16000, MAX_DYNAMODB_ITEM_KEY_LEN = 4096, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS, tabsStore;
17174
18800
  var init_tabs_store = __esm(() => {
17175
18801
  init_json_store();
17176
18802
  VALID_VIEWS = new Set([
@@ -17294,6 +18920,9 @@ async function resolveDb(cwd, dbParam, omitDirNames, schemaParam, signal) {
17294
18920
  if (info.kind === "s3") {
17295
18921
  return textError("s3 services must use the /_db/s3/* routes", 400);
17296
18922
  }
18923
+ if (info.kind === "dynamodb") {
18924
+ return textError("dynamodb services must use the /_db/dynamodb/* routes", 400);
18925
+ }
17297
18926
  const resolved2 = parsed.database ? { ...info, database: parsed.database } : info;
17298
18927
  const requestedSchema = normalizeSchemaParam(schemaParam);
17299
18928
  if (requestedSchema instanceof Response)
@@ -17537,7 +19166,7 @@ function groupFiltersByValue(filters) {
17537
19166
  }
17538
19167
  return grouped;
17539
19168
  }
17540
- async function handleTable(cwd, url, omitDirNames, signal) {
19169
+ async function handleTable2(cwd, url, omitDirNames, signal) {
17541
19170
  const r = await resolveDb(cwd, url.searchParams.get("db"), omitDirNames, url.searchParams.get("schema"), signal);
17542
19171
  if (r instanceof Response)
17543
19172
  return r;
@@ -18528,6 +20157,10 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
18528
20157
  const { handleS3Route: handleS3Route2 } = await Promise.resolve().then(() => (init_handle_s3(), exports_handle_s3));
18529
20158
  return handleS3Route2(req, url, cwd, sideEffectAllowed, omitDirNames);
18530
20159
  }
20160
+ if (url.pathname.startsWith("/_db/dynamodb/")) {
20161
+ const { handleDynamoDbRoute: handleDynamoDbRoute2 } = await Promise.resolve().then(() => (init_handle_dynamodb(), exports_handle_dynamodb));
20162
+ return handleDynamoDbRoute2(req, url, cwd, sideEffectAllowed, omitDirNames);
20163
+ }
18531
20164
  const start = Date.now();
18532
20165
  const method = req.method;
18533
20166
  const wrapResponse = (res) => {
@@ -18551,7 +20184,7 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
18551
20184
  },
18552
20185
  "/_db/table": {
18553
20186
  methods: ["GET"],
18554
- handler: () => handleTable(cwd, url, omitDirNames, req.signal)
20187
+ handler: () => handleTable2(cwd, url, omitDirNames, req.signal)
18555
20188
  },
18556
20189
  "/_db/table-count": {
18557
20190
  methods: ["GET"],
@@ -18666,6 +20299,7 @@ var init_handle = __esm(() => {
18666
20299
  init_connection_pool();
18667
20300
  init_discovery();
18668
20301
  init_global_search();
20302
+ init_handle_dynamodb();
18669
20303
  init_handle_elasticsearch();
18670
20304
  init_handle_redis();
18671
20305
  init_handle_s3();
@@ -18695,7 +20329,8 @@ var init_handle = __esm(() => {
18695
20329
  },
18696
20330
  redis: closeRedisAdapter,
18697
20331
  elasticsearch: closeElasticsearchAdapter,
18698
- s3: closeS3Adapter
20332
+ s3: closeS3Adapter,
20333
+ dynamodb: closeDynamoDbAdapter
18699
20334
  };
18700
20335
  SNAPSHOT_DOCKER_SOURCE_REGISTRY = {
18701
20336
  redis: async (info, requestedContainers, signal) => {
@@ -20636,10 +22271,18 @@ var init_journal2 = __esm(() => {
20636
22271
  // web-src/server/search-service.ts
20637
22272
  import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "node:fs";
20638
22273
  import { join as join17, relative as relative6 } from "node:path";
20639
- function rgAvailable(cwd) {
22274
+ async function rgAvailableAsync(cwd) {
20640
22275
  if (rgAvailableCache !== null)
20641
22276
  return rgAvailableCache;
20642
- const proc = runSync([commandForExternal("rg"), "--version"], cwd);
22277
+ const proc = await spawnTextAsync({
22278
+ command: commandForExternal("rg"),
22279
+ args: ["--version"],
22280
+ cwd,
22281
+ timeoutMs: 5000,
22282
+ abortMessage: "rg version aborted",
22283
+ timeoutMessage: "rg version timed out after 5000ms",
22284
+ rejectOnError: false
22285
+ });
20643
22286
  rgAvailableCache = proc.code === 0;
20644
22287
  return rgAvailableCache;
20645
22288
  }
@@ -20712,15 +22355,21 @@ function grepWorktreeFallback(env, query, max, paths) {
20712
22355
  }
20713
22356
  return matches;
20714
22357
  }
20715
- function grepWorktree(env, req) {
22358
+ async function grepWorktreeAsync(env, req) {
20716
22359
  const paths = filterCallerPaths(env, req.paths);
20717
- if (rgAvailable(env.cwd)) {
22360
+ if (await rgAvailableAsync(env.cwd)) {
20718
22361
  const safePaths = paths.filter((path) => safeWorktreePath(env, path));
20719
22362
  const args = buildRgArgs(req.query, req.max, safePaths, req.regex, env.omitDirNames, env.excludeNames);
20720
- args[0] = commandForExternal("rg");
20721
- const proc = runSync(args, env.cwd, { timeout: 5000 });
20722
- const stdout = proc.stdout;
20723
- const matches2 = parseRgOutput(stdout, req.max, env.omitDirNames, env.excludeNames).filter((match) => isSafePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, env.omitDirNames, env.excludeNames) && !!safeWorktreePath(env, match.path));
22363
+ const proc = await spawnTextAsync({
22364
+ command: commandForExternal("rg"),
22365
+ args: args.slice(1),
22366
+ cwd: env.cwd,
22367
+ timeoutMs: 5000,
22368
+ abortMessage: "grep aborted",
22369
+ timeoutMessage: "grep timed out after 5000ms",
22370
+ rejectOnError: false
22371
+ });
22372
+ const matches2 = parseRgOutput(proc.stdout, req.max, env.omitDirNames, env.excludeNames).filter((match) => isSafePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, env.omitDirNames, env.excludeNames) && !!safeWorktreePath(env, match.path));
20724
22373
  return {
20725
22374
  ref: "worktree",
20726
22375
  engine: "rg",
@@ -20744,10 +22393,9 @@ function grepWorktree(env, req) {
20744
22393
  matches
20745
22394
  };
20746
22395
  }
20747
- function grepTreeRef(env, req) {
22396
+ async function grepTreeRefAsync(env, req) {
20748
22397
  const safePaths = filterCallerPaths(env, req.paths);
20749
22398
  const args = [
20750
- commandForExternal("git"),
20751
22399
  "-c",
20752
22400
  "core.quotepath=false",
20753
22401
  "grep",
@@ -20762,9 +22410,16 @@ function grepTreeRef(env, req) {
20762
22410
  "--",
20763
22411
  ...safePaths
20764
22412
  ];
20765
- const proc = runSync(args, env.cwd, { timeout: 5000 });
20766
- const stdout = proc.stdout;
20767
- const matches = parseGitGrepOutput(stdout, req.ref, req.max, env.omitDirNames, env.excludeNames).slice(0, req.max);
22413
+ const proc = await spawnTextAsync({
22414
+ command: commandForExternal("git"),
22415
+ args,
22416
+ cwd: env.cwd,
22417
+ timeoutMs: 5000,
22418
+ abortMessage: "git grep aborted",
22419
+ timeoutMessage: "git grep timed out after 5000ms",
22420
+ rejectOnError: false
22421
+ });
22422
+ const matches = parseGitGrepOutput(proc.stdout, req.ref, req.max, env.omitDirNames, env.excludeNames).slice(0, req.max);
20768
22423
  return {
20769
22424
  ref: req.ref,
20770
22425
  engine: "git",
@@ -20772,10 +22427,10 @@ function grepTreeRef(env, req) {
20772
22427
  matches
20773
22428
  };
20774
22429
  }
20775
- function grepRepo(env, req) {
22430
+ async function grepRepoAsync(env, req) {
20776
22431
  const isWorktree = req.ref === "worktree" || req.ref === "";
20777
22432
  if (!isWorktree) {
20778
- const refCheck = verifyTreeRefResult(req.ref, env.cwd);
22433
+ const refCheck = await verifyTreeRefResultAsync(req.ref, env.cwd);
20779
22434
  if (refCheck.ok !== true) {
20780
22435
  return {
20781
22436
  ok: false,
@@ -20796,13 +22451,13 @@ function grepRepo(env, req) {
20796
22451
  };
20797
22452
  }
20798
22453
  if (isWorktree) {
20799
- return { ok: true, value: grepWorktree(env, req) };
22454
+ return { ok: true, value: await grepWorktreeAsync(env, req) };
20800
22455
  }
20801
- return { ok: true, value: grepTreeRef(env, req) };
22456
+ return { ok: true, value: await grepTreeRefAsync(env, req) };
20802
22457
  }
20803
- function listRepoFiles(env, ref, generation) {
22458
+ async function listRepoFilesAsync(env, ref, generation) {
20804
22459
  if (ref !== "worktree" && ref !== "") {
20805
- const refCheck = verifyTreeRefResult(ref, env.cwd);
22460
+ const refCheck = await verifyTreeRefResultAsync(ref, env.cwd);
20806
22461
  if (refCheck.ok !== true) {
20807
22462
  return {
20808
22463
  ok: false,
@@ -20812,7 +22467,7 @@ function listRepoFiles(env, ref, generation) {
20812
22467
  }
20813
22468
  }
20814
22469
  const effectiveRef = ref || "worktree";
20815
- const tree = listTreeResult(effectiveRef, "", env.cwd, {
22470
+ const tree = await listTreeResultAsync(effectiveRef, "", env.cwd, {
20816
22471
  recursive: true,
20817
22472
  omitDirNames: env.omitDirNames,
20818
22473
  excludeNames: env.excludeNames
@@ -20829,8 +22484,8 @@ function listRepoFiles(env, ref, generation) {
20829
22484
  var rgAvailableCache = null;
20830
22485
  var init_search_service = __esm(() => {
20831
22486
  init_command_resolver();
22487
+ init_spawn_runner();
20832
22488
  init_git();
20833
- init_runtime();
20834
22489
  init_search();
20835
22490
  });
20836
22491
 
@@ -21434,7 +23089,7 @@ function validateMcpIntegerLimit(raw, fallback, min, max, flag) {
21434
23089
  }
21435
23090
  return { ok: true, value: raw };
21436
23091
  }
21437
- function runFileShowTool(input, defaultCwd) {
23092
+ async function runFileShowTool(input, defaultCwd) {
21438
23093
  const params = isPlainObject(input) ? input : {};
21439
23094
  const pathRaw = params.path;
21440
23095
  if (typeof pathRaw !== "string") {
@@ -21487,7 +23142,7 @@ function runFileShowTool(input, defaultCwd) {
21487
23142
  json: true
21488
23143
  };
21489
23144
  try {
21490
- const report = buildFileShowReport(resolved.root, command);
23145
+ const report = await buildFileShowReportAsync(resolved.root, command);
21491
23146
  return {
21492
23147
  text: JSON.stringify(report, null, 2),
21493
23148
  isError: report.error !== undefined
@@ -21497,7 +23152,7 @@ function runFileShowTool(input, defaultCwd) {
21497
23152
  return { text: `file show failed: ${detail}`, isError: true };
21498
23153
  }
21499
23154
  }
21500
- function runFileBlameTool(input, defaultCwd) {
23155
+ async function runFileBlameTool(input, defaultCwd) {
21501
23156
  const params = isPlainObject(input) ? input : {};
21502
23157
  const pathRaw = params.path;
21503
23158
  if (typeof pathRaw !== "string") {
@@ -21534,7 +23189,7 @@ function runFileBlameTool(input, defaultCwd) {
21534
23189
  json: true
21535
23190
  };
21536
23191
  try {
21537
- const report = buildFileBlameReport(resolved.root, command);
23192
+ const report = await buildFileBlameReportAsync(resolved.root, command);
21538
23193
  return {
21539
23194
  text: JSON.stringify(report, null, 2),
21540
23195
  isError: report.result.error !== undefined
@@ -21544,7 +23199,7 @@ function runFileBlameTool(input, defaultCwd) {
21544
23199
  return { text: `file blame failed: ${detail}`, isError: true };
21545
23200
  }
21546
23201
  }
21547
- function runFileHistoryTool(input, defaultCwd) {
23202
+ async function runFileHistoryTool(input, defaultCwd) {
21548
23203
  const params = isPlainObject(input) ? input : {};
21549
23204
  const pathRaw = params.path;
21550
23205
  if (typeof pathRaw !== "string") {
@@ -21597,7 +23252,7 @@ function runFileHistoryTool(input, defaultCwd) {
21597
23252
  json: true
21598
23253
  };
21599
23254
  try {
21600
- const report = buildFileHistoryReport(resolved.root, command);
23255
+ const report = await buildFileHistoryReportAsync(resolved.root, command);
21601
23256
  return {
21602
23257
  text: JSON.stringify(report, null, 2),
21603
23258
  isError: report.result.error !== undefined
@@ -21607,7 +23262,7 @@ function runFileHistoryTool(input, defaultCwd) {
21607
23262
  return { text: `file history failed: ${detail}`, isError: true };
21608
23263
  }
21609
23264
  }
21610
- function runFileDiffTool(input, defaultCwd) {
23265
+ async function runFileDiffTool(input, defaultCwd) {
21611
23266
  const params = isPlainObject(input) ? input : {};
21612
23267
  const pathRaw = params.path;
21613
23268
  if (typeof pathRaw !== "string") {
@@ -21711,7 +23366,7 @@ function runFileDiffTool(input, defaultCwd) {
21711
23366
  json: true
21712
23367
  };
21713
23368
  try {
21714
- const report = buildFileDiffReport(resolved.root, command);
23369
+ const report = await buildFileDiffReportAsync(resolved.root, command);
21715
23370
  return {
21716
23371
  text: JSON.stringify(report, null, 2),
21717
23372
  isError: report.error !== undefined
@@ -21721,7 +23376,7 @@ function runFileDiffTool(input, defaultCwd) {
21721
23376
  return { text: `file diff failed: ${detail}`, isError: true };
21722
23377
  }
21723
23378
  }
21724
- function runSearchFilesTool(input, defaultCwd) {
23379
+ async function runSearchFilesTool(input, defaultCwd) {
21725
23380
  const params = isPlainObject(input) ? input : {};
21726
23381
  const termRaw = params.term;
21727
23382
  if (typeof termRaw !== "string") {
@@ -21756,7 +23411,7 @@ function runSearchFilesTool(input, defaultCwd) {
21756
23411
  omitDirNames: [],
21757
23412
  excludeNames: DEFAULT_EXCLUDE_NAMES
21758
23413
  };
21759
- const listResult = listRepoFiles(env, refParsed.ref, 1);
23414
+ const listResult = await listRepoFilesAsync(env, refParsed.ref, 1);
21760
23415
  if (listResult.ok !== true) {
21761
23416
  return { text: listResult.error, isError: true };
21762
23417
  }
@@ -21783,7 +23438,7 @@ function runSearchFilesTool(input, defaultCwd) {
21783
23438
  };
21784
23439
  return { text: JSON.stringify(payload, null, 2) };
21785
23440
  }
21786
- function runSearchCodeTool(input, defaultCwd) {
23441
+ async function runSearchCodeTool(input, defaultCwd) {
21787
23442
  const params = isPlainObject(input) ? input : {};
21788
23443
  const termRaw = params.term;
21789
23444
  if (typeof termRaw !== "string") {
@@ -21835,7 +23490,7 @@ function runSearchCodeTool(input, defaultCwd) {
21835
23490
  omitDirNames: [],
21836
23491
  excludeNames: DEFAULT_EXCLUDE_NAMES
21837
23492
  };
21838
- const result = grepRepo(env, {
23493
+ const result = await grepRepoAsync(env, {
21839
23494
  query: termRaw,
21840
23495
  ref: refParsed.ref,
21841
23496
  paths,
@@ -22391,12 +24046,12 @@ var init_state_route = __esm(() => {
22391
24046
  // web-src/server/preview.ts
22392
24047
  var exports_preview = {};
22393
24048
  import {
22394
- closeSync as closeSync2,
24049
+ closeSync as closeSync3,
22395
24050
  constants as constants3,
22396
24051
  existsSync as existsSync8,
22397
24052
  lstatSync as lstatSync5,
22398
24053
  mkdirSync as mkdirSync4,
22399
- openSync as openSync2,
24054
+ openSync as openSync3,
22400
24055
  readFileSync as readFileSync9,
22401
24056
  realpathSync as realpathSync6,
22402
24057
  renameSync,
@@ -22513,6 +24168,7 @@ Examples:
22513
24168
  process.exit(1);
22514
24169
  }
22515
24170
  const candidate = repoRoot(cwd);
24171
+ cwdHasGitRepository = !!candidate;
22516
24172
  if (cwdWasExplicit) {
22517
24173
  if (candidate === cwd)
22518
24174
  cwd = candidate;
@@ -22728,23 +24384,23 @@ function fileToMeta(file, range, extraQs) {
22728
24384
  untracked: file.untracked || false
22729
24385
  };
22730
24386
  }
22731
- function computePayload(extras, range, pathFilter = "") {
24387
+ async function computePayload(extras, range, pathFilter = "", responseGeneration = generation) {
22732
24388
  if (isSameWorktreeRange(range)) {
22733
24389
  return {
22734
24390
  files: [],
22735
24391
  totals: { files: 0, additions: 0, deletions: 0 },
22736
24392
  range: "worktree .. worktree",
22737
24393
  project: basename3(cwd),
22738
- branch: currentBranch(cwd) || undefined,
22739
- generation
24394
+ branch: await currentBranchMetadata(),
24395
+ generation: responseGeneration
22740
24396
  };
22741
24397
  }
22742
24398
  const { args, refs } = buildRangeArgs(range);
22743
24399
  const fullArgs = [...extras, ...args];
22744
- const metaResult = fileMetaResult(fullArgs, cwd, false);
24400
+ const metaResult = await fileMetaResultAsync(fullArgs, cwd, false);
22745
24401
  const files = metaResult.files;
22746
24402
  if (!metaResult.error && includeUntracked(range, refs)) {
22747
- files.push(...untrackedMeta(cwd));
24403
+ files.push(...await untrackedMetaAsync(cwd));
22748
24404
  }
22749
24405
  const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
22750
24406
  filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
@@ -22771,12 +24427,13 @@ function computePayload(extras, range, pathFilter = "") {
22771
24427
  totals,
22772
24428
  range: label || "HEAD",
22773
24429
  project: basename3(cwd),
22774
- branch: currentBranch(cwd) || undefined,
22775
- generation,
24430
+ branch: await currentBranchMetadata(),
24431
+ generation: responseGeneration,
22776
24432
  ...metaResult.error ? { error: metaResult.error } : {}
22777
24433
  };
22778
24434
  }
22779
- function handleDiffJson(url) {
24435
+ async function handleDiffJson(url) {
24436
+ const responseGeneration = generation;
22780
24437
  const extras = [];
22781
24438
  if (url.searchParams.get("ignore_ws") === "1")
22782
24439
  extras.push("-w");
@@ -22790,45 +24447,40 @@ function handleDiffJson(url) {
22790
24447
  if (path && !safePath(path))
22791
24448
  return text("invalid path", 400);
22792
24449
  const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}|${path}`;
22793
- if (url.searchParams.get("nocache") === "1") {
22794
- const payload2 = computePayload(extras, range, path);
22795
- const sig = JSON.stringify({ ...payload2, generation: undefined });
22796
- const cached2 = metaCache.get(key);
22797
- if (!cached2 || cached2.sig !== sig) {
22798
- generation++;
22799
- payload2.generation = generation;
22800
- metaCache.clear();
22801
- fileCache.clear();
22802
- }
22803
- const body2 = JSON.stringify(payload2);
22804
- setTimedCacheEntry(metaCache, key, { body: body2, sig });
22805
- return new Response(body2, {
24450
+ const noCache = url.searchParams.get("nocache") === "1";
24451
+ const cached = metaCache.get(key);
24452
+ if (!noCache && cacheFresh(cached))
24453
+ return new Response(cached.body, {
22806
24454
  headers: {
22807
24455
  "Content-Type": "application/json; charset=utf-8",
22808
24456
  "Cache-Control": "no-store"
22809
24457
  }
22810
24458
  });
22811
- }
22812
- const cached = metaCache.get(key);
22813
- if (cacheFresh(cached))
22814
- return new Response(cached.body, {
24459
+ const requestSequence = ++diffMetaRequestSequence;
24460
+ latestDiffMetaRequest.set(key, requestSequence);
24461
+ try {
24462
+ const payload = await computePayload(extras, range, path, responseGeneration);
24463
+ if (latestDiffMetaRequest.get(key) !== requestSequence || responseGeneration !== generation)
24464
+ return json2(payload);
24465
+ const sig = JSON.stringify({ ...payload, generation: undefined });
24466
+ if (noCache && (!cached || cached.sig !== sig)) {
24467
+ generation++;
24468
+ payload.generation = generation;
24469
+ metaCache.clear();
24470
+ fileCache.clear();
24471
+ }
24472
+ const body = JSON.stringify(payload);
24473
+ setTimedCacheEntry(metaCache, key, { body, sig });
24474
+ return new Response(body, {
22815
24475
  headers: {
22816
24476
  "Content-Type": "application/json; charset=utf-8",
22817
24477
  "Cache-Control": "no-store"
22818
24478
  }
22819
24479
  });
22820
- const payload = computePayload(extras, range, path);
22821
- const body = JSON.stringify(payload);
22822
- setTimedCacheEntry(metaCache, key, {
22823
- body,
22824
- sig: JSON.stringify({ ...payload, generation: undefined })
22825
- });
22826
- return new Response(body, {
22827
- headers: {
22828
- "Content-Type": "application/json; charset=utf-8",
22829
- "Cache-Control": "no-store"
22830
- }
22831
- });
24480
+ } finally {
24481
+ if (latestDiffMetaRequest.get(key) === requestSequence)
24482
+ latestDiffMetaRequest.delete(key);
24483
+ }
22832
24484
  }
22833
24485
  function safeRepoPath(path) {
22834
24486
  return path === "" || safePath(path);
@@ -22926,16 +24578,16 @@ function worktreeFileMetadata(path, knownSize) {
22926
24578
  return {};
22927
24579
  }
22928
24580
  }
22929
- function gitFileMetadata(ref, path, knownSize) {
22930
- const size = knownSize ?? rawFileSize(path, ref);
22931
- const commitUpdatedAt = lastCommitDateForPath(ref, path, cwd) || undefined;
24581
+ async function gitFileMetadata(ref, path, knownSize) {
24582
+ const size = knownSize ?? await rawFileSize(path, ref);
24583
+ const commitUpdatedAt = await lastCommitDateForPathAsync(ref, path, cwd) || undefined;
22932
24584
  return {
22933
24585
  size: size == null ? undefined : size,
22934
24586
  updated_at: commitUpdatedAt,
22935
24587
  commit_updated_at: commitUpdatedAt
22936
24588
  };
22937
24589
  }
22938
- function directoryMetadata(target, path) {
24590
+ async function directoryMetadata(target, path) {
22939
24591
  if (target === "worktree" || target === "") {
22940
24592
  const full = path === "" ? safeOpenWorktreePath("") : safeWorktreePath2(path);
22941
24593
  if (!full)
@@ -22950,22 +24602,22 @@ function directoryMetadata(target, path) {
22950
24602
  return {};
22951
24603
  }
22952
24604
  }
22953
- const commitUpdatedAt = lastCommitDateForPath(target, path || ".", cwd) || undefined;
24605
+ const commitUpdatedAt = await lastCommitDateForPathAsync(target, path || ".", cwd) || undefined;
22954
24606
  return { updated_at: commitUpdatedAt, commit_updated_at: commitUpdatedAt };
22955
24607
  }
22956
- function fileMetadataForTarget(target, path) {
24608
+ async function fileMetadataForTarget(target, path) {
22957
24609
  return target === "worktree" || target === "" ? worktreeFileMetadata(path) : gitFileMetadata(target, path);
22958
24610
  }
22959
- function attachTreeEntryMetadata(target, entry) {
24611
+ async function attachTreeEntryMetadata(target, entry) {
22960
24612
  if (entry.type === "tree")
22961
- return { ...entry, ...directoryMetadata(target, entry.path) };
24613
+ return { ...entry, ...await directoryMetadata(target, entry.path) };
22962
24614
  if (entry.type === "commit" && !entry.submodule && (target === "worktree" || target === ""))
22963
- return { ...entry, ...directoryMetadata(target, entry.path) };
24615
+ return { ...entry, ...await directoryMetadata(target, entry.path) };
22964
24616
  if (entry.type !== "blob")
22965
24617
  return entry;
22966
- return { ...entry, ...fileMetadataForTarget(target, entry.path) };
24618
+ return { ...entry, ...await fileMetadataForTarget(target, entry.path) };
22967
24619
  }
22968
- function readReadme(target, dirPath) {
24620
+ async function readReadme(target, dirPath) {
22969
24621
  const candidates = ["README.md", "readme.md", "README.markdown", "README"];
22970
24622
  for (const name of candidates) {
22971
24623
  const path = dirPath ? `${dirPath}/${name}` : name;
@@ -22979,13 +24631,13 @@ function readReadme(target, dirPath) {
22979
24631
  continue;
22980
24632
  }
22981
24633
  }
22982
- const res = show(target, path, cwd);
24634
+ const res = await showAsync(target, path, cwd);
22983
24635
  if (res.code === 0)
22984
24636
  return { path, text: res.stdout };
22985
24637
  }
22986
24638
  return null;
22987
24639
  }
22988
- function handleTree(url) {
24640
+ async function handleTree(url) {
22989
24641
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
22990
24642
  const path = (url.searchParams.get("path") || "").replace(/^\/+|\/+$/g, "");
22991
24643
  if (!safeRepoPath(path))
@@ -22993,7 +24645,7 @@ function handleTree(url) {
22993
24645
  if ((target === "worktree" || target === "") && isGitInternalPath(path))
22994
24646
  return text("forbidden", 403);
22995
24647
  if (target !== "worktree") {
22996
- const refCheck = verifyTreeRefResult(target, cwd);
24648
+ const refCheck = await verifyTreeRefResultAsync(target, cwd);
22997
24649
  if (refCheck.ok !== true)
22998
24650
  return text(refCheck.error, refCheck.status ?? 400);
22999
24651
  }
@@ -23003,7 +24655,7 @@ function handleTree(url) {
23003
24655
  if (invalidScopeExcludeNamesQuery(url))
23004
24656
  return text("invalid exclude names", 400);
23005
24657
  const excludeNames = scopeExcludeNamesFromQuery(url);
23006
- const tree = listTreeResult(target, path, cwd, {
24658
+ const tree = await listTreeResultAsync(target, path, cwd, {
23007
24659
  recursive,
23008
24660
  omitDirNames: scopeOmitDirNamesFromQuery(url),
23009
24661
  excludeNames
@@ -23015,17 +24667,17 @@ function handleTree(url) {
23015
24667
  ref: target,
23016
24668
  path,
23017
24669
  project: basename3(cwd),
23018
- branch: currentBranch(cwd) || undefined,
23019
- entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
23020
- readme: readReadme(target, path),
24670
+ branch: await currentBranchMetadata(),
24671
+ entries: recursive ? entries : await Promise.all(entries.map((entry) => attachTreeEntryMetadata(target, entry))),
24672
+ readme: await readReadme(target, path),
23021
24673
  upload_enabled: uploadEnabled && (target === "worktree" || target === "")
23022
24674
  });
23023
24675
  }
23024
- function handleSettings() {
24676
+ async function handleSettings() {
23025
24677
  return json2({
23026
24678
  project: basename3(cwd),
23027
- branch: currentBranch(cwd) || undefined,
23028
- repo_web_url: remoteWebUrl(cwd),
24679
+ branch: await currentBranchMetadata(),
24680
+ repo_web_url: cwdHasGitRepository ? await remoteWebUrlAsync(cwd) : null,
23029
24681
  scope: {
23030
24682
  omit_dirs_effective: scopeOmitDirNames,
23031
24683
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
@@ -23060,7 +24712,13 @@ function currentSearchEnv(omitOverride, excludeOverride) {
23060
24712
  excludeNames: excludeOverride ?? scopeExcludeNames
23061
24713
  };
23062
24714
  }
23063
- function handleFiles2(url) {
24715
+ async function currentBranchMetadata() {
24716
+ if (!cwdHasGitRepository)
24717
+ return;
24718
+ return await currentBranchAsync(cwd) || undefined;
24719
+ }
24720
+ async function handleFiles2(url) {
24721
+ const responseGeneration = generation;
23064
24722
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
23065
24723
  if (invalidScopeOmitDirNamesQuery(url))
23066
24724
  return text("invalid omit dirs", 400);
@@ -23072,13 +24730,24 @@ function handleFiles2(url) {
23072
24730
  const cached = fileListCache.get(key);
23073
24731
  if (cached && cached.generation === generation)
23074
24732
  return json2(cached.body);
23075
- const result = listRepoFiles(currentSearchEnv(omitDirNames, excludeNames), target, generation);
24733
+ const result = await listRepoFilesAsync(currentSearchEnv(omitDirNames, excludeNames), target, responseGeneration);
23076
24734
  if (result.ok !== true)
23077
24735
  return text(result.error, result.status ?? 400);
23078
- fileListCache.set(key, { generation, body: result.value });
24736
+ if (responseGeneration !== generation)
24737
+ return json2(result.value);
24738
+ fileListCache.set(key, {
24739
+ generation: responseGeneration,
24740
+ body: result.value
24741
+ });
24742
+ while (fileListCache.size > MAX_TIMED_CACHE_ENTRIES) {
24743
+ const oldest = fileListCache.keys().next().value;
24744
+ if (oldest === undefined)
24745
+ break;
24746
+ fileListCache.delete(oldest);
24747
+ }
23079
24748
  return json2(result.value);
23080
24749
  }
23081
- function handleGrep(url) {
24750
+ async function handleGrep(url) {
23082
24751
  const query = url.searchParams.get("q") || "";
23083
24752
  const ref = url.searchParams.get("ref") || "worktree";
23084
24753
  const max = normalizeGrepMax(url.searchParams.get("max"));
@@ -23090,7 +24759,7 @@ function handleGrep(url) {
23090
24759
  const excludeNames = scopeExcludeNamesFromQuery(url);
23091
24760
  const paths = url.searchParams.getAll("path");
23092
24761
  const regex = url.searchParams.get("regex") === "1";
23093
- const result = grepRepo(currentSearchEnv(omitDirNames, excludeNames), {
24762
+ const result = await grepRepoAsync(currentSearchEnv(omitDirNames, excludeNames), {
23094
24763
  query,
23095
24764
  ref,
23096
24765
  paths,
@@ -23101,25 +24770,26 @@ function handleGrep(url) {
23101
24770
  return text(result.error, result.status ?? 400);
23102
24771
  return json2(result.value);
23103
24772
  }
23104
- function handleRefCommits(url) {
24773
+ async function handleRefCommits(url) {
23105
24774
  const query = url.searchParams.get("q") || "";
23106
24775
  const parsedMax = Number(url.searchParams.get("max") || "");
23107
24776
  const parsedSkip = Number(url.searchParams.get("skip") || "0");
23108
24777
  const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
23109
24778
  const skip = Number.isFinite(parsedSkip) && parsedSkip > 0 ? parsedSkip : undefined;
23110
- const result = refCommitPageResult(cwd, { query, max, skip });
24779
+ const result = await refCommitPageResultAsync(cwd, { query, max, skip });
23111
24780
  if (result.error)
23112
24781
  return text(result.error, result.status ?? 500);
23113
24782
  return json2({ commits: result.commits, hasMore: result.hasMore });
23114
24783
  }
23115
- function handleLog(url) {
24784
+ async function handleLog(url) {
24785
+ const responseGeneration = generation;
23116
24786
  const ref = url.searchParams.get("ref") || "HEAD";
23117
24787
  const skip = Number(url.searchParams.get("skip") || "0");
23118
24788
  const limit = Number(url.searchParams.get("limit") || "50");
23119
24789
  const path = url.searchParams.get("path") || "";
23120
24790
  if (path && !safePath(path))
23121
24791
  return text("invalid path", 400);
23122
- const result = commitHistory(cwd, {
24792
+ const result = await commitHistoryAsync(cwd, {
23123
24793
  ref,
23124
24794
  skip: Number.isFinite(skip) ? skip : 0,
23125
24795
  limit: Number.isFinite(limit) ? limit : 50,
@@ -23132,7 +24802,7 @@ function handleLog(url) {
23132
24802
  let commits = result.commits;
23133
24803
  let hasWorktree = false;
23134
24804
  if (wantsWorktreeHead) {
23135
- const status = statusPorcelainForPath(path, cwd);
24805
+ const status = await statusPorcelainForPathAsync(path, cwd);
23136
24806
  if (status.ok && status.stdout.length > 0) {
23137
24807
  const parts = status.stdout.split("\x00").filter(Boolean);
23138
24808
  if (parts.length > 0) {
@@ -23154,7 +24824,7 @@ function handleLog(url) {
23154
24824
  return json2({
23155
24825
  commits,
23156
24826
  hasMore: result.hasMore,
23157
- generation,
24827
+ generation: responseGeneration,
23158
24828
  ...hasWorktree ? { hasWorktree: true } : {}
23159
24829
  });
23160
24830
  }
@@ -23177,7 +24847,8 @@ function rememberBlame(key, value) {
23177
24847
  blameCache.delete(oldest.value);
23178
24848
  }
23179
24849
  }
23180
- function handleFileBlame(url) {
24850
+ async function handleFileBlame(url) {
24851
+ const responseGeneration = generation;
23181
24852
  const path = url.searchParams.get("path") || "";
23182
24853
  if (!safePath(path))
23183
24854
  return text("invalid path", 400);
@@ -23192,7 +24863,7 @@ function handleFileBlame(url) {
23192
24863
  if (base === "worktree") {
23193
24864
  cacheKey = `worktree|${path}|${blamePathKey(path)}`;
23194
24865
  } else {
23195
- const resolved = verifyCommit(normalized.ref, cwd);
24866
+ const resolved = await verifyCommitAsync(normalized.ref, cwd);
23196
24867
  if (resolved.ok === false) {
23197
24868
  const status = resolved.error === commandNotFoundDetail("git") ? 503 : 400;
23198
24869
  return text(resolved.error || "unknown ref", status);
@@ -23205,16 +24876,21 @@ function handleFileBlame(url) {
23205
24876
  blameCache.delete(cacheKey);
23206
24877
  blameCache.set(cacheKey, cached);
23207
24878
  }
23208
- return json2({ ...cached, base, ref, generation });
24879
+ return json2({ ...cached, base, ref, generation: responseGeneration });
23209
24880
  }
23210
- const result = blame(cwd, { path, ref: normalized.ref, base });
24881
+ const result = await blameAsync(cwd, {
24882
+ path,
24883
+ ref: normalized.ref,
24884
+ base
24885
+ });
23211
24886
  if (result.error && result.status)
23212
24887
  return text(result.error, result.status);
23213
- if (!result.error)
24888
+ if (!result.error && responseGeneration === generation)
23214
24889
  rememberBlame(cacheKey, result);
23215
- return json2({ ...result, base, ref, generation });
24890
+ return json2({ ...result, base, ref, generation: responseGeneration });
23216
24891
  }
23217
- function handleFileDiff(url) {
24892
+ async function handleFileDiff(url) {
24893
+ const responseGeneration = generation;
23218
24894
  const path = url.searchParams.get("path") || "";
23219
24895
  if (!safePath(path))
23220
24896
  return text("invalid path", 400);
@@ -23240,7 +24916,7 @@ function handleFileDiff(url) {
23240
24916
  line_count: 0,
23241
24917
  truncated: false,
23242
24918
  binary: false,
23243
- generation
24919
+ generation: responseGeneration
23244
24920
  });
23245
24921
  }
23246
24922
  const { args } = buildRangeArgs(range);
@@ -23267,21 +24943,21 @@ function handleFileDiff(url) {
23267
24943
  diffText = cached.diffText;
23268
24944
  } else {
23269
24945
  if (isUntracked) {
23270
- const res = untrackedFileDiff(extras, path, cwd);
24946
+ const res = await untrackedFileDiffAsync(extras, path, cwd);
23271
24947
  diffText = res.stdout || "";
23272
- if (res.code !== 0) {
23273
- errText = res.stderr;
23274
- errStatus = res.status;
24948
+ if (res.code !== 0 && !(res.code === 1 && diffText)) {
24949
+ errText = res.stderr || "diff failed";
24950
+ errStatus = res.status ?? 500;
23275
24951
  }
23276
24952
  } else {
23277
- const res = fileDiffText([...extras, ...args], oldPath ? [oldPath, path] : path, cwd);
24953
+ const res = await fileDiffTextAsync([...extras, ...args], oldPath ? [oldPath, path] : path, cwd);
23278
24954
  diffText = res.stdout || "";
23279
24955
  if (res.code !== 0) {
23280
- errText = res.stderr;
23281
- errStatus = res.status;
24956
+ errText = res.stderr || "diff failed";
24957
+ errStatus = res.status ?? 500;
23282
24958
  }
23283
24959
  }
23284
- if (!errText)
24960
+ if (!errText && responseGeneration === generation)
23285
24961
  setTimedCacheEntry(fileCache, cacheKey, { diffText });
23286
24962
  }
23287
24963
  if (errStatus)
@@ -23300,7 +24976,7 @@ function handleFileDiff(url) {
23300
24976
  truncated: mode === "preview" && (truncated.totalHunks > truncated.renderedHunks || truncated.lineTruncated),
23301
24977
  binary: diffText.includes("Binary files"),
23302
24978
  error: errText,
23303
- generation
24979
+ generation: responseGeneration
23304
24980
  };
23305
24981
  return json2(body);
23306
24982
  }
@@ -23454,6 +25130,7 @@ async function handleFileRange(url) {
23454
25130
  const full = safeWorktreePath2(path);
23455
25131
  if (!full)
23456
25132
  return text("no file", 404);
25133
+ const responseGeneration = generation;
23457
25134
  const result = await collectIndexedWorktreeLineRange(full, start, end);
23458
25135
  const body = {
23459
25136
  path,
@@ -23463,17 +25140,18 @@ async function handleFileRange(url) {
23463
25140
  lines: result.lines,
23464
25141
  total: result.total,
23465
25142
  complete: result.complete,
23466
- generation
25143
+ generation: responseGeneration
23467
25144
  };
23468
25145
  return json2(body);
23469
25146
  } else {
23470
- const refCheck = verifyTreeRefResult(ref, cwd);
25147
+ const responseGeneration = generation;
25148
+ const refCheck = await verifyTreeRefResultAsync(ref, cwd);
23471
25149
  if (refCheck.ok !== true)
23472
25150
  return text(refCheck.error, refCheck.status ?? 400);
23473
- const oid = objectId(ref, path, cwd);
25151
+ const oid = await objectIdAsync(ref, path, cwd);
23474
25152
  if (oid.code !== 0 || !oid.oid)
23475
25153
  return text("not in ref", 404);
23476
- const size = objectByteSize(oid.oid, cwd);
25154
+ const size = await objectByteSizeAsync(oid.oid, cwd);
23477
25155
  if (size.code !== 0)
23478
25156
  return text("cannot read ref", 500);
23479
25157
  const result = await collectIndexedGitBlobLineRange(path, oid.oid, size.size, start, end);
@@ -23487,41 +25165,71 @@ async function handleFileRange(url) {
23487
25165
  lines: result.lines,
23488
25166
  total: result.total,
23489
25167
  complete: result.complete,
23490
- generation
25168
+ generation: responseGeneration
23491
25169
  };
23492
25170
  return json2(body);
23493
25171
  }
23494
25172
  }
23495
- function handleRawFile(req, url) {
25173
+ async function handleRawFile(req, url) {
23496
25174
  const path = url.searchParams.get("path") || "";
23497
25175
  if (!safePath(path))
23498
25176
  return text("forbidden", 403);
23499
25177
  const ref = url.searchParams.get("ref") || "worktree";
23500
- let body;
23501
25178
  if (ref !== "worktree" && ref !== "") {
23502
- const refCheck = verifyTreeRefResult(ref, cwd);
25179
+ const refCheck = await verifyTreeRefResultAsync(ref, cwd);
23503
25180
  if (refCheck.ok !== true)
23504
25181
  return text(refCheck.error, refCheck.status ?? 400);
23505
- const size = rawFileSize(path, ref);
23506
- if (size == null)
25182
+ const oid = await objectIdAsync(ref, path, cwd);
25183
+ if (oid.code !== 0 || !oid.oid)
23507
25184
  return text("not in ref", 404);
23508
- const metadata = gitFileMetadata(ref, path, size);
25185
+ const sizeResult = await objectByteSizeAsync(oid.oid, cwd);
25186
+ if (sizeResult.code !== 0)
25187
+ return text("cannot read ref", 500);
25188
+ const size = sizeResult.size;
25189
+ const metadata = await gitFileMetadata(ref, path, size);
25190
+ const rangeResult = req.headers.get("range") ? parseHttpByteRange(req.headers.get("range"), size) : null;
25191
+ if (rangeResult?.kind === "unsatisfiable") {
25192
+ return new Response(null, {
25193
+ status: 416,
25194
+ headers: {
25195
+ ...rawFileHeaders(path, { size, metadata }),
25196
+ "Content-Range": `bytes */${size}`,
25197
+ "Content-Length": "0"
25198
+ }
25199
+ });
25200
+ }
25201
+ if (rangeResult?.kind === "range") {
25202
+ const range = rangeResult.range;
25203
+ if (req.method === "HEAD") {
25204
+ return new Response(null, {
25205
+ status: 206,
25206
+ headers: rawFileHeaders(path, { size, range, metadata })
25207
+ });
25208
+ }
25209
+ const shown2 = catFileBlobStream(oid.oid, cwd);
25210
+ const bytes = await collectByteRangeFromStream(shown2.stream, range.start, range.end + 1);
25211
+ const code = await shown2.exited;
25212
+ if (code !== 0)
25213
+ return text("not in ref", 404);
25214
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
25215
+ return new Response(body, {
25216
+ status: 206,
25217
+ headers: rawFileHeaders(path, { size, range, metadata })
25218
+ });
25219
+ }
23509
25220
  if (req.method === "HEAD")
23510
25221
  return new Response(null, {
23511
25222
  headers: rawFileHeaders(path, { size, metadata })
23512
25223
  });
23513
- const res = showBytes(ref, path, cwd);
23514
- if (res.code !== 0)
23515
- return text("not in ref", 404);
23516
- body = res.stdout.buffer.slice(res.stdout.byteOffset, res.stdout.byteOffset + res.stdout.byteLength);
23517
- return new Response(body, {
25224
+ const shown = catFileBlobStream(oid.oid, cwd);
25225
+ return new Response(shown.stream, {
23518
25226
  headers: rawFileHeaders(path, { size, metadata })
23519
25227
  });
23520
25228
  } else {
23521
25229
  const full = safeWorktreePath2(path);
23522
25230
  if (!full)
23523
25231
  return text("not found", 404);
23524
- const size = rawFileSize(path, ref);
25232
+ const size = await rawFileSize(path, ref);
23525
25233
  if (size == null)
23526
25234
  return text("not found", 404);
23527
25235
  const metadata = worktreeFileMetadata(path, size);
@@ -23558,11 +25266,12 @@ function handleRawFile(req, url) {
23558
25266
  });
23559
25267
  }
23560
25268
  }
23561
- function rawFileSize(path, ref) {
25269
+ async function rawFileSize(path, ref) {
23562
25270
  if (ref !== "worktree" && ref !== "") {
23563
- if (!verifyTreeRef(ref, cwd))
25271
+ const refCheck = await verifyTreeRefResultAsync(ref, cwd);
25272
+ if (refCheck.ok !== true)
23564
25273
  return null;
23565
- const res = objectSize(ref, path, cwd);
25274
+ const res = await objectSizeAsync(ref, path, cwd);
23566
25275
  return res.code === 0 ? res.size : null;
23567
25276
  }
23568
25277
  const full = safeWorktreePath2(path);
@@ -23664,11 +25373,11 @@ async function handleUploadFiles(req) {
23664
25373
  const written = [];
23665
25374
  try {
23666
25375
  for (const upload of uploads) {
23667
- const fd = openSync2(upload.target, uploadOpenFlags(), 420);
25376
+ const fd = openSync3(upload.target, uploadOpenFlags(), 420);
23668
25377
  try {
23669
25378
  writeFileSync2(fd, new Uint8Array(await upload.file.arrayBuffer()));
23670
25379
  } finally {
23671
- closeSync2(fd);
25380
+ closeSync3(fd);
23672
25381
  }
23673
25382
  written.push(upload.target);
23674
25383
  }
@@ -23787,13 +25496,13 @@ function moveMacPathIntoTrash(path) {
23787
25496
  return { ok: false, error: String(error) };
23788
25497
  }
23789
25498
  }
23790
- function movePathToTrash(path) {
25499
+ async function movePathToTrash(path) {
23791
25500
  lstatSync5(path);
23792
25501
  if (process.platform === "darwin") {
23793
25502
  return moveMacPathIntoTrash(path);
23794
25503
  }
23795
25504
  if (process.platform === "win32") {
23796
- const res = runSync([
25505
+ const res = await runAsync([
23797
25506
  "powershell.exe",
23798
25507
  "-NoProfile",
23799
25508
  "-NonInteractive",
@@ -23806,7 +25515,7 @@ function movePathToTrash(path) {
23806
25515
  }
23807
25516
  return { ok: false, error: "trash unsupported" };
23808
25517
  }
23809
- function restoreTrashPath(originalPath, trashPath) {
25518
+ async function restoreTrashPath(originalPath, trashPath) {
23810
25519
  const parent = parentRepoPath(originalPath);
23811
25520
  const parentFullPath = safeOpenWorktreePath(parent);
23812
25521
  if (!parentFullPath)
@@ -23832,7 +25541,7 @@ function restoreTrashPath(originalPath, trashPath) {
23832
25541
  }
23833
25542
  }
23834
25543
  if (process.platform === "win32") {
23835
- const res = runSync([
25544
+ const res = await runAsync([
23836
25545
  "powershell.exe",
23837
25546
  "-NoProfile",
23838
25547
  "-NonInteractive",
@@ -23915,7 +25624,7 @@ async function handleTrashPath(req) {
23915
25624
  const originalFullPath = safeWorktreePath2(path);
23916
25625
  if (!originalFullPath)
23917
25626
  return text("not found", 404);
23918
- const moved = movePathToTrash(worktreePath(path));
25627
+ const moved = await movePathToTrash(worktreePath(path));
23919
25628
  if (!moved.ok)
23920
25629
  return text(moved.error || "trash failed", 500);
23921
25630
  const undo = {
@@ -24009,7 +25718,7 @@ async function handleRestoreTrash(req) {
24009
25718
  return text("invalid restore target", 400);
24010
25719
  if (isGitInternalPath(originalPath))
24011
25720
  return text("forbidden", 403);
24012
- const restored = restoreTrashPath(originalPath, trashPath || undefined);
25721
+ const restored = await restoreTrashPath(originalPath, trashPath || undefined);
24013
25722
  if (!restored.ok)
24014
25723
  return text(restored.error || "undo failed", 409);
24015
25724
  triggerUpdate();
@@ -24143,7 +25852,7 @@ async function handleJournal(req) {
24143
25852
  const labels = bodyStringList(body, "labels");
24144
25853
  if (label)
24145
25854
  labels.push(label);
24146
- const issues = readGithubIssueList({
25855
+ const issues = await readGithubIssueListAsync({
24147
25856
  cwd,
24148
25857
  repo: bodyString(body, "repo"),
24149
25858
  labels,
@@ -24547,7 +26256,7 @@ function restartWorktreeWatch() {
24547
26256
  }
24548
26257
  worktreeWatch = startScopedWorktreeWatch();
24549
26258
  }
24550
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, commandOverrides, cwdWasExplicit = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, safePath, MCP_MAX_BODY_BYTES = 1048576, MCP_INSTRUCTIONS, JournalRequestError, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
26259
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, commandOverrides, cwdWasExplicit = false, cwdHasGitRepository = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, diffMetaRequestSequence = 0, latestDiffMetaRequest, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, safePath, MCP_MAX_BODY_BYTES = 1048576, MCP_INSTRUCTIONS, JournalRequestError, isCodeViewerInternalPath, watchLimitReached = null, databaseHandleModule, server, worktreeWatch = null, shuttingDown = false;
24551
26260
  var init_preview = __esm(async () => {
24552
26261
  init_journal();
24553
26262
  init_routes();
@@ -24625,6 +26334,7 @@ var init_preview = __esm(async () => {
24625
26334
  fileCache = new Map;
24626
26335
  blameCache = new Map;
24627
26336
  metaCache = new Map;
26337
+ latestDiffMetaRequest = new Map;
24628
26338
  fileListCache = new Map;
24629
26339
  lineIndexCache = new Map;
24630
26340
  blobLineIndexCache = new Map;
@@ -24641,6 +26351,7 @@ var init_preview = __esm(async () => {
24641
26351
  isCodeViewerInternalPath = isToolInternalPath;
24642
26352
  parseCli();
24643
26353
  applyPersistedSettings(await loadAppSettingsState(cwd));
26354
+ databaseHandleModule = Promise.resolve().then(() => (init_handle(), exports_handle));
24644
26355
  server = await startServer({
24645
26356
  hostname: "127.0.0.1",
24646
26357
  port: listenPort,
@@ -24652,9 +26363,9 @@ var init_preview = __esm(async () => {
24652
26363
  if (staticResponse)
24653
26364
  return staticResponse;
24654
26365
  if (url.pathname === "/diff.json")
24655
- return handleDiffJson(url);
26366
+ return await handleDiffJson(url);
24656
26367
  if (url.pathname === "/_settings")
24657
- return handleSettings();
26368
+ return await handleSettings();
24658
26369
  if (url.pathname === "/_doctor")
24659
26370
  return handleDoctor({
24660
26371
  cwd,
@@ -24662,23 +26373,23 @@ var init_preview = __esm(async () => {
24662
26373
  listenPort
24663
26374
  });
24664
26375
  if (url.pathname === "/_tree")
24665
- return handleTree(url);
26376
+ return await handleTree(url);
24666
26377
  if (url.pathname === "/_files")
24667
- return handleFiles2(url);
26378
+ return await handleFiles2(url);
24668
26379
  if (url.pathname === "/_grep")
24669
- return handleGrep(url);
26380
+ return await handleGrep(url);
24670
26381
  if (url.pathname === "/_commits")
24671
- return handleRefCommits(url);
26382
+ return await handleRefCommits(url);
24672
26383
  if (url.pathname === "/_log")
24673
- return handleLog(url);
26384
+ return await handleLog(url);
24674
26385
  if (url.pathname === "/_file_blame")
24675
- return handleFileBlame(url);
26386
+ return await handleFileBlame(url);
24676
26387
  if (url.pathname === "/file_diff")
24677
- return handleFileDiff(url);
26388
+ return await handleFileDiff(url);
24678
26389
  if (url.pathname === "/file_range")
24679
26390
  return handleFileRange(url);
24680
26391
  if (url.pathname === "/_file")
24681
- return handleRawFile(req, url);
26392
+ return await handleRawFile(req, url);
24682
26393
  if (url.pathname === "/_open_path")
24683
26394
  return handleOpenPath(req);
24684
26395
  if (url.pathname === "/_trash_path")
@@ -24690,7 +26401,7 @@ var init_preview = __esm(async () => {
24690
26401
  if (url.pathname === "/_upload_files")
24691
26402
  return handleUploadFiles(req);
24692
26403
  if (url.pathname.startsWith("/_db/")) {
24693
- const { handleDatabaseRoute: handleDatabaseRoute2 } = await Promise.resolve().then(() => (init_handle(), exports_handle));
26404
+ const { handleDatabaseRoute: handleDatabaseRoute2 } = await databaseHandleModule;
24694
26405
  const dbResponse = await handleDatabaseRoute2(req, url, cwd, scopeOmitDirNames, sideEffectRequestAllowed, sendSse);
24695
26406
  if (dbResponse)
24696
26407
  return dbResponse;
@@ -24708,7 +26419,7 @@ var init_preview = __esm(async () => {
24708
26419
  if (url.pathname === "/_annotations")
24709
26420
  return handleAnnotations(req);
24710
26421
  if (url.pathname === "/_refs") {
24711
- const result = refsResult(cwd);
26422
+ const result = await refsResultAsync(cwd);
24712
26423
  if (result.error)
24713
26424
  return text(result.error, result.status ?? 500);
24714
26425
  return json2(result.refs);