@youtyan/code-viewer 0.6.2 → 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
  }
@@ -1439,7 +1769,7 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
1439
1769
  return omitDirNames.has(name) ? "heavy" : undefined;
1440
1770
  }
1441
1771
  function worktreeSubmodulePaths(cwd) {
1442
- if (!existsSync(join2(cwd, ".gitmodules")))
1772
+ if (!existsSync(join3(cwd, ".gitmodules")))
1443
1773
  return new Set;
1444
1774
  const res = run(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
1445
1775
  if (res.code !== 0)
@@ -1458,7 +1788,7 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
1458
1788
  type: isDirectory ? "tree" : "blob"
1459
1789
  };
1460
1790
  const entryPath = base ? `${base}/${name}` : name;
1461
- const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
1791
+ const type = isDirectory ? hasDotGitEntry(join3(dir, name)) ? "commit" : "tree" : "blob";
1462
1792
  const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
1463
1793
  const submodule = type === "commit" && submodulePaths.has(entryPath) ? true : undefined;
1464
1794
  const baseEntry = submodule ? { name, path: entryPath, type, submodule } : { name, path: entryPath, type };
@@ -1470,7 +1800,7 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
1470
1800
  }
1471
1801
  function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
1472
1802
  const base = normalizeTreePath(path);
1473
- const root = join2(cwd, base);
1803
+ const root = join3(cwd, base);
1474
1804
  const omitDirNameSet = new Set(omitDirNames);
1475
1805
  const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
1476
1806
  const submodulePaths = worktreeSubmodulePaths(cwd);
@@ -1517,7 +1847,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
1517
1847
  if (excludeNameSet.has(entry.name.toLowerCase()))
1518
1848
  continue;
1519
1849
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1520
- const full = join2(dir, entry.name);
1850
+ const full = join3(dir, entry.name);
1521
1851
  if (entry.isDirectory()) {
1522
1852
  const omittedReason = omittedWorktreeDirectoryReason(entry.name, omitDirNameSet);
1523
1853
  if (omittedReason) {
@@ -1549,7 +1879,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
1549
1879
  }
1550
1880
  function hasDotGitEntry(dir) {
1551
1881
  try {
1552
- lstatSync(join2(dir, ".git"));
1882
+ lstatSync(join3(dir, ".git"));
1553
1883
  return true;
1554
1884
  } catch (err) {
1555
1885
  return !!err && typeof err === "object" && "code" in err && err.code !== "ENOENT";
@@ -1612,14 +1942,20 @@ function listTree(ref, path, cwd, options = {}) {
1612
1942
  stderr: ""
1613
1943
  };
1614
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
+ }
1615
1951
  function untrackedMeta(cwd) {
1616
1952
  return untracked(cwd).flatMap((path) => {
1617
- const full = join2(cwd, path);
1953
+ const full = join3(cwd, path);
1618
1954
  let binary = false;
1619
1955
  let lines = 0;
1620
1956
  let fileExists = false;
1621
1957
  try {
1622
- fileExists = existsSync(full) && statSync(full).isFile();
1958
+ fileExists = existsSync(full) && statSync2(full).isFile();
1623
1959
  } catch {
1624
1960
  fileExists = false;
1625
1961
  }
@@ -1645,11 +1981,15 @@ function untrackedMeta(cwd) {
1645
1981
  ];
1646
1982
  });
1647
1983
  }
1648
- function fileMeta(args, cwd, includeUntracked = false) {
1649
- const ns = nameStatus(args, cwd);
1650
- const nm = numstatZ(args, cwd);
1651
- const byPath = new Map(nm.map((file) => [file.path, file]));
1652
- 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) => {
1653
1993
  const stats = byPath.get(file.path);
1654
1994
  return {
1655
1995
  ...file,
@@ -1658,11 +1998,13 @@ function fileMeta(args, cwd, includeUntracked = false) {
1658
1998
  binary: stats?.binary || false
1659
1999
  };
1660
2000
  });
1661
- return includeUntracked ? files.concat(untrackedMeta(cwd)) : files;
2001
+ return {
2002
+ files: includeUntracked ? files.concat(untrackedMeta(cwd)) : files
2003
+ };
1662
2004
  }
1663
2005
  function fileDiffText(args, path, cwd) {
1664
2006
  const paths = Array.isArray(path) ? path : [path];
1665
- return run([
2007
+ const res = run([
1666
2008
  "git",
1667
2009
  "-c",
1668
2010
  "core.quotepath=false",
@@ -1674,9 +2016,13 @@ function fileDiffText(args, path, cwd) {
1674
2016
  "--",
1675
2017
  ...paths
1676
2018
  ], cwd);
2019
+ if (isCommandNotFoundResult("git", res)) {
2020
+ return { ...res, stderr: commandNotFoundDetail("git"), status: 503 };
2021
+ }
2022
+ return res;
1677
2023
  }
1678
2024
  function untrackedFileDiff(extras, path, cwd) {
1679
- return run([
2025
+ const res = run([
1680
2026
  "git",
1681
2027
  "-c",
1682
2028
  "core.quotepath=false",
@@ -1688,6 +2034,10 @@ function untrackedFileDiff(extras, path, cwd) {
1688
2034
  "/dev/null",
1689
2035
  path
1690
2036
  ], cwd);
2037
+ if (isCommandNotFoundResult("git", res)) {
2038
+ return { ...res, stderr: commandNotFoundDetail("git"), status: 503 };
2039
+ }
2040
+ return res;
1691
2041
  }
1692
2042
  function splitHunks(diffText) {
1693
2043
  if (!diffText)
@@ -1764,6 +2114,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
1764
2114
  }
1765
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;
1766
2116
  var init_git = __esm(() => {
2117
+ init_command_resolver();
1767
2118
  init_runtime();
1768
2119
  DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
1769
2120
  "node_modules",
@@ -1820,16 +2171,16 @@ import {
1820
2171
  writeFileSync
1821
2172
  } from "node:fs";
1822
2173
  import { homedir } from "node:os";
1823
- import { join as join3 } from "node:path";
2174
+ import { join as join4 } from "node:path";
1824
2175
  function registryDir() {
1825
2176
  const override = process.env.CODE_VIEWER_TEST_SERVER_REGISTRY_DIR;
1826
2177
  if (override)
1827
2178
  return override;
1828
- return join3(homedir(), ".cache", "code-viewer", "servers");
2179
+ return join4(homedir(), ".cache", "code-viewer", "servers");
1829
2180
  }
1830
2181
  function serverRegistryFilePath(root) {
1831
2182
  const hash = createHash("sha256").update(root).digest("hex").slice(0, 16);
1832
- return join3(registryDir(), `${hash}.json`);
2183
+ return join4(registryDir(), `${hash}.json`);
1833
2184
  }
1834
2185
  function writeServerRegistry(entry) {
1835
2186
  try {
@@ -1870,7 +2221,7 @@ function removeServerRegistry(root, pid) {
1870
2221
  var init_server_registry = () => {};
1871
2222
 
1872
2223
  // web-src/server/cli-helpers.ts
1873
- import { realpathSync } from "node:fs";
2224
+ import { realpathSync as realpathSync2 } from "node:fs";
1874
2225
  function takeValue(argv, index, flag) {
1875
2226
  const value = argv[index + 1];
1876
2227
  if (value === undefined)
@@ -1918,14 +2269,21 @@ function validateRepoRelativePathValue(value, flag) {
1918
2269
  }
1919
2270
  function resolveRepoRootSafe(cwdOption) {
1920
2271
  const base = cwdOption || process.cwd();
2272
+ let baseReal;
1921
2273
  try {
1922
- return { ok: true, root: repoRoot(base) || realpathSync(base) };
2274
+ baseReal = realpathSync2(base);
1923
2275
  } catch {
1924
2276
  return {
1925
2277
  ok: false,
1926
2278
  error: `--cwd must point to an existing directory: ${base}`
1927
2279
  };
1928
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 };
1929
2287
  }
1930
2288
  function resolveRepoRoot(cwdOption) {
1931
2289
  const result = resolveRepoRootSafe(cwdOption);
@@ -3024,8 +3382,8 @@ __export(exports_file_cli, {
3024
3382
  FILE_DEFAULT_HISTORY_LIMIT: () => FILE_DEFAULT_HISTORY_LIMIT,
3025
3383
  FILE_AGENT_HELP: () => FILE_AGENT_HELP
3026
3384
  });
3027
- import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
3028
- 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";
3029
3387
  function validatePath(value) {
3030
3388
  return validateRepoRelativePathValue(value, "--path");
3031
3389
  }
@@ -3043,6 +3401,7 @@ function parseFileArgs(argv) {
3043
3401
  let cwd;
3044
3402
  const options = new Map;
3045
3403
  const flags = new Set;
3404
+ const commandOverrides = [];
3046
3405
  for (let i = 0;i < argv.length; i++) {
3047
3406
  const arg = argv[i];
3048
3407
  if (arg === "--help" || arg === "-h") {
@@ -3054,6 +3413,17 @@ function parseFileArgs(argv) {
3054
3413
  return { ok: false, error: taken.error };
3055
3414
  cwd = taken.value;
3056
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;
3057
3427
  } else if (VALUE_FLAGS.has(arg)) {
3058
3428
  const taken = takeValue(argv, i, arg);
3059
3429
  if ("error" in taken)
@@ -3115,7 +3485,8 @@ function parseFileArgs(argv) {
3115
3485
  base,
3116
3486
  json
3117
3487
  },
3118
- cwd
3488
+ cwd,
3489
+ ...commandOverrides.length ? { commandOverrides } : {}
3119
3490
  }
3120
3491
  };
3121
3492
  }
@@ -3153,7 +3524,8 @@ function parseFileArgs(argv) {
3153
3524
  query,
3154
3525
  json
3155
3526
  },
3156
- cwd
3527
+ cwd,
3528
+ ...commandOverrides.length ? { commandOverrides } : {}
3157
3529
  }
3158
3530
  };
3159
3531
  }
@@ -3192,7 +3564,8 @@ function parseFileArgs(argv) {
3192
3564
  end,
3193
3565
  json
3194
3566
  },
3195
- cwd
3567
+ cwd,
3568
+ ...commandOverrides.length ? { commandOverrides } : {}
3196
3569
  }
3197
3570
  };
3198
3571
  }
@@ -3281,7 +3654,8 @@ function parseFileArgs(argv) {
3281
3654
  maxLines,
3282
3655
  json
3283
3656
  },
3284
- cwd
3657
+ cwd,
3658
+ ...commandOverrides.length ? { commandOverrides } : {}
3285
3659
  }
3286
3660
  };
3287
3661
  }
@@ -3383,13 +3757,13 @@ function sliceLines(text, start, end) {
3383
3757
  function safeWorktreePathFromRoot(root, path) {
3384
3758
  if (validatePath(path))
3385
3759
  return null;
3386
- const full = join4(root, path);
3760
+ const full = join5(root, path);
3387
3761
  if (!existsSync3(full))
3388
3762
  return null;
3389
3763
  try {
3390
- const realRoot = realpathSync2(root);
3391
- const realFull = realpathSync2(full);
3392
- const rel = relative(realRoot, realFull);
3764
+ const realRoot = realpathSync3(root);
3765
+ const realFull = realpathSync3(full);
3766
+ const rel = relative2(realRoot, realFull);
3393
3767
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\")) {
3394
3768
  return null;
3395
3769
  }
@@ -3413,7 +3787,7 @@ function readShowText(root, command) {
3413
3787
  };
3414
3788
  }
3415
3789
  try {
3416
- const stat = statSync2(full);
3790
+ const stat = statSync3(full);
3417
3791
  if (!stat.isFile()) {
3418
3792
  return { code: 1, stdout: "", stderr: "not a file" };
3419
3793
  }
@@ -3466,12 +3840,12 @@ function runShow(root, command) {
3466
3840
  }
3467
3841
  }
3468
3842
  function buildFileDiffRangeArgs(from, to) {
3469
- const refs2 = [];
3843
+ const refs = [];
3470
3844
  if (from && from !== "worktree")
3471
- refs2.push(from);
3845
+ refs.push(from);
3472
3846
  if (to && to !== "worktree")
3473
- refs2.push(to);
3474
- return refs2;
3847
+ refs.push(to);
3848
+ return refs;
3475
3849
  }
3476
3850
  function buildFileDiffReport(root, command) {
3477
3851
  const base = {
@@ -3550,7 +3924,7 @@ async function runFileCli(argv) {
3550
3924
  console.error('Run "code-viewer file --help" for usage.');
3551
3925
  process.exit(1);
3552
3926
  }
3553
- const { command, cwd } = parsed.args;
3927
+ const { command, cwd, commandOverrides = [] } = parsed.args;
3554
3928
  if (command.kind === "help") {
3555
3929
  console.log(FILE_HELP);
3556
3930
  return;
@@ -3559,6 +3933,15 @@ async function runFileCli(argv) {
3559
3933
  console.log(FILE_AGENT_HELP);
3560
3934
  return;
3561
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
+ }
3562
3945
  const root = resolveRepoRoot(cwd);
3563
3946
  if (command.kind === "blame")
3564
3947
  return runBlame(root, command);
@@ -3572,13 +3955,14 @@ async function runFileCli(argv) {
3572
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";
3573
3956
  var init_file_cli = __esm(() => {
3574
3957
  init_cli_helpers();
3958
+ init_command_resolver();
3575
3959
  init_git();
3576
3960
  FILE_HELP = `code-viewer file — inspect a path's blame, history, contents, or diff
3577
3961
 
3578
3962
  Usage:
3579
- code-viewer file blame --path <path> [--ref <ref>] [--base <worktree|HEAD>] [--json] [--cwd <dir>]
3580
- code-viewer file history --path <path> [--ref <ref>] [--limit <n>] [--skip <n>] [--query <text>] [--json] [--cwd <dir>]
3581
- 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>]
3582
3966
  code-viewer file diff --path <path> [--from <ref>] [--to <ref>] [--old-path <path>] [--untracked]
3583
3967
  [--ignore-ws] [--ignore-blank] [--max-hunks <n>] [--max-lines <n>] [--full] [--json] [--cwd <dir>]
3584
3968
  code-viewer file --help
@@ -3591,6 +3975,7 @@ Common options:
3591
3975
  HEAD. Single-line, no NUL, no leading "-". blame/show accept
3592
3976
  the literal "worktree" to mean the working tree.
3593
3977
  --cwd <dir> Repository to target (default: process.cwd()).
3978
+ --bin git=<p> Override git executable path.
3594
3979
  --json Emit a structured JSON payload instead of plain text.
3595
3980
  --help, -h Show this help.
3596
3981
 
@@ -3657,7 +4042,8 @@ Diff Viewer views.
3657
4042
  - Run from inside the repository, or pass --cwd <repo>.
3658
4043
  - No code-viewer server is required (this command reads git refs or the
3659
4044
  worktree directly).
3660
- - 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.
3661
4047
 
3662
4048
  ## How to call
3663
4049
 
@@ -3902,6 +4288,7 @@ function parseQueryArgs(argv) {
3902
4288
  let server;
3903
4289
  const options = new Map;
3904
4290
  const flags = new Set;
4291
+ const commandOverrides = [];
3905
4292
  for (let i = 0;i < argv.length; i++) {
3906
4293
  const arg = argv[i];
3907
4294
  if (arg === "--help" || arg === "-h")
@@ -3915,6 +4302,17 @@ function parseQueryArgs(argv) {
3915
4302
  else
3916
4303
  server = taken.value;
3917
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;
3918
4316
  } else if (VALUE_FLAGS2.has(arg)) {
3919
4317
  const taken = takeValue(argv, i, arg);
3920
4318
  if ("error" in taken)
@@ -3932,7 +4330,11 @@ function parseQueryArgs(argv) {
3932
4330
  const subcommand = rest[0];
3933
4331
  if (!subcommand)
3934
4332
  return { ok: true, args: { command: { kind: "help" } } };
3935
- const globalArgs = { cwd, server };
4333
+ const globalArgs = {
4334
+ cwd,
4335
+ server,
4336
+ ...commandOverrides.length ? { commandOverrides } : {}
4337
+ };
3936
4338
  if (subcommand === "agent-help") {
3937
4339
  return { ok: true, args: { command: { kind: "agent-help" } } };
3938
4340
  }
@@ -4707,7 +5109,7 @@ async function runQueryCli(argv) {
4707
5109
  console.error('Run "code-viewer query --help" for usage.');
4708
5110
  process.exit(1);
4709
5111
  }
4710
- const { command, cwd, server } = parsed.args;
5112
+ const { command, cwd, server, commandOverrides = [] } = parsed.args;
4711
5113
  if (command.kind === "help") {
4712
5114
  console.log(QUERY_HELP);
4713
5115
  return;
@@ -4716,7 +5118,16 @@ async function runQueryCli(argv) {
4716
5118
  console.log(QUERY_AGENT_HELP);
4717
5119
  return;
4718
5120
  }
4719
- 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);
4720
5131
  const serverUrl = await ensureServerUrl(root, server, "/");
4721
5132
  if (command.kind === "sources")
4722
5133
  return runSources(serverUrl, command);
@@ -5561,6 +5972,8 @@ Usage:
5561
5972
  Global options:
5562
5973
  --cwd <dir> repository directory (default: current directory)
5563
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.
5564
5977
 
5565
5978
  Examples:
5566
5979
  code-viewer query sources --json
@@ -5966,6 +6379,7 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
5966
6379
  var init_query_cli = __esm(() => {
5967
6380
  init_routes();
5968
6381
  init_cli_helpers();
6382
+ init_command_resolver();
5969
6383
  init_cli_helpers();
5970
6384
  VALUE_FLAGS2 = new Set([
5971
6385
  "--db",
@@ -6458,6 +6872,7 @@ function parseSearchArgs(argv) {
6458
6872
  const options = new Map;
6459
6873
  const paths = [];
6460
6874
  const flags = new Set;
6875
+ const commandOverrides = [];
6461
6876
  for (let i = 0;i < argv.length; i++) {
6462
6877
  const arg = argv[i];
6463
6878
  if (arg === "--help" || arg === "-h") {
@@ -6472,6 +6887,17 @@ function parseSearchArgs(argv) {
6472
6887
  else
6473
6888
  server = taken.value;
6474
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;
6475
6901
  } else if (REPEATABLE_VALUE_FLAGS.has(arg)) {
6476
6902
  const taken = takeValue(argv, i, arg);
6477
6903
  if ("error" in taken)
@@ -6559,7 +6985,8 @@ function parseSearchArgs(argv) {
6559
6985
  json: flags.has("--json")
6560
6986
  },
6561
6987
  cwd,
6562
- server
6988
+ server,
6989
+ ...commandOverrides.length ? { commandOverrides } : {}
6563
6990
  }
6564
6991
  };
6565
6992
  }
@@ -6594,7 +7021,8 @@ function parseSearchArgs(argv) {
6594
7021
  json: flags.has("--json")
6595
7022
  },
6596
7023
  cwd,
6597
- server
7024
+ server,
7025
+ ...commandOverrides.length ? { commandOverrides } : {}
6598
7026
  }
6599
7027
  };
6600
7028
  }
@@ -6686,7 +7114,7 @@ async function runSearchCli(argv) {
6686
7114
  console.error('Run "code-viewer search --help" for usage.');
6687
7115
  process.exit(1);
6688
7116
  }
6689
- const { command, cwd, server } = parsed.args;
7117
+ const { command, cwd, server, commandOverrides = [] } = parsed.args;
6690
7118
  if (command.kind === "help") {
6691
7119
  console.log(SEARCH_HELP);
6692
7120
  return;
@@ -6695,7 +7123,16 @@ async function runSearchCli(argv) {
6695
7123
  console.log(SEARCH_AGENT_HELP);
6696
7124
  return;
6697
7125
  }
6698
- 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);
6699
7136
  const serverUrl = await ensureServerUrl(root, server, "/");
6700
7137
  if (command.kind === "code")
6701
7138
  return runCode(serverUrl, command);
@@ -6705,15 +7142,16 @@ async function runSearchCli(argv) {
6705
7142
  var FILE_NAME_SEARCH_DEFAULT_MAX = 50, SEARCH_HELP, SEARCH_AGENT_HELP, VALUE_FLAGS3, REPEATABLE_VALUE_FLAGS, BOOL_FLAGS3;
6706
7143
  var init_search_cli = __esm(() => {
6707
7144
  init_cli_helpers();
7145
+ init_command_resolver();
6708
7146
  init_search();
6709
7147
  SEARCH_HELP = `code-viewer search — text and filename search across the worktree or a git ref
6710
7148
 
6711
7149
  Usage:
6712
7150
  code-viewer search code --term <text> [--ref <ref>] [--path <path>...]
6713
7151
  [--regex] [--max <n>] [--json]
6714
- [--cwd <dir>] [--server <url>]
7152
+ [--cwd <dir>] [--server <url>] [--bin <name>=<path>]
6715
7153
  code-viewer search files --term <pattern> [--ref <ref>] [--max <n>] [--json]
6716
- [--cwd <dir>] [--server <url>]
7154
+ [--cwd <dir>] [--server <url>] [--bin <name>=<path>]
6717
7155
  code-viewer search --help
6718
7156
  code-viewer search agent-help
6719
7157
 
@@ -6726,6 +7164,8 @@ Common options:
6726
7164
  --json Emit a structured JSON payload instead of plain lines.
6727
7165
  --cwd <dir> Repository to target (default: process.cwd()).
6728
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.
6729
7169
  --help, -h Show this help.
6730
7170
 
6731
7171
  search code only:
@@ -6852,24 +7292,24 @@ Parse failures and unreachable servers exit 1.
6852
7292
 
6853
7293
  // web-src/server/root.ts
6854
7294
  import { existsSync as existsSync4 } from "node:fs";
6855
- import { dirname as dirname2, join as join5, normalize } from "node:path";
7295
+ import { dirname as dirname3, join as join6, normalize } from "node:path";
6856
7296
  import { fileURLToPath } from "node:url";
6857
7297
  function findRoot(start) {
6858
7298
  let current = start;
6859
7299
  for (let i = 0;i < 5; i++) {
6860
- if (existsSync4(join5(current, "package.json")) && existsSync4(join5(current, "web"))) {
7300
+ if (existsSync4(join6(current, "package.json")) && existsSync4(join6(current, "web"))) {
6861
7301
  return normalize(current);
6862
7302
  }
6863
- const parent = dirname2(current);
7303
+ const parent = dirname3(current);
6864
7304
  if (parent === current)
6865
7305
  break;
6866
7306
  current = parent;
6867
7307
  }
6868
- return normalize(join5(start, "..", ".."));
7308
+ return normalize(join6(start, "..", ".."));
6869
7309
  }
6870
7310
  var ROOT;
6871
7311
  var init_root = __esm(() => {
6872
- ROOT = findRoot(dirname2(fileURLToPath(import.meta.url)));
7312
+ ROOT = findRoot(dirname3(fileURLToPath(import.meta.url)));
6873
7313
  });
6874
7314
 
6875
7315
  // web-src/server/skill-cli.ts
@@ -6884,7 +7324,7 @@ __export(exports_skill_cli, {
6884
7324
  });
6885
7325
  import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
6886
7326
  import { homedir as homedir2 } from "node:os";
6887
- import { join as join6, resolve } from "node:path";
7327
+ import { join as join7, resolve } from "node:path";
6888
7328
  function parseAgentList(value) {
6889
7329
  if (value === "all")
6890
7330
  return [...AGENT_NAMES];
@@ -6944,7 +7384,7 @@ function parseSkillArgs(argv) {
6944
7384
  function discoverBundledSkills(skillsRoot) {
6945
7385
  if (!existsSync5(skillsRoot))
6946
7386
  return [];
6947
- 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();
6948
7388
  }
6949
7389
  function installSkill(args, deps) {
6950
7390
  const skills = discoverBundledSkills(deps.skillsRoot);
@@ -6958,8 +7398,8 @@ function installSkill(args, deps) {
6958
7398
  const results = [];
6959
7399
  for (const agent of args.agents) {
6960
7400
  for (const skill of skills) {
6961
- const sourceDir = join6(deps.skillsRoot, skill);
6962
- 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);
6963
7403
  const action = existsSync5(target) ? "updated" : "installed";
6964
7404
  try {
6965
7405
  mkdirSync2(target, { recursive: true });
@@ -6988,7 +7428,7 @@ function runSkillCli(argv) {
6988
7428
  return;
6989
7429
  }
6990
7430
  const result = installSkill(parsed.args, {
6991
- skillsRoot: join6(ROOT, "skills"),
7431
+ skillsRoot: join7(ROOT, "skills"),
6992
7432
  homeDir: homedir2(),
6993
7433
  projectDir: process.cwd()
6994
7434
  });
@@ -7122,6 +7562,7 @@ function parseStatusArgs(argv) {
7122
7562
  const options = new Map;
7123
7563
  const flags = new Set;
7124
7564
  const positional = [];
7565
+ const commandOverrides = [];
7125
7566
  for (let i = 0;i < argv.length; i++) {
7126
7567
  const arg = argv[i];
7127
7568
  if (arg === "--help" || arg === "-h") {
@@ -7133,6 +7574,17 @@ function parseStatusArgs(argv) {
7133
7574
  return { ok: false, error: taken.error };
7134
7575
  cwd = taken.value;
7135
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;
7136
7588
  } else if (VALUE_FLAGS4.has(arg)) {
7137
7589
  const taken = takeValue(argv, i, arg);
7138
7590
  if ("error" in taken)
@@ -7183,7 +7635,8 @@ function parseStatusArgs(argv) {
7183
7635
  limit,
7184
7636
  json: flags.has("--json")
7185
7637
  },
7186
- cwd
7638
+ cwd,
7639
+ ...commandOverrides.length ? { commandOverrides } : {}
7187
7640
  }
7188
7641
  };
7189
7642
  }
@@ -7196,9 +7649,13 @@ function sumTotals(files) {
7196
7649
  }
7197
7650
  return { files: files.length, additions, deletions };
7198
7651
  }
7199
- function buildGroup(files) {
7652
+ function buildGroup(files, error) {
7200
7653
  const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
7201
- return { files: sorted, totals: sumTotals(sorted) };
7654
+ return {
7655
+ files: sorted,
7656
+ totals: sumTotals(sorted),
7657
+ ...error ? { error } : {}
7658
+ };
7202
7659
  }
7203
7660
  function metaPathKey(file) {
7204
7661
  return `${file.old_path || ""}\x00${file.path}`;
@@ -7215,6 +7672,9 @@ function mergeMissingByPath(primary, fallback) {
7215
7672
  }
7216
7673
  return merged;
7217
7674
  }
7675
+ function isMissingDiffBaseError(error) {
7676
+ return !!error && /ambiguous argument 'HEAD'|bad revision 'HEAD'/i.test(error);
7677
+ }
7218
7678
  function joinCli(parts, serverUrl) {
7219
7679
  const head = parts[0];
7220
7680
  const tail = parts.slice(1).join(" ");
@@ -7259,6 +7719,8 @@ function formatGroupText(label, group) {
7259
7719
  const totals = group.totals;
7260
7720
  const summary = totals.files === 0 ? `${label}: 0 files` : `${label}: ${totals.files} files (+${totals.additions} / -${totals.deletions})`;
7261
7721
  lines.push(summary);
7722
+ if (group.error)
7723
+ lines.push(` # error: ${group.error}`);
7262
7724
  for (const file of group.files)
7263
7725
  lines.push(formatChangedLine(file));
7264
7726
  return lines;
@@ -7274,10 +7736,13 @@ function formatRecentText(commits) {
7274
7736
  }
7275
7737
  function buildStatusReport(opts) {
7276
7738
  const { root, ref, limit } = opts;
7277
- const stagedFiles = fileMeta(["--cached"], root, false);
7278
- const changedFiles = mergeMissingByPath(fileMeta(["HEAD"], root, true), stagedFiles);
7279
- const changed = buildGroup(changedFiles);
7280
- 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);
7281
7746
  const history = commitHistory(root, { ref, skip: 0, limit });
7282
7747
  const branch = currentBranch(root);
7283
7748
  const remote = remoteWebUrl(root);
@@ -7325,7 +7790,7 @@ function runStatusCli(argv) {
7325
7790
  console.error('Run "code-viewer status --help" for usage.');
7326
7791
  process.exit(1);
7327
7792
  }
7328
- const { command, cwd } = parsed.args;
7793
+ const { command, cwd, commandOverrides = [] } = parsed.args;
7329
7794
  if (command.kind === "help") {
7330
7795
  console.log(STATUS_HELP);
7331
7796
  return;
@@ -7334,6 +7799,15 @@ function runStatusCli(argv) {
7334
7799
  console.log(STATUS_AGENT_HELP);
7335
7800
  return;
7336
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
+ }
7337
7811
  const root = resolveRepoRoot(cwd);
7338
7812
  const report = buildStatusReport({
7339
7813
  root,
@@ -7342,9 +7816,13 @@ function runStatusCli(argv) {
7342
7816
  });
7343
7817
  if (command.json) {
7344
7818
  console.log(JSON.stringify(report, null, 2));
7819
+ if (report.changed.error || report.staged.error)
7820
+ process.exit(1);
7345
7821
  return;
7346
7822
  }
7347
7823
  console.log(formatStatusReportText(report));
7824
+ if (report.changed.error || report.staged.error)
7825
+ process.exit(1);
7348
7826
  }
7349
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
7350
7828
 
@@ -7376,11 +7854,11 @@ browser home page uses — no server, no SQLite, no docker required.
7376
7854
  { repoRoot, branch, remoteWebUrl, changed, staged, recentCommits,
7377
7855
  nextCommands }
7378
7856
  changed / staged share shape { files: GitFileMeta[], totals: { files,
7379
- additions, deletions } } so AI can iterate both the same way.
7857
+ additions, deletions }, error? } so AI can iterate both the same way.
7380
7858
  recentCommitsError is present when --ref cannot resolve; the command
7381
7859
  still emits changed / staged / nextCommands and exits 0.
7382
- - exit 0 on success. exit 1 only on argument errors or when --cwd does
7383
- 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.
7384
7862
 
7385
7863
  ## Suggested usage
7386
7864
 
@@ -7390,16 +7868,18 @@ browser home page uses — no server, no SQLite, no docker required.
7390
7868
  `, VALUE_FLAGS4, BOOL_FLAGS4;
7391
7869
  var init_status_cli = __esm(() => {
7392
7870
  init_cli_helpers();
7871
+ init_command_resolver();
7393
7872
  init_git();
7394
7873
  init_server_registry();
7395
7874
  STATUS_HELP = `code-viewer status — snapshot the current repo for AI agents
7396
7875
 
7397
7876
  Usage:
7398
- code-viewer status [--cwd <repo>] [--ref <ref>] [--limit <N>] [--json]
7877
+ code-viewer status [--cwd <repo>] [--bin git=<path>] [--ref <ref>] [--limit <N>] [--json]
7399
7878
  code-viewer status agent-help
7400
7879
 
7401
7880
  Options:
7402
7881
  --cwd <repo> Repository to inspect (default: current directory).
7882
+ --bin git=<p> Override git executable path.
7403
7883
  --ref <ref> Ref to read recent commits from (default: ${STATUS_DEFAULT_REF}).
7404
7884
  --limit <N> Recent commits to include (default: ${STATUS_DEFAULT_LIMIT}, max ${STATUS_HARD_CAP_LIMIT}).
7405
7885
  --json Emit a structured JSON payload instead of plain text.
@@ -7928,7 +8408,22 @@ var init_spawn_runner = () => {};
7928
8408
  // web-src/server/database/adapters/docker-utils.ts
7929
8409
  import { spawnSync as spawnSync2 } from "node:child_process";
7930
8410
  function isDockerComposeServiceUnavailableError(err) {
7931
- 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 });
7932
8427
  }
7933
8428
  function parseComposePsOutput(stdout) {
7934
8429
  const output = stdout.trim();
@@ -7942,18 +8437,21 @@ function parseComposePsOutput(stdout) {
7942
8437
  }
7943
8438
  return byService;
7944
8439
  }
7945
- function cacheComposePsFailure(cwd, stderr, now) {
7946
- 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;
7947
8443
  composePsCache.set(cwd, {
7948
8444
  containers: null,
7949
8445
  positiveExpiresAt: now,
7950
- negativeExpiresAt: now + failureTtl
8446
+ negativeExpiresAt: now + failureTtl,
8447
+ error: stderr
7951
8448
  });
7952
8449
  }
7953
8450
  function runComposePsAsync(cwd, signal) {
7954
8451
  throwIfAborted(signal, "docker compose ps aborted");
7955
8452
  if (spawnSyncImpl !== spawnSync2) {
7956
- 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"], {
7957
8455
  encoding: "utf8",
7958
8456
  timeout: 5000,
7959
8457
  stdio: ["ignore", "pipe", "pipe"],
@@ -7961,19 +8459,21 @@ function runComposePsAsync(cwd, signal) {
7961
8459
  });
7962
8460
  return Promise.resolve({
7963
8461
  stdout: String(proc.stdout || ""),
7964
- stderr: String(proc.stderr || ""),
8462
+ stderr: `${String(proc.stderr || "")}${proc.error ? `
8463
+ ${proc.error.message}` : ""}`,
7965
8464
  code: proc.status ?? 1
7966
8465
  });
7967
8466
  }
7968
8467
  return spawnTextAsync({
7969
- command: "docker",
8468
+ command: dockerCommand(),
7970
8469
  args: ["compose", "ps", "--format", "json", "--status", "running"],
7971
8470
  cwd,
7972
8471
  timeoutMs: 5000,
7973
8472
  signal,
7974
8473
  killSignal: "SIGKILL",
7975
8474
  abortMessage: "docker compose ps aborted",
7976
- timeoutMessage: "docker compose ps timed out"
8475
+ timeoutMessage: "docker compose ps timed out",
8476
+ rejectOnError: false
7977
8477
  });
7978
8478
  }
7979
8479
  async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal) {
@@ -8002,11 +8502,14 @@ async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal)
8002
8502
  } catch (err) {
8003
8503
  if (isAbortLikeError(err, controller.signal))
8004
8504
  throw err;
8005
- 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);
8006
8509
  return null;
8007
8510
  }
8008
8511
  if (proc.code !== 0) {
8009
- cacheComposePsFailure(cwd, proc.stderr || "", startedAt);
8512
+ cacheComposePsFailure(cwd, proc, startedAt);
8010
8513
  return null;
8011
8514
  }
8012
8515
  try {
@@ -8021,7 +8524,8 @@ async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal)
8021
8524
  composePsCache.set(cwd, {
8022
8525
  containers: null,
8023
8526
  positiveExpiresAt: startedAt,
8024
- 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"
8025
8529
  });
8026
8530
  return null;
8027
8531
  }
@@ -8064,12 +8568,14 @@ async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal)
8064
8568
  async function resolveRunningComposeContainerNameOrThrowAsync(serviceName, cwd, signal) {
8065
8569
  const containerName = await resolveRunningComposeContainerNameAsync(serviceName, cwd, signal);
8066
8570
  if (!containerName) {
8571
+ throwIfCachedComposeDockerCommandUnavailable(cwd);
8067
8572
  throw new DockerComposeServiceUnavailableError(serviceName, cwd);
8068
8573
  }
8069
8574
  return containerName;
8070
8575
  }
8071
- 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;
8072
8577
  var init_docker_utils = __esm(() => {
8578
+ init_command_resolver();
8073
8579
  init_spawn_runner();
8074
8580
  composePsCache = new Map;
8075
8581
  composePsPending = new Map;
@@ -8085,6 +8591,13 @@ var init_docker_utils = __esm(() => {
8085
8591
  this.cwd = cwd;
8086
8592
  }
8087
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
+ };
8088
8601
  });
8089
8602
 
8090
8603
  // web-src/server/database/adapters/sql-capture.ts
@@ -8135,7 +8648,7 @@ function fallbackDockerDatabases(defaultDb) {
8135
8648
  function buildExecArgs(config, sql) {
8136
8649
  if (config.kind === "postgresql") {
8137
8650
  return [
8138
- "docker",
8651
+ dockerCommand(),
8139
8652
  "exec",
8140
8653
  "-i",
8141
8654
  "-e",
@@ -8161,7 +8674,7 @@ function buildExecArgs(config, sql) {
8161
8674
  ];
8162
8675
  }
8163
8676
  return [
8164
- "docker",
8677
+ dockerCommand(),
8165
8678
  "exec",
8166
8679
  "-i",
8167
8680
  "-e",
@@ -8186,7 +8699,8 @@ function execInContainer(config, sql, timeoutMs = 1e4) {
8186
8699
  });
8187
8700
  return {
8188
8701
  stdout: proc.stdout || "",
8189
- stderr: proc.stderr || "",
8702
+ stderr: `${proc.stderr || ""}${proc.error ? `
8703
+ ${proc.error.message}` : ""}`,
8190
8704
  code: proc.status ?? 1
8191
8705
  };
8192
8706
  }
@@ -8207,11 +8721,20 @@ async function readStreamText(stream) {
8207
8721
  }
8208
8722
  async function execWithBunSpawn(spawnFn, args, timeoutMs, signal) {
8209
8723
  throwIfAborted(signal, "query aborted");
8210
- const proc = spawnFn(args, {
8211
- stdin: "ignore",
8212
- stdout: "pipe",
8213
- stderr: "pipe"
8214
- });
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
+ }
8215
8738
  let timedOut = false;
8216
8739
  let aborted = false;
8217
8740
  const abort = () => {
@@ -8261,13 +8784,17 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
8261
8784
  }
8262
8785
  async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
8263
8786
  recordSql(sql);
8264
- if (spawnSyncImpl2 !== spawnSync3)
8265
- 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
+ }
8266
8793
  const args = buildExecArgs(config, sql);
8267
8794
  const bunSpawn = globalThis.Bun?.spawn;
8268
- if (bunSpawn)
8269
- return execWithBunSpawn(bunSpawn, args, timeoutMs, signal);
8270
- 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;
8271
8798
  }
8272
8799
  function stripFinalLineBreak(text) {
8273
8800
  if (text.endsWith(`\r
@@ -8798,14 +9325,22 @@ function createDockerAdapter(config) {
8798
9325
  try {
8799
9326
  const result = await execAsync(`SHOW CREATE TABLE ${tableIdentifier(table)}`, signal);
8800
9327
  return result.rows.length > 0 ? result.rows[0][1] || "" : "";
8801
- } catch {
9328
+ } catch (err) {
9329
+ if (isAbortLikeError(err, signal))
9330
+ throw err;
9331
+ if (err instanceof DockerCommandUnavailableError)
9332
+ throw err;
8802
9333
  return "";
8803
9334
  }
8804
9335
  }
8805
9336
  try {
8806
9337
  const result = await execAsync(`SELECT 'CREATE TABLE ' || ${escapeSqlString(tableIdentifier(table))} || ' (...)' AS ddl`, signal);
8807
9338
  return result.rows.length > 0 ? result.rows[0][0] || "" : "";
8808
- } catch {
9339
+ } catch (err) {
9340
+ if (isAbortLikeError(err, signal))
9341
+ throw err;
9342
+ if (err instanceof DockerCommandUnavailableError)
9343
+ throw err;
8809
9344
  return "";
8810
9345
  }
8811
9346
  },
@@ -8822,7 +9357,11 @@ function createDockerAdapter(config) {
8822
9357
  name: row[0],
8823
9358
  sql: row[1] || ""
8824
9359
  }));
8825
- } catch {
9360
+ } catch (err) {
9361
+ if (isAbortLikeError(err, signal))
9362
+ throw err;
9363
+ if (err instanceof DockerCommandUnavailableError)
9364
+ throw err;
8826
9365
  return [];
8827
9366
  }
8828
9367
  },
@@ -8873,6 +9412,7 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
8873
9412
  return [...cached.value];
8874
9413
  const containerName = await resolveRunningComposeContainerNameAsync(serviceName, cwd, signal);
8875
9414
  if (!containerName) {
9415
+ throwIfCachedComposeDockerCommandUnavailable(cwd);
8876
9416
  return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
8877
9417
  }
8878
9418
  const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || env.POSTGRES_USERNAME || env.MYSQL_USERNAME || env.USER || "root";
@@ -8906,6 +9446,8 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
8906
9446
  } catch (err) {
8907
9447
  if (isAbortLikeError(err, signal))
8908
9448
  throw err;
9449
+ if (err instanceof DockerCommandUnavailableError)
9450
+ throw err;
8909
9451
  const fallback = fallbackDockerDatabases(defaultDb);
8910
9452
  if (fallback.length > 0)
8911
9453
  return fallback;
@@ -8925,6 +9467,7 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
8925
9467
  return [...cached.value];
8926
9468
  const containerName = await resolveRunningComposeContainerNameAsync(serviceName, cwd, signal);
8927
9469
  if (!containerName) {
9470
+ throwIfCachedComposeDockerCommandUnavailable(cwd);
8928
9471
  return setDockerSchemasCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
8929
9472
  }
8930
9473
  const config = {
@@ -8947,6 +9490,8 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
8947
9490
  } catch (err) {
8948
9491
  if (isAbortLikeError(err, signal))
8949
9492
  throw err;
9493
+ if (err instanceof DockerCommandUnavailableError)
9494
+ throw err;
8950
9495
  return setDockerSchemasCache(cacheKey, ["public"], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
8951
9496
  }
8952
9497
  }
@@ -9039,11 +9584,11 @@ __ES_STATUS__:%{http_code}
9039
9584
  ];
9040
9585
  return { args, input: curlConfig };
9041
9586
  }
9042
- function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signal) {
9587
+ async function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signal) {
9043
9588
  throwIfAborted(signal, "elasticsearch request aborted");
9044
9589
  const invocation = buildEsRequestInvocation(config, method, path, body);
9045
- return spawnTextAsync({
9046
- command: "docker",
9590
+ const result = await spawnTextAsync({
9591
+ command: dockerCommand(),
9047
9592
  args: invocation.args,
9048
9593
  env: process.env,
9049
9594
  input: invocation.input,
@@ -9053,6 +9598,8 @@ function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signa
9053
9598
  timeoutMessage: `elasticsearch request timed out after ${timeoutMs}ms`,
9054
9599
  rejectOnError: false
9055
9600
  });
9601
+ throwIfDockerCommandUnavailableResult(result);
9602
+ return result;
9056
9603
  }
9057
9604
  function parseEsResponse(stdout) {
9058
9605
  const marker = "__ES_STATUS__:";
@@ -9400,11 +9947,11 @@ function buildRedisCliInvocation(config, args) {
9400
9947
  const spawnEnv = hasPassword ? { ...process.env, REDISCLI_AUTH: config.password } : process.env;
9401
9948
  return { args: dockerArgs, env: spawnEnv };
9402
9949
  }
9403
- function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
9950
+ async function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
9404
9951
  throwIfAborted(signal, "redis-cli aborted");
9405
9952
  const invocation = buildRedisCliInvocation(config, args);
9406
- return spawnTextAsync({
9407
- command: "docker",
9953
+ const result = await spawnTextAsync({
9954
+ command: dockerCommand(),
9408
9955
  args: invocation.args,
9409
9956
  env: invocation.env,
9410
9957
  timeoutMs,
@@ -9413,6 +9960,8 @@ function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
9413
9960
  timeoutMessage: `redis-cli timed out after ${timeoutMs}ms`,
9414
9961
  rejectOnError: false
9415
9962
  });
9963
+ throwIfDockerCommandUnavailableResult(result);
9964
+ return result;
9416
9965
  }
9417
9966
  function parseInfoKeyspace(stdout) {
9418
9967
  const counts = new Map;
@@ -10655,14 +11204,19 @@ function timeoutReadableStream(body, signal) {
10655
11204
  async function dockerCurlFetch(opts) {
10656
11205
  const { args, input } = dockerCurlCommand(opts);
10657
11206
  if (spawnSyncImplIsTestOverride) {
10658
- const proc2 = spawnSyncImpl3("docker", args, {
11207
+ const proc2 = spawnSyncImpl3(dockerCommand(), args, {
10659
11208
  encoding: "buffer",
10660
11209
  input,
10661
11210
  timeout: s3DockerCurlTimeoutMs,
10662
11211
  stdio: ["pipe", "pipe", "pipe"]
10663
11212
  });
10664
11213
  if ((proc2.status ?? 1) !== 0) {
10665
- 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
+ });
10666
11220
  throw new S3HttpError(503, `S3 HTTP transport failed via docker exec${stderr ? `: ${stderr.slice(0, 240)}` : ""}`);
10667
11221
  }
10668
11222
  return responseFromCurlOutput(new Uint8Array(proc2.stdout || new Uint8Array));
@@ -10671,16 +11225,21 @@ async function dockerCurlFetch(opts) {
10671
11225
  throw new S3HttpError(503, "S3 HTTP transport aborted");
10672
11226
  }
10673
11227
  const proc = await spawnCollectAsync({
10674
- command: "docker",
11228
+ command: dockerCommand(),
10675
11229
  args,
10676
11230
  input,
10677
11231
  timeoutMs: s3DockerCurlTimeoutMs,
10678
11232
  signal: opts.signal,
10679
11233
  abortMessage: "S3 HTTP transport aborted",
10680
- timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`
11234
+ timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`,
11235
+ rejectOnError: false
10681
11236
  });
10682
11237
  if (proc.code !== 0) {
10683
11238
  const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
11239
+ throwIfDockerCommandUnavailableResult({
11240
+ code: proc.code,
11241
+ stderr
11242
+ });
10684
11243
  throw new S3HttpError(503, `S3 HTTP transport failed via docker exec${stderr ? `: ${stderr.slice(0, 240)}` : ""}`);
10685
11244
  }
10686
11245
  return responseFromCurlOutput(new Uint8Array(proc.stdout));
@@ -11329,14 +11888,14 @@ import {
11329
11888
  existsSync as existsSync6,
11330
11889
  openSync,
11331
11890
  readSync,
11332
- realpathSync as realpathSync3,
11333
- statSync as statSync3
11891
+ realpathSync as realpathSync4,
11892
+ statSync as statSync4
11334
11893
  } from "node:fs";
11335
11894
  import { lstat, open, readdir, readFile as readFile2, stat } from "node:fs/promises";
11336
- import { basename, join as join7, relative as relative2 } from "node:path";
11895
+ import { basename, join as join8, relative as relative3 } from "node:path";
11337
11896
  function isSqliteFile(fullPath) {
11338
11897
  try {
11339
- const stat2 = statSync3(fullPath);
11898
+ const stat2 = statSync4(fullPath);
11340
11899
  if (!stat2.isFile() || stat2.size < 16)
11341
11900
  return false;
11342
11901
  const buf = Buffer.alloc(16);
@@ -11404,7 +11963,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
11404
11963
  return;
11405
11964
  if (omitSet.has(entry.toLowerCase()))
11406
11965
  continue;
11407
- const full = join7(dir, entry);
11966
+ const full = join8(dir, entry);
11408
11967
  let entryStat;
11409
11968
  try {
11410
11969
  entryStat = await lstat(full);
@@ -11421,7 +11980,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
11421
11980
  continue;
11422
11981
  if (!await isSqliteFileAsync(full))
11423
11982
  continue;
11424
- const rel = relative2(cwd, full);
11983
+ const rel = relative3(cwd, full);
11425
11984
  if (rel.startsWith("..") || rel.startsWith("/"))
11426
11985
  continue;
11427
11986
  results.push({
@@ -11448,18 +12007,18 @@ function validateDbPath(cwd, dbPath) {
11448
12007
  const parts = dbPath.split(/[\\/]+/);
11449
12008
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
11450
12009
  return null;
11451
- const full = join7(cwd, dbPath);
12010
+ const full = join8(cwd, dbPath);
11452
12011
  if (!existsSync6(full))
11453
12012
  return null;
11454
12013
  let realCwd;
11455
12014
  let realFull;
11456
12015
  try {
11457
- realCwd = realpathSync3(cwd);
11458
- realFull = realpathSync3(full);
12016
+ realCwd = realpathSync4(cwd);
12017
+ realFull = realpathSync4(full);
11459
12018
  } catch {
11460
12019
  return null;
11461
12020
  }
11462
- const rel = relative2(realCwd, realFull);
12021
+ const rel = relative3(realCwd, realFull);
11463
12022
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/"))
11464
12023
  return null;
11465
12024
  if (!isSqliteFile(realFull))
@@ -11585,7 +12144,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
11585
12144
  }
11586
12145
  async function readDotenvAsync(composeDir) {
11587
12146
  try {
11588
- const content = await readFile2(join7(composeDir, ".env"), "utf-8");
12147
+ const content = await readFile2(join8(composeDir, ".env"), "utf-8");
11589
12148
  return parseDotenvContent(content);
11590
12149
  } catch {
11591
12150
  return {};
@@ -11707,7 +12266,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
11707
12266
  for (let match = serviceRegex.exec(servicesBlock);match !== null; match = serviceRegex.exec(servicesBlock)) {
11708
12267
  servicePositions.push({ name: match[1], start: match.index });
11709
12268
  }
11710
- const relDir = relative2(cwd, composeDir);
12269
+ const relDir = relative3(cwd, composeDir);
11711
12270
  const isRoot = relDir === "" || relDir === ".";
11712
12271
  const relDirSlash = relDir.replace(/\\/g, "/");
11713
12272
  const filename = basename(filepath);
@@ -11807,7 +12366,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
11807
12366
  if (depth > MAX_SCAN_DEPTH)
11808
12367
  return;
11809
12368
  for (const filename of COMPOSE_FILENAMES) {
11810
- const filepath = join7(dir, filename);
12369
+ const filepath = join8(dir, filename);
11811
12370
  if (await pathExistsAsync(filepath)) {
11812
12371
  await parseComposeFileAsync(filepath, dir, cwd, results);
11813
12372
  break;
@@ -11830,7 +12389,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
11830
12389
  return;
11831
12390
  if (omitSet.has(entry.toLowerCase()))
11832
12391
  continue;
11833
- const full = join7(dir, entry);
12392
+ const full = join8(dir, entry);
11834
12393
  let entryStat;
11835
12394
  try {
11836
12395
  entryStat = await lstat(full);
@@ -11974,12 +12533,12 @@ import {
11974
12533
  readdirSync as nodeReaddirSync,
11975
12534
  watch as nodeWatch
11976
12535
  } from "node:fs";
11977
- import { join as join8, relative as relative3 } from "node:path";
12536
+ import { join as join9, relative as relative4 } from "node:path";
11978
12537
  function normalizeRelativePath(path) {
11979
12538
  return path.replace(/\\/g, "/").replace(/^\/+/, "");
11980
12539
  }
11981
12540
  function isInsideRoot(root, path) {
11982
- const rel = relative3(root, path).replace(/\\/g, "/");
12541
+ const rel = relative4(root, path).replace(/\\/g, "/");
11983
12542
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
11984
12543
  }
11985
12544
  function startWorktreeUpdateWatch(options) {
@@ -12017,7 +12576,7 @@ function startWorktreeUpdateWatch(options) {
12017
12576
  const pendingChangedPaths = new Set;
12018
12577
  let watchLimitReported = false;
12019
12578
  const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
12020
- const directoryRelativePath = (dir) => normalizeRelativePath(relative3(options.root, dir));
12579
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative4(options.root, dir));
12021
12580
  const ignoredDirectory = (dir) => {
12022
12581
  const rel = directoryRelativePath(dir);
12023
12582
  return Boolean(rel && ignored(rel));
@@ -12083,7 +12642,7 @@ function startWorktreeUpdateWatch(options) {
12083
12642
  for (const entry of entries) {
12084
12643
  if (!entry.isDirectory())
12085
12644
  continue;
12086
- const child = join8(dir, entry.name);
12645
+ const child = join9(dir, entry.name);
12087
12646
  if (ignoredDirectory(child))
12088
12647
  continue;
12089
12648
  children.push(child);
@@ -12167,10 +12726,10 @@ function startWorktreeUpdateWatch(options) {
12167
12726
  scheduleUpdate();
12168
12727
  return;
12169
12728
  }
12170
- const changed = normalizeRelativePath(join8(rel, filename.toString()));
12729
+ const changed = normalizeRelativePath(join9(rel, filename.toString()));
12171
12730
  if (ignored(changed))
12172
12731
  return;
12173
- const fullChangedPath = join8(options.root, changed);
12732
+ const fullChangedPath = join9(options.root, changed);
12174
12733
  if (!isInsideRoot(options.root, fullChangedPath))
12175
12734
  return;
12176
12735
  if (initialScanAsync) {
@@ -12219,9 +12778,9 @@ var init_worktree_watcher = __esm(() => {
12219
12778
  });
12220
12779
 
12221
12780
  // web-src/server/state-store.ts
12222
- import { join as join9 } from "node:path";
12781
+ import { join as join10 } from "node:path";
12223
12782
  function codeViewerPath(root, fileName) {
12224
- return join9(root, CODE_VIEWER_DIR2, fileName);
12783
+ return join10(root, CODE_VIEWER_DIR2, fileName);
12225
12784
  }
12226
12785
  function isRecord(value) {
12227
12786
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -14142,9 +14701,9 @@ var init_handle_s3 = __esm(() => {
14142
14701
  });
14143
14702
 
14144
14703
  // web-src/server/database/query-history.ts
14145
- import { join as join10 } from "node:path";
14704
+ import { join as join11 } from "node:path";
14146
14705
  function historyFilePath(root) {
14147
- return join10(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
14706
+ return join11(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
14148
14707
  }
14149
14708
  function emptyState() {
14150
14709
  return { version: 1, entries: [] };
@@ -14294,9 +14853,9 @@ var init_query_history = __esm(() => {
14294
14853
  // web-src/server/database/snapshot-store.ts
14295
14854
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
14296
14855
  import { mkdirSync as mkdirSync3 } from "node:fs";
14297
- import { join as join11 } from "node:path";
14856
+ import { join as join12 } from "node:path";
14298
14857
  async function getStoreDb(cwd) {
14299
- const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
14858
+ const dbPath = join12(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
14300
14859
  if (storeDb && storeDbPath === dbPath)
14301
14860
  return storeDb;
14302
14861
  if (storeDb) {
@@ -14304,7 +14863,7 @@ async function getStoreDb(cwd) {
14304
14863
  storeDb.close();
14305
14864
  } catch {}
14306
14865
  }
14307
- mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
14866
+ mkdirSync3(join12(cwd, CODE_VIEWER_DIR4), { recursive: true });
14308
14867
  const DbClass = await loadSqliteClass();
14309
14868
  storeDb = new DbClass(dbPath);
14310
14869
  storeDbPath = dbPath;
@@ -14910,9 +15469,9 @@ var init_snapshot_runner = __esm(() => {
14910
15469
  });
14911
15470
 
14912
15471
  // web-src/server/database/tabs-store.ts
14913
- import { join as join12 } from "node:path";
15472
+ import { join as join13 } from "node:path";
14914
15473
  function tabsFilePath(root) {
14915
- return join12(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
15474
+ return join13(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
14916
15475
  }
14917
15476
  function emptyState2() {
14918
15477
  return { version: 1, tabs: [], activeTabId: null };
@@ -16576,8 +17135,8 @@ var init_handle = __esm(() => {
16576
17135
  });
16577
17136
 
16578
17137
  // web-src/server/doctor.ts
16579
- import { accessSync, constants, readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
16580
- 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";
16581
17140
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16582
17141
  function statusWorse(a, b) {
16583
17142
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -16666,12 +17225,12 @@ function findCodeViewerPackageJson() {
16666
17225
  try {
16667
17226
  let cursor;
16668
17227
  try {
16669
- cursor = dirname3(fileURLToPath2(import.meta.url));
17228
+ cursor = dirname4(fileURLToPath2(import.meta.url));
16670
17229
  } catch {
16671
- cursor = dirname3(process.argv[1] || ".");
17230
+ cursor = dirname4(process.argv[1] || ".");
16672
17231
  }
16673
17232
  for (let depth = 0;depth < 8; depth += 1) {
16674
- const candidate = join13(cursor, "package.json");
17233
+ const candidate = join14(cursor, "package.json");
16675
17234
  try {
16676
17235
  const raw = readFileSync5(candidate, "utf8");
16677
17236
  const pkg = JSON.parse(raw);
@@ -16679,7 +17238,7 @@ function findCodeViewerPackageJson() {
16679
17238
  return { version: pkg.version, path: candidate };
16680
17239
  }
16681
17240
  } catch {}
16682
- const next = dirname3(cursor);
17241
+ const next = dirname4(cursor);
16683
17242
  if (next === cursor)
16684
17243
  break;
16685
17244
  cursor = next;
@@ -16767,9 +17326,9 @@ async function checkSqlite(cwd) {
16767
17326
  return { id: "sqlite", title: "SQLite driver", rows };
16768
17327
  }
16769
17328
  async function trySnapshotDbOpen(cwd) {
16770
- const dbPath = join13(cwd, SNAPSHOT_DB_REL);
17329
+ const dbPath = join14(cwd, SNAPSHOT_DB_REL);
16771
17330
  try {
16772
- statSync4(dbPath);
17331
+ statSync5(dbPath);
16773
17332
  } catch {
16774
17333
  return { kind: "skipped" };
16775
17334
  }
@@ -16788,17 +17347,17 @@ async function trySnapshotDbOpen(cwd) {
16788
17347
  }
16789
17348
  }
16790
17349
  function checkSnapshotStore(cwd) {
16791
- const dbPath = join13(cwd, SNAPSHOT_DB_REL);
16792
- const dir = dirname3(dbPath);
17350
+ const dbPath = join14(cwd, SNAPSHOT_DB_REL);
17351
+ const dir = dirname4(dbPath);
16793
17352
  let dirStatus = "ok";
16794
17353
  let dirDetail = dir;
16795
17354
  let dirHint;
16796
17355
  try {
16797
- accessSync(dir, constants.W_OK);
17356
+ accessSync2(dir, constants2.W_OK);
16798
17357
  dirDetail = `${dir} (writable)`;
16799
17358
  } catch {
16800
17359
  try {
16801
- statSync4(dir);
17360
+ statSync5(dir);
16802
17361
  dirStatus = "error";
16803
17362
  dirDetail = `${dir} (not writable)`;
16804
17363
  dirHint = "Snapshot creation will fail until the directory is writable. " + "Check filesystem permissions on the .code-viewer directory.";
@@ -16808,7 +17367,7 @@ function checkSnapshotStore(cwd) {
16808
17367
  }
16809
17368
  let dbDetail = dbPath;
16810
17369
  try {
16811
- const stat2 = statSync4(dbPath);
17370
+ const stat2 = statSync5(dbPath);
16812
17371
  dbDetail = `${dbPath} (${stat2.size.toLocaleString()} bytes)`;
16813
17372
  } catch {
16814
17373
  dbDetail = `${dbPath} (not created yet — created on first snapshot)`;
@@ -16834,7 +17393,7 @@ function checkSnapshotStore(cwd) {
16834
17393
  };
16835
17394
  }
16836
17395
  async function checkGit(cwd, signal) {
16837
- 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);
16838
17397
  if (!versionRes || versionRes.code !== 0) {
16839
17398
  return {
16840
17399
  id: "git",
@@ -16844,7 +17403,7 @@ async function checkGit(cwd, signal) {
16844
17403
  id: "git.binary",
16845
17404
  title: "git binary",
16846
17405
  status: "error",
16847
- detail: "not found in PATH",
17406
+ detail: versionRes && isCommandNotFoundResult("git", versionRes) ? commandNotFoundDetail("git") : "git command failed",
16848
17407
  hint: "git is required for diff, history, and blame features. Install git and ensure it is on PATH."
16849
17408
  }
16850
17409
  ]
@@ -16858,11 +17417,11 @@ async function checkGit(cwd, signal) {
16858
17417
  detail: firstLine2(versionRes.stdout)
16859
17418
  }
16860
17419
  ];
16861
- 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);
16862
17421
  if (repoCheck && repoCheck.code === 0 && /true/.test(repoCheck.stdout)) {
16863
- 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);
16864
17423
  const top = topRes?.stdout.trim() || cwd;
16865
- const insideCwd = relative4(top, cwd) || ".";
17424
+ const insideCwd = relative5(top, cwd) || ".";
16866
17425
  rows.push({
16867
17426
  id: "git.repo",
16868
17427
  title: "Working tree",
@@ -16881,10 +17440,10 @@ async function checkGit(cwd, signal) {
16881
17440
  return { id: "git", title: "Git", rows };
16882
17441
  }
16883
17442
  async function detectComposeBinary(signal) {
16884
- 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);
16885
17444
  if (v2 && v2.code === 0) {
16886
17445
  return {
16887
- cmd: { binary: "docker", subcommand: ["compose"] },
17446
+ cmd: { binary: commandForExternal("docker"), subcommand: ["compose"] },
16888
17447
  v2Version: firstLine2(v2.stdout)
16889
17448
  };
16890
17449
  }
@@ -16937,13 +17496,13 @@ async function checkDocker(signal, discoveryResult) {
16937
17496
  const summary = summarizeDockerSources(discoveryResult);
16938
17497
  const dockerSourcesPresent = summary.total > 0;
16939
17498
  const rows = [];
16940
- 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);
16941
17500
  const dockerOk = dockerVersion?.code === 0;
16942
17501
  rows.push({
16943
17502
  id: "docker.binary",
16944
17503
  title: "docker CLI",
16945
17504
  status: dockerOk ? "ok" : dockerSourcesPresent ? "error" : "warn",
16946
- detail: dockerOk ? firstLine2(dockerVersion.stdout) : "not found in PATH",
17505
+ detail: dockerOk ? firstLine2(dockerVersion.stdout) : dockerVersion && isCommandNotFoundResult("docker", dockerVersion) ? commandNotFoundDetail("docker") : "docker command failed",
16947
17506
  ...dockerOk ? {} : {
16948
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."
16949
17508
  }
@@ -16974,7 +17533,7 @@ async function checkDocker(signal, discoveryResult) {
16974
17533
  hint: "docker-compose v1 standalone is legacy. Prefer the Docker Compose v2 plugin (`docker compose ...`)."
16975
17534
  });
16976
17535
  }
16977
- 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;
16978
17537
  if (dockerOk) {
16979
17538
  if (dockerInfo && dockerInfo.code === 0) {
16980
17539
  rows.push({
@@ -17502,6 +18061,7 @@ async function handleDoctor(ctx) {
17502
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;
17503
18062
  var init_doctor = __esm(() => {
17504
18063
  init_cli_helpers();
18064
+ init_command_resolver();
17505
18065
  init_docker();
17506
18066
  init_elasticsearch();
17507
18067
  init_redis();
@@ -17549,6 +18109,7 @@ function parseDoctorCliArgs(argv) {
17549
18109
  let cwd = process.cwd();
17550
18110
  let port = 0;
17551
18111
  let json2 = false;
18112
+ const commandOverrides = [];
17552
18113
  for (let i = 0;i < argv.length; i++) {
17553
18114
  const arg = argv[i];
17554
18115
  if (arg === "--help" || arg === "-h" || arg === "help") {
@@ -17568,6 +18129,23 @@ function parseDoctorCliArgs(argv) {
17568
18129
  cwd = next;
17569
18130
  continue;
17570
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
+ }
17571
18149
  if (arg === "--port") {
17572
18150
  const next = argv[++i];
17573
18151
  if (!next)
@@ -17584,7 +18162,15 @@ function parseDoctorCliArgs(argv) {
17584
18162
  }
17585
18163
  return { kind: "error", message: `unknown argument: ${arg}` };
17586
18164
  }
17587
- 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
+ };
17588
18174
  }
17589
18175
  function formatDoctorReportText(report) {
17590
18176
  const lines = [];
@@ -17635,7 +18221,17 @@ async function runDoctorCli(argv) {
17635
18221
  `);
17636
18222
  return;
17637
18223
  }
17638
- 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
+ }
17639
18235
  const report = await buildDoctorReport({
17640
18236
  cwd,
17641
18237
  scopeOmitDirNames: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
@@ -17655,12 +18251,13 @@ async function runDoctorCli(argv) {
17655
18251
  var DOCTOR_HELP = `code-viewer doctor — diagnose the current environment
17656
18252
 
17657
18253
  Usage:
17658
- code-viewer doctor [--cwd <path>] [--port <N>] [--json]
18254
+ code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker>=<path>]
17659
18255
  code-viewer doctor agent-help
17660
18256
 
17661
18257
  Options:
17662
18258
  --cwd <path> Working directory to inspect (default: process.cwd()).
17663
18259
  --port <N> Listening port to mention in the report (default: 0 = no server).
18260
+ --bin <n>=<p> Override git/docker executable path. Repeatable.
17664
18261
  --json Print the full DoctorReport as JSON instead of a summary.
17665
18262
  --help, -h Show this help.
17666
18263
 
@@ -17672,6 +18269,7 @@ Exit codes:
17672
18269
  2 invalid arguments
17673
18270
  `, STATUS_SYMBOL;
17674
18271
  var init_doctor_cli = __esm(() => {
18272
+ init_command_resolver();
17675
18273
  init_doctor();
17676
18274
  init_git();
17677
18275
  STATUS_SYMBOL = {
@@ -17700,7 +18298,7 @@ function normalizeNewDirectoryName(name) {
17700
18298
 
17701
18299
  // web-src/server/cache.ts
17702
18300
  import { lstatSync as lstatSync3 } from "node:fs";
17703
- import { join as join14 } from "node:path";
18301
+ import { join as join15 } from "node:path";
17704
18302
  function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
17705
18303
  return !!cached && now - cached.storedAt <= ttlMs;
17706
18304
  }
@@ -17715,7 +18313,7 @@ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MA
17715
18313
  }
17716
18314
  function worktreeFileSignature(path, cwd) {
17717
18315
  try {
17718
- const stats = lstatSync3(join14(cwd, path));
18316
+ const stats = lstatSync3(join15(cwd, path));
17719
18317
  const inode = "ino" in stats ? stats.ino : 0;
17720
18318
  return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
17721
18319
  } catch {
@@ -17761,12 +18359,12 @@ function startDevAssetReload(options) {
17761
18359
  var init_dev_assets = () => {};
17762
18360
 
17763
18361
  // web-src/server/search-service.ts
17764
- import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync6, realpathSync as realpathSync4 } from "node:fs";
17765
- 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";
17766
18364
  function rgAvailable(cwd) {
17767
18365
  if (rgAvailableCache !== null)
17768
18366
  return rgAvailableCache;
17769
- const proc = runSync(["rg", "--version"], cwd);
18367
+ const proc = runSync([commandForExternal("rg"), "--version"], cwd);
17770
18368
  rgAvailableCache = proc.code === 0;
17771
18369
  return rgAvailableCache;
17772
18370
  }
@@ -17783,18 +18381,18 @@ function safeWorktreePath(env, path) {
17783
18381
  return null;
17784
18382
  if (isGitInternalPath(path))
17785
18383
  return null;
17786
- const full = join15(env.cwd, path);
18384
+ const full = join16(env.cwd, path);
17787
18385
  if (!existsSync7(full))
17788
18386
  return null;
17789
18387
  let realCwd;
17790
18388
  let realFull;
17791
18389
  try {
17792
- realCwd = realpathSync4(env.cwd);
17793
- realFull = realpathSync4(full);
18390
+ realCwd = realpathSync5(env.cwd);
18391
+ realFull = realpathSync5(full);
17794
18392
  } catch {
17795
18393
  return null;
17796
18394
  }
17797
- const rel = relative5(realCwd, realFull);
18395
+ const rel = relative6(realCwd, realFull);
17798
18396
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
17799
18397
  return null;
17800
18398
  if (isGitInternalPath(rel))
@@ -17844,6 +18442,7 @@ function grepWorktree(env, req) {
17844
18442
  if (rgAvailable(env.cwd)) {
17845
18443
  const safePaths = paths.filter((path) => safeWorktreePath(env, path));
17846
18444
  const args = buildRgArgs(req.query, req.max, safePaths, req.regex, env.omitDirNames, env.excludeNames);
18445
+ args[0] = commandForExternal("rg");
17847
18446
  const proc = runSync(args, env.cwd, { timeout: 5000 });
17848
18447
  const stdout = proc.stdout;
17849
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));
@@ -17873,7 +18472,7 @@ function grepWorktree(env, req) {
17873
18472
  function grepTreeRef(env, req) {
17874
18473
  const safePaths = filterCallerPaths(env, req.paths);
17875
18474
  const args = [
17876
- "git",
18475
+ commandForExternal("git"),
17877
18476
  "-c",
17878
18477
  "core.quotepath=false",
17879
18478
  "grep",
@@ -17899,6 +18498,17 @@ function grepTreeRef(env, req) {
17899
18498
  };
17900
18499
  }
17901
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
+ }
17902
18512
  if (!req.query.trim()) {
17903
18513
  return {
17904
18514
  ok: true,
@@ -17910,24 +18520,32 @@ function grepRepo(env, req) {
17910
18520
  }
17911
18521
  };
17912
18522
  }
17913
- if (req.ref === "worktree" || req.ref === "") {
18523
+ if (isWorktree) {
17914
18524
  return { ok: true, value: grepWorktree(env, req) };
17915
18525
  }
17916
- if (!verifyTreeRef(req.ref, env.cwd)) {
17917
- return { ok: false, error: "invalid target" };
17918
- }
17919
18526
  return { ok: true, value: grepTreeRef(env, req) };
17920
18527
  }
17921
18528
  function listRepoFiles(env, ref, generation) {
17922
- if (ref !== "worktree" && !verifyTreeRef(ref, env.cwd)) {
17923
- 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
+ }
17924
18538
  }
17925
18539
  const effectiveRef = ref || "worktree";
17926
- const entries = listTree(effectiveRef, "", env.cwd, {
18540
+ const tree = listTreeResult(effectiveRef, "", env.cwd, {
17927
18541
  recursive: true,
17928
18542
  omitDirNames: env.omitDirNames,
17929
18543
  excludeNames: env.excludeNames
17930
- }).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));
17931
18549
  return {
17932
18550
  ok: true,
17933
18551
  value: buildFileSearchList(effectiveRef, generation, entries)
@@ -17935,6 +18553,7 @@ function listRepoFiles(env, ref, generation) {
17935
18553
  }
17936
18554
  var rgAvailableCache = null;
17937
18555
  var init_search_service = __esm(() => {
18556
+ init_command_resolver();
17938
18557
  init_git();
17939
18558
  init_runtime();
17940
18559
  init_search();
@@ -17942,7 +18561,7 @@ var init_search_service = __esm(() => {
17942
18561
 
17943
18562
  // web-src/server/mcp.ts
17944
18563
  import { readFileSync as readFileSync7 } from "node:fs";
17945
- import { join as join16 } from "node:path";
18564
+ import { join as join17 } from "node:path";
17946
18565
  function defaultMcpTools(options = {}) {
17947
18566
  return [
17948
18567
  {
@@ -18481,7 +19100,10 @@ function runStatusTool(input, defaultCwd) {
18481
19100
  }
18482
19101
  try {
18483
19102
  const report = buildStatusReport({ root: resolved.root, ref, limit });
18484
- 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
+ };
18485
19107
  } catch (err) {
18486
19108
  const detail = err instanceof Error ? err.message : String(err);
18487
19109
  return { text: `status failed: ${detail}`, isError: true };
@@ -19413,7 +20035,7 @@ var init_mcp = __esm(() => {
19413
20035
  init_search_cli();
19414
20036
  init_search_service();
19415
20037
  init_status_cli();
19416
- PACKAGE_VERSION = JSON.parse(readFileSync7(join16(ROOT, "package.json"), "utf8")).version;
20038
+ PACKAGE_VERSION = JSON.parse(readFileSync7(join17(ROOT, "package.json"), "utf8")).version;
19417
20039
  MCP_SERVER_INFO = {
19418
20040
  name: "code-viewer",
19419
20041
  title: "code-viewer",
@@ -19495,21 +20117,21 @@ var init_state_route = __esm(() => {
19495
20117
  var exports_preview = {};
19496
20118
  import {
19497
20119
  closeSync as closeSync2,
19498
- constants as constants2,
20120
+ constants as constants3,
19499
20121
  existsSync as existsSync8,
19500
20122
  lstatSync as lstatSync5,
19501
20123
  mkdirSync as mkdirSync4,
19502
20124
  openSync as openSync2,
19503
20125
  readFileSync as readFileSync8,
19504
- realpathSync as realpathSync5,
20126
+ realpathSync as realpathSync6,
19505
20127
  renameSync,
19506
- statSync as statSync5,
20128
+ statSync as statSync6,
19507
20129
  unlinkSync as unlinkSync2,
19508
20130
  watch,
19509
20131
  writeFileSync as writeFileSync2
19510
20132
  } from "node:fs";
19511
20133
  import { homedir as homedir3 } from "node:os";
19512
- 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";
19513
20135
  function parseCli() {
19514
20136
  const rest = [];
19515
20137
  for (let i = 2;i < process.argv.length; i++) {
@@ -19518,15 +20140,15 @@ function parseCli() {
19518
20140
  console.log(`code-viewer ${VERSION}
19519
20141
 
19520
20142
  Usage:
19521
- code-viewer [--cwd <repo>] [--port <port>] [--open] [git-diff-args...]
19522
- 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]
19523
20145
  code-viewer annotate <start|add|add-db|rename|edit|move|list|delete|clear> [options]
19524
- code-viewer query <sources|schemas|schema|columns|ddl|exec|list|clear|snapshot|diff|search|redis|elasticsearch|s3> [options]
19525
- code-viewer search code --term <text> [--ref <ref>] [--path <p>...] [--regex] [--max <n>] [--json]
19526
- code-viewer search files --term <pattern> [--ref <ref>] [--max <n>] [--json]
19527
- 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>]
19528
20150
  code-viewer skill install [--agent <list>] [--global]
19529
- code-viewer doctor [--cwd <path>] [--port <N>] [--json]
20151
+ code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker>=<path>]
19530
20152
  code-viewer agent-help
19531
20153
  code-viewer help
19532
20154
 
@@ -19562,9 +20184,8 @@ Examples:
19562
20184
  process.exit(1);
19563
20185
  }
19564
20186
  try {
19565
- const nextReal = realpathSync5(next);
19566
- const candidate = repoRoot(next);
19567
- cwd = candidate === nextReal ? candidate : nextReal;
20187
+ cwd = realpathSync6(next);
20188
+ cwdWasExplicit = true;
19568
20189
  } catch {
19569
20190
  console.error("--cwd must point to an existing directory");
19570
20191
  process.exit(1);
@@ -19579,6 +20200,18 @@ Examples:
19579
20200
  listenPort = parsed;
19580
20201
  } else if (arg === "--open") {
19581
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);
19582
20215
  } else if (arg === "--allow-upload") {} else if (arg === "--scope-omit-dir") {
19583
20216
  const next = process.argv[++i];
19584
20217
  if (!next) {
@@ -19595,6 +20228,21 @@ Examples:
19595
20228
  }
19596
20229
  if (rest.length)
19597
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
+ }
19598
20246
  warnIfLegacyConfigPresent();
19599
20247
  if (scopeOmitDirCliOverride) {
19600
20248
  scopeOmitDirNames = scopeOmitDirCliOverride;
@@ -19603,7 +20251,7 @@ Examples:
19603
20251
  }
19604
20252
  function warnIfLegacyConfigPresent() {
19605
20253
  try {
19606
- if (existsSync8(join17(cwd, ".code-viewer.json"))) {
20254
+ if (existsSync8(join18(cwd, ".code-viewer.json"))) {
19607
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.");
19608
20256
  }
19609
20257
  } catch {}
@@ -19710,7 +20358,7 @@ function staticFile(pathname) {
19710
20358
  const spec = map[pathname];
19711
20359
  if (!spec)
19712
20360
  return null;
19713
- const full = join17(WEB_ROOT, spec[0]);
20361
+ const full = join18(WEB_ROOT, spec[0]);
19714
20362
  if (!existsSync8(full))
19715
20363
  return text("not found", 404);
19716
20364
  return new Response(readFileSync8(full), {
@@ -19718,17 +20366,17 @@ function staticFile(pathname) {
19718
20366
  });
19719
20367
  }
19720
20368
  function buildRangeArgs(range) {
19721
- const refs2 = [];
20369
+ const refs = [];
19722
20370
  if (range.from && range.from !== "worktree")
19723
- refs2.push(range.from);
20371
+ refs.push(range.from);
19724
20372
  if (range.to && range.to !== "worktree")
19725
- refs2.push(range.to);
19726
- return { args: refs2.length ? refs2 : cliArgs, refs: refs2 };
20373
+ refs.push(range.to);
20374
+ return { args: refs.length ? refs : cliArgs, refs };
19727
20375
  }
19728
- function includeUntracked(range, refs2) {
20376
+ function includeUntracked(range, refs) {
19729
20377
  const toWorktree = !range.to || range.to === "worktree";
19730
- if (refs2.length > 0)
19731
- return toWorktree && refs2.length < 2;
20378
+ if (refs.length > 0)
20379
+ return toWorktree && refs.length < 2;
19732
20380
  return cliArgs.length === 0 || cliArgs.length === 1 && cliArgs[0] === "HEAD";
19733
20381
  }
19734
20382
  function guessMediaKind(path) {
@@ -19815,11 +20463,13 @@ function computePayload(extras, range, pathFilter = "") {
19815
20463
  generation
19816
20464
  };
19817
20465
  }
19818
- const { args, refs: refs2 } = buildRangeArgs(range);
20466
+ const { args, refs } = buildRangeArgs(range);
19819
20467
  const fullArgs = [...extras, ...args];
19820
- const files = fileMeta(fullArgs, cwd, false);
19821
- if (includeUntracked(range, refs2))
20468
+ const metaResult = fileMetaResult(fullArgs, cwd, false);
20469
+ const files = metaResult.files;
20470
+ if (!metaResult.error && includeUntracked(range, refs)) {
19822
20471
  files.push(...untrackedMeta(cwd));
20472
+ }
19823
20473
  const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
19824
20474
  filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
19825
20475
  filteredFiles.forEach((file, i) => {
@@ -19839,14 +20489,15 @@ function computePayload(extras, range, pathFilter = "") {
19839
20489
  return acc;
19840
20490
  }, { files: meta.length, additions: 0, deletions: 0 });
19841
20491
  const toWorktree = !range.to || range.to === "worktree";
19842
- 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(" ");
19843
20493
  return {
19844
20494
  files: meta,
19845
20495
  totals,
19846
20496
  range: label || "HEAD",
19847
20497
  project: basename3(cwd),
19848
20498
  branch: currentBranch(cwd) || undefined,
19849
- generation
20499
+ generation,
20500
+ ...metaResult.error ? { error: metaResult.error } : {}
19850
20501
  };
19851
20502
  }
19852
20503
  function handleDiffJson(url) {
@@ -19962,12 +20613,12 @@ function safeWorktreePath2(path) {
19962
20613
  return safeWorktreePath(currentSearchEnv(), path);
19963
20614
  }
19964
20615
  function worktreePath(path) {
19965
- return join17(cwd, path);
20616
+ return join18(cwd, path);
19966
20617
  }
19967
20618
  function safeOpenWorktreePath(path) {
19968
20619
  if (path === "") {
19969
20620
  try {
19970
- const realCwd = realpathSync5(cwd);
20621
+ const realCwd = realpathSync6(cwd);
19971
20622
  if (isGitInternalPath(realCwd))
19972
20623
  return null;
19973
20624
  return realCwd;
@@ -19978,7 +20629,7 @@ function safeOpenWorktreePath(path) {
19978
20629
  return safeWorktreePath2(path);
19979
20630
  }
19980
20631
  function parentRepoPath(path) {
19981
- const parent = dirname4(path);
20632
+ const parent = dirname5(path);
19982
20633
  return parent === "." ? "" : parent;
19983
20634
  }
19984
20635
  function isoDate(ms) {
@@ -19989,7 +20640,7 @@ function worktreeFileMetadata(path, knownSize) {
19989
20640
  if (!full)
19990
20641
  return {};
19991
20642
  try {
19992
- const stat2 = statSync5(full);
20643
+ const stat2 = statSync6(full);
19993
20644
  return {
19994
20645
  size: knownSize ?? stat2.size,
19995
20646
  created_at: isoDate(stat2.birthtimeMs),
@@ -20014,7 +20665,7 @@ function directoryMetadata(target, path) {
20014
20665
  if (!full)
20015
20666
  return {};
20016
20667
  try {
20017
- const stat2 = statSync5(full);
20668
+ const stat2 = statSync6(full);
20018
20669
  return {
20019
20670
  created_at: isoDate(stat2.birthtimeMs),
20020
20671
  updated_at: isoDate(stat2.mtimeMs)
@@ -20065,19 +20716,25 @@ function handleTree(url) {
20065
20716
  return text("invalid path", 400);
20066
20717
  if ((target === "worktree" || target === "") && isGitInternalPath(path))
20067
20718
  return text("forbidden", 403);
20068
- if (target !== "worktree" && !verifyTreeRef(target, cwd))
20069
- 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
+ }
20070
20724
  const recursive = url.searchParams.get("recursive") === "1";
20071
20725
  if (invalidScopeOmitDirNamesQuery(url))
20072
20726
  return text("invalid omit dirs", 400);
20073
20727
  if (invalidScopeExcludeNamesQuery(url))
20074
20728
  return text("invalid exclude names", 400);
20075
20729
  const excludeNames = scopeExcludeNamesFromQuery(url);
20076
- const entries = listTree(target, path, cwd, {
20730
+ const tree = listTreeResult(target, path, cwd, {
20077
20731
  recursive,
20078
20732
  omitDirNames: scopeOmitDirNamesFromQuery(url),
20079
20733
  excludeNames
20080
- }).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));
20081
20738
  return json2({
20082
20739
  ref: target,
20083
20740
  path,
@@ -20141,7 +20798,7 @@ function handleFiles2(url) {
20141
20798
  return json2(cached.body);
20142
20799
  const result = listRepoFiles(currentSearchEnv(omitDirNames, excludeNames), target, generation);
20143
20800
  if (result.ok !== true)
20144
- return text(result.error, 400);
20801
+ return text(result.error, result.status ?? 400);
20145
20802
  fileListCache.set(key, { generation, body: result.value });
20146
20803
  return json2(result.value);
20147
20804
  }
@@ -20165,7 +20822,7 @@ function handleGrep(url) {
20165
20822
  max
20166
20823
  });
20167
20824
  if (result.ok !== true)
20168
- return text(result.error, 400);
20825
+ return text(result.error, result.status ?? 400);
20169
20826
  return json2(result.value);
20170
20827
  }
20171
20828
  function handleRefCommits(url) {
@@ -20174,7 +20831,10 @@ function handleRefCommits(url) {
20174
20831
  const parsedSkip = Number(url.searchParams.get("skip") || "0");
20175
20832
  const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
20176
20833
  const skip = Number.isFinite(parsedSkip) && parsedSkip > 0 ? parsedSkip : undefined;
20177
- 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 });
20178
20838
  }
20179
20839
  function handleLog(url) {
20180
20840
  const ref = url.searchParams.get("ref") || "HEAD";
@@ -20191,23 +20851,13 @@ function handleLog(url) {
20191
20851
  ...path ? { path } : {}
20192
20852
  });
20193
20853
  if (result.error)
20194
- return text(result.error, 400);
20854
+ return text(result.error, result.status ?? 400);
20195
20855
  const wantsWorktreeHead = path && skip === 0 && (ref === "worktree" || url.searchParams.get("worktree") === "1");
20196
20856
  let commits = result.commits;
20197
20857
  let hasWorktree = false;
20198
20858
  if (wantsWorktreeHead) {
20199
- const status = runSync([
20200
- "git",
20201
- "-c",
20202
- "core.quotepath=false",
20203
- "status",
20204
- "--porcelain=v1",
20205
- "-z",
20206
- "--untracked-files=normal",
20207
- "--",
20208
- path
20209
- ], cwd);
20210
- if (status.code === 0 && status.stdout.length > 0) {
20859
+ const status = statusPorcelainForPath(path, cwd);
20860
+ if (status.ok && status.stdout.length > 0) {
20211
20861
  const parts = status.stdout.split("\x00").filter(Boolean);
20212
20862
  if (parts.length > 0) {
20213
20863
  hasWorktree = true;
@@ -20234,7 +20884,7 @@ function handleLog(url) {
20234
20884
  }
20235
20885
  function blamePathKey(p) {
20236
20886
  try {
20237
- const st = statSync5(join17(cwd, p));
20887
+ const st = statSync6(join18(cwd, p));
20238
20888
  return `${st.mtimeMs}:${st.size}`;
20239
20889
  } catch {
20240
20890
  return "missing";
@@ -20266,10 +20916,12 @@ function handleFileBlame(url) {
20266
20916
  if (base === "worktree") {
20267
20917
  cacheKey = `worktree|${path}|${blamePathKey(path)}`;
20268
20918
  } else {
20269
- const resolved = runSync(["git", "rev-parse", "--verify", `${normalized.ref}^{commit}`], cwd);
20270
- if (resolved.code !== 0)
20271
- return text("unknown ref", 400);
20272
- 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}`;
20273
20925
  }
20274
20926
  const cached = blameCache.get(cacheKey);
20275
20927
  if (cached) {
@@ -20280,6 +20932,8 @@ function handleFileBlame(url) {
20280
20932
  return json2({ ...cached, base, ref, generation });
20281
20933
  }
20282
20934
  const result = blame(cwd, { path, ref: normalized.ref, base });
20935
+ if (result.error && result.status)
20936
+ return text(result.error, result.status);
20283
20937
  if (!result.error)
20284
20938
  rememberBlame(cacheKey, result);
20285
20939
  return json2({ ...result, base, ref, generation });
@@ -20332,19 +20986,30 @@ function handleFileDiff(url) {
20332
20986
  const cached = fileCache.get(cacheKey);
20333
20987
  let diffText;
20334
20988
  let errText = "";
20989
+ let errStatus;
20335
20990
  if (cacheFresh(cached)) {
20336
20991
  diffText = cached.diffText;
20337
20992
  } else {
20338
20993
  if (isUntracked) {
20339
- 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
+ }
20340
21000
  } else {
20341
21001
  const res = fileDiffText([...extras, ...args], oldPath ? [oldPath, path] : path, cwd);
20342
21002
  diffText = res.stdout || "";
20343
- if (res.code !== 0)
21003
+ if (res.code !== 0) {
20344
21004
  errText = res.stderr;
21005
+ errStatus = res.status;
21006
+ }
20345
21007
  }
20346
- setTimedCacheEntry(fileCache, cacheKey, { diffText });
21008
+ if (!errText)
21009
+ setTimedCacheEntry(fileCache, cacheKey, { diffText });
20347
21010
  }
21011
+ if (errStatus)
21012
+ return text(errText || "diff failed", errStatus);
20348
21013
  const mode = url.searchParams.get("mode") || "full";
20349
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);
20350
21015
  const body = {
@@ -20365,7 +21030,7 @@ function handleFileDiff(url) {
20365
21030
  }
20366
21031
  function worktreeLineIndexSignature(full) {
20367
21032
  try {
20368
- const stat2 = statSync5(full);
21033
+ const stat2 = statSync6(full);
20369
21034
  return `size:${stat2.size}|mtime:${stat2.mtimeMs}|ctime:${stat2.ctimeMs}|ino:${stat2.ino || 0}`;
20370
21035
  } catch {
20371
21036
  return null;
@@ -20381,7 +21046,7 @@ async function getWorktreeLineIndex(full) {
20381
21046
  lineIndexCache.set(full, cached);
20382
21047
  return cached.index;
20383
21048
  }
20384
- const stat2 = statSync5(full);
21049
+ const stat2 = statSync6(full);
20385
21050
  if (stat2.size > LINE_INDEX_MAX_FILE_BYTES)
20386
21051
  return null;
20387
21052
  const index = await buildLineOffsetIndexFromStream(fileReadableStream(full), stat2.size);
@@ -20526,8 +21191,9 @@ async function handleFileRange(url) {
20526
21191
  };
20527
21192
  return json2(body);
20528
21193
  } else {
20529
- if (!verifyTreeRef(ref, cwd))
20530
- return text("invalid ref", 400);
21194
+ const refCheck = verifyTreeRefResult(ref, cwd);
21195
+ if (refCheck.ok !== true)
21196
+ return text(refCheck.error, refCheck.status ?? 400);
20531
21197
  const oid = objectId(ref, path, cwd);
20532
21198
  if (oid.code !== 0 || !oid.oid)
20533
21199
  return text("not in ref", 404);
@@ -20557,8 +21223,9 @@ function handleRawFile(req, url) {
20557
21223
  const ref = url.searchParams.get("ref") || "worktree";
20558
21224
  let body;
20559
21225
  if (ref !== "worktree" && ref !== "") {
20560
- if (!verifyTreeRef(ref, cwd))
20561
- return text("invalid ref", 400);
21226
+ const refCheck = verifyTreeRefResult(ref, cwd);
21227
+ if (refCheck.ok !== true)
21228
+ return text(refCheck.error, refCheck.status ?? 400);
20562
21229
  const size = rawFileSize(path, ref);
20563
21230
  if (size == null)
20564
21231
  return text("not in ref", 404);
@@ -20626,7 +21293,7 @@ function rawFileSize(path, ref) {
20626
21293
  if (!full)
20627
21294
  return null;
20628
21295
  try {
20629
- return statSync5(full).size;
21296
+ return statSync6(full).size;
20630
21297
  } catch {
20631
21298
  return null;
20632
21299
  }
@@ -20651,7 +21318,7 @@ function safeUploadFileName(name) {
20651
21318
  return trimmed;
20652
21319
  }
20653
21320
  function uploadOpenFlags() {
20654
- 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);
20655
21322
  }
20656
21323
  async function handleUploadFiles(req) {
20657
21324
  if (!uploadEnabled)
@@ -20687,7 +21354,7 @@ async function handleUploadFiles(req) {
20687
21354
  const realDir = safeOpenWorktreePath(dir);
20688
21355
  if (!realDir)
20689
21356
  return text("not found", 404);
20690
- const stats = statSync5(realDir);
21357
+ const stats = statSync6(realDir);
20691
21358
  if (!stats.isDirectory())
20692
21359
  return text("not a directory", 400);
20693
21360
  const files = form.getAll("files").filter((item) => item instanceof File);
@@ -20711,8 +21378,8 @@ async function handleUploadFiles(req) {
20711
21378
  total += file.size;
20712
21379
  if (total > MAX_UPLOAD_TOTAL_BYTES)
20713
21380
  return text("upload too large", 413);
20714
- const target = join17(realDir, safeName);
20715
- if (relative6(realDir, dirname4(target)) !== "")
21381
+ const target = join18(realDir, safeName);
21382
+ if (relative7(realDir, dirname5(target)) !== "")
20716
21383
  return text("invalid filename", 400);
20717
21384
  if (existsSync8(target))
20718
21385
  return text("file exists", 409);
@@ -20833,9 +21500,9 @@ function triggerUpdate(changedPaths) {
20833
21500
  sendSse("update", data);
20834
21501
  }
20835
21502
  function moveMacPathIntoTrash(path) {
20836
- const trashDir = join17(homedir3(), ".Trash");
21503
+ const trashDir = join18(homedir3(), ".Trash");
20837
21504
  const base = basename3(path) || "code-viewer-trash-item";
20838
- 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)}`);
20839
21506
  try {
20840
21507
  mkdirSync4(trashDir, { recursive: true });
20841
21508
  renameSync(path, target);
@@ -20877,11 +21544,11 @@ function restoreTrashPath(originalPath, trashPath) {
20877
21544
  if (!existsSync8(trashPath))
20878
21545
  return { ok: false, error: "trash item not found" };
20879
21546
  try {
20880
- const trashRoot = join17(homedir3(), ".Trash");
20881
- const trashRelative = relative6(trashRoot, trashPath);
21547
+ const trashRoot = join18(homedir3(), ".Trash");
21548
+ const trashRelative = relative7(trashRoot, trashPath);
20882
21549
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
20883
21550
  return { ok: false, error: "invalid trash handle" };
20884
- mkdirSync4(dirname4(original), { recursive: true });
21551
+ mkdirSync4(dirname5(original), { recursive: true });
20885
21552
  renameSync(trashPath, original);
20886
21553
  return { ok: true };
20887
21554
  } catch (error) {
@@ -20936,7 +21603,7 @@ async function handleOpenPath(req) {
20936
21603
  const target = safeOpenWorktreePath(targetPath);
20937
21604
  if (!target)
20938
21605
  return text("not found", 404);
20939
- const stats = statSync5(target);
21606
+ const stats = statSync6(target);
20940
21607
  if (!stats.isDirectory())
20941
21608
  return text("not a directory", 400);
20942
21609
  openOsPath(target);
@@ -21021,13 +21688,13 @@ async function handleCreateDirectory(req) {
21021
21688
  const parent = safeOpenWorktreePath(dir);
21022
21689
  if (!parent)
21023
21690
  return text("not found", 404);
21024
- const stats = statSync5(parent);
21691
+ const stats = statSync6(parent);
21025
21692
  if (!stats.isDirectory())
21026
21693
  return text("not a directory", 400);
21027
21694
  const targetPath = dir ? `${dir}/${name}` : name;
21028
21695
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
21029
21696
  return text("invalid target", 400);
21030
- const target = join17(parent, name);
21697
+ const target = join18(parent, name);
21031
21698
  if (existsSync8(target))
21032
21699
  return text("already exists", 409);
21033
21700
  try {
@@ -21328,11 +21995,12 @@ function restartWorktreeWatch() {
21328
21995
  }
21329
21996
  worktreeWatch = startScopedWorktreeWatch();
21330
21997
  }
21331
- 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;
21332
21999
  var init_preview = __esm(async () => {
21333
22000
  init_routes();
21334
22001
  init_annotations();
21335
22002
  init_cache();
22003
+ init_command_resolver();
21336
22004
  init_dev_assets();
21337
22005
  init_doctor();
21338
22006
  init_git();
@@ -21345,8 +22013,8 @@ var init_preview = __esm(async () => {
21345
22013
  init_server_registry();
21346
22014
  init_state_store();
21347
22015
  init_worktree_watcher();
21348
- WEB_ROOT = join17(ROOT, "web");
21349
- 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;
21350
22018
  DEFAULT_ARGS = ["HEAD"];
21351
22019
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
21352
22020
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -21390,8 +22058,9 @@ var init_preview = __esm(async () => {
21390
22058
  ".scss",
21391
22059
  ".html"
21392
22060
  ]);
21393
- cwd = repoRoot(process.cwd()) || process.cwd();
22061
+ cwd = process.cwd();
21394
22062
  cliArgs = DEFAULT_ARGS;
22063
+ commandOverrides = [];
21395
22064
  scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
21396
22065
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
21397
22066
  scopeWatchLimit = DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
@@ -21474,8 +22143,12 @@ var init_preview = __esm(async () => {
21474
22143
  return handleMcp(req);
21475
22144
  if (url.pathname === "/_annotations")
21476
22145
  return handleAnnotations(req);
21477
- if (url.pathname === "/_refs")
21478
- 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
+ }
21479
22152
  if (url.pathname === "/refresh" && req.method === "POST") {
21480
22153
  if (!sideEffectRequestAllowed(req))
21481
22154
  return text("forbidden", 403);