@youtyan/code-viewer 0.6.1 → 0.6.3

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.
@@ -659,6 +659,175 @@ var init_annotations = __esm(() => {
659
659
  });
660
660
  });
661
661
 
662
+ // web-src/server/command-resolver.ts
663
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
664
+ import { dirname as dirname2, isAbsolute, join as join2, relative } from "node:path";
665
+ function isExternalCommandName(value) {
666
+ return commandNameSet.has(value);
667
+ }
668
+ function parseExternalCommandOverride(raw, flag = "--bin", allowedNames = EXTERNAL_COMMAND_NAMES) {
669
+ const eq = raw.indexOf("=");
670
+ if (eq <= 0 || eq === raw.length - 1) {
671
+ return { ok: false, error: `${flag} requires <name>=<absolute-path>` };
672
+ }
673
+ const name = raw.slice(0, eq).trim();
674
+ const path = raw.slice(eq + 1);
675
+ if (!isExternalCommandName(name)) {
676
+ return {
677
+ ok: false,
678
+ error: `${flag} unsupported command: ${name}`
679
+ };
680
+ }
681
+ if (!allowedNames.includes(name)) {
682
+ return {
683
+ ok: false,
684
+ error: `${flag} unsupported command: ${name}`
685
+ };
686
+ }
687
+ return { ok: true, override: { name, path } };
688
+ }
689
+ function configureExternalCommands(opts) {
690
+ const env = opts.env ?? process.env;
691
+ const allowedNames = opts.allowedNames ?? EXTERNAL_COMMAND_NAMES;
692
+ const selected = new Map;
693
+ for (const name of allowedNames) {
694
+ const value = env[envNameForCommand(name)];
695
+ if (value)
696
+ selected.set(name, { path: value, source: "env" });
697
+ }
698
+ for (const override of opts.cliOverrides ?? []) {
699
+ if (!allowedNames.includes(override.name)) {
700
+ return {
701
+ ok: false,
702
+ error: `--bin unsupported command: ${override.name}`
703
+ };
704
+ }
705
+ selected.set(override.name, { path: override.path, source: "cli" });
706
+ }
707
+ if (selected.size === 0) {
708
+ activeOverrides.clear();
709
+ return { ok: true };
710
+ }
711
+ const roots = forbiddenExecutableRoots(opts.cwd);
712
+ if ("error" in roots)
713
+ return { ok: false, error: roots.error };
714
+ const resolved = new Map;
715
+ for (const [name, value] of selected) {
716
+ const validated = validateExecutablePath(value.path, roots.roots);
717
+ if ("error" in validated) {
718
+ return {
719
+ ok: false,
720
+ error: `${sourceLabel(value.source)} ${name}: ${validated.error}`
721
+ };
722
+ }
723
+ resolved.set(name, {
724
+ path: value.path,
725
+ realPath: validated.realPath,
726
+ source: value.source
727
+ });
728
+ }
729
+ activeOverrides.clear();
730
+ for (const [name, value] of resolved)
731
+ activeOverrides.set(name, value);
732
+ return { ok: true };
733
+ }
734
+ function commandForExternal(name) {
735
+ return activeOverrides.get(name)?.realPath ?? name;
736
+ }
737
+ function isCommandNotFoundResult(command, result) {
738
+ if (result.code === 0)
739
+ return false;
740
+ return isCommandNotFoundMessage(command, result.stderr || "");
741
+ }
742
+ function commandNotFoundDetail(command) {
743
+ const resolved = commandForExternal(command);
744
+ if (resolved === command)
745
+ return `${command} not found in PATH`;
746
+ return `${command} binary not found or not executable: ${resolved}`;
747
+ }
748
+ function envNameForCommand(name) {
749
+ return `CODE_VIEWER_BIN_${name.toUpperCase().replace(/-/g, "_")}`;
750
+ }
751
+ function sourceLabel(source) {
752
+ return source === "cli" ? "--bin" : "environment override";
753
+ }
754
+ function validateExecutablePath(raw, forbiddenRoots) {
755
+ if (!raw)
756
+ return { error: "path must not be empty" };
757
+ if (raw.includes("\x00") || /[\r\n]/.test(raw)) {
758
+ return { error: "path must be single-line and must not contain NUL" };
759
+ }
760
+ if (!isAbsolute(raw))
761
+ return { error: "path must be absolute" };
762
+ let realPath;
763
+ try {
764
+ realPath = realpathSync(raw);
765
+ const st = statSync(realPath);
766
+ if (!st.isFile())
767
+ return { error: "path must point to a file" };
768
+ accessSync(realPath, constants.X_OK);
769
+ } catch {
770
+ return { error: "path must point to an executable file" };
771
+ }
772
+ for (const root of forbiddenRoots) {
773
+ if (sameOrInside(realPath, root)) {
774
+ return {
775
+ error: "path must not point inside the current repository or working directory"
776
+ };
777
+ }
778
+ }
779
+ return { realPath };
780
+ }
781
+ function forbiddenExecutableRoots(cwd) {
782
+ let cwdReal;
783
+ try {
784
+ cwdReal = realpathSync(cwd);
785
+ } catch {
786
+ return { error: `--cwd must point to an existing directory: ${cwd}` };
787
+ }
788
+ const roots = [cwdReal];
789
+ const gitRoot = findGitRootByWalking(cwdReal);
790
+ if (gitRoot && !roots.some((root) => sameOrInside(gitRoot, root))) {
791
+ roots.push(gitRoot);
792
+ }
793
+ return { roots };
794
+ }
795
+ function findGitRootByWalking(start) {
796
+ let current = start;
797
+ for (;; ) {
798
+ try {
799
+ statSync(join2(current, ".git"));
800
+ return realpathSync(current);
801
+ } catch {}
802
+ const parent = dirname2(current);
803
+ if (parent === current)
804
+ return null;
805
+ current = parent;
806
+ }
807
+ }
808
+ function sameOrInside(path, root) {
809
+ const rel = relative(root, path);
810
+ return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
811
+ }
812
+ function isCommandNotFoundMessage(command, message) {
813
+ const lower = message.toLowerCase();
814
+ if (lower.includes("enoent"))
815
+ return true;
816
+ if (lower.includes(`spawn ${command.toLowerCase()}`))
817
+ return true;
818
+ if (lower.includes(`${command.toLowerCase()}: command not found`))
819
+ return true;
820
+ if (lower.includes(`${command.toLowerCase()}: not found`))
821
+ return true;
822
+ return false;
823
+ }
824
+ var EXTERNAL_COMMAND_NAMES, commandNameSet, activeOverrides;
825
+ var init_command_resolver = __esm(() => {
826
+ EXTERNAL_COMMAND_NAMES = ["git", "rg", "docker"];
827
+ commandNameSet = new Set(EXTERNAL_COMMAND_NAMES);
828
+ activeOverrides = new Map;
829
+ });
830
+
662
831
  // web-src/server/runtime.ts
663
832
  import { spawn, spawnSync } from "node:child_process";
664
833
  import { createReadStream, promises as fs } from "node:fs";
@@ -677,7 +846,7 @@ function runSync(args, cwd, options = {}) {
677
846
  return {
678
847
  code: proc.status ?? (proc.error ? 1 : 0),
679
848
  stdout: new TextDecoder().decode(proc.stdout || new Uint8Array),
680
- stderr: new TextDecoder().decode(proc.stderr || new Uint8Array)
849
+ stderr: appendProcessError(new TextDecoder().decode(proc.stderr || new Uint8Array), proc.error)
681
850
  };
682
851
  }
683
852
  function runBytesSync(args, cwd, options = {}) {
@@ -691,7 +860,7 @@ function runBytesSync(args, cwd, options = {}) {
691
860
  return {
692
861
  code: proc.status ?? (proc.error ? 1 : 0),
693
862
  stdout: new Uint8Array(proc.stdout || new Uint8Array),
694
- stderr: new TextDecoder().decode(proc.stderr || new Uint8Array)
863
+ stderr: appendProcessError(new TextDecoder().decode(proc.stderr || new Uint8Array), proc.error)
695
864
  };
696
865
  }
697
866
  function spawnDetached(args) {
@@ -699,6 +868,9 @@ function spawnDetached(args) {
699
868
  detached: true,
700
869
  stdio: "ignore"
701
870
  });
871
+ child.on("error", (err) => {
872
+ console.warn("[code-viewer] failed to start detached command:", err.message);
873
+ });
702
874
  child.unref();
703
875
  }
704
876
  function spawnStream(args, cwd) {
@@ -706,12 +878,32 @@ function spawnStream(args, cwd) {
706
878
  cwd,
707
879
  stdio: ["ignore", "pipe", "ignore"]
708
880
  });
881
+ let errorCode = 0;
882
+ proc.on("error", () => {
883
+ errorCode = 1;
884
+ });
709
885
  return {
710
886
  stream: Readable.toWeb(proc.stdout),
711
- exited: new Promise((resolve) => proc.on("close", (code) => resolve(code ?? 1))),
887
+ exited: new Promise((resolve) => {
888
+ let settled = false;
889
+ const done = (code) => {
890
+ if (settled)
891
+ return;
892
+ settled = true;
893
+ resolve(code);
894
+ };
895
+ proc.on("error", () => done(1));
896
+ proc.on("close", (code) => done(errorCode || (code ?? 1)));
897
+ }),
712
898
  kill: (signal) => proc.kill(signal)
713
899
  };
714
900
  }
901
+ function appendProcessError(stderr, err) {
902
+ if (!err)
903
+ return stderr;
904
+ return `${stderr}${stderr ? `
905
+ ` : ""}${err.message}`;
906
+ }
715
907
  function fileReadableStream(path) {
716
908
  return Readable.toWeb(createReadStream(path));
717
909
  }
@@ -847,9 +1039,9 @@ import {
847
1039
  lstatSync,
848
1040
  readdirSync,
849
1041
  readFileSync,
850
- statSync
1042
+ statSync as statSync2
851
1043
  } from "node:fs";
852
- import { join as join2 } from "node:path";
1044
+ import { join as join3 } from "node:path";
853
1045
  function normalizeBlameRef(ref, base) {
854
1046
  const rawRef = ref || "worktree";
855
1047
  if (base === "worktree" && rawRef !== "worktree") {
@@ -861,19 +1053,75 @@ function normalizeBlameRef(ref, base) {
861
1053
  return { base, ref: rawRef };
862
1054
  }
863
1055
  function run(args, cwd) {
864
- return runSync(args, cwd);
1056
+ return runSync(resolveGitArgs(args), cwd);
865
1057
  }
866
1058
  function runBytes(args, cwd) {
867
- return runBytesSync(args, cwd);
1059
+ return runBytesSync(resolveGitArgs(args), cwd);
1060
+ }
1061
+ function resolveGitArgs(args) {
1062
+ if (args[0] !== "git")
1063
+ return args;
1064
+ return [commandForExternal("git"), ...args.slice(1)];
1065
+ }
1066
+ function gitFailureMessage(res, fallback) {
1067
+ if (isCommandNotFoundResult("git", res))
1068
+ return commandNotFoundDetail("git");
1069
+ return res.stderr?.trim() || fallback;
1070
+ }
1071
+ function gitFailureResult(res, fallback) {
1072
+ if (!isCommandNotFoundResult("git", res)) {
1073
+ return { error: fallback };
1074
+ }
1075
+ return {
1076
+ error: commandNotFoundDetail("git"),
1077
+ status: 503
1078
+ };
868
1079
  }
869
1080
  function repoRoot(cwd) {
870
1081
  const res = run(["git", "rev-parse", "--show-toplevel"], cwd);
871
1082
  return res.code === 0 ? res.stdout.trimEnd() : null;
872
1083
  }
1084
+ function repoRootResult(cwd) {
1085
+ const res = run(["git", "rev-parse", "--show-toplevel"], cwd);
1086
+ if (res.code === 0)
1087
+ return { kind: "root", root: res.stdout.trimEnd() };
1088
+ if (isCommandNotFoundResult("git", res)) {
1089
+ return { kind: "error", error: commandNotFoundDetail("git") };
1090
+ }
1091
+ const stderr = res.stderr.trim();
1092
+ if (/not a git repository/i.test(stderr))
1093
+ return { kind: "outside" };
1094
+ return { kind: "error", error: stderr || "git rev-parse failed" };
1095
+ }
873
1096
  function currentBranch(cwd) {
874
1097
  const res = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd);
875
1098
  return res.code === 0 ? res.stdout.trimEnd() : null;
876
1099
  }
1100
+ function verifyCommit(ref, cwd) {
1101
+ const res = run(["git", "rev-parse", "--verify", `${ref}^{commit}`], cwd);
1102
+ if (res.code === 0)
1103
+ return { ok: true, sha: res.stdout.trim() };
1104
+ return { ok: false, error: gitFailureMessage(res, "unknown ref") };
1105
+ }
1106
+ function statusPorcelainForPath(path, cwd) {
1107
+ const res = run([
1108
+ "git",
1109
+ "-c",
1110
+ "core.quotepath=false",
1111
+ "status",
1112
+ "--porcelain=v1",
1113
+ "-z",
1114
+ "--untracked-files=normal",
1115
+ "--",
1116
+ path
1117
+ ], cwd);
1118
+ if (res.code === 0)
1119
+ return { ok: true, stdout: res.stdout };
1120
+ return {
1121
+ ok: false,
1122
+ error: gitFailureMessage(res, "git status failed")
1123
+ };
1124
+ }
877
1125
  function show(ref, path, cwd) {
878
1126
  return run(["git", "show", `${ref}:${path}`], cwd);
879
1127
  }
@@ -881,7 +1129,7 @@ function showBytes(ref, path, cwd) {
881
1129
  return runBytes(["git", "show", `${ref}:${path}`], cwd);
882
1130
  }
883
1131
  function catFileBlobStream(oid, cwd) {
884
- return spawnStream(["git", "cat-file", "blob", oid], cwd);
1132
+ return spawnStream(resolveGitArgs(["git", "cat-file", "blob", oid]), cwd);
885
1133
  }
886
1134
  function objectSize(ref, path, cwd) {
887
1135
  const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
@@ -917,14 +1165,19 @@ function objectId(ref, path, cwd) {
917
1165
  return { code: 0, oid, stderr: "" };
918
1166
  }
919
1167
  function verifyTreeRef(ref, cwd) {
1168
+ return verifyTreeRefResult(ref, cwd).ok;
1169
+ }
1170
+ function verifyTreeRefResult(ref, cwd) {
920
1171
  if (!ref || ref === "worktree")
921
- return false;
1172
+ return { ok: false, error: "invalid target", status: 400 };
922
1173
  if (ref.startsWith("-"))
923
- return false;
1174
+ return { ok: false, error: "invalid target", status: 400 };
924
1175
  const res = run(["git", "rev-parse", "--verify", `${ref}^{tree}`], cwd);
925
- return res.code === 0;
1176
+ if (res.code === 0)
1177
+ return { ok: true };
1178
+ return { ok: false, ...gitFailureResult(res, "invalid target") };
926
1179
  }
927
- function refs(cwd) {
1180
+ function refsResult(cwd) {
928
1181
  const out = {
929
1182
  branches: [],
930
1183
  tags: [],
@@ -939,6 +1192,9 @@ function refs(cwd) {
939
1192
  "refs/heads",
940
1193
  "refs/remotes"
941
1194
  ], cwd);
1195
+ if (branches.code !== 0 && isCommandNotFoundResult("git", branches)) {
1196
+ return { refs: out, ...gitFailureResult(branches, "git refs failed") };
1197
+ }
942
1198
  if (branches.code === 0) {
943
1199
  for (const line of branches.stdout.split(`
944
1200
  `)) {
@@ -955,6 +1211,9 @@ function refs(cwd) {
955
1211
  "--format=%(refname:short)%09%(creatordate:iso-strict)",
956
1212
  "refs/tags"
957
1213
  ], cwd);
1214
+ if (tags.code !== 0 && isCommandNotFoundResult("git", tags)) {
1215
+ return { refs: out, ...gitFailureResult(tags, "git refs failed") };
1216
+ }
958
1217
  if (tags.code === 0) {
959
1218
  for (const line of tags.stdout.split(`
960
1219
  `)) {
@@ -964,9 +1223,16 @@ function refs(cwd) {
964
1223
  out.tags.push({ name, when });
965
1224
  }
966
1225
  }
967
- out.commits = refCommits(cwd, "", DEFAULT_REF_COMMIT_LIMIT);
1226
+ const commits = refCommitPageResult(cwd, {
1227
+ query: "",
1228
+ max: DEFAULT_REF_COMMIT_LIMIT
1229
+ });
1230
+ if (commits.error) {
1231
+ return { refs: out, error: commits.error, status: commits.status };
1232
+ }
1233
+ out.commits = commits.commits;
968
1234
  out.current = currentBranch(cwd) || "";
969
- return out;
1235
+ return { refs: out };
970
1236
  }
971
1237
  function clampCommitLimit(max) {
972
1238
  return Math.max(1, Math.min(max, MAX_REF_COMMIT_LIMIT));
@@ -1019,14 +1285,15 @@ function mergeCommitResults(limit, ...groups) {
1019
1285
  }
1020
1286
  return merged;
1021
1287
  }
1022
- function runCommitLog(cwd, args) {
1288
+ function runCommitLogResult(cwd, args) {
1023
1289
  const commits = run(args, cwd);
1024
- return commits.code === 0 ? parseCommitLog(commits.stdout) : [];
1290
+ if (commits.code === 0)
1291
+ return { commits: parseCommitLog(commits.stdout) };
1292
+ if (isCommandNotFoundResult("git", commits))
1293
+ return { commits: [], ...gitFailureResult(commits, "git log failed") };
1294
+ return { commits: [] };
1025
1295
  }
1026
- function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
1027
- return refCommitPage(cwd, { query, max }).commits;
1028
- }
1029
- function refCommitPage(cwd, options = {}) {
1296
+ function refCommitPageResult(cwd, options = {}) {
1030
1297
  const limit = clampCommitLimit(options.max ?? DEFAULT_REF_COMMIT_LIMIT);
1031
1298
  const skip = clampCommitSkip(options.skip ?? 0);
1032
1299
  const fetchLimit = limit + 1;
@@ -1034,6 +1301,13 @@ function refCommitPage(cwd, options = {}) {
1034
1301
  const trimmed = (options.query || "").trim().slice(0, 200).replace(/\0/g, "");
1035
1302
  if (skip === 0 && /^[0-9a-f]{4,40}$/i.test(trimmed)) {
1036
1303
  const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
1304
+ if (verified.code !== 0 && isCommandNotFoundResult("git", verified)) {
1305
+ return {
1306
+ commits: [],
1307
+ hasMore: false,
1308
+ ...gitFailureResult(verified, "unknown ref")
1309
+ };
1310
+ }
1037
1311
  const single = run([
1038
1312
  "git",
1039
1313
  "log",
@@ -1042,30 +1316,62 @@ function refCommitPage(cwd, options = {}) {
1042
1316
  `--format=${COMMIT_FORMAT}`,
1043
1317
  verified.code === 0 && verified.stdout.trim() ? verified.stdout.trim() : trimmed
1044
1318
  ], cwd);
1319
+ if (single.code !== 0 && isCommandNotFoundResult("git", single)) {
1320
+ return {
1321
+ commits: [],
1322
+ hasMore: false,
1323
+ ...gitFailureResult(single, "git log failed")
1324
+ };
1325
+ }
1045
1326
  if (single.code === 0 && single.stdout.trim()) {
1046
1327
  hashMatches.push(...parseCommitLog(single.stdout));
1047
1328
  }
1048
1329
  }
1049
1330
  if (!trimmed) {
1050
- const commits = runCommitLog(cwd, commitLogArgs(fetchLimit, skip));
1331
+ const result = runCommitLogResult(cwd, commitLogArgs(fetchLimit, skip));
1332
+ if (result.error) {
1333
+ return {
1334
+ commits: [],
1335
+ hasMore: false,
1336
+ error: result.error,
1337
+ status: result.status
1338
+ };
1339
+ }
1340
+ const commits = result.commits;
1051
1341
  return {
1052
1342
  commits: commits.slice(0, limit),
1053
1343
  hasMore: commits.length > limit
1054
1344
  };
1055
1345
  }
1056
- const subjectMatches = runCommitLog(cwd, [
1346
+ const subjectMatches = runCommitLogResult(cwd, [
1057
1347
  ...commitLogArgs(fetchLimit, skip),
1058
1348
  "--regexp-ignore-case",
1059
1349
  "--fixed-strings",
1060
1350
  `--grep=${trimmed}`
1061
1351
  ]);
1062
- const authorMatches = runCommitLog(cwd, [
1352
+ if (subjectMatches.error) {
1353
+ return {
1354
+ commits: [],
1355
+ hasMore: false,
1356
+ error: subjectMatches.error,
1357
+ status: subjectMatches.status
1358
+ };
1359
+ }
1360
+ const authorMatches = runCommitLogResult(cwd, [
1063
1361
  ...commitLogArgs(fetchLimit, skip),
1064
1362
  "--regexp-ignore-case",
1065
1363
  "--fixed-strings",
1066
1364
  `--author=${trimmed}`
1067
1365
  ]);
1068
- const merged = mergeCommitResults(fetchLimit, hashMatches, subjectMatches, authorMatches);
1366
+ if (authorMatches.error) {
1367
+ return {
1368
+ commits: [],
1369
+ hasMore: false,
1370
+ error: authorMatches.error,
1371
+ status: authorMatches.status
1372
+ };
1373
+ }
1374
+ const merged = mergeCommitResults(fetchLimit, hashMatches, subjectMatches.commits, authorMatches.commits);
1069
1375
  return {
1070
1376
  commits: merged.slice(0, limit),
1071
1377
  hasMore: merged.length > limit
@@ -1160,7 +1466,11 @@ function commitHistory(cwd, options) {
1160
1466
  return { commits: [], hasMore: false, error: "invalid ref" };
1161
1467
  const verified = run(["git", "rev-parse", "--verify", `${ref}^{commit}`], cwd);
1162
1468
  if (verified.code !== 0)
1163
- return { commits: [], hasMore: false, error: "unknown ref" };
1469
+ return {
1470
+ commits: [],
1471
+ hasMore: false,
1472
+ ...gitFailureResult(verified, "unknown ref")
1473
+ };
1164
1474
  const skip = Math.max(0, Math.floor(options.skip) || 0);
1165
1475
  const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
1166
1476
  const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
@@ -1184,7 +1494,11 @@ function commitHistory(cwd, options) {
1184
1494
  ...pathArgs
1185
1495
  ], cwd);
1186
1496
  if (res.code !== 0)
1187
- return { commits: [], hasMore: false, error: "git log failed" };
1497
+ return {
1498
+ commits: [],
1499
+ hasMore: false,
1500
+ ...gitFailureResult(res, "git log failed")
1501
+ };
1188
1502
  let parsed = parseHistoryLog(res.stdout);
1189
1503
  if (shaTerm && skip === 0) {
1190
1504
  const bySha = run(["git", "rev-parse", "--verify", `${shaTerm}^{commit}`], cwd);
@@ -1200,7 +1514,7 @@ function commitHistory(cwd, options) {
1200
1514
  const hasMore = parsed.length > limit;
1201
1515
  return { commits: hasMore ? parsed.slice(0, limit) : parsed, hasMore };
1202
1516
  }
1203
- function nameStatus(args, cwd) {
1517
+ function nameStatusResult(args, cwd) {
1204
1518
  const res = run([
1205
1519
  "git",
1206
1520
  "-c",
@@ -1213,8 +1527,12 @@ function nameStatus(args, cwd) {
1213
1527
  "-z",
1214
1528
  ...args
1215
1529
  ], cwd);
1216
- if (res.code !== 0)
1217
- return [];
1530
+ if (res.code !== 0) {
1531
+ return {
1532
+ files: [],
1533
+ error: gitFailureMessage(res, "git diff --name-status failed")
1534
+ };
1535
+ }
1218
1536
  const parts = res.stdout.split("\x00");
1219
1537
  const files = [];
1220
1538
  for (let i = 0;i < parts.length; ) {
@@ -1238,9 +1556,9 @@ function nameStatus(args, cwd) {
1238
1556
  files.push({ status: kind, path });
1239
1557
  }
1240
1558
  }
1241
- return files;
1559
+ return { files };
1242
1560
  }
1243
- function numstatZ(args, cwd) {
1561
+ function numstatZResult(args, cwd) {
1244
1562
  const res = run([
1245
1563
  "git",
1246
1564
  "-c",
@@ -1253,8 +1571,12 @@ function numstatZ(args, cwd) {
1253
1571
  "-z",
1254
1572
  ...args
1255
1573
  ], cwd);
1256
- if (res.code !== 0)
1257
- return [];
1574
+ if (res.code !== 0) {
1575
+ return {
1576
+ files: [],
1577
+ error: gitFailureMessage(res, "git diff --numstat failed")
1578
+ };
1579
+ }
1258
1580
  const parts = res.stdout.split("\x00");
1259
1581
  const files = [];
1260
1582
  for (let i = 0;i < parts.length; ) {
@@ -1277,7 +1599,7 @@ function numstatZ(args, cwd) {
1277
1599
  files.push({ path: rest, additions, deletions, binary });
1278
1600
  }
1279
1601
  }
1280
- return files;
1602
+ return { files };
1281
1603
  }
1282
1604
  function pathHasSegment(path, segment) {
1283
1605
  const target = segment.toLowerCase();
@@ -1290,9 +1612,9 @@ function isGitInternalPath(path) {
1290
1612
  return pathHasSegment(path, ".git");
1291
1613
  }
1292
1614
  function syntheticUncommittedBlameFromWorktree(cwd, path) {
1293
- const filePath = join2(cwd, path);
1615
+ const filePath = join3(cwd, path);
1294
1616
  try {
1295
- const stat = statSync(filePath);
1617
+ const stat = statSync2(filePath);
1296
1618
  if (!stat.isFile())
1297
1619
  return { lines: [], commits: {}, error: "not a file" };
1298
1620
  const text = readFileSync(filePath, "utf8");
@@ -1344,6 +1666,14 @@ function blame(cwd, options) {
1344
1666
  args.push("--", path);
1345
1667
  const res = run(args, cwd);
1346
1668
  if (res.code !== 0) {
1669
+ if (isCommandNotFoundResult("git", res)) {
1670
+ return {
1671
+ lines: [],
1672
+ commits: {},
1673
+ error: commandNotFoundDetail("git"),
1674
+ status: 503
1675
+ };
1676
+ }
1347
1677
  if (normalized.base === "worktree") {
1348
1678
  return syntheticUncommittedBlameFromWorktree(cwd, path);
1349
1679
  }
@@ -1438,7 +1768,19 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
1438
1768
  return "internal";
1439
1769
  return omitDirNames.has(name) ? "heavy" : undefined;
1440
1770
  }
1441
- function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
1771
+ function worktreeSubmodulePaths(cwd) {
1772
+ if (!existsSync(join3(cwd, ".gitmodules")))
1773
+ return new Set;
1774
+ const res = run(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
1775
+ if (res.code !== 0)
1776
+ return new Set;
1777
+ return new Set(res.stdout.split(`
1778
+ `).map((line) => {
1779
+ const split = line.indexOf(" ");
1780
+ return split >= 0 ? normalizeTreePath(line.slice(split + 1)) : "";
1781
+ }).filter(Boolean));
1782
+ }
1783
+ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames, submodulePaths) {
1442
1784
  if (excludeNames.has(name.toLowerCase()))
1443
1785
  return {
1444
1786
  name,
@@ -1446,25 +1788,26 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
1446
1788
  type: isDirectory ? "tree" : "blob"
1447
1789
  };
1448
1790
  const entryPath = base ? `${base}/${name}` : name;
1449
- const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
1791
+ const type = isDirectory ? hasDotGitEntry(join3(dir, name)) ? "commit" : "tree" : "blob";
1450
1792
  const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
1793
+ const submodule = type === "commit" && submodulePaths.has(entryPath) ? true : undefined;
1794
+ const baseEntry = submodule ? { name, path: entryPath, type, submodule } : { name, path: entryPath, type };
1451
1795
  return omittedReason ? {
1452
- name,
1453
- path: entryPath,
1454
- type,
1796
+ ...baseEntry,
1455
1797
  children_omitted: true,
1456
1798
  children_omitted_reason: omittedReason
1457
- } : { name, path: entryPath, type };
1799
+ } : baseEntry;
1458
1800
  }
1459
1801
  function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
1460
1802
  const base = normalizeTreePath(path);
1461
- const root = join2(cwd, base);
1803
+ const root = join3(cwd, base);
1462
1804
  const omitDirNameSet = new Set(omitDirNames);
1463
1805
  const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
1806
+ const submodulePaths = worktreeSubmodulePaths(cwd);
1464
1807
  let directEntries;
1465
1808
  try {
1466
1809
  const dirents = readdirSync(root, { withFileTypes: true });
1467
- directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet)).filter((entry) => entry.path));
1810
+ directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
1468
1811
  } catch {
1469
1812
  return [];
1470
1813
  }
@@ -1504,7 +1847,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
1504
1847
  if (excludeNameSet.has(entry.name.toLowerCase()))
1505
1848
  continue;
1506
1849
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1507
- const full = join2(dir, entry.name);
1850
+ const full = join3(dir, entry.name);
1508
1851
  if (entry.isDirectory()) {
1509
1852
  const omittedReason = omittedWorktreeDirectoryReason(entry.name, omitDirNameSet);
1510
1853
  if (omittedReason) {
@@ -1536,7 +1879,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
1536
1879
  }
1537
1880
  function hasDotGitEntry(dir) {
1538
1881
  try {
1539
- lstatSync(join2(dir, ".git"));
1882
+ lstatSync(join3(dir, ".git"));
1540
1883
  return true;
1541
1884
  } catch (err) {
1542
1885
  return !!err && typeof err === "object" && "code" in err && err.code !== "ENOENT";
@@ -1599,14 +1942,20 @@ function listTree(ref, path, cwd, options = {}) {
1599
1942
  stderr: ""
1600
1943
  };
1601
1944
  }
1945
+ function listTreeResult(ref, path, cwd, options = {}) {
1946
+ const result = listTree(ref, path, cwd, options);
1947
+ if (result.code === 0)
1948
+ return { entries: result.entries };
1949
+ return { entries: [], ...gitFailureResult(result, "git ls-tree failed") };
1950
+ }
1602
1951
  function untrackedMeta(cwd) {
1603
1952
  return untracked(cwd).flatMap((path) => {
1604
- const full = join2(cwd, path);
1953
+ const full = join3(cwd, path);
1605
1954
  let binary = false;
1606
1955
  let lines = 0;
1607
1956
  let fileExists = false;
1608
1957
  try {
1609
- fileExists = existsSync(full) && statSync(full).isFile();
1958
+ fileExists = existsSync(full) && statSync2(full).isFile();
1610
1959
  } catch {
1611
1960
  fileExists = false;
1612
1961
  }
@@ -1632,11 +1981,15 @@ function untrackedMeta(cwd) {
1632
1981
  ];
1633
1982
  });
1634
1983
  }
1635
- function fileMeta(args, cwd, includeUntracked = false) {
1636
- const ns = nameStatus(args, cwd);
1637
- const nm = numstatZ(args, cwd);
1638
- const byPath = new Map(nm.map((file) => [file.path, file]));
1639
- const files = ns.map((file) => {
1984
+ function fileMetaResult(args, cwd, includeUntracked = false) {
1985
+ const ns = nameStatusResult(args, cwd);
1986
+ if (ns.error)
1987
+ return { files: [], error: ns.error };
1988
+ const nm = numstatZResult(args, cwd);
1989
+ if (nm.error)
1990
+ return { files: [], error: nm.error };
1991
+ const byPath = new Map(nm.files.map((file) => [file.path, file]));
1992
+ const files = ns.files.map((file) => {
1640
1993
  const stats = byPath.get(file.path);
1641
1994
  return {
1642
1995
  ...file,
@@ -1645,11 +1998,13 @@ function fileMeta(args, cwd, includeUntracked = false) {
1645
1998
  binary: stats?.binary || false
1646
1999
  };
1647
2000
  });
1648
- return includeUntracked ? files.concat(untrackedMeta(cwd)) : files;
2001
+ return {
2002
+ files: includeUntracked ? files.concat(untrackedMeta(cwd)) : files
2003
+ };
1649
2004
  }
1650
2005
  function fileDiffText(args, path, cwd) {
1651
2006
  const paths = Array.isArray(path) ? path : [path];
1652
- return run([
2007
+ const res = run([
1653
2008
  "git",
1654
2009
  "-c",
1655
2010
  "core.quotepath=false",
@@ -1661,9 +2016,13 @@ function fileDiffText(args, path, cwd) {
1661
2016
  "--",
1662
2017
  ...paths
1663
2018
  ], cwd);
2019
+ if (isCommandNotFoundResult("git", res)) {
2020
+ return { ...res, stderr: commandNotFoundDetail("git"), status: 503 };
2021
+ }
2022
+ return res;
1664
2023
  }
1665
2024
  function untrackedFileDiff(extras, path, cwd) {
1666
- return run([
2025
+ const res = run([
1667
2026
  "git",
1668
2027
  "-c",
1669
2028
  "core.quotepath=false",
@@ -1675,6 +2034,10 @@ function untrackedFileDiff(extras, path, cwd) {
1675
2034
  "/dev/null",
1676
2035
  path
1677
2036
  ], cwd);
2037
+ if (isCommandNotFoundResult("git", res)) {
2038
+ return { ...res, stderr: commandNotFoundDetail("git"), status: 503 };
2039
+ }
2040
+ return res;
1678
2041
  }
1679
2042
  function splitHunks(diffText) {
1680
2043
  if (!diffText)
@@ -1751,6 +2114,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
1751
2114
  }
1752
2115
  var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
1753
2116
  var init_git = __esm(() => {
2117
+ init_command_resolver();
1754
2118
  init_runtime();
1755
2119
  DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
1756
2120
  "node_modules",
@@ -1807,16 +2171,16 @@ import {
1807
2171
  writeFileSync
1808
2172
  } from "node:fs";
1809
2173
  import { homedir } from "node:os";
1810
- import { join as join3 } from "node:path";
2174
+ import { join as join4 } from "node:path";
1811
2175
  function registryDir() {
1812
2176
  const override = process.env.CODE_VIEWER_TEST_SERVER_REGISTRY_DIR;
1813
2177
  if (override)
1814
2178
  return override;
1815
- return join3(homedir(), ".cache", "code-viewer", "servers");
2179
+ return join4(homedir(), ".cache", "code-viewer", "servers");
1816
2180
  }
1817
2181
  function serverRegistryFilePath(root) {
1818
2182
  const hash = createHash("sha256").update(root).digest("hex").slice(0, 16);
1819
- return join3(registryDir(), `${hash}.json`);
2183
+ return join4(registryDir(), `${hash}.json`);
1820
2184
  }
1821
2185
  function writeServerRegistry(entry) {
1822
2186
  try {
@@ -1857,7 +2221,7 @@ function removeServerRegistry(root, pid) {
1857
2221
  var init_server_registry = () => {};
1858
2222
 
1859
2223
  // web-src/server/cli-helpers.ts
1860
- import { realpathSync } from "node:fs";
2224
+ import { realpathSync as realpathSync2 } from "node:fs";
1861
2225
  function takeValue(argv, index, flag) {
1862
2226
  const value = argv[index + 1];
1863
2227
  if (value === undefined)
@@ -1905,14 +2269,21 @@ function validateRepoRelativePathValue(value, flag) {
1905
2269
  }
1906
2270
  function resolveRepoRootSafe(cwdOption) {
1907
2271
  const base = cwdOption || process.cwd();
2272
+ let baseReal;
1908
2273
  try {
1909
- return { ok: true, root: repoRoot(base) || realpathSync(base) };
2274
+ baseReal = realpathSync2(base);
1910
2275
  } catch {
1911
2276
  return {
1912
2277
  ok: false,
1913
2278
  error: `--cwd must point to an existing directory: ${base}`
1914
2279
  };
1915
2280
  }
2281
+ const root = repoRootResult(baseReal);
2282
+ if (root.kind === "root")
2283
+ return { ok: true, root: root.root };
2284
+ if (root.kind === "error")
2285
+ return { ok: false, error: root.error };
2286
+ return { ok: true, root: baseReal };
1916
2287
  }
1917
2288
  function resolveRepoRoot(cwdOption) {
1918
2289
  const result = resolveRepoRootSafe(cwdOption);
@@ -3011,8 +3382,8 @@ __export(exports_file_cli, {
3011
3382
  FILE_DEFAULT_HISTORY_LIMIT: () => FILE_DEFAULT_HISTORY_LIMIT,
3012
3383
  FILE_AGENT_HELP: () => FILE_AGENT_HELP
3013
3384
  });
3014
- import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
3015
- import { join as join4, relative } from "node:path";
3385
+ import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
3386
+ import { join as join5, relative as relative2 } from "node:path";
3016
3387
  function validatePath(value) {
3017
3388
  return validateRepoRelativePathValue(value, "--path");
3018
3389
  }
@@ -3030,6 +3401,7 @@ function parseFileArgs(argv) {
3030
3401
  let cwd;
3031
3402
  const options = new Map;
3032
3403
  const flags = new Set;
3404
+ const commandOverrides = [];
3033
3405
  for (let i = 0;i < argv.length; i++) {
3034
3406
  const arg = argv[i];
3035
3407
  if (arg === "--help" || arg === "-h") {
@@ -3041,6 +3413,17 @@ function parseFileArgs(argv) {
3041
3413
  return { ok: false, error: taken.error };
3042
3414
  cwd = taken.value;
3043
3415
  i = taken.next;
3416
+ } else if (arg === "--bin") {
3417
+ const taken = takeValue(argv, i, arg);
3418
+ if ("error" in taken)
3419
+ return { ok: false, error: taken.error };
3420
+ const parsed = parseExternalCommandOverride(taken.value, "--bin", [
3421
+ "git"
3422
+ ]);
3423
+ if (parsed.ok === false)
3424
+ return { ok: false, error: parsed.error };
3425
+ commandOverrides.push(parsed.override);
3426
+ i = taken.next;
3044
3427
  } else if (VALUE_FLAGS.has(arg)) {
3045
3428
  const taken = takeValue(argv, i, arg);
3046
3429
  if ("error" in taken)
@@ -3102,7 +3485,8 @@ function parseFileArgs(argv) {
3102
3485
  base,
3103
3486
  json
3104
3487
  },
3105
- cwd
3488
+ cwd,
3489
+ ...commandOverrides.length ? { commandOverrides } : {}
3106
3490
  }
3107
3491
  };
3108
3492
  }
@@ -3140,7 +3524,8 @@ function parseFileArgs(argv) {
3140
3524
  query,
3141
3525
  json
3142
3526
  },
3143
- cwd
3527
+ cwd,
3528
+ ...commandOverrides.length ? { commandOverrides } : {}
3144
3529
  }
3145
3530
  };
3146
3531
  }
@@ -3179,7 +3564,8 @@ function parseFileArgs(argv) {
3179
3564
  end,
3180
3565
  json
3181
3566
  },
3182
- cwd
3567
+ cwd,
3568
+ ...commandOverrides.length ? { commandOverrides } : {}
3183
3569
  }
3184
3570
  };
3185
3571
  }
@@ -3268,7 +3654,8 @@ function parseFileArgs(argv) {
3268
3654
  maxLines,
3269
3655
  json
3270
3656
  },
3271
- cwd
3657
+ cwd,
3658
+ ...commandOverrides.length ? { commandOverrides } : {}
3272
3659
  }
3273
3660
  };
3274
3661
  }
@@ -3370,13 +3757,13 @@ function sliceLines(text, start, end) {
3370
3757
  function safeWorktreePathFromRoot(root, path) {
3371
3758
  if (validatePath(path))
3372
3759
  return null;
3373
- const full = join4(root, path);
3760
+ const full = join5(root, path);
3374
3761
  if (!existsSync3(full))
3375
3762
  return null;
3376
3763
  try {
3377
- const realRoot = realpathSync2(root);
3378
- const realFull = realpathSync2(full);
3379
- const rel = relative(realRoot, realFull);
3764
+ const realRoot = realpathSync3(root);
3765
+ const realFull = realpathSync3(full);
3766
+ const rel = relative2(realRoot, realFull);
3380
3767
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\")) {
3381
3768
  return null;
3382
3769
  }
@@ -3400,7 +3787,7 @@ function readShowText(root, command) {
3400
3787
  };
3401
3788
  }
3402
3789
  try {
3403
- const stat = statSync2(full);
3790
+ const stat = statSync3(full);
3404
3791
  if (!stat.isFile()) {
3405
3792
  return { code: 1, stdout: "", stderr: "not a file" };
3406
3793
  }
@@ -3453,12 +3840,12 @@ function runShow(root, command) {
3453
3840
  }
3454
3841
  }
3455
3842
  function buildFileDiffRangeArgs(from, to) {
3456
- const refs2 = [];
3843
+ const refs = [];
3457
3844
  if (from && from !== "worktree")
3458
- refs2.push(from);
3845
+ refs.push(from);
3459
3846
  if (to && to !== "worktree")
3460
- refs2.push(to);
3461
- return refs2;
3847
+ refs.push(to);
3848
+ return refs;
3462
3849
  }
3463
3850
  function buildFileDiffReport(root, command) {
3464
3851
  const base = {
@@ -3537,7 +3924,7 @@ async function runFileCli(argv) {
3537
3924
  console.error('Run "code-viewer file --help" for usage.');
3538
3925
  process.exit(1);
3539
3926
  }
3540
- const { command, cwd } = parsed.args;
3927
+ const { command, cwd, commandOverrides = [] } = parsed.args;
3541
3928
  if (command.kind === "help") {
3542
3929
  console.log(FILE_HELP);
3543
3930
  return;
@@ -3546,6 +3933,15 @@ async function runFileCli(argv) {
3546
3933
  console.log(FILE_AGENT_HELP);
3547
3934
  return;
3548
3935
  }
3936
+ const commandConfig = configureExternalCommands({
3937
+ cwd: cwd || process.cwd(),
3938
+ cliOverrides: commandOverrides,
3939
+ allowedNames: ["git"]
3940
+ });
3941
+ if (commandConfig.ok === false) {
3942
+ console.error(commandConfig.error);
3943
+ process.exit(1);
3944
+ }
3549
3945
  const root = resolveRepoRoot(cwd);
3550
3946
  if (command.kind === "blame")
3551
3947
  return runBlame(root, command);
@@ -3559,13 +3955,14 @@ async function runFileCli(argv) {
3559
3955
  var FILE_DEFAULT_HISTORY_LIMIT = 20, FILE_HISTORY_HARD_CAP = 200, FILE_DIFF_DEFAULT_MAX_HUNKS = 3, FILE_DIFF_DEFAULT_MAX_LINES = 1200, FILE_DIFF_HUNK_HARD_CAP = 100, FILE_DIFF_LINE_HARD_CAP = 1e5, FILE_HELP, FILE_AGENT_HELP, VALUE_FLAGS, BOOL_FLAGS, DEFAULT_BLAME_REF = "worktree", DEFAULT_BLAME_BASE = "worktree", DEFAULT_HISTORY_REF = "HEAD", DEFAULT_SHOW_REF = "worktree", DEFAULT_DIFF_FROM = "HEAD", DEFAULT_DIFF_TO = "worktree";
3560
3956
  var init_file_cli = __esm(() => {
3561
3957
  init_cli_helpers();
3958
+ init_command_resolver();
3562
3959
  init_git();
3563
3960
  FILE_HELP = `code-viewer file — inspect a path's blame, history, contents, or diff
3564
3961
 
3565
3962
  Usage:
3566
- code-viewer file blame --path <path> [--ref <ref>] [--base <worktree|HEAD>] [--json] [--cwd <dir>]
3567
- code-viewer file history --path <path> [--ref <ref>] [--limit <n>] [--skip <n>] [--query <text>] [--json] [--cwd <dir>]
3568
- code-viewer file show --path <path> [--ref <ref>] [--start <line>] [--end <line>] [--json] [--cwd <dir>]
3963
+ code-viewer file blame --path <path> [--ref <ref>] [--base <worktree|HEAD>] [--json] [--cwd <dir>] [--bin git=<path>]
3964
+ code-viewer file history --path <path> [--ref <ref>] [--limit <n>] [--skip <n>] [--query <text>] [--json] [--cwd <dir>] [--bin git=<path>]
3965
+ code-viewer file show --path <path> [--ref <ref>] [--start <line>] [--end <line>] [--json] [--cwd <dir>] [--bin git=<path>]
3569
3966
  code-viewer file diff --path <path> [--from <ref>] [--to <ref>] [--old-path <path>] [--untracked]
3570
3967
  [--ignore-ws] [--ignore-blank] [--max-hunks <n>] [--max-lines <n>] [--full] [--json] [--cwd <dir>]
3571
3968
  code-viewer file --help
@@ -3578,6 +3975,7 @@ Common options:
3578
3975
  HEAD. Single-line, no NUL, no leading "-". blame/show accept
3579
3976
  the literal "worktree" to mean the working tree.
3580
3977
  --cwd <dir> Repository to target (default: process.cwd()).
3978
+ --bin git=<p> Override git executable path.
3581
3979
  --json Emit a structured JSON payload instead of plain text.
3582
3980
  --help, -h Show this help.
3583
3981
 
@@ -3644,7 +4042,8 @@ Diff Viewer views.
3644
4042
  - Run from inside the repository, or pass --cwd <repo>.
3645
4043
  - No code-viewer server is required (this command reads git refs or the
3646
4044
  worktree directly).
3647
- - Git must be available on PATH.
4045
+ - Git must be available on PATH, or supplied with --bin git=/absolute/path
4046
+ / CODE_VIEWER_BIN_GIT.
3648
4047
 
3649
4048
  ## How to call
3650
4049
 
@@ -3889,6 +4288,7 @@ function parseQueryArgs(argv) {
3889
4288
  let server;
3890
4289
  const options = new Map;
3891
4290
  const flags = new Set;
4291
+ const commandOverrides = [];
3892
4292
  for (let i = 0;i < argv.length; i++) {
3893
4293
  const arg = argv[i];
3894
4294
  if (arg === "--help" || arg === "-h")
@@ -3902,6 +4302,17 @@ function parseQueryArgs(argv) {
3902
4302
  else
3903
4303
  server = taken.value;
3904
4304
  i = taken.next;
4305
+ } else if (arg === "--bin") {
4306
+ const taken = takeValue(argv, i, arg);
4307
+ if ("error" in taken)
4308
+ return { ok: false, error: taken.error };
4309
+ const parsed = parseExternalCommandOverride(taken.value, "--bin", [
4310
+ "git"
4311
+ ]);
4312
+ if (parsed.ok === false)
4313
+ return { ok: false, error: parsed.error };
4314
+ commandOverrides.push(parsed.override);
4315
+ i = taken.next;
3905
4316
  } else if (VALUE_FLAGS2.has(arg)) {
3906
4317
  const taken = takeValue(argv, i, arg);
3907
4318
  if ("error" in taken)
@@ -3919,7 +4330,11 @@ function parseQueryArgs(argv) {
3919
4330
  const subcommand = rest[0];
3920
4331
  if (!subcommand)
3921
4332
  return { ok: true, args: { command: { kind: "help" } } };
3922
- const globalArgs = { cwd, server };
4333
+ const globalArgs = {
4334
+ cwd,
4335
+ server,
4336
+ ...commandOverrides.length ? { commandOverrides } : {}
4337
+ };
3923
4338
  if (subcommand === "agent-help") {
3924
4339
  return { ok: true, args: { command: { kind: "agent-help" } } };
3925
4340
  }
@@ -4694,7 +5109,7 @@ async function runQueryCli(argv) {
4694
5109
  console.error('Run "code-viewer query --help" for usage.');
4695
5110
  process.exit(1);
4696
5111
  }
4697
- const { command, cwd, server } = parsed.args;
5112
+ const { command, cwd, server, commandOverrides = [] } = parsed.args;
4698
5113
  if (command.kind === "help") {
4699
5114
  console.log(QUERY_HELP);
4700
5115
  return;
@@ -4703,7 +5118,16 @@ async function runQueryCli(argv) {
4703
5118
  console.log(QUERY_AGENT_HELP);
4704
5119
  return;
4705
5120
  }
4706
- const root = resolveRepoRoot(cwd);
5121
+ const commandConfig = configureExternalCommands({
5122
+ cwd: cwd || process.cwd(),
5123
+ cliOverrides: commandOverrides,
5124
+ allowedNames: ["git"]
5125
+ });
5126
+ if (commandConfig.ok === false) {
5127
+ console.error(commandConfig.error);
5128
+ process.exit(1);
5129
+ }
5130
+ const root = server ? cwd || process.cwd() : resolveRepoRoot(cwd);
4707
5131
  const serverUrl = await ensureServerUrl(root, server, "/");
4708
5132
  if (command.kind === "sources")
4709
5133
  return runSources(serverUrl, command);
@@ -5548,6 +5972,8 @@ Usage:
5548
5972
  Global options:
5549
5973
  --cwd <dir> repository directory (default: current directory)
5550
5974
  --server <url> code-viewer server URL (default: auto-discovered)
5975
+ --bin git=<p> override the CLI-side git path used for server discovery.
5976
+ Server-side rg/docker paths are set when starting code-viewer.
5551
5977
 
5552
5978
  Examples:
5553
5979
  code-viewer query sources --json
@@ -5953,6 +6379,7 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
5953
6379
  var init_query_cli = __esm(() => {
5954
6380
  init_routes();
5955
6381
  init_cli_helpers();
6382
+ init_command_resolver();
5956
6383
  init_cli_helpers();
5957
6384
  VALUE_FLAGS2 = new Set([
5958
6385
  "--db",
@@ -6445,6 +6872,7 @@ function parseSearchArgs(argv) {
6445
6872
  const options = new Map;
6446
6873
  const paths = [];
6447
6874
  const flags = new Set;
6875
+ const commandOverrides = [];
6448
6876
  for (let i = 0;i < argv.length; i++) {
6449
6877
  const arg = argv[i];
6450
6878
  if (arg === "--help" || arg === "-h") {
@@ -6459,6 +6887,17 @@ function parseSearchArgs(argv) {
6459
6887
  else
6460
6888
  server = taken.value;
6461
6889
  i = taken.next;
6890
+ } else if (arg === "--bin") {
6891
+ const taken = takeValue(argv, i, arg);
6892
+ if ("error" in taken)
6893
+ return { ok: false, error: taken.error };
6894
+ const parsed = parseExternalCommandOverride(taken.value, "--bin", [
6895
+ "git"
6896
+ ]);
6897
+ if (parsed.ok === false)
6898
+ return { ok: false, error: parsed.error };
6899
+ commandOverrides.push(parsed.override);
6900
+ i = taken.next;
6462
6901
  } else if (REPEATABLE_VALUE_FLAGS.has(arg)) {
6463
6902
  const taken = takeValue(argv, i, arg);
6464
6903
  if ("error" in taken)
@@ -6546,7 +6985,8 @@ function parseSearchArgs(argv) {
6546
6985
  json: flags.has("--json")
6547
6986
  },
6548
6987
  cwd,
6549
- server
6988
+ server,
6989
+ ...commandOverrides.length ? { commandOverrides } : {}
6550
6990
  }
6551
6991
  };
6552
6992
  }
@@ -6581,7 +7021,8 @@ function parseSearchArgs(argv) {
6581
7021
  json: flags.has("--json")
6582
7022
  },
6583
7023
  cwd,
6584
- server
7024
+ server,
7025
+ ...commandOverrides.length ? { commandOverrides } : {}
6585
7026
  }
6586
7027
  };
6587
7028
  }
@@ -6673,7 +7114,7 @@ async function runSearchCli(argv) {
6673
7114
  console.error('Run "code-viewer search --help" for usage.');
6674
7115
  process.exit(1);
6675
7116
  }
6676
- const { command, cwd, server } = parsed.args;
7117
+ const { command, cwd, server, commandOverrides = [] } = parsed.args;
6677
7118
  if (command.kind === "help") {
6678
7119
  console.log(SEARCH_HELP);
6679
7120
  return;
@@ -6682,7 +7123,16 @@ async function runSearchCli(argv) {
6682
7123
  console.log(SEARCH_AGENT_HELP);
6683
7124
  return;
6684
7125
  }
6685
- const root = resolveRepoRoot(cwd);
7126
+ const commandConfig = configureExternalCommands({
7127
+ cwd: cwd || process.cwd(),
7128
+ cliOverrides: commandOverrides,
7129
+ allowedNames: ["git"]
7130
+ });
7131
+ if (commandConfig.ok === false) {
7132
+ console.error(commandConfig.error);
7133
+ process.exit(1);
7134
+ }
7135
+ const root = server ? cwd || process.cwd() : resolveRepoRoot(cwd);
6686
7136
  const serverUrl = await ensureServerUrl(root, server, "/");
6687
7137
  if (command.kind === "code")
6688
7138
  return runCode(serverUrl, command);
@@ -6692,15 +7142,16 @@ async function runSearchCli(argv) {
6692
7142
  var FILE_NAME_SEARCH_DEFAULT_MAX = 50, SEARCH_HELP, SEARCH_AGENT_HELP, VALUE_FLAGS3, REPEATABLE_VALUE_FLAGS, BOOL_FLAGS3;
6693
7143
  var init_search_cli = __esm(() => {
6694
7144
  init_cli_helpers();
7145
+ init_command_resolver();
6695
7146
  init_search();
6696
7147
  SEARCH_HELP = `code-viewer search — text and filename search across the worktree or a git ref
6697
7148
 
6698
7149
  Usage:
6699
7150
  code-viewer search code --term <text> [--ref <ref>] [--path <path>...]
6700
7151
  [--regex] [--max <n>] [--json]
6701
- [--cwd <dir>] [--server <url>]
7152
+ [--cwd <dir>] [--server <url>] [--bin <name>=<path>]
6702
7153
  code-viewer search files --term <pattern> [--ref <ref>] [--max <n>] [--json]
6703
- [--cwd <dir>] [--server <url>]
7154
+ [--cwd <dir>] [--server <url>] [--bin <name>=<path>]
6704
7155
  code-viewer search --help
6705
7156
  code-viewer search agent-help
6706
7157
 
@@ -6713,6 +7164,8 @@ Common options:
6713
7164
  --json Emit a structured JSON payload instead of plain lines.
6714
7165
  --cwd <dir> Repository to target (default: process.cwd()).
6715
7166
  --server <url> code-viewer server URL (default: auto-discover).
7167
+ --bin git=<p> Override the CLI-side git path used for server discovery.
7168
+ Server-side rg/docker paths are set when starting code-viewer.
6716
7169
  --help, -h Show this help.
6717
7170
 
6718
7171
  search code only:
@@ -6839,24 +7292,24 @@ Parse failures and unreachable servers exit 1.
6839
7292
 
6840
7293
  // web-src/server/root.ts
6841
7294
  import { existsSync as existsSync4 } from "node:fs";
6842
- import { dirname as dirname2, join as join5, normalize } from "node:path";
7295
+ import { dirname as dirname3, join as join6, normalize } from "node:path";
6843
7296
  import { fileURLToPath } from "node:url";
6844
7297
  function findRoot(start) {
6845
7298
  let current = start;
6846
7299
  for (let i = 0;i < 5; i++) {
6847
- if (existsSync4(join5(current, "package.json")) && existsSync4(join5(current, "web"))) {
7300
+ if (existsSync4(join6(current, "package.json")) && existsSync4(join6(current, "web"))) {
6848
7301
  return normalize(current);
6849
7302
  }
6850
- const parent = dirname2(current);
7303
+ const parent = dirname3(current);
6851
7304
  if (parent === current)
6852
7305
  break;
6853
7306
  current = parent;
6854
7307
  }
6855
- return normalize(join5(start, "..", ".."));
7308
+ return normalize(join6(start, "..", ".."));
6856
7309
  }
6857
7310
  var ROOT;
6858
7311
  var init_root = __esm(() => {
6859
- ROOT = findRoot(dirname2(fileURLToPath(import.meta.url)));
7312
+ ROOT = findRoot(dirname3(fileURLToPath(import.meta.url)));
6860
7313
  });
6861
7314
 
6862
7315
  // web-src/server/skill-cli.ts
@@ -6871,7 +7324,7 @@ __export(exports_skill_cli, {
6871
7324
  });
6872
7325
  import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
6873
7326
  import { homedir as homedir2 } from "node:os";
6874
- import { join as join6, resolve } from "node:path";
7327
+ import { join as join7, resolve } from "node:path";
6875
7328
  function parseAgentList(value) {
6876
7329
  if (value === "all")
6877
7330
  return [...AGENT_NAMES];
@@ -6931,7 +7384,7 @@ function parseSkillArgs(argv) {
6931
7384
  function discoverBundledSkills(skillsRoot) {
6932
7385
  if (!existsSync5(skillsRoot))
6933
7386
  return [];
6934
- return readdirSync2(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync5(join6(skillsRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
7387
+ return readdirSync2(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync5(join7(skillsRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
6935
7388
  }
6936
7389
  function installSkill(args, deps) {
6937
7390
  const skills = discoverBundledSkills(deps.skillsRoot);
@@ -6945,8 +7398,8 @@ function installSkill(args, deps) {
6945
7398
  const results = [];
6946
7399
  for (const agent of args.agents) {
6947
7400
  for (const skill of skills) {
6948
- const sourceDir = join6(deps.skillsRoot, skill);
6949
- const target = join6(base, AGENT_SKILL_DIRS[agent], "skills", skill);
7401
+ const sourceDir = join7(deps.skillsRoot, skill);
7402
+ const target = join7(base, AGENT_SKILL_DIRS[agent], "skills", skill);
6950
7403
  const action = existsSync5(target) ? "updated" : "installed";
6951
7404
  try {
6952
7405
  mkdirSync2(target, { recursive: true });
@@ -6975,7 +7428,7 @@ function runSkillCli(argv) {
6975
7428
  return;
6976
7429
  }
6977
7430
  const result = installSkill(parsed.args, {
6978
- skillsRoot: join6(ROOT, "skills"),
7431
+ skillsRoot: join7(ROOT, "skills"),
6979
7432
  homeDir: homedir2(),
6980
7433
  projectDir: process.cwd()
6981
7434
  });
@@ -7109,6 +7562,7 @@ function parseStatusArgs(argv) {
7109
7562
  const options = new Map;
7110
7563
  const flags = new Set;
7111
7564
  const positional = [];
7565
+ const commandOverrides = [];
7112
7566
  for (let i = 0;i < argv.length; i++) {
7113
7567
  const arg = argv[i];
7114
7568
  if (arg === "--help" || arg === "-h") {
@@ -7120,6 +7574,17 @@ function parseStatusArgs(argv) {
7120
7574
  return { ok: false, error: taken.error };
7121
7575
  cwd = taken.value;
7122
7576
  i = taken.next;
7577
+ } else if (arg === "--bin") {
7578
+ const taken = takeValue(argv, i, arg);
7579
+ if ("error" in taken)
7580
+ return { ok: false, error: taken.error };
7581
+ const parsed = parseExternalCommandOverride(taken.value, "--bin", [
7582
+ "git"
7583
+ ]);
7584
+ if (parsed.ok === false)
7585
+ return { ok: false, error: parsed.error };
7586
+ commandOverrides.push(parsed.override);
7587
+ i = taken.next;
7123
7588
  } else if (VALUE_FLAGS4.has(arg)) {
7124
7589
  const taken = takeValue(argv, i, arg);
7125
7590
  if ("error" in taken)
@@ -7170,7 +7635,8 @@ function parseStatusArgs(argv) {
7170
7635
  limit,
7171
7636
  json: flags.has("--json")
7172
7637
  },
7173
- cwd
7638
+ cwd,
7639
+ ...commandOverrides.length ? { commandOverrides } : {}
7174
7640
  }
7175
7641
  };
7176
7642
  }
@@ -7183,9 +7649,13 @@ function sumTotals(files) {
7183
7649
  }
7184
7650
  return { files: files.length, additions, deletions };
7185
7651
  }
7186
- function buildGroup(files) {
7652
+ function buildGroup(files, error) {
7187
7653
  const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
7188
- return { files: sorted, totals: sumTotals(sorted) };
7654
+ return {
7655
+ files: sorted,
7656
+ totals: sumTotals(sorted),
7657
+ ...error ? { error } : {}
7658
+ };
7189
7659
  }
7190
7660
  function metaPathKey(file) {
7191
7661
  return `${file.old_path || ""}\x00${file.path}`;
@@ -7202,6 +7672,9 @@ function mergeMissingByPath(primary, fallback) {
7202
7672
  }
7203
7673
  return merged;
7204
7674
  }
7675
+ function isMissingDiffBaseError(error) {
7676
+ return !!error && /ambiguous argument 'HEAD'|bad revision 'HEAD'/i.test(error);
7677
+ }
7205
7678
  function joinCli(parts, serverUrl) {
7206
7679
  const head = parts[0];
7207
7680
  const tail = parts.slice(1).join(" ");
@@ -7246,6 +7719,8 @@ function formatGroupText(label, group) {
7246
7719
  const totals = group.totals;
7247
7720
  const summary = totals.files === 0 ? `${label}: 0 files` : `${label}: ${totals.files} files (+${totals.additions} / -${totals.deletions})`;
7248
7721
  lines.push(summary);
7722
+ if (group.error)
7723
+ lines.push(` # error: ${group.error}`);
7249
7724
  for (const file of group.files)
7250
7725
  lines.push(formatChangedLine(file));
7251
7726
  return lines;
@@ -7261,10 +7736,13 @@ function formatRecentText(commits) {
7261
7736
  }
7262
7737
  function buildStatusReport(opts) {
7263
7738
  const { root, ref, limit } = opts;
7264
- const stagedFiles = fileMeta(["--cached"], root, false);
7265
- const changedFiles = mergeMissingByPath(fileMeta(["HEAD"], root, true), stagedFiles);
7266
- const changed = buildGroup(changedFiles);
7267
- const staged = buildGroup(stagedFiles);
7739
+ const stagedResult = fileMetaResult(["--cached"], root, false);
7740
+ const changedResult = fileMetaResult(["HEAD"], root, true);
7741
+ const worktreeFallbackResult = isMissingDiffBaseError(changedResult.error) ? fileMetaResult([], root, true) : null;
7742
+ const stagedFiles = stagedResult.files;
7743
+ const changedFiles = changedResult.error ? worktreeFallbackResult ? mergeMissingByPath(worktreeFallbackResult.files, stagedFiles) : [] : mergeMissingByPath(changedResult.files, stagedFiles);
7744
+ const changed = buildGroup(changedFiles, worktreeFallbackResult ? worktreeFallbackResult.error || stagedResult.error : changedResult.error || stagedResult.error);
7745
+ const staged = buildGroup(stagedFiles, stagedResult.error);
7268
7746
  const history = commitHistory(root, { ref, skip: 0, limit });
7269
7747
  const branch = currentBranch(root);
7270
7748
  const remote = remoteWebUrl(root);
@@ -7312,7 +7790,7 @@ function runStatusCli(argv) {
7312
7790
  console.error('Run "code-viewer status --help" for usage.');
7313
7791
  process.exit(1);
7314
7792
  }
7315
- const { command, cwd } = parsed.args;
7793
+ const { command, cwd, commandOverrides = [] } = parsed.args;
7316
7794
  if (command.kind === "help") {
7317
7795
  console.log(STATUS_HELP);
7318
7796
  return;
@@ -7321,6 +7799,15 @@ function runStatusCli(argv) {
7321
7799
  console.log(STATUS_AGENT_HELP);
7322
7800
  return;
7323
7801
  }
7802
+ const commandConfig = configureExternalCommands({
7803
+ cwd: cwd || process.cwd(),
7804
+ cliOverrides: commandOverrides,
7805
+ allowedNames: ["git"]
7806
+ });
7807
+ if (commandConfig.ok === false) {
7808
+ console.error(commandConfig.error);
7809
+ process.exit(1);
7810
+ }
7324
7811
  const root = resolveRepoRoot(cwd);
7325
7812
  const report = buildStatusReport({
7326
7813
  root,
@@ -7329,9 +7816,13 @@ function runStatusCli(argv) {
7329
7816
  });
7330
7817
  if (command.json) {
7331
7818
  console.log(JSON.stringify(report, null, 2));
7819
+ if (report.changed.error || report.staged.error)
7820
+ process.exit(1);
7332
7821
  return;
7333
7822
  }
7334
7823
  console.log(formatStatusReportText(report));
7824
+ if (report.changed.error || report.staged.error)
7825
+ process.exit(1);
7335
7826
  }
7336
7827
  var STATUS_DEFAULT_LIMIT = 10, STATUS_HARD_CAP_LIMIT = 100, STATUS_DEFAULT_REF = "HEAD", STATUS_HELP, STATUS_AGENT_HELP = `code-viewer status — agent guide
7337
7828
 
@@ -7363,11 +7854,11 @@ browser home page uses — no server, no SQLite, no docker required.
7363
7854
  { repoRoot, branch, remoteWebUrl, changed, staged, recentCommits,
7364
7855
  nextCommands }
7365
7856
  changed / staged share shape { files: GitFileMeta[], totals: { files,
7366
- additions, deletions } } so AI can iterate both the same way.
7857
+ additions, deletions }, error? } so AI can iterate both the same way.
7367
7858
  recentCommitsError is present when --ref cannot resolve; the command
7368
7859
  still emits changed / staged / nextCommands and exits 0.
7369
- - exit 0 on success. exit 1 only on argument errors or when --cwd does
7370
- not resolve to a directory.
7860
+ - exit 0 on success. exit 1 on argument errors, when --cwd does not
7861
+ resolve to a directory, or when changed / staged git collection fails.
7371
7862
 
7372
7863
  ## Suggested usage
7373
7864
 
@@ -7377,16 +7868,18 @@ browser home page uses — no server, no SQLite, no docker required.
7377
7868
  `, VALUE_FLAGS4, BOOL_FLAGS4;
7378
7869
  var init_status_cli = __esm(() => {
7379
7870
  init_cli_helpers();
7871
+ init_command_resolver();
7380
7872
  init_git();
7381
7873
  init_server_registry();
7382
7874
  STATUS_HELP = `code-viewer status — snapshot the current repo for AI agents
7383
7875
 
7384
7876
  Usage:
7385
- code-viewer status [--cwd <repo>] [--ref <ref>] [--limit <N>] [--json]
7877
+ code-viewer status [--cwd <repo>] [--bin git=<path>] [--ref <ref>] [--limit <N>] [--json]
7386
7878
  code-viewer status agent-help
7387
7879
 
7388
7880
  Options:
7389
7881
  --cwd <repo> Repository to inspect (default: current directory).
7882
+ --bin git=<p> Override git executable path.
7390
7883
  --ref <ref> Ref to read recent commits from (default: ${STATUS_DEFAULT_REF}).
7391
7884
  --limit <N> Recent commits to include (default: ${STATUS_DEFAULT_LIMIT}, max ${STATUS_HARD_CAP_LIMIT}).
7392
7885
  --json Emit a structured JSON payload instead of plain text.
@@ -7915,7 +8408,22 @@ var init_spawn_runner = () => {};
7915
8408
  // web-src/server/database/adapters/docker-utils.ts
7916
8409
  import { spawnSync as spawnSync2 } from "node:child_process";
7917
8410
  function isDockerComposeServiceUnavailableError(err) {
7918
- return err instanceof DockerComposeServiceUnavailableError;
8411
+ return err instanceof DockerComposeServiceUnavailableError || err instanceof DockerCommandUnavailableError;
8412
+ }
8413
+ function dockerCommand() {
8414
+ return commandForExternal("docker");
8415
+ }
8416
+ function isDockerCommandUnavailableResult(result) {
8417
+ return isCommandNotFoundResult("docker", result);
8418
+ }
8419
+ function throwIfDockerCommandUnavailableResult(result) {
8420
+ if (isDockerCommandUnavailableResult(result)) {
8421
+ throw new DockerCommandUnavailableError;
8422
+ }
8423
+ }
8424
+ function throwIfCachedComposeDockerCommandUnavailable(cwd) {
8425
+ const failure = composePsCache.get(cwd)?.error || "";
8426
+ throwIfDockerCommandUnavailableResult({ code: 1, stderr: failure });
7919
8427
  }
7920
8428
  function parseComposePsOutput(stdout) {
7921
8429
  const output = stdout.trim();
@@ -7929,18 +8437,21 @@ function parseComposePsOutput(stdout) {
7929
8437
  }
7930
8438
  return byService;
7931
8439
  }
7932
- function cacheComposePsFailure(cwd, stderr, now) {
7933
- const failureTtl = /docker daemon|cannot connect|is the docker daemon running/i.test(stderr) ? COMPOSE_PS_FAILURE_TTL_MS : COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS;
8440
+ function cacheComposePsFailure(cwd, result, now) {
8441
+ const stderr = result.stderr || "";
8442
+ const failureTtl = isDockerCommandUnavailableResult(result) || /docker daemon|cannot connect|is the docker daemon running/i.test(stderr) ? COMPOSE_PS_FAILURE_TTL_MS : COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS;
7934
8443
  composePsCache.set(cwd, {
7935
8444
  containers: null,
7936
8445
  positiveExpiresAt: now,
7937
- negativeExpiresAt: now + failureTtl
8446
+ negativeExpiresAt: now + failureTtl,
8447
+ error: stderr
7938
8448
  });
7939
8449
  }
7940
8450
  function runComposePsAsync(cwd, signal) {
7941
8451
  throwIfAborted(signal, "docker compose ps aborted");
7942
8452
  if (spawnSyncImpl !== spawnSync2) {
7943
- const proc = spawnSyncImpl("docker", ["compose", "ps", "--format", "json", "--status", "running"], {
8453
+ const command = dockerCommand();
8454
+ const proc = spawnSyncImpl(command, ["compose", "ps", "--format", "json", "--status", "running"], {
7944
8455
  encoding: "utf8",
7945
8456
  timeout: 5000,
7946
8457
  stdio: ["ignore", "pipe", "pipe"],
@@ -7948,19 +8459,21 @@ function runComposePsAsync(cwd, signal) {
7948
8459
  });
7949
8460
  return Promise.resolve({
7950
8461
  stdout: String(proc.stdout || ""),
7951
- stderr: String(proc.stderr || ""),
8462
+ stderr: `${String(proc.stderr || "")}${proc.error ? `
8463
+ ${proc.error.message}` : ""}`,
7952
8464
  code: proc.status ?? 1
7953
8465
  });
7954
8466
  }
7955
8467
  return spawnTextAsync({
7956
- command: "docker",
8468
+ command: dockerCommand(),
7957
8469
  args: ["compose", "ps", "--format", "json", "--status", "running"],
7958
8470
  cwd,
7959
8471
  timeoutMs: 5000,
7960
8472
  signal,
7961
8473
  killSignal: "SIGKILL",
7962
8474
  abortMessage: "docker compose ps aborted",
7963
- timeoutMessage: "docker compose ps timed out"
8475
+ timeoutMessage: "docker compose ps timed out",
8476
+ rejectOnError: false
7964
8477
  });
7965
8478
  }
7966
8479
  async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal) {
@@ -7989,11 +8502,14 @@ async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal)
7989
8502
  } catch (err) {
7990
8503
  if (isAbortLikeError(err, controller.signal))
7991
8504
  throw err;
7992
- cacheComposePsFailure(cwd, err instanceof Error ? err.message : String(err), startedAt);
8505
+ cacheComposePsFailure(cwd, {
8506
+ code: 1,
8507
+ stderr: err instanceof Error ? err.message : String(err)
8508
+ }, startedAt);
7993
8509
  return null;
7994
8510
  }
7995
8511
  if (proc.code !== 0) {
7996
- cacheComposePsFailure(cwd, proc.stderr || "", startedAt);
8512
+ cacheComposePsFailure(cwd, proc, startedAt);
7997
8513
  return null;
7998
8514
  }
7999
8515
  try {
@@ -8008,7 +8524,8 @@ async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal)
8008
8524
  composePsCache.set(cwd, {
8009
8525
  containers: null,
8010
8526
  positiveExpiresAt: startedAt,
8011
- negativeExpiresAt: startedAt + COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS
8527
+ negativeExpiresAt: startedAt + COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS,
8528
+ error: "could not parse docker compose ps output"
8012
8529
  });
8013
8530
  return null;
8014
8531
  }
@@ -8051,12 +8568,14 @@ async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal)
8051
8568
  async function resolveRunningComposeContainerNameOrThrowAsync(serviceName, cwd, signal) {
8052
8569
  const containerName = await resolveRunningComposeContainerNameAsync(serviceName, cwd, signal);
8053
8570
  if (!containerName) {
8571
+ throwIfCachedComposeDockerCommandUnavailable(cwd);
8054
8572
  throw new DockerComposeServiceUnavailableError(serviceName, cwd);
8055
8573
  }
8056
8574
  return containerName;
8057
8575
  }
8058
- var COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS = 30000, COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS = 3000, COMPOSE_PS_FAILURE_TTL_MS = 15000, composePsCache, composePsPending, spawnSyncImpl, DockerComposeServiceUnavailableError;
8576
+ var COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS = 30000, COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS = 3000, COMPOSE_PS_FAILURE_TTL_MS = 15000, composePsCache, composePsPending, spawnSyncImpl, DockerComposeServiceUnavailableError, DockerCommandUnavailableError;
8059
8577
  var init_docker_utils = __esm(() => {
8578
+ init_command_resolver();
8060
8579
  init_spawn_runner();
8061
8580
  composePsCache = new Map;
8062
8581
  composePsPending = new Map;
@@ -8072,6 +8591,13 @@ var init_docker_utils = __esm(() => {
8072
8591
  this.cwd = cwd;
8073
8592
  }
8074
8593
  };
8594
+ DockerCommandUnavailableError = class DockerCommandUnavailableError extends Error {
8595
+ status = 503;
8596
+ constructor() {
8597
+ super(`${commandNotFoundDetail("docker")}. Install Docker or pass --bin docker=/absolute/path.`);
8598
+ this.name = "DockerCommandUnavailableError";
8599
+ }
8600
+ };
8075
8601
  });
8076
8602
 
8077
8603
  // web-src/server/database/adapters/sql-capture.ts
@@ -8122,7 +8648,7 @@ function fallbackDockerDatabases(defaultDb) {
8122
8648
  function buildExecArgs(config, sql) {
8123
8649
  if (config.kind === "postgresql") {
8124
8650
  return [
8125
- "docker",
8651
+ dockerCommand(),
8126
8652
  "exec",
8127
8653
  "-i",
8128
8654
  "-e",
@@ -8148,7 +8674,7 @@ function buildExecArgs(config, sql) {
8148
8674
  ];
8149
8675
  }
8150
8676
  return [
8151
- "docker",
8677
+ dockerCommand(),
8152
8678
  "exec",
8153
8679
  "-i",
8154
8680
  "-e",
@@ -8173,7 +8699,8 @@ function execInContainer(config, sql, timeoutMs = 1e4) {
8173
8699
  });
8174
8700
  return {
8175
8701
  stdout: proc.stdout || "",
8176
- stderr: proc.stderr || "",
8702
+ stderr: `${proc.stderr || ""}${proc.error ? `
8703
+ ${proc.error.message}` : ""}`,
8177
8704
  code: proc.status ?? 1
8178
8705
  };
8179
8706
  }
@@ -8194,11 +8721,20 @@ async function readStreamText(stream) {
8194
8721
  }
8195
8722
  async function execWithBunSpawn(spawnFn, args, timeoutMs, signal) {
8196
8723
  throwIfAborted(signal, "query aborted");
8197
- const proc = spawnFn(args, {
8198
- stdin: "ignore",
8199
- stdout: "pipe",
8200
- stderr: "pipe"
8201
- });
8724
+ let proc;
8725
+ try {
8726
+ proc = spawnFn(args, {
8727
+ stdin: "ignore",
8728
+ stdout: "pipe",
8729
+ stderr: "pipe"
8730
+ });
8731
+ } catch (err) {
8732
+ return {
8733
+ code: 1,
8734
+ stdout: "",
8735
+ stderr: err instanceof Error ? err.message : String(err)
8736
+ };
8737
+ }
8202
8738
  let timedOut = false;
8203
8739
  let aborted = false;
8204
8740
  const abort = () => {
@@ -8248,13 +8784,17 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
8248
8784
  }
8249
8785
  async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
8250
8786
  recordSql(sql);
8251
- if (spawnSyncImpl2 !== spawnSync3)
8252
- return execInContainer(config, sql, timeoutMs);
8787
+ let result;
8788
+ if (spawnSyncImpl2 !== spawnSync3) {
8789
+ result = execInContainer(config, sql, timeoutMs);
8790
+ throwIfDockerCommandUnavailableResult(result);
8791
+ return result;
8792
+ }
8253
8793
  const args = buildExecArgs(config, sql);
8254
8794
  const bunSpawn = globalThis.Bun?.spawn;
8255
- if (bunSpawn)
8256
- return execWithBunSpawn(bunSpawn, args, timeoutMs, signal);
8257
- return execWithNodeSpawn(args, timeoutMs, signal);
8795
+ result = bunSpawn ? await execWithBunSpawn(bunSpawn, args, timeoutMs, signal) : await execWithNodeSpawn(args, timeoutMs, signal);
8796
+ throwIfDockerCommandUnavailableResult(result);
8797
+ return result;
8258
8798
  }
8259
8799
  function stripFinalLineBreak(text) {
8260
8800
  if (text.endsWith(`\r
@@ -8785,14 +9325,22 @@ function createDockerAdapter(config) {
8785
9325
  try {
8786
9326
  const result = await execAsync(`SHOW CREATE TABLE ${tableIdentifier(table)}`, signal);
8787
9327
  return result.rows.length > 0 ? result.rows[0][1] || "" : "";
8788
- } catch {
9328
+ } catch (err) {
9329
+ if (isAbortLikeError(err, signal))
9330
+ throw err;
9331
+ if (err instanceof DockerCommandUnavailableError)
9332
+ throw err;
8789
9333
  return "";
8790
9334
  }
8791
9335
  }
8792
9336
  try {
8793
9337
  const result = await execAsync(`SELECT 'CREATE TABLE ' || ${escapeSqlString(tableIdentifier(table))} || ' (...)' AS ddl`, signal);
8794
9338
  return result.rows.length > 0 ? result.rows[0][0] || "" : "";
8795
- } catch {
9339
+ } catch (err) {
9340
+ if (isAbortLikeError(err, signal))
9341
+ throw err;
9342
+ if (err instanceof DockerCommandUnavailableError)
9343
+ throw err;
8796
9344
  return "";
8797
9345
  }
8798
9346
  },
@@ -8809,7 +9357,11 @@ function createDockerAdapter(config) {
8809
9357
  name: row[0],
8810
9358
  sql: row[1] || ""
8811
9359
  }));
8812
- } catch {
9360
+ } catch (err) {
9361
+ if (isAbortLikeError(err, signal))
9362
+ throw err;
9363
+ if (err instanceof DockerCommandUnavailableError)
9364
+ throw err;
8813
9365
  return [];
8814
9366
  }
8815
9367
  },
@@ -8860,6 +9412,7 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
8860
9412
  return [...cached.value];
8861
9413
  const containerName = await resolveRunningComposeContainerNameAsync(serviceName, cwd, signal);
8862
9414
  if (!containerName) {
9415
+ throwIfCachedComposeDockerCommandUnavailable(cwd);
8863
9416
  return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
8864
9417
  }
8865
9418
  const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || env.POSTGRES_USERNAME || env.MYSQL_USERNAME || env.USER || "root";
@@ -8893,6 +9446,8 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
8893
9446
  } catch (err) {
8894
9447
  if (isAbortLikeError(err, signal))
8895
9448
  throw err;
9449
+ if (err instanceof DockerCommandUnavailableError)
9450
+ throw err;
8896
9451
  const fallback = fallbackDockerDatabases(defaultDb);
8897
9452
  if (fallback.length > 0)
8898
9453
  return fallback;
@@ -8912,6 +9467,7 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
8912
9467
  return [...cached.value];
8913
9468
  const containerName = await resolveRunningComposeContainerNameAsync(serviceName, cwd, signal);
8914
9469
  if (!containerName) {
9470
+ throwIfCachedComposeDockerCommandUnavailable(cwd);
8915
9471
  return setDockerSchemasCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
8916
9472
  }
8917
9473
  const config = {
@@ -8934,6 +9490,8 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
8934
9490
  } catch (err) {
8935
9491
  if (isAbortLikeError(err, signal))
8936
9492
  throw err;
9493
+ if (err instanceof DockerCommandUnavailableError)
9494
+ throw err;
8937
9495
  return setDockerSchemasCache(cacheKey, ["public"], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
8938
9496
  }
8939
9497
  }
@@ -9026,11 +9584,11 @@ __ES_STATUS__:%{http_code}
9026
9584
  ];
9027
9585
  return { args, input: curlConfig };
9028
9586
  }
9029
- function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signal) {
9587
+ async function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signal) {
9030
9588
  throwIfAborted(signal, "elasticsearch request aborted");
9031
9589
  const invocation = buildEsRequestInvocation(config, method, path, body);
9032
- return spawnTextAsync({
9033
- command: "docker",
9590
+ const result = await spawnTextAsync({
9591
+ command: dockerCommand(),
9034
9592
  args: invocation.args,
9035
9593
  env: process.env,
9036
9594
  input: invocation.input,
@@ -9040,6 +9598,8 @@ function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signa
9040
9598
  timeoutMessage: `elasticsearch request timed out after ${timeoutMs}ms`,
9041
9599
  rejectOnError: false
9042
9600
  });
9601
+ throwIfDockerCommandUnavailableResult(result);
9602
+ return result;
9043
9603
  }
9044
9604
  function parseEsResponse(stdout) {
9045
9605
  const marker = "__ES_STATUS__:";
@@ -9387,11 +9947,11 @@ function buildRedisCliInvocation(config, args) {
9387
9947
  const spawnEnv = hasPassword ? { ...process.env, REDISCLI_AUTH: config.password } : process.env;
9388
9948
  return { args: dockerArgs, env: spawnEnv };
9389
9949
  }
9390
- function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
9950
+ async function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
9391
9951
  throwIfAborted(signal, "redis-cli aborted");
9392
9952
  const invocation = buildRedisCliInvocation(config, args);
9393
- return spawnTextAsync({
9394
- command: "docker",
9953
+ const result = await spawnTextAsync({
9954
+ command: dockerCommand(),
9395
9955
  args: invocation.args,
9396
9956
  env: invocation.env,
9397
9957
  timeoutMs,
@@ -9400,6 +9960,8 @@ function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
9400
9960
  timeoutMessage: `redis-cli timed out after ${timeoutMs}ms`,
9401
9961
  rejectOnError: false
9402
9962
  });
9963
+ throwIfDockerCommandUnavailableResult(result);
9964
+ return result;
9403
9965
  }
9404
9966
  function parseInfoKeyspace(stdout) {
9405
9967
  const counts = new Map;
@@ -10642,14 +11204,19 @@ function timeoutReadableStream(body, signal) {
10642
11204
  async function dockerCurlFetch(opts) {
10643
11205
  const { args, input } = dockerCurlCommand(opts);
10644
11206
  if (spawnSyncImplIsTestOverride) {
10645
- const proc2 = spawnSyncImpl3("docker", args, {
11207
+ const proc2 = spawnSyncImpl3(dockerCommand(), args, {
10646
11208
  encoding: "buffer",
10647
11209
  input,
10648
11210
  timeout: s3DockerCurlTimeoutMs,
10649
11211
  stdio: ["pipe", "pipe", "pipe"]
10650
11212
  });
10651
11213
  if ((proc2.status ?? 1) !== 0) {
10652
- const stderr = new TextDecoder().decode(proc2.stderr || new Uint8Array).replace(/\s+/g, " ").trim();
11214
+ const stderr = new TextDecoder().decode(proc2.stderr || new Uint8Array).concat(proc2.error ? `
11215
+ ${proc2.error.message}` : "").replace(/\s+/g, " ").trim();
11216
+ throwIfDockerCommandUnavailableResult({
11217
+ code: proc2.status ?? 1,
11218
+ stderr
11219
+ });
10653
11220
  throw new S3HttpError(503, `S3 HTTP transport failed via docker exec${stderr ? `: ${stderr.slice(0, 240)}` : ""}`);
10654
11221
  }
10655
11222
  return responseFromCurlOutput(new Uint8Array(proc2.stdout || new Uint8Array));
@@ -10658,16 +11225,21 @@ async function dockerCurlFetch(opts) {
10658
11225
  throw new S3HttpError(503, "S3 HTTP transport aborted");
10659
11226
  }
10660
11227
  const proc = await spawnCollectAsync({
10661
- command: "docker",
11228
+ command: dockerCommand(),
10662
11229
  args,
10663
11230
  input,
10664
11231
  timeoutMs: s3DockerCurlTimeoutMs,
10665
11232
  signal: opts.signal,
10666
11233
  abortMessage: "S3 HTTP transport aborted",
10667
- timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`
11234
+ timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`,
11235
+ rejectOnError: false
10668
11236
  });
10669
11237
  if (proc.code !== 0) {
10670
11238
  const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
11239
+ throwIfDockerCommandUnavailableResult({
11240
+ code: proc.code,
11241
+ stderr
11242
+ });
10671
11243
  throw new S3HttpError(503, `S3 HTTP transport failed via docker exec${stderr ? `: ${stderr.slice(0, 240)}` : ""}`);
10672
11244
  }
10673
11245
  return responseFromCurlOutput(new Uint8Array(proc.stdout));
@@ -11316,14 +11888,14 @@ import {
11316
11888
  existsSync as existsSync6,
11317
11889
  openSync,
11318
11890
  readSync,
11319
- realpathSync as realpathSync3,
11320
- statSync as statSync3
11891
+ realpathSync as realpathSync4,
11892
+ statSync as statSync4
11321
11893
  } from "node:fs";
11322
11894
  import { lstat, open, readdir, readFile as readFile2, stat } from "node:fs/promises";
11323
- import { basename, join as join7, relative as relative2 } from "node:path";
11895
+ import { basename, join as join8, relative as relative3 } from "node:path";
11324
11896
  function isSqliteFile(fullPath) {
11325
11897
  try {
11326
- const stat2 = statSync3(fullPath);
11898
+ const stat2 = statSync4(fullPath);
11327
11899
  if (!stat2.isFile() || stat2.size < 16)
11328
11900
  return false;
11329
11901
  const buf = Buffer.alloc(16);
@@ -11391,7 +11963,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
11391
11963
  return;
11392
11964
  if (omitSet.has(entry.toLowerCase()))
11393
11965
  continue;
11394
- const full = join7(dir, entry);
11966
+ const full = join8(dir, entry);
11395
11967
  let entryStat;
11396
11968
  try {
11397
11969
  entryStat = await lstat(full);
@@ -11408,7 +11980,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
11408
11980
  continue;
11409
11981
  if (!await isSqliteFileAsync(full))
11410
11982
  continue;
11411
- const rel = relative2(cwd, full);
11983
+ const rel = relative3(cwd, full);
11412
11984
  if (rel.startsWith("..") || rel.startsWith("/"))
11413
11985
  continue;
11414
11986
  results.push({
@@ -11435,18 +12007,18 @@ function validateDbPath(cwd, dbPath) {
11435
12007
  const parts = dbPath.split(/[\\/]+/);
11436
12008
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
11437
12009
  return null;
11438
- const full = join7(cwd, dbPath);
12010
+ const full = join8(cwd, dbPath);
11439
12011
  if (!existsSync6(full))
11440
12012
  return null;
11441
12013
  let realCwd;
11442
12014
  let realFull;
11443
12015
  try {
11444
- realCwd = realpathSync3(cwd);
11445
- realFull = realpathSync3(full);
12016
+ realCwd = realpathSync4(cwd);
12017
+ realFull = realpathSync4(full);
11446
12018
  } catch {
11447
12019
  return null;
11448
12020
  }
11449
- const rel = relative2(realCwd, realFull);
12021
+ const rel = relative3(realCwd, realFull);
11450
12022
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/"))
11451
12023
  return null;
11452
12024
  if (!isSqliteFile(realFull))
@@ -11572,7 +12144,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
11572
12144
  }
11573
12145
  async function readDotenvAsync(composeDir) {
11574
12146
  try {
11575
- const content = await readFile2(join7(composeDir, ".env"), "utf-8");
12147
+ const content = await readFile2(join8(composeDir, ".env"), "utf-8");
11576
12148
  return parseDotenvContent(content);
11577
12149
  } catch {
11578
12150
  return {};
@@ -11694,7 +12266,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
11694
12266
  for (let match = serviceRegex.exec(servicesBlock);match !== null; match = serviceRegex.exec(servicesBlock)) {
11695
12267
  servicePositions.push({ name: match[1], start: match.index });
11696
12268
  }
11697
- const relDir = relative2(cwd, composeDir);
12269
+ const relDir = relative3(cwd, composeDir);
11698
12270
  const isRoot = relDir === "" || relDir === ".";
11699
12271
  const relDirSlash = relDir.replace(/\\/g, "/");
11700
12272
  const filename = basename(filepath);
@@ -11794,7 +12366,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
11794
12366
  if (depth > MAX_SCAN_DEPTH)
11795
12367
  return;
11796
12368
  for (const filename of COMPOSE_FILENAMES) {
11797
- const filepath = join7(dir, filename);
12369
+ const filepath = join8(dir, filename);
11798
12370
  if (await pathExistsAsync(filepath)) {
11799
12371
  await parseComposeFileAsync(filepath, dir, cwd, results);
11800
12372
  break;
@@ -11817,7 +12389,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
11817
12389
  return;
11818
12390
  if (omitSet.has(entry.toLowerCase()))
11819
12391
  continue;
11820
- const full = join7(dir, entry);
12392
+ const full = join8(dir, entry);
11821
12393
  let entryStat;
11822
12394
  try {
11823
12395
  entryStat = await lstat(full);
@@ -11961,12 +12533,12 @@ import {
11961
12533
  readdirSync as nodeReaddirSync,
11962
12534
  watch as nodeWatch
11963
12535
  } from "node:fs";
11964
- import { join as join8, relative as relative3 } from "node:path";
12536
+ import { join as join9, relative as relative4 } from "node:path";
11965
12537
  function normalizeRelativePath(path) {
11966
12538
  return path.replace(/\\/g, "/").replace(/^\/+/, "");
11967
12539
  }
11968
12540
  function isInsideRoot(root, path) {
11969
- const rel = relative3(root, path).replace(/\\/g, "/");
12541
+ const rel = relative4(root, path).replace(/\\/g, "/");
11970
12542
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
11971
12543
  }
11972
12544
  function startWorktreeUpdateWatch(options) {
@@ -12004,7 +12576,7 @@ function startWorktreeUpdateWatch(options) {
12004
12576
  const pendingChangedPaths = new Set;
12005
12577
  let watchLimitReported = false;
12006
12578
  const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
12007
- const directoryRelativePath = (dir) => normalizeRelativePath(relative3(options.root, dir));
12579
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative4(options.root, dir));
12008
12580
  const ignoredDirectory = (dir) => {
12009
12581
  const rel = directoryRelativePath(dir);
12010
12582
  return Boolean(rel && ignored(rel));
@@ -12070,7 +12642,7 @@ function startWorktreeUpdateWatch(options) {
12070
12642
  for (const entry of entries) {
12071
12643
  if (!entry.isDirectory())
12072
12644
  continue;
12073
- const child = join8(dir, entry.name);
12645
+ const child = join9(dir, entry.name);
12074
12646
  if (ignoredDirectory(child))
12075
12647
  continue;
12076
12648
  children.push(child);
@@ -12154,10 +12726,10 @@ function startWorktreeUpdateWatch(options) {
12154
12726
  scheduleUpdate();
12155
12727
  return;
12156
12728
  }
12157
- const changed = normalizeRelativePath(join8(rel, filename.toString()));
12729
+ const changed = normalizeRelativePath(join9(rel, filename.toString()));
12158
12730
  if (ignored(changed))
12159
12731
  return;
12160
- const fullChangedPath = join8(options.root, changed);
12732
+ const fullChangedPath = join9(options.root, changed);
12161
12733
  if (!isInsideRoot(options.root, fullChangedPath))
12162
12734
  return;
12163
12735
  if (initialScanAsync) {
@@ -12206,9 +12778,9 @@ var init_worktree_watcher = __esm(() => {
12206
12778
  });
12207
12779
 
12208
12780
  // web-src/server/state-store.ts
12209
- import { join as join9 } from "node:path";
12781
+ import { join as join10 } from "node:path";
12210
12782
  function codeViewerPath(root, fileName) {
12211
- return join9(root, CODE_VIEWER_DIR2, fileName);
12783
+ return join10(root, CODE_VIEWER_DIR2, fileName);
12212
12784
  }
12213
12785
  function isRecord(value) {
12214
12786
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -14129,9 +14701,9 @@ var init_handle_s3 = __esm(() => {
14129
14701
  });
14130
14702
 
14131
14703
  // web-src/server/database/query-history.ts
14132
- import { join as join10 } from "node:path";
14704
+ import { join as join11 } from "node:path";
14133
14705
  function historyFilePath(root) {
14134
- return join10(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
14706
+ return join11(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
14135
14707
  }
14136
14708
  function emptyState() {
14137
14709
  return { version: 1, entries: [] };
@@ -14281,9 +14853,9 @@ var init_query_history = __esm(() => {
14281
14853
  // web-src/server/database/snapshot-store.ts
14282
14854
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
14283
14855
  import { mkdirSync as mkdirSync3 } from "node:fs";
14284
- import { join as join11 } from "node:path";
14856
+ import { join as join12 } from "node:path";
14285
14857
  async function getStoreDb(cwd) {
14286
- const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
14858
+ const dbPath = join12(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
14287
14859
  if (storeDb && storeDbPath === dbPath)
14288
14860
  return storeDb;
14289
14861
  if (storeDb) {
@@ -14291,7 +14863,7 @@ async function getStoreDb(cwd) {
14291
14863
  storeDb.close();
14292
14864
  } catch {}
14293
14865
  }
14294
- mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
14866
+ mkdirSync3(join12(cwd, CODE_VIEWER_DIR4), { recursive: true });
14295
14867
  const DbClass = await loadSqliteClass();
14296
14868
  storeDb = new DbClass(dbPath);
14297
14869
  storeDbPath = dbPath;
@@ -14897,9 +15469,9 @@ var init_snapshot_runner = __esm(() => {
14897
15469
  });
14898
15470
 
14899
15471
  // web-src/server/database/tabs-store.ts
14900
- import { join as join12 } from "node:path";
15472
+ import { join as join13 } from "node:path";
14901
15473
  function tabsFilePath(root) {
14902
- return join12(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
15474
+ return join13(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
14903
15475
  }
14904
15476
  function emptyState2() {
14905
15477
  return { version: 1, tabs: [], activeTabId: null };
@@ -15437,6 +16009,38 @@ async function handleTable(cwd, url, omitDirNames, signal) {
15437
16009
  return handleError("database", "read table", err, signal);
15438
16010
  }
15439
16011
  }
16012
+ async function handleTableCount(cwd, url, omitDirNames, signal) {
16013
+ const r = await resolveDb(cwd, url.searchParams.get("db"), omitDirNames, url.searchParams.get("schema"), signal);
16014
+ if (r instanceof Response)
16015
+ return r;
16016
+ const table = url.searchParams.get("table");
16017
+ if (!table)
16018
+ return textError("missing table parameter", 400);
16019
+ try {
16020
+ const adapter = await getAdapter(r, cwd, signal);
16021
+ const { result, executedSql } = await captureSql(async () => {
16022
+ const db = asAsync(adapter);
16023
+ const tables = await db.tables(signal);
16024
+ const entry = tables.find((candidate) => candidate.name === table);
16025
+ if (!entry)
16026
+ throw new Error(`unknown table: ${table}`);
16027
+ if (entry.type !== "table")
16028
+ return { rowCount: null };
16029
+ const counts = await db.tableRowCounts([table], signal);
16030
+ return { rowCount: counts.get(table) ?? null };
16031
+ });
16032
+ const body = {
16033
+ dbId: r.dbId,
16034
+ ...r.schema ? { schema: r.schema } : {},
16035
+ table,
16036
+ rowCount: result.rowCount,
16037
+ executedSql
16038
+ };
16039
+ return json(body);
16040
+ } catch (err) {
16041
+ return handleError("database", "read table count", err, signal);
16042
+ }
16043
+ }
15440
16044
  function makeHistoryId() {
15441
16045
  return makeId("qh");
15442
16046
  }
@@ -16365,6 +16969,10 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
16365
16969
  methods: ["GET"],
16366
16970
  handler: () => handleTable(cwd, url, omitDirNames, req.signal)
16367
16971
  },
16972
+ "/_db/table-count": {
16973
+ methods: ["GET"],
16974
+ handler: () => handleTableCount(cwd, url, omitDirNames, req.signal)
16975
+ },
16368
16976
  "/_db/columns": {
16369
16977
  methods: ["GET"],
16370
16978
  handler: () => handleColumns(cwd, url, omitDirNames, req.signal)
@@ -16527,8 +17135,8 @@ var init_handle = __esm(() => {
16527
17135
  });
16528
17136
 
16529
17137
  // web-src/server/doctor.ts
16530
- import { accessSync, constants, readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
16531
- import { dirname as dirname3, join as join13, relative as relative4 } from "node:path";
17138
+ import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync5, statSync as statSync5 } from "node:fs";
17139
+ import { dirname as dirname4, join as join14, relative as relative5 } from "node:path";
16532
17140
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16533
17141
  function statusWorse(a, b) {
16534
17142
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -16617,12 +17225,12 @@ function findCodeViewerPackageJson() {
16617
17225
  try {
16618
17226
  let cursor;
16619
17227
  try {
16620
- cursor = dirname3(fileURLToPath2(import.meta.url));
17228
+ cursor = dirname4(fileURLToPath2(import.meta.url));
16621
17229
  } catch {
16622
- cursor = dirname3(process.argv[1] || ".");
17230
+ cursor = dirname4(process.argv[1] || ".");
16623
17231
  }
16624
17232
  for (let depth = 0;depth < 8; depth += 1) {
16625
- const candidate = join13(cursor, "package.json");
17233
+ const candidate = join14(cursor, "package.json");
16626
17234
  try {
16627
17235
  const raw = readFileSync5(candidate, "utf8");
16628
17236
  const pkg = JSON.parse(raw);
@@ -16630,7 +17238,7 @@ function findCodeViewerPackageJson() {
16630
17238
  return { version: pkg.version, path: candidate };
16631
17239
  }
16632
17240
  } catch {}
16633
- const next = dirname3(cursor);
17241
+ const next = dirname4(cursor);
16634
17242
  if (next === cursor)
16635
17243
  break;
16636
17244
  cursor = next;
@@ -16718,9 +17326,9 @@ async function checkSqlite(cwd) {
16718
17326
  return { id: "sqlite", title: "SQLite driver", rows };
16719
17327
  }
16720
17328
  async function trySnapshotDbOpen(cwd) {
16721
- const dbPath = join13(cwd, SNAPSHOT_DB_REL);
17329
+ const dbPath = join14(cwd, SNAPSHOT_DB_REL);
16722
17330
  try {
16723
- statSync4(dbPath);
17331
+ statSync5(dbPath);
16724
17332
  } catch {
16725
17333
  return { kind: "skipped" };
16726
17334
  }
@@ -16739,17 +17347,17 @@ async function trySnapshotDbOpen(cwd) {
16739
17347
  }
16740
17348
  }
16741
17349
  function checkSnapshotStore(cwd) {
16742
- const dbPath = join13(cwd, SNAPSHOT_DB_REL);
16743
- const dir = dirname3(dbPath);
17350
+ const dbPath = join14(cwd, SNAPSHOT_DB_REL);
17351
+ const dir = dirname4(dbPath);
16744
17352
  let dirStatus = "ok";
16745
17353
  let dirDetail = dir;
16746
17354
  let dirHint;
16747
17355
  try {
16748
- accessSync(dir, constants.W_OK);
17356
+ accessSync2(dir, constants2.W_OK);
16749
17357
  dirDetail = `${dir} (writable)`;
16750
17358
  } catch {
16751
17359
  try {
16752
- statSync4(dir);
17360
+ statSync5(dir);
16753
17361
  dirStatus = "error";
16754
17362
  dirDetail = `${dir} (not writable)`;
16755
17363
  dirHint = "Snapshot creation will fail until the directory is writable. " + "Check filesystem permissions on the .code-viewer directory.";
@@ -16759,7 +17367,7 @@ function checkSnapshotStore(cwd) {
16759
17367
  }
16760
17368
  let dbDetail = dbPath;
16761
17369
  try {
16762
- const stat2 = statSync4(dbPath);
17370
+ const stat2 = statSync5(dbPath);
16763
17371
  dbDetail = `${dbPath} (${stat2.size.toLocaleString()} bytes)`;
16764
17372
  } catch {
16765
17373
  dbDetail = `${dbPath} (not created yet — created on first snapshot)`;
@@ -16785,7 +17393,7 @@ function checkSnapshotStore(cwd) {
16785
17393
  };
16786
17394
  }
16787
17395
  async function checkGit(cwd, signal) {
16788
- const versionRes = await runCached(versionCache, TTL.version, "git", ["--version"], TIMEOUT.version, signal);
17396
+ const versionRes = await runCached(versionCache, TTL.version, commandForExternal("git"), ["--version"], TIMEOUT.version, signal);
16789
17397
  if (!versionRes || versionRes.code !== 0) {
16790
17398
  return {
16791
17399
  id: "git",
@@ -16795,7 +17403,7 @@ async function checkGit(cwd, signal) {
16795
17403
  id: "git.binary",
16796
17404
  title: "git binary",
16797
17405
  status: "error",
16798
- detail: "not found in PATH",
17406
+ detail: versionRes && isCommandNotFoundResult("git", versionRes) ? commandNotFoundDetail("git") : "git command failed",
16799
17407
  hint: "git is required for diff, history, and blame features. Install git and ensure it is on PATH."
16800
17408
  }
16801
17409
  ]
@@ -16809,11 +17417,11 @@ async function checkGit(cwd, signal) {
16809
17417
  detail: firstLine2(versionRes.stdout)
16810
17418
  }
16811
17419
  ];
16812
- const repoCheck = await runCached(gitCache, TTL.gitRepo, "git", ["rev-parse", "--is-inside-work-tree"], TIMEOUT.git, signal, cwd);
17420
+ const repoCheck = await runCached(gitCache, TTL.gitRepo, commandForExternal("git"), ["rev-parse", "--is-inside-work-tree"], TIMEOUT.git, signal, cwd);
16813
17421
  if (repoCheck && repoCheck.code === 0 && /true/.test(repoCheck.stdout)) {
16814
- const topRes = await runCached(gitCache, TTL.gitRepo, "git", ["rev-parse", "--show-toplevel"], TIMEOUT.git, signal, cwd);
17422
+ const topRes = await runCached(gitCache, TTL.gitRepo, commandForExternal("git"), ["rev-parse", "--show-toplevel"], TIMEOUT.git, signal, cwd);
16815
17423
  const top = topRes?.stdout.trim() || cwd;
16816
- const insideCwd = relative4(top, cwd) || ".";
17424
+ const insideCwd = relative5(top, cwd) || ".";
16817
17425
  rows.push({
16818
17426
  id: "git.repo",
16819
17427
  title: "Working tree",
@@ -16832,10 +17440,10 @@ async function checkGit(cwd, signal) {
16832
17440
  return { id: "git", title: "Git", rows };
16833
17441
  }
16834
17442
  async function detectComposeBinary(signal) {
16835
- const v2 = await runCached(versionCache, TTL.version, "docker", ["compose", "version", "--short"], TIMEOUT.version, signal);
17443
+ const v2 = await runCached(versionCache, TTL.version, commandForExternal("docker"), ["compose", "version", "--short"], TIMEOUT.version, signal);
16836
17444
  if (v2 && v2.code === 0) {
16837
17445
  return {
16838
- cmd: { binary: "docker", subcommand: ["compose"] },
17446
+ cmd: { binary: commandForExternal("docker"), subcommand: ["compose"] },
16839
17447
  v2Version: firstLine2(v2.stdout)
16840
17448
  };
16841
17449
  }
@@ -16888,13 +17496,13 @@ async function checkDocker(signal, discoveryResult) {
16888
17496
  const summary = summarizeDockerSources(discoveryResult);
16889
17497
  const dockerSourcesPresent = summary.total > 0;
16890
17498
  const rows = [];
16891
- const dockerVersion = await runCached(versionCache, TTL.version, "docker", ["--version"], TIMEOUT.version, signal);
17499
+ const dockerVersion = await runCached(versionCache, TTL.version, commandForExternal("docker"), ["--version"], TIMEOUT.version, signal);
16892
17500
  const dockerOk = dockerVersion?.code === 0;
16893
17501
  rows.push({
16894
17502
  id: "docker.binary",
16895
17503
  title: "docker CLI",
16896
17504
  status: dockerOk ? "ok" : dockerSourcesPresent ? "error" : "warn",
16897
- detail: dockerOk ? firstLine2(dockerVersion.stdout) : "not found in PATH",
17505
+ detail: dockerOk ? firstLine2(dockerVersion.stdout) : dockerVersion && isCommandNotFoundResult("docker", dockerVersion) ? commandNotFoundDetail("docker") : "docker command failed",
16898
17506
  ...dockerOk ? {} : {
16899
17507
  hint: dockerSourcesPresent ? "Compose files reference Docker services that need the docker CLI. Install Docker Desktop or the docker engine." : "docker is optional unless this project uses Docker compose data sources."
16900
17508
  }
@@ -16925,7 +17533,7 @@ async function checkDocker(signal, discoveryResult) {
16925
17533
  hint: "docker-compose v1 standalone is legacy. Prefer the Docker Compose v2 plugin (`docker compose ...`)."
16926
17534
  });
16927
17535
  }
16928
- const dockerInfo = dockerOk ? await runCached(dockerInfoCache, TTL.dockerInfo, "docker", ["info", "--format", "{{.ServerVersion}}"], TIMEOUT.dockerInfo, signal) : null;
17536
+ const dockerInfo = dockerOk ? await runCached(dockerInfoCache, TTL.dockerInfo, commandForExternal("docker"), ["info", "--format", "{{.ServerVersion}}"], TIMEOUT.dockerInfo, signal) : null;
16929
17537
  if (dockerOk) {
16930
17538
  if (dockerInfo && dockerInfo.code === 0) {
16931
17539
  rows.push({
@@ -17453,6 +18061,7 @@ async function handleDoctor(ctx) {
17453
18061
  var SNAPSHOT_DB_REL = ".code-viewer/db-snapshots.sqlite", REQUIRED_NODE_MAJOR = 20, TTL, TIMEOUT, versionCache, gitCache, dockerInfoCache, CACHE_KEY_SEP = "\x01", composeConfigCache, composePsCache2, doctorGeneration = 0, DEFAULT_DATASTORE_PROBE_TIMEOUT_MS = 2000, DEFAULT_DATASTORE_CONNECTIVITY_DEPS;
17454
18062
  var init_doctor = __esm(() => {
17455
18063
  init_cli_helpers();
18064
+ init_command_resolver();
17456
18065
  init_docker();
17457
18066
  init_elasticsearch();
17458
18067
  init_redis();
@@ -17500,6 +18109,7 @@ function parseDoctorCliArgs(argv) {
17500
18109
  let cwd = process.cwd();
17501
18110
  let port = 0;
17502
18111
  let json2 = false;
18112
+ const commandOverrides = [];
17503
18113
  for (let i = 0;i < argv.length; i++) {
17504
18114
  const arg = argv[i];
17505
18115
  if (arg === "--help" || arg === "-h" || arg === "help") {
@@ -17519,6 +18129,23 @@ function parseDoctorCliArgs(argv) {
17519
18129
  cwd = next;
17520
18130
  continue;
17521
18131
  }
18132
+ if (arg === "--bin") {
18133
+ const next = argv[++i];
18134
+ if (!next) {
18135
+ return {
18136
+ kind: "error",
18137
+ message: "--bin requires <name>=<absolute-path>"
18138
+ };
18139
+ }
18140
+ const parsed = parseExternalCommandOverride(next, "--bin", [
18141
+ "git",
18142
+ "docker"
18143
+ ]);
18144
+ if (parsed.ok === false)
18145
+ return { kind: "error", message: parsed.error };
18146
+ commandOverrides.push(parsed.override);
18147
+ continue;
18148
+ }
17522
18149
  if (arg === "--port") {
17523
18150
  const next = argv[++i];
17524
18151
  if (!next)
@@ -17535,7 +18162,15 @@ function parseDoctorCliArgs(argv) {
17535
18162
  }
17536
18163
  return { kind: "error", message: `unknown argument: ${arg}` };
17537
18164
  }
17538
- return { kind: "run", args: { cwd, port, json: json2 } };
18165
+ return {
18166
+ kind: "run",
18167
+ args: {
18168
+ cwd,
18169
+ port,
18170
+ json: json2,
18171
+ ...commandOverrides.length ? { commandOverrides } : {}
18172
+ }
18173
+ };
17539
18174
  }
17540
18175
  function formatDoctorReportText(report) {
17541
18176
  const lines = [];
@@ -17586,7 +18221,17 @@ async function runDoctorCli(argv) {
17586
18221
  `);
17587
18222
  return;
17588
18223
  }
17589
- const { cwd, port, json: json2 } = parsed.args;
18224
+ const { cwd, port, json: json2, commandOverrides = [] } = parsed.args;
18225
+ const commandConfig = configureExternalCommands({
18226
+ cwd,
18227
+ cliOverrides: commandOverrides,
18228
+ allowedNames: ["git", "docker"]
18229
+ });
18230
+ if (commandConfig.ok === false) {
18231
+ process.stderr.write(`code-viewer doctor: ${commandConfig.error}
18232
+ `);
18233
+ process.exit(2);
18234
+ }
17590
18235
  const report = await buildDoctorReport({
17591
18236
  cwd,
17592
18237
  scopeOmitDirNames: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
@@ -17606,12 +18251,13 @@ async function runDoctorCli(argv) {
17606
18251
  var DOCTOR_HELP = `code-viewer doctor — diagnose the current environment
17607
18252
 
17608
18253
  Usage:
17609
- code-viewer doctor [--cwd <path>] [--port <N>] [--json]
18254
+ code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker>=<path>]
17610
18255
  code-viewer doctor agent-help
17611
18256
 
17612
18257
  Options:
17613
18258
  --cwd <path> Working directory to inspect (default: process.cwd()).
17614
18259
  --port <N> Listening port to mention in the report (default: 0 = no server).
18260
+ --bin <n>=<p> Override git/docker executable path. Repeatable.
17615
18261
  --json Print the full DoctorReport as JSON instead of a summary.
17616
18262
  --help, -h Show this help.
17617
18263
 
@@ -17623,6 +18269,7 @@ Exit codes:
17623
18269
  2 invalid arguments
17624
18270
  `, STATUS_SYMBOL;
17625
18271
  var init_doctor_cli = __esm(() => {
18272
+ init_command_resolver();
17626
18273
  init_doctor();
17627
18274
  init_git();
17628
18275
  STATUS_SYMBOL = {
@@ -17651,7 +18298,7 @@ function normalizeNewDirectoryName(name) {
17651
18298
 
17652
18299
  // web-src/server/cache.ts
17653
18300
  import { lstatSync as lstatSync3 } from "node:fs";
17654
- import { join as join14 } from "node:path";
18301
+ import { join as join15 } from "node:path";
17655
18302
  function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
17656
18303
  return !!cached && now - cached.storedAt <= ttlMs;
17657
18304
  }
@@ -17666,7 +18313,7 @@ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MA
17666
18313
  }
17667
18314
  function worktreeFileSignature(path, cwd) {
17668
18315
  try {
17669
- const stats = lstatSync3(join14(cwd, path));
18316
+ const stats = lstatSync3(join15(cwd, path));
17670
18317
  const inode = "ino" in stats ? stats.ino : 0;
17671
18318
  return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
17672
18319
  } catch {
@@ -17712,12 +18359,12 @@ function startDevAssetReload(options) {
17712
18359
  var init_dev_assets = () => {};
17713
18360
 
17714
18361
  // web-src/server/search-service.ts
17715
- import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync6, realpathSync as realpathSync4 } from "node:fs";
17716
- import { join as join15, relative as relative5 } from "node:path";
18362
+ import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync6, realpathSync as realpathSync5 } from "node:fs";
18363
+ import { join as join16, relative as relative6 } from "node:path";
17717
18364
  function rgAvailable(cwd) {
17718
18365
  if (rgAvailableCache !== null)
17719
18366
  return rgAvailableCache;
17720
- const proc = runSync(["rg", "--version"], cwd);
18367
+ const proc = runSync([commandForExternal("rg"), "--version"], cwd);
17721
18368
  rgAvailableCache = proc.code === 0;
17722
18369
  return rgAvailableCache;
17723
18370
  }
@@ -17734,18 +18381,18 @@ function safeWorktreePath(env, path) {
17734
18381
  return null;
17735
18382
  if (isGitInternalPath(path))
17736
18383
  return null;
17737
- const full = join15(env.cwd, path);
18384
+ const full = join16(env.cwd, path);
17738
18385
  if (!existsSync7(full))
17739
18386
  return null;
17740
18387
  let realCwd;
17741
18388
  let realFull;
17742
18389
  try {
17743
- realCwd = realpathSync4(env.cwd);
17744
- realFull = realpathSync4(full);
18390
+ realCwd = realpathSync5(env.cwd);
18391
+ realFull = realpathSync5(full);
17745
18392
  } catch {
17746
18393
  return null;
17747
18394
  }
17748
- const rel = relative5(realCwd, realFull);
18395
+ const rel = relative6(realCwd, realFull);
17749
18396
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
17750
18397
  return null;
17751
18398
  if (isGitInternalPath(rel))
@@ -17795,6 +18442,7 @@ function grepWorktree(env, req) {
17795
18442
  if (rgAvailable(env.cwd)) {
17796
18443
  const safePaths = paths.filter((path) => safeWorktreePath(env, path));
17797
18444
  const args = buildRgArgs(req.query, req.max, safePaths, req.regex, env.omitDirNames, env.excludeNames);
18445
+ args[0] = commandForExternal("rg");
17798
18446
  const proc = runSync(args, env.cwd, { timeout: 5000 });
17799
18447
  const stdout = proc.stdout;
17800
18448
  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));
@@ -17824,7 +18472,7 @@ function grepWorktree(env, req) {
17824
18472
  function grepTreeRef(env, req) {
17825
18473
  const safePaths = filterCallerPaths(env, req.paths);
17826
18474
  const args = [
17827
- "git",
18475
+ commandForExternal("git"),
17828
18476
  "-c",
17829
18477
  "core.quotepath=false",
17830
18478
  "grep",
@@ -17850,6 +18498,17 @@ function grepTreeRef(env, req) {
17850
18498
  };
17851
18499
  }
17852
18500
  function grepRepo(env, req) {
18501
+ const isWorktree = req.ref === "worktree" || req.ref === "";
18502
+ if (!isWorktree) {
18503
+ const refCheck = verifyTreeRefResult(req.ref, env.cwd);
18504
+ if (refCheck.ok !== true) {
18505
+ return {
18506
+ ok: false,
18507
+ error: refCheck.error,
18508
+ status: refCheck.status
18509
+ };
18510
+ }
18511
+ }
17853
18512
  if (!req.query.trim()) {
17854
18513
  return {
17855
18514
  ok: true,
@@ -17861,24 +18520,32 @@ function grepRepo(env, req) {
17861
18520
  }
17862
18521
  };
17863
18522
  }
17864
- if (req.ref === "worktree" || req.ref === "") {
18523
+ if (isWorktree) {
17865
18524
  return { ok: true, value: grepWorktree(env, req) };
17866
18525
  }
17867
- if (!verifyTreeRef(req.ref, env.cwd)) {
17868
- return { ok: false, error: "invalid target" };
17869
- }
17870
18526
  return { ok: true, value: grepTreeRef(env, req) };
17871
18527
  }
17872
18528
  function listRepoFiles(env, ref, generation) {
17873
- if (ref !== "worktree" && !verifyTreeRef(ref, env.cwd)) {
17874
- return { ok: false, error: "invalid target" };
18529
+ if (ref !== "worktree" && ref !== "") {
18530
+ const refCheck = verifyTreeRefResult(ref, env.cwd);
18531
+ if (refCheck.ok !== true) {
18532
+ return {
18533
+ ok: false,
18534
+ error: refCheck.error,
18535
+ status: refCheck.status
18536
+ };
18537
+ }
17875
18538
  }
17876
18539
  const effectiveRef = ref || "worktree";
17877
- const entries = listTree(effectiveRef, "", env.cwd, {
18540
+ const tree = listTreeResult(effectiveRef, "", env.cwd, {
17878
18541
  recursive: true,
17879
18542
  omitDirNames: env.omitDirNames,
17880
18543
  excludeNames: env.excludeNames
17881
- }).entries.filter((entry) => !isExcludedScopePath(entry.path, env.excludeNames));
18544
+ });
18545
+ if (tree.error) {
18546
+ return { ok: false, error: tree.error, status: tree.status };
18547
+ }
18548
+ const entries = tree.entries.filter((entry) => !isExcludedScopePath(entry.path, env.excludeNames));
17882
18549
  return {
17883
18550
  ok: true,
17884
18551
  value: buildFileSearchList(effectiveRef, generation, entries)
@@ -17886,6 +18553,7 @@ function listRepoFiles(env, ref, generation) {
17886
18553
  }
17887
18554
  var rgAvailableCache = null;
17888
18555
  var init_search_service = __esm(() => {
18556
+ init_command_resolver();
17889
18557
  init_git();
17890
18558
  init_runtime();
17891
18559
  init_search();
@@ -17893,7 +18561,7 @@ var init_search_service = __esm(() => {
17893
18561
 
17894
18562
  // web-src/server/mcp.ts
17895
18563
  import { readFileSync as readFileSync7 } from "node:fs";
17896
- import { join as join16 } from "node:path";
18564
+ import { join as join17 } from "node:path";
17897
18565
  function defaultMcpTools(options = {}) {
17898
18566
  return [
17899
18567
  {
@@ -18432,7 +19100,10 @@ function runStatusTool(input, defaultCwd) {
18432
19100
  }
18433
19101
  try {
18434
19102
  const report = buildStatusReport({ root: resolved.root, ref, limit });
18435
- return { text: JSON.stringify(report, null, 2) };
19103
+ return {
19104
+ text: JSON.stringify(report, null, 2),
19105
+ isError: !!(report.changed.error || report.staged.error)
19106
+ };
18436
19107
  } catch (err) {
18437
19108
  const detail = err instanceof Error ? err.message : String(err);
18438
19109
  return { text: `status failed: ${detail}`, isError: true };
@@ -19364,7 +20035,7 @@ var init_mcp = __esm(() => {
19364
20035
  init_search_cli();
19365
20036
  init_search_service();
19366
20037
  init_status_cli();
19367
- PACKAGE_VERSION = JSON.parse(readFileSync7(join16(ROOT, "package.json"), "utf8")).version;
20038
+ PACKAGE_VERSION = JSON.parse(readFileSync7(join17(ROOT, "package.json"), "utf8")).version;
19368
20039
  MCP_SERVER_INFO = {
19369
20040
  name: "code-viewer",
19370
20041
  title: "code-viewer",
@@ -19446,21 +20117,21 @@ var init_state_route = __esm(() => {
19446
20117
  var exports_preview = {};
19447
20118
  import {
19448
20119
  closeSync as closeSync2,
19449
- constants as constants2,
20120
+ constants as constants3,
19450
20121
  existsSync as existsSync8,
19451
20122
  lstatSync as lstatSync5,
19452
20123
  mkdirSync as mkdirSync4,
19453
20124
  openSync as openSync2,
19454
20125
  readFileSync as readFileSync8,
19455
- realpathSync as realpathSync5,
20126
+ realpathSync as realpathSync6,
19456
20127
  renameSync,
19457
- statSync as statSync5,
20128
+ statSync as statSync6,
19458
20129
  unlinkSync as unlinkSync2,
19459
20130
  watch,
19460
20131
  writeFileSync as writeFileSync2
19461
20132
  } from "node:fs";
19462
20133
  import { homedir as homedir3 } from "node:os";
19463
- import { basename as basename3, dirname as dirname4, extname as extname2, join as join17, relative as relative6 } from "node:path";
20134
+ import { basename as basename3, dirname as dirname5, extname as extname2, join as join18, relative as relative7 } from "node:path";
19464
20135
  function parseCli() {
19465
20136
  const rest = [];
19466
20137
  for (let i = 2;i < process.argv.length; i++) {
@@ -19469,15 +20140,15 @@ function parseCli() {
19469
20140
  console.log(`code-viewer ${VERSION}
19470
20141
 
19471
20142
  Usage:
19472
- code-viewer [--cwd <repo>] [--port <port>] [--open] [git-diff-args...]
19473
- code-viewer status [--cwd <repo>] [--ref <ref>] [--limit <N>] [--json]
20143
+ code-viewer [--cwd <repo>] [--port <port>] [--open] [--bin <name>=<path>] [git-diff-args...]
20144
+ code-viewer status [--cwd <repo>] [--bin git=<path>] [--ref <ref>] [--limit <N>] [--json]
19474
20145
  code-viewer annotate <start|add|add-db|rename|edit|move|list|delete|clear> [options]
19475
- code-viewer query <sources|schemas|schema|columns|ddl|exec|list|clear|snapshot|diff|search|redis|elasticsearch|s3> [options]
19476
- code-viewer search code --term <text> [--ref <ref>] [--path <p>...] [--regex] [--max <n>] [--json]
19477
- code-viewer search files --term <pattern> [--ref <ref>] [--max <n>] [--json]
19478
- code-viewer file <blame|history|show|diff> --path <p> [--ref <ref>] [...subcommand options] [--json]
20146
+ code-viewer query <sources|schemas|schema|columns|ddl|exec|list|clear|snapshot|diff|search|redis|elasticsearch|s3> [options] [--bin git=<path>]
20147
+ code-viewer search code --term <text> [--ref <ref>] [--path <p>...] [--regex] [--max <n>] [--json] [--bin git=<path>]
20148
+ code-viewer search files --term <pattern> [--ref <ref>] [--max <n>] [--json] [--bin git=<path>]
20149
+ code-viewer file <blame|history|show|diff> --path <p> [--ref <ref>] [...subcommand options] [--json] [--bin git=<path>]
19479
20150
  code-viewer skill install [--agent <list>] [--global]
19480
- code-viewer doctor [--cwd <path>] [--port <N>] [--json]
20151
+ code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker>=<path>]
19481
20152
  code-viewer agent-help
19482
20153
  code-viewer help
19483
20154
 
@@ -19513,9 +20184,8 @@ Examples:
19513
20184
  process.exit(1);
19514
20185
  }
19515
20186
  try {
19516
- const nextReal = realpathSync5(next);
19517
- const candidate = repoRoot(next);
19518
- cwd = candidate === nextReal ? candidate : nextReal;
20187
+ cwd = realpathSync6(next);
20188
+ cwdWasExplicit = true;
19519
20189
  } catch {
19520
20190
  console.error("--cwd must point to an existing directory");
19521
20191
  process.exit(1);
@@ -19530,6 +20200,18 @@ Examples:
19530
20200
  listenPort = parsed;
19531
20201
  } else if (arg === "--open") {
19532
20202
  openAfterStart = true;
20203
+ } else if (arg === "--bin") {
20204
+ const next = process.argv[++i];
20205
+ if (!next) {
20206
+ console.error("--bin requires <name>=<absolute-path>");
20207
+ process.exit(1);
20208
+ }
20209
+ const parsed = parseExternalCommandOverride(next);
20210
+ if (parsed.ok === false) {
20211
+ console.error(parsed.error);
20212
+ process.exit(1);
20213
+ }
20214
+ commandOverrides.push(parsed.override);
19533
20215
  } else if (arg === "--allow-upload") {} else if (arg === "--scope-omit-dir") {
19534
20216
  const next = process.argv[++i];
19535
20217
  if (!next) {
@@ -19546,6 +20228,21 @@ Examples:
19546
20228
  }
19547
20229
  if (rest.length)
19548
20230
  cliArgs = rest;
20231
+ const commandConfig = configureExternalCommands({
20232
+ cwd,
20233
+ cliOverrides: commandOverrides
20234
+ });
20235
+ if (commandConfig.ok === false) {
20236
+ console.error(commandConfig.error);
20237
+ process.exit(1);
20238
+ }
20239
+ const candidate = repoRoot(cwd);
20240
+ if (cwdWasExplicit) {
20241
+ if (candidate === cwd)
20242
+ cwd = candidate;
20243
+ } else if (candidate) {
20244
+ cwd = candidate;
20245
+ }
19549
20246
  warnIfLegacyConfigPresent();
19550
20247
  if (scopeOmitDirCliOverride) {
19551
20248
  scopeOmitDirNames = scopeOmitDirCliOverride;
@@ -19554,7 +20251,7 @@ Examples:
19554
20251
  }
19555
20252
  function warnIfLegacyConfigPresent() {
19556
20253
  try {
19557
- if (existsSync8(join17(cwd, ".code-viewer.json"))) {
20254
+ if (existsSync8(join18(cwd, ".code-viewer.json"))) {
19558
20255
  console.warn("[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed.");
19559
20256
  }
19560
20257
  } catch {}
@@ -19661,7 +20358,7 @@ function staticFile(pathname) {
19661
20358
  const spec = map[pathname];
19662
20359
  if (!spec)
19663
20360
  return null;
19664
- const full = join17(WEB_ROOT, spec[0]);
20361
+ const full = join18(WEB_ROOT, spec[0]);
19665
20362
  if (!existsSync8(full))
19666
20363
  return text("not found", 404);
19667
20364
  return new Response(readFileSync8(full), {
@@ -19669,17 +20366,17 @@ function staticFile(pathname) {
19669
20366
  });
19670
20367
  }
19671
20368
  function buildRangeArgs(range) {
19672
- const refs2 = [];
20369
+ const refs = [];
19673
20370
  if (range.from && range.from !== "worktree")
19674
- refs2.push(range.from);
20371
+ refs.push(range.from);
19675
20372
  if (range.to && range.to !== "worktree")
19676
- refs2.push(range.to);
19677
- return { args: refs2.length ? refs2 : cliArgs, refs: refs2 };
20373
+ refs.push(range.to);
20374
+ return { args: refs.length ? refs : cliArgs, refs };
19678
20375
  }
19679
- function includeUntracked(range, refs2) {
20376
+ function includeUntracked(range, refs) {
19680
20377
  const toWorktree = !range.to || range.to === "worktree";
19681
- if (refs2.length > 0)
19682
- return toWorktree && refs2.length < 2;
20378
+ if (refs.length > 0)
20379
+ return toWorktree && refs.length < 2;
19683
20380
  return cliArgs.length === 0 || cliArgs.length === 1 && cliArgs[0] === "HEAD";
19684
20381
  }
19685
20382
  function guessMediaKind(path) {
@@ -19766,11 +20463,13 @@ function computePayload(extras, range, pathFilter = "") {
19766
20463
  generation
19767
20464
  };
19768
20465
  }
19769
- const { args, refs: refs2 } = buildRangeArgs(range);
20466
+ const { args, refs } = buildRangeArgs(range);
19770
20467
  const fullArgs = [...extras, ...args];
19771
- const files = fileMeta(fullArgs, cwd, false);
19772
- if (includeUntracked(range, refs2))
20468
+ const metaResult = fileMetaResult(fullArgs, cwd, false);
20469
+ const files = metaResult.files;
20470
+ if (!metaResult.error && includeUntracked(range, refs)) {
19773
20471
  files.push(...untrackedMeta(cwd));
20472
+ }
19774
20473
  const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
19775
20474
  filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
19776
20475
  filteredFiles.forEach((file, i) => {
@@ -19790,14 +20489,15 @@ function computePayload(extras, range, pathFilter = "") {
19790
20489
  return acc;
19791
20490
  }, { files: meta.length, additions: 0, deletions: 0 });
19792
20491
  const toWorktree = !range.to || range.to === "worktree";
19793
- const label = refs2.length ? `${refs2.join(" .. ")}${toWorktree && refs2.length === 1 ? " .. worktree" : ""}` : cliArgs.join(" ");
20492
+ const label = refs.length ? `${refs.join(" .. ")}${toWorktree && refs.length === 1 ? " .. worktree" : ""}` : cliArgs.join(" ");
19794
20493
  return {
19795
20494
  files: meta,
19796
20495
  totals,
19797
20496
  range: label || "HEAD",
19798
20497
  project: basename3(cwd),
19799
20498
  branch: currentBranch(cwd) || undefined,
19800
- generation
20499
+ generation,
20500
+ ...metaResult.error ? { error: metaResult.error } : {}
19801
20501
  };
19802
20502
  }
19803
20503
  function handleDiffJson(url) {
@@ -19913,12 +20613,12 @@ function safeWorktreePath2(path) {
19913
20613
  return safeWorktreePath(currentSearchEnv(), path);
19914
20614
  }
19915
20615
  function worktreePath(path) {
19916
- return join17(cwd, path);
20616
+ return join18(cwd, path);
19917
20617
  }
19918
20618
  function safeOpenWorktreePath(path) {
19919
20619
  if (path === "") {
19920
20620
  try {
19921
- const realCwd = realpathSync5(cwd);
20621
+ const realCwd = realpathSync6(cwd);
19922
20622
  if (isGitInternalPath(realCwd))
19923
20623
  return null;
19924
20624
  return realCwd;
@@ -19929,7 +20629,7 @@ function safeOpenWorktreePath(path) {
19929
20629
  return safeWorktreePath2(path);
19930
20630
  }
19931
20631
  function parentRepoPath(path) {
19932
- const parent = dirname4(path);
20632
+ const parent = dirname5(path);
19933
20633
  return parent === "." ? "" : parent;
19934
20634
  }
19935
20635
  function isoDate(ms) {
@@ -19940,7 +20640,7 @@ function worktreeFileMetadata(path, knownSize) {
19940
20640
  if (!full)
19941
20641
  return {};
19942
20642
  try {
19943
- const stat2 = statSync5(full);
20643
+ const stat2 = statSync6(full);
19944
20644
  return {
19945
20645
  size: knownSize ?? stat2.size,
19946
20646
  created_at: isoDate(stat2.birthtimeMs),
@@ -19965,7 +20665,7 @@ function directoryMetadata(target, path) {
19965
20665
  if (!full)
19966
20666
  return {};
19967
20667
  try {
19968
- const stat2 = statSync5(full);
20668
+ const stat2 = statSync6(full);
19969
20669
  return {
19970
20670
  created_at: isoDate(stat2.birthtimeMs),
19971
20671
  updated_at: isoDate(stat2.mtimeMs)
@@ -19983,6 +20683,8 @@ function fileMetadataForTarget(target, path) {
19983
20683
  function attachTreeEntryMetadata(target, entry) {
19984
20684
  if (entry.type === "tree")
19985
20685
  return { ...entry, ...directoryMetadata(target, entry.path) };
20686
+ if (entry.type === "commit" && !entry.submodule && (target === "worktree" || target === ""))
20687
+ return { ...entry, ...directoryMetadata(target, entry.path) };
19986
20688
  if (entry.type !== "blob")
19987
20689
  return entry;
19988
20690
  return { ...entry, ...fileMetadataForTarget(target, entry.path) };
@@ -20014,19 +20716,25 @@ function handleTree(url) {
20014
20716
  return text("invalid path", 400);
20015
20717
  if ((target === "worktree" || target === "") && isGitInternalPath(path))
20016
20718
  return text("forbidden", 403);
20017
- if (target !== "worktree" && !verifyTreeRef(target, cwd))
20018
- return text("invalid target", 400);
20719
+ if (target !== "worktree") {
20720
+ const refCheck = verifyTreeRefResult(target, cwd);
20721
+ if (refCheck.ok !== true)
20722
+ return text(refCheck.error, refCheck.status ?? 400);
20723
+ }
20019
20724
  const recursive = url.searchParams.get("recursive") === "1";
20020
20725
  if (invalidScopeOmitDirNamesQuery(url))
20021
20726
  return text("invalid omit dirs", 400);
20022
20727
  if (invalidScopeExcludeNamesQuery(url))
20023
20728
  return text("invalid exclude names", 400);
20024
20729
  const excludeNames = scopeExcludeNamesFromQuery(url);
20025
- const entries = listTree(target, path, cwd, {
20730
+ const tree = listTreeResult(target, path, cwd, {
20026
20731
  recursive,
20027
20732
  omitDirNames: scopeOmitDirNamesFromQuery(url),
20028
20733
  excludeNames
20029
- }).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
20734
+ });
20735
+ if (tree.error)
20736
+ return text(tree.error, tree.status ?? 500);
20737
+ const entries = tree.entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
20030
20738
  return json2({
20031
20739
  ref: target,
20032
20740
  path,
@@ -20090,7 +20798,7 @@ function handleFiles2(url) {
20090
20798
  return json2(cached.body);
20091
20799
  const result = listRepoFiles(currentSearchEnv(omitDirNames, excludeNames), target, generation);
20092
20800
  if (result.ok !== true)
20093
- return text(result.error, 400);
20801
+ return text(result.error, result.status ?? 400);
20094
20802
  fileListCache.set(key, { generation, body: result.value });
20095
20803
  return json2(result.value);
20096
20804
  }
@@ -20114,7 +20822,7 @@ function handleGrep(url) {
20114
20822
  max
20115
20823
  });
20116
20824
  if (result.ok !== true)
20117
- return text(result.error, 400);
20825
+ return text(result.error, result.status ?? 400);
20118
20826
  return json2(result.value);
20119
20827
  }
20120
20828
  function handleRefCommits(url) {
@@ -20123,7 +20831,10 @@ function handleRefCommits(url) {
20123
20831
  const parsedSkip = Number(url.searchParams.get("skip") || "0");
20124
20832
  const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
20125
20833
  const skip = Number.isFinite(parsedSkip) && parsedSkip > 0 ? parsedSkip : undefined;
20126
- return json2(refCommitPage(cwd, { query, max, skip }));
20834
+ const result = refCommitPageResult(cwd, { query, max, skip });
20835
+ if (result.error)
20836
+ return text(result.error, result.status ?? 500);
20837
+ return json2({ commits: result.commits, hasMore: result.hasMore });
20127
20838
  }
20128
20839
  function handleLog(url) {
20129
20840
  const ref = url.searchParams.get("ref") || "HEAD";
@@ -20140,23 +20851,13 @@ function handleLog(url) {
20140
20851
  ...path ? { path } : {}
20141
20852
  });
20142
20853
  if (result.error)
20143
- return text(result.error, 400);
20854
+ return text(result.error, result.status ?? 400);
20144
20855
  const wantsWorktreeHead = path && skip === 0 && (ref === "worktree" || url.searchParams.get("worktree") === "1");
20145
20856
  let commits = result.commits;
20146
20857
  let hasWorktree = false;
20147
20858
  if (wantsWorktreeHead) {
20148
- const status = runSync([
20149
- "git",
20150
- "-c",
20151
- "core.quotepath=false",
20152
- "status",
20153
- "--porcelain=v1",
20154
- "-z",
20155
- "--untracked-files=normal",
20156
- "--",
20157
- path
20158
- ], cwd);
20159
- if (status.code === 0 && status.stdout.length > 0) {
20859
+ const status = statusPorcelainForPath(path, cwd);
20860
+ if (status.ok && status.stdout.length > 0) {
20160
20861
  const parts = status.stdout.split("\x00").filter(Boolean);
20161
20862
  if (parts.length > 0) {
20162
20863
  hasWorktree = true;
@@ -20183,7 +20884,7 @@ function handleLog(url) {
20183
20884
  }
20184
20885
  function blamePathKey(p) {
20185
20886
  try {
20186
- const st = statSync5(join17(cwd, p));
20887
+ const st = statSync6(join18(cwd, p));
20187
20888
  return `${st.mtimeMs}:${st.size}`;
20188
20889
  } catch {
20189
20890
  return "missing";
@@ -20215,10 +20916,12 @@ function handleFileBlame(url) {
20215
20916
  if (base === "worktree") {
20216
20917
  cacheKey = `worktree|${path}|${blamePathKey(path)}`;
20217
20918
  } else {
20218
- const resolved = runSync(["git", "rev-parse", "--verify", `${normalized.ref}^{commit}`], cwd);
20219
- if (resolved.code !== 0)
20220
- return text("unknown ref", 400);
20221
- cacheKey = `HEAD|${path}|${resolved.stdout.trim()}`;
20919
+ const resolved = verifyCommit(normalized.ref, cwd);
20920
+ if (resolved.ok === false) {
20921
+ const status = resolved.error === commandNotFoundDetail("git") ? 503 : 400;
20922
+ return text(resolved.error || "unknown ref", status);
20923
+ }
20924
+ cacheKey = `HEAD|${path}|${resolved.sha}`;
20222
20925
  }
20223
20926
  const cached = blameCache.get(cacheKey);
20224
20927
  if (cached) {
@@ -20229,6 +20932,8 @@ function handleFileBlame(url) {
20229
20932
  return json2({ ...cached, base, ref, generation });
20230
20933
  }
20231
20934
  const result = blame(cwd, { path, ref: normalized.ref, base });
20935
+ if (result.error && result.status)
20936
+ return text(result.error, result.status);
20232
20937
  if (!result.error)
20233
20938
  rememberBlame(cacheKey, result);
20234
20939
  return json2({ ...result, base, ref, generation });
@@ -20281,19 +20986,30 @@ function handleFileDiff(url) {
20281
20986
  const cached = fileCache.get(cacheKey);
20282
20987
  let diffText;
20283
20988
  let errText = "";
20989
+ let errStatus;
20284
20990
  if (cacheFresh(cached)) {
20285
20991
  diffText = cached.diffText;
20286
20992
  } else {
20287
20993
  if (isUntracked) {
20288
- diffText = untrackedFileDiff(extras, path, cwd).stdout || "";
20994
+ const res = untrackedFileDiff(extras, path, cwd);
20995
+ diffText = res.stdout || "";
20996
+ if (res.code !== 0) {
20997
+ errText = res.stderr;
20998
+ errStatus = res.status;
20999
+ }
20289
21000
  } else {
20290
21001
  const res = fileDiffText([...extras, ...args], oldPath ? [oldPath, path] : path, cwd);
20291
21002
  diffText = res.stdout || "";
20292
- if (res.code !== 0)
21003
+ if (res.code !== 0) {
20293
21004
  errText = res.stderr;
21005
+ errStatus = res.status;
21006
+ }
20294
21007
  }
20295
- setTimedCacheEntry(fileCache, cacheKey, { diffText });
21008
+ if (!errText)
21009
+ setTimedCacheEntry(fileCache, cacheKey, { diffText });
20296
21010
  }
21011
+ if (errStatus)
21012
+ return text(errText || "diff failed", errStatus);
20297
21013
  const mode = url.searchParams.get("mode") || "full";
20298
21014
  const truncated = mode === "preview" ? truncateToNHunks(diffText, Number(url.searchParams.get("max_hunks")) || PREVIEW_HUNKS_DEFAULT, Number(url.searchParams.get("max_lines")) || PREVIEW_LINES_DEFAULT) : truncateToNHunks(diffText, 1e9);
20299
21015
  const body = {
@@ -20314,7 +21030,7 @@ function handleFileDiff(url) {
20314
21030
  }
20315
21031
  function worktreeLineIndexSignature(full) {
20316
21032
  try {
20317
- const stat2 = statSync5(full);
21033
+ const stat2 = statSync6(full);
20318
21034
  return `size:${stat2.size}|mtime:${stat2.mtimeMs}|ctime:${stat2.ctimeMs}|ino:${stat2.ino || 0}`;
20319
21035
  } catch {
20320
21036
  return null;
@@ -20330,7 +21046,7 @@ async function getWorktreeLineIndex(full) {
20330
21046
  lineIndexCache.set(full, cached);
20331
21047
  return cached.index;
20332
21048
  }
20333
- const stat2 = statSync5(full);
21049
+ const stat2 = statSync6(full);
20334
21050
  if (stat2.size > LINE_INDEX_MAX_FILE_BYTES)
20335
21051
  return null;
20336
21052
  const index = await buildLineOffsetIndexFromStream(fileReadableStream(full), stat2.size);
@@ -20475,8 +21191,9 @@ async function handleFileRange(url) {
20475
21191
  };
20476
21192
  return json2(body);
20477
21193
  } else {
20478
- if (!verifyTreeRef(ref, cwd))
20479
- return text("invalid ref", 400);
21194
+ const refCheck = verifyTreeRefResult(ref, cwd);
21195
+ if (refCheck.ok !== true)
21196
+ return text(refCheck.error, refCheck.status ?? 400);
20480
21197
  const oid = objectId(ref, path, cwd);
20481
21198
  if (oid.code !== 0 || !oid.oid)
20482
21199
  return text("not in ref", 404);
@@ -20506,8 +21223,9 @@ function handleRawFile(req, url) {
20506
21223
  const ref = url.searchParams.get("ref") || "worktree";
20507
21224
  let body;
20508
21225
  if (ref !== "worktree" && ref !== "") {
20509
- if (!verifyTreeRef(ref, cwd))
20510
- return text("invalid ref", 400);
21226
+ const refCheck = verifyTreeRefResult(ref, cwd);
21227
+ if (refCheck.ok !== true)
21228
+ return text(refCheck.error, refCheck.status ?? 400);
20511
21229
  const size = rawFileSize(path, ref);
20512
21230
  if (size == null)
20513
21231
  return text("not in ref", 404);
@@ -20575,7 +21293,7 @@ function rawFileSize(path, ref) {
20575
21293
  if (!full)
20576
21294
  return null;
20577
21295
  try {
20578
- return statSync5(full).size;
21296
+ return statSync6(full).size;
20579
21297
  } catch {
20580
21298
  return null;
20581
21299
  }
@@ -20600,7 +21318,7 @@ function safeUploadFileName(name) {
20600
21318
  return trimmed;
20601
21319
  }
20602
21320
  function uploadOpenFlags() {
20603
- return constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW || 0);
21321
+ return constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | (constants3.O_NOFOLLOW || 0);
20604
21322
  }
20605
21323
  async function handleUploadFiles(req) {
20606
21324
  if (!uploadEnabled)
@@ -20636,7 +21354,7 @@ async function handleUploadFiles(req) {
20636
21354
  const realDir = safeOpenWorktreePath(dir);
20637
21355
  if (!realDir)
20638
21356
  return text("not found", 404);
20639
- const stats = statSync5(realDir);
21357
+ const stats = statSync6(realDir);
20640
21358
  if (!stats.isDirectory())
20641
21359
  return text("not a directory", 400);
20642
21360
  const files = form.getAll("files").filter((item) => item instanceof File);
@@ -20660,8 +21378,8 @@ async function handleUploadFiles(req) {
20660
21378
  total += file.size;
20661
21379
  if (total > MAX_UPLOAD_TOTAL_BYTES)
20662
21380
  return text("upload too large", 413);
20663
- const target = join17(realDir, safeName);
20664
- if (relative6(realDir, dirname4(target)) !== "")
21381
+ const target = join18(realDir, safeName);
21382
+ if (relative7(realDir, dirname5(target)) !== "")
20665
21383
  return text("invalid filename", 400);
20666
21384
  if (existsSync8(target))
20667
21385
  return text("file exists", 409);
@@ -20782,9 +21500,9 @@ function triggerUpdate(changedPaths) {
20782
21500
  sendSse("update", data);
20783
21501
  }
20784
21502
  function moveMacPathIntoTrash(path) {
20785
- const trashDir = join17(homedir3(), ".Trash");
21503
+ const trashDir = join18(homedir3(), ".Trash");
20786
21504
  const base = basename3(path) || "code-viewer-trash-item";
20787
- const target = join17(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
21505
+ const target = join18(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
20788
21506
  try {
20789
21507
  mkdirSync4(trashDir, { recursive: true });
20790
21508
  renameSync(path, target);
@@ -20826,11 +21544,11 @@ function restoreTrashPath(originalPath, trashPath) {
20826
21544
  if (!existsSync8(trashPath))
20827
21545
  return { ok: false, error: "trash item not found" };
20828
21546
  try {
20829
- const trashRoot = join17(homedir3(), ".Trash");
20830
- const trashRelative = relative6(trashRoot, trashPath);
21547
+ const trashRoot = join18(homedir3(), ".Trash");
21548
+ const trashRelative = relative7(trashRoot, trashPath);
20831
21549
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
20832
21550
  return { ok: false, error: "invalid trash handle" };
20833
- mkdirSync4(dirname4(original), { recursive: true });
21551
+ mkdirSync4(dirname5(original), { recursive: true });
20834
21552
  renameSync(trashPath, original);
20835
21553
  return { ok: true };
20836
21554
  } catch (error) {
@@ -20885,7 +21603,7 @@ async function handleOpenPath(req) {
20885
21603
  const target = safeOpenWorktreePath(targetPath);
20886
21604
  if (!target)
20887
21605
  return text("not found", 404);
20888
- const stats = statSync5(target);
21606
+ const stats = statSync6(target);
20889
21607
  if (!stats.isDirectory())
20890
21608
  return text("not a directory", 400);
20891
21609
  openOsPath(target);
@@ -20970,13 +21688,13 @@ async function handleCreateDirectory(req) {
20970
21688
  const parent = safeOpenWorktreePath(dir);
20971
21689
  if (!parent)
20972
21690
  return text("not found", 404);
20973
- const stats = statSync5(parent);
21691
+ const stats = statSync6(parent);
20974
21692
  if (!stats.isDirectory())
20975
21693
  return text("not a directory", 400);
20976
21694
  const targetPath = dir ? `${dir}/${name}` : name;
20977
21695
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
20978
21696
  return text("invalid target", 400);
20979
- const target = join17(parent, name);
21697
+ const target = join18(parent, name);
20980
21698
  if (existsSync8(target))
20981
21699
  return text("already exists", 409);
20982
21700
  try {
@@ -21277,11 +21995,12 @@ function restartWorktreeWatch() {
21277
21995
  }
21278
21996
  worktreeWatch = startScopedWorktreeWatch();
21279
21997
  }
21280
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, 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, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
21998
+ 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, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
21281
21999
  var init_preview = __esm(async () => {
21282
22000
  init_routes();
21283
22001
  init_annotations();
21284
22002
  init_cache();
22003
+ init_command_resolver();
21285
22004
  init_dev_assets();
21286
22005
  init_doctor();
21287
22006
  init_git();
@@ -21294,8 +22013,8 @@ var init_preview = __esm(async () => {
21294
22013
  init_server_registry();
21295
22014
  init_state_store();
21296
22015
  init_worktree_watcher();
21297
- WEB_ROOT = join17(ROOT, "web");
21298
- VERSION = JSON.parse(readFileSync8(join17(ROOT, "package.json"), "utf8")).version;
22016
+ WEB_ROOT = join18(ROOT, "web");
22017
+ VERSION = JSON.parse(readFileSync8(join18(ROOT, "package.json"), "utf8")).version;
21299
22018
  DEFAULT_ARGS = ["HEAD"];
21300
22019
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
21301
22020
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -21339,8 +22058,9 @@ var init_preview = __esm(async () => {
21339
22058
  ".scss",
21340
22059
  ".html"
21341
22060
  ]);
21342
- cwd = repoRoot(process.cwd()) || process.cwd();
22061
+ cwd = process.cwd();
21343
22062
  cliArgs = DEFAULT_ARGS;
22063
+ commandOverrides = [];
21344
22064
  scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
21345
22065
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
21346
22066
  scopeWatchLimit = DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
@@ -21423,8 +22143,12 @@ var init_preview = __esm(async () => {
21423
22143
  return handleMcp(req);
21424
22144
  if (url.pathname === "/_annotations")
21425
22145
  return handleAnnotations(req);
21426
- if (url.pathname === "/_refs")
21427
- return json2(refs(cwd));
22146
+ if (url.pathname === "/_refs") {
22147
+ const result = refsResult(cwd);
22148
+ if (result.error)
22149
+ return text(result.error, result.status ?? 500);
22150
+ return json2(result.refs);
22151
+ }
21428
22152
  if (url.pathname === "/refresh" && req.method === "POST") {
21429
22153
  if (!sideEffectRequestAllowed(req))
21430
22154
  return text("forbidden", 403);