@youtyan/code-viewer 0.2.5 → 0.2.6

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.
Files changed (3) hide show
  1. package/dist/code-viewer.js +404 -95
  2. package/package.json +1 -1
  3. package/web/app.js +2867 -2820
@@ -1458,6 +1458,9 @@ var init_git = __esm(() => {
1458
1458
  "vendor",
1459
1459
  ".cache",
1460
1460
  "coverage",
1461
+ "tmp",
1462
+ "log",
1463
+ "storage",
1461
1464
  "DerivedData",
1462
1465
  "Pods",
1463
1466
  "bin",
@@ -3681,14 +3684,23 @@ function startWorktreeUpdateWatch(options) {
3681
3684
  const setTimer = options.setTimeoutFn || setTimeout;
3682
3685
  const clearTimer = options.clearTimeoutFn || clearTimeout;
3683
3686
  const debounceMs = options.debounceMs ?? 250;
3687
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
3684
3688
  const watchers = new Map;
3685
3689
  const signatures = new Map;
3686
3690
  const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
3687
3691
  const initialScanQueue = [];
3688
3692
  let initialScanTimer = null;
3693
+ const pendingPathInspections = new Map;
3694
+ let pathInspectionTimer = null;
3689
3695
  let timer = null;
3690
3696
  const pendingChangedPaths = new Set;
3697
+ let watchLimitReported = false;
3691
3698
  const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
3699
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
3700
+ const ignoredDirectory = (dir) => {
3701
+ const rel = directoryRelativePath(dir);
3702
+ return Boolean(rel && ignored(rel));
3703
+ };
3692
3704
  const scheduleUpdate = (changedPath) => {
3693
3705
  if (changedPath)
3694
3706
  pendingChangedPaths.add(changedPath);
@@ -3701,6 +3713,12 @@ function startWorktreeUpdateWatch(options) {
3701
3713
  options.onUpdate(paths);
3702
3714
  }, debounceMs);
3703
3715
  };
3716
+ const reportWatchLimit = () => {
3717
+ if (watchLimitReported)
3718
+ return;
3719
+ watchLimitReported = true;
3720
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
3721
+ };
3704
3722
  const closeSubtree = (dir) => {
3705
3723
  for (const [watchedDir, watcher] of [...watchers]) {
3706
3724
  if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
@@ -3717,7 +3735,12 @@ function startWorktreeUpdateWatch(options) {
3717
3735
  clearTimer(initialScanTimer);
3718
3736
  initialScanTimer = null;
3719
3737
  }
3738
+ if (pathInspectionTimer) {
3739
+ clearTimer(pathInspectionTimer);
3740
+ pathInspectionTimer = null;
3741
+ }
3720
3742
  initialScanQueue.length = 0;
3743
+ pendingPathInspections.clear();
3721
3744
  for (const watcher of [...watchers.values()]) {
3722
3745
  try {
3723
3746
  watcher.close?.();
@@ -3738,27 +3761,82 @@ function startWorktreeUpdateWatch(options) {
3738
3761
  for (const entry of entries) {
3739
3762
  if (!entry.isDirectory())
3740
3763
  continue;
3741
- children.push(join7(dir, entry.name));
3764
+ const child = join7(dir, entry.name);
3765
+ if (ignoredDirectory(child))
3766
+ continue;
3767
+ children.push(child);
3742
3768
  }
3743
3769
  return children;
3744
3770
  };
3745
3771
  const processInitialScanQueue = () => {
3746
3772
  initialScanTimer = null;
3773
+ if (watchers.size >= maxWatchedDirectories) {
3774
+ reportWatchLimit();
3775
+ initialScanQueue.length = 0;
3776
+ return;
3777
+ }
3747
3778
  const next = initialScanQueue.shift();
3748
3779
  if (next)
3749
3780
  watchDirectory(next, true);
3781
+ if (watchers.size >= maxWatchedDirectories) {
3782
+ reportWatchLimit();
3783
+ initialScanQueue.length = 0;
3784
+ }
3750
3785
  if (initialScanQueue.length)
3751
3786
  initialScanTimer = setTimer(processInitialScanQueue, 50);
3752
3787
  };
3753
3788
  const queueInitialChildren = (dir) => {
3754
- initialScanQueue.push(...readChildDirectories(dir));
3789
+ const remaining = maxWatchedDirectories - watchers.size;
3790
+ if (remaining <= 0) {
3791
+ reportWatchLimit();
3792
+ return;
3793
+ }
3794
+ const children = readChildDirectories(dir);
3795
+ if (children.length > remaining)
3796
+ reportWatchLimit();
3797
+ initialScanQueue.push(...children.slice(0, remaining));
3755
3798
  if (!initialScanTimer)
3756
3799
  initialScanTimer = setTimer(processInitialScanQueue, 5000);
3757
3800
  };
3801
+ const processChangedPath = (changed, fullChangedPath) => {
3802
+ const known = watchers.has(fullChangedPath);
3803
+ if (isDirectory(fullChangedPath)) {
3804
+ if (known) {
3805
+ const signature = directorySignature(fullChangedPath);
3806
+ if (signature && signature !== signatures.get(fullChangedPath)) {
3807
+ closeSubtree(fullChangedPath);
3808
+ watchDirectory(fullChangedPath, initialScanAsync);
3809
+ }
3810
+ scheduleUpdate(changed);
3811
+ return;
3812
+ }
3813
+ watchDirectory(fullChangedPath, initialScanAsync);
3814
+ } else if (known) {
3815
+ closeSubtree(fullChangedPath);
3816
+ }
3817
+ scheduleUpdate(changed);
3818
+ };
3819
+ const processPathInspections = () => {
3820
+ pathInspectionTimer = null;
3821
+ const entries = [...pendingPathInspections];
3822
+ pendingPathInspections.clear();
3823
+ for (const [changed, fullChangedPath] of entries) {
3824
+ processChangedPath(changed, fullChangedPath);
3825
+ }
3826
+ };
3827
+ const queuePathInspection = (changed, fullChangedPath) => {
3828
+ pendingPathInspections.set(changed, fullChangedPath);
3829
+ if (!pathInspectionTimer)
3830
+ pathInspectionTimer = setTimer(processPathInspections, 25);
3831
+ };
3758
3832
  const watchDirectory = (dir, initialScan = false) => {
3759
3833
  if (watchers.has(dir))
3760
3834
  return;
3761
- const rel = normalizeRelativePath(relative(options.root, dir));
3835
+ if (watchers.size >= maxWatchedDirectories) {
3836
+ reportWatchLimit();
3837
+ return;
3838
+ }
3839
+ const rel = directoryRelativePath(dir);
3762
3840
  if (rel && ignored(rel))
3763
3841
  return;
3764
3842
  try {
@@ -3773,22 +3851,11 @@ function startWorktreeUpdateWatch(options) {
3773
3851
  const fullChangedPath = join7(options.root, changed);
3774
3852
  if (!isInsideRoot(options.root, fullChangedPath))
3775
3853
  return;
3776
- const known = watchers.has(fullChangedPath);
3777
- if (isDirectory(fullChangedPath)) {
3778
- if (known) {
3779
- const signature2 = directorySignature(fullChangedPath);
3780
- if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
3781
- closeSubtree(fullChangedPath);
3782
- watchDirectory(fullChangedPath);
3783
- }
3784
- scheduleUpdate(changed);
3785
- return;
3786
- }
3787
- watchDirectory(fullChangedPath);
3788
- } else if (known) {
3789
- closeSubtree(fullChangedPath);
3854
+ if (initialScanAsync) {
3855
+ queuePathInspection(changed, fullChangedPath);
3856
+ return;
3790
3857
  }
3791
- scheduleUpdate(changed);
3858
+ processChangedPath(changed, fullChangedPath);
3792
3859
  }) || {};
3793
3860
  watchers.set(dir, watcher);
3794
3861
  const signature = directorySignature(dir);
@@ -3814,12 +3881,17 @@ function startWorktreeUpdateWatch(options) {
3814
3881
  queueInitialChildren(dir);
3815
3882
  return;
3816
3883
  }
3884
+ if (watchers.size >= maxWatchedDirectories) {
3885
+ reportWatchLimit();
3886
+ return;
3887
+ }
3817
3888
  for (const child of readChildDirectories(dir))
3818
3889
  watchDirectory(child);
3819
3890
  };
3820
3891
  watchDirectory(options.root, true);
3821
3892
  return { started: watchers.size > 0, close: closeAll };
3822
3893
  }
3894
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
3823
3895
  var init_worktree_watcher = __esm(() => {
3824
3896
  init_search();
3825
3897
  });
@@ -5855,7 +5927,7 @@ function parseComposeEnv(serviceBlock, composeDirEnv = {}) {
5855
5927
  }
5856
5928
  return env;
5857
5929
  }
5858
- function parseComposePortMappings(serviceBlock) {
5930
+ function parseComposePortMappings(serviceBlock, composeDirEnv = {}) {
5859
5931
  const portsMatch = serviceBlock.match(/^[ \t]+ports:\s*\n((?:[ \t]+- [^\n]+\n?)*)/m);
5860
5932
  if (!portsMatch)
5861
5933
  return [];
@@ -5865,26 +5937,45 @@ function parseComposePortMappings(serviceBlock) {
5865
5937
  const trimmed = line.trim();
5866
5938
  if (!trimmed.startsWith("-"))
5867
5939
  continue;
5868
- const value = trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim();
5940
+ const value = resolveEnvValue(trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim(), composeDirEnv);
5869
5941
  if (!value || value.includes("target:"))
5870
5942
  continue;
5871
5943
  const parts = value.split(":");
5872
5944
  const container = parts.pop()?.trim() || "";
5873
5945
  const host = parts.pop()?.trim() || "";
5874
- if (!/^\d+$/.test(container))
5946
+ const containerPorts = expandPortRange(container);
5947
+ const hostPorts = host ? expandPortRange(host) : [];
5948
+ if (!containerPorts)
5875
5949
  continue;
5876
- if (host && !/^\d+$/.test(host))
5950
+ if (host && (!hostPorts || hostPorts.length !== containerPorts.length)) {
5877
5951
  continue;
5878
- mappings.push({ host, container });
5952
+ }
5953
+ for (let i = 0;i < containerPorts.length; i++) {
5954
+ mappings.push({
5955
+ host: hostPorts ? (hostPorts[i] ?? "").toString() : "",
5956
+ container: containerPorts[i].toString()
5957
+ });
5958
+ }
5879
5959
  }
5880
5960
  return mappings;
5881
5961
  }
5882
- function parseComposePorts(serviceBlock) {
5883
- const first = parseComposePortMappings(serviceBlock).find((m) => m.host);
5962
+ function expandPortRange(value) {
5963
+ const match = value.match(/^(\d+)(?:-(\d+))?$/);
5964
+ if (!match)
5965
+ return null;
5966
+ const start = Number(match[1]);
5967
+ const end = Number(match[2] || match[1]);
5968
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end > 65535 || end < start) {
5969
+ return null;
5970
+ }
5971
+ return Array.from({ length: end - start + 1 }, (_, idx) => start + idx);
5972
+ }
5973
+ function parseComposePorts(serviceBlock, composeDirEnv = {}) {
5974
+ const first = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.host);
5884
5975
  return first?.host || null;
5885
5976
  }
5886
- function parseComposeHostPortForContainer(serviceBlock, containerPort) {
5887
- const found = parseComposePortMappings(serviceBlock).find((m) => m.container === containerPort && m.host);
5977
+ function parseComposeHostPortForContainer(serviceBlock, containerPort, composeDirEnv = {}) {
5978
+ const found = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.container === containerPort && m.host);
5888
5979
  return found?.host || null;
5889
5980
  }
5890
5981
  function parseComposeContainerPort(serviceBlock) {
@@ -5935,7 +6026,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
5935
6026
  continue;
5936
6027
  const defaultPort = defaultPortFor(kind, image, env);
5937
6028
  const serviceContainerPort = kind === "s3" ? defaultPort : containerPort || defaultPort;
5938
- const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort) || parseComposeHostPortForContainer(svcBlock, defaultPort) || parseComposePorts(svcBlock);
6029
+ const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort, composeDirEnv) || parseComposeHostPortForContainer(svcBlock, defaultPort, composeDirEnv) || parseComposePorts(svcBlock, composeDirEnv);
5939
6030
  const hostPort = kind === "s3" ? publishedHostPort || undefined : publishedHostPort || defaultPort;
5940
6031
  const imageLabel = image ?? `build:${kind}`;
5941
6032
  const id = isRoot ? `docker:${svc.name}` : `docker:${svc.name}@${encodeURIComponent(relDirSlash)}`;
@@ -6672,6 +6763,37 @@ function textError(message, status) {
6672
6763
  }
6673
6764
  });
6674
6765
  }
6766
+ function waitForCallerAbort(promise, signal, message) {
6767
+ if (!signal)
6768
+ return promise;
6769
+ if (signal.aborted)
6770
+ return Promise.reject(abortError(message));
6771
+ return new Promise((resolve2, reject) => {
6772
+ let settled = false;
6773
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
6774
+ const onAbort = () => {
6775
+ if (settled)
6776
+ return;
6777
+ settled = true;
6778
+ cleanup();
6779
+ reject(abortError(message));
6780
+ };
6781
+ signal.addEventListener("abort", onAbort, { once: true });
6782
+ promise.then((value) => {
6783
+ if (settled)
6784
+ return;
6785
+ settled = true;
6786
+ cleanup();
6787
+ resolve2(value);
6788
+ }, (err) => {
6789
+ if (settled)
6790
+ return;
6791
+ settled = true;
6792
+ cleanup();
6793
+ reject(err);
6794
+ });
6795
+ });
6796
+ }
6675
6797
  async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omitDirNames, signal) {
6676
6798
  if (!dbParam)
6677
6799
  return textError("missing db parameter", 400);
@@ -6681,10 +6803,26 @@ async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omi
6681
6803
  const parsed = parseDockerDbId(dbParam);
6682
6804
  if (!parsed)
6683
6805
  return textError("invalid docker db id", 400);
6684
- const info = await findDockerServiceByDbIdAsync(cwd, dbParam, kind, omitDirNames, signal);
6806
+ let info;
6807
+ try {
6808
+ info = await findDockerServiceByDbIdAsync(cwd, dbParam, kind, omitDirNames, signal);
6809
+ } catch (err) {
6810
+ if (isAbortLikeError(err, signal)) {
6811
+ return textError(`${kind} lookup aborted`, 503);
6812
+ }
6813
+ throw err;
6814
+ }
6685
6815
  if (!info)
6686
6816
  return textError(`${kind} service not found`, 404);
6687
- const explorer = await cache.getOrOpenAsync(dbParam, () => openFn(info));
6817
+ let explorer;
6818
+ try {
6819
+ explorer = await waitForCallerAbort(cache.getOrOpenAsync(dbParam, () => openFn(info)), signal, `${kind} open aborted`);
6820
+ } catch (err) {
6821
+ if (isAbortLikeError(err, signal)) {
6822
+ return textError(`${kind} open aborted`, 503);
6823
+ }
6824
+ throw err;
6825
+ }
6688
6826
  return { dbId: dbParam, explorer };
6689
6827
  }
6690
6828
  async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
@@ -7591,6 +7729,31 @@ function s3ObjectName(key) {
7591
7729
  // web-src/server/database/adapters/s3.ts
7592
7730
  import { spawnSync as spawnSync4 } from "node:child_process";
7593
7731
  import { createHash as createHash4, createHmac } from "node:crypto";
7732
+ function createS3RequestDeadline() {
7733
+ const timeoutMs = s3RequestTimeoutMs;
7734
+ return {
7735
+ expiresAt: Date.now() + timeoutMs,
7736
+ timeoutMs
7737
+ };
7738
+ }
7739
+ function createS3DockerCurlDeadline() {
7740
+ const timeoutMs = s3DockerCurlTimeoutMs;
7741
+ return {
7742
+ expiresAt: Date.now() + timeoutMs,
7743
+ timeoutMs
7744
+ };
7745
+ }
7746
+ function createS3TransportDeadline(config) {
7747
+ return config.dockerContainerName ? createS3DockerCurlDeadline() : createS3RequestDeadline();
7748
+ }
7749
+ function s3TimeoutError(deadline) {
7750
+ return new S3HttpError(503, `S3 request timed out after ${deadline?.timeoutMs ?? s3RequestTimeoutMs}ms`);
7751
+ }
7752
+ function remainingS3TimeoutMs(deadline) {
7753
+ if (!deadline)
7754
+ return s3RequestTimeoutMs;
7755
+ return Math.max(0, deadline.expiresAt - Date.now());
7756
+ }
7594
7757
  function hmac(key, value) {
7595
7758
  return createHmac("sha256", key).update(value, "utf8").digest();
7596
7759
  }
@@ -7725,13 +7888,139 @@ function dockerCurlCommand(opts) {
7725
7888
  input: Buffer.from(curlHeaderConfig(opts.headers), "utf8")
7726
7889
  };
7727
7890
  }
7891
+ function guardedS3Transport(signal, operation, deadline) {
7892
+ if (signal?.aborted) {
7893
+ return Promise.reject(new S3HttpError(503, "S3 HTTP transport aborted"));
7894
+ }
7895
+ const timeoutMs = remainingS3TimeoutMs(deadline);
7896
+ if (timeoutMs <= 0) {
7897
+ return Promise.reject(s3TimeoutError(deadline));
7898
+ }
7899
+ const controller = new AbortController;
7900
+ let timedOut = false;
7901
+ let settled = false;
7902
+ let timer;
7903
+ let cleanupParent = () => {};
7904
+ const abort = (err, reject) => {
7905
+ if (settled)
7906
+ return;
7907
+ settled = true;
7908
+ controller.abort(err);
7909
+ reject(err);
7910
+ };
7911
+ const guarded = new Promise((resolve2, reject) => {
7912
+ const onParentAbort = () => abort(new S3HttpError(503, "S3 HTTP transport aborted"), reject);
7913
+ if (signal) {
7914
+ signal.addEventListener("abort", onParentAbort, { once: true });
7915
+ cleanupParent = () => signal.removeEventListener("abort", onParentAbort);
7916
+ }
7917
+ timer = setTimeout(() => {
7918
+ timedOut = true;
7919
+ abort(s3TimeoutError(deadline), reject);
7920
+ }, timeoutMs);
7921
+ operation(controller.signal).then((value) => {
7922
+ if (settled)
7923
+ return;
7924
+ settled = true;
7925
+ resolve2(value);
7926
+ }, (err) => {
7927
+ if (settled)
7928
+ return;
7929
+ settled = true;
7930
+ if (timedOut) {
7931
+ reject(s3TimeoutError(deadline));
7932
+ } else if (signal?.aborted) {
7933
+ reject(new S3HttpError(503, "S3 HTTP transport aborted"));
7934
+ } else {
7935
+ reject(err);
7936
+ }
7937
+ });
7938
+ });
7939
+ return guarded.finally(() => {
7940
+ if (timer)
7941
+ clearTimeout(timer);
7942
+ cleanupParent();
7943
+ });
7944
+ }
7945
+ async function readStreamChunkWithTimeout(reader, signal, deadline) {
7946
+ return guardedS3Transport(signal, (transportSignal) => {
7947
+ const cancelRead = () => {
7948
+ reader.cancel(transportSignal.reason).catch(() => {});
7949
+ };
7950
+ if (transportSignal.aborted) {
7951
+ cancelRead();
7952
+ } else {
7953
+ transportSignal.addEventListener("abort", cancelRead, { once: true });
7954
+ }
7955
+ return reader.read().finally(() => {
7956
+ transportSignal.removeEventListener("abort", cancelRead);
7957
+ });
7958
+ }, deadline);
7959
+ }
7960
+ async function readResponseBytesWithTimeout(res, signal, deadline) {
7961
+ if (!res.body)
7962
+ return new Uint8Array;
7963
+ const reader = res.body.getReader();
7964
+ const chunks = [];
7965
+ let total = 0;
7966
+ try {
7967
+ for (;; ) {
7968
+ const { done, value } = await readStreamChunkWithTimeout(reader, signal, deadline);
7969
+ if (done)
7970
+ break;
7971
+ if (!value?.byteLength)
7972
+ continue;
7973
+ chunks.push(value);
7974
+ total += value.byteLength;
7975
+ }
7976
+ } finally {
7977
+ try {
7978
+ reader.releaseLock();
7979
+ } catch {}
7980
+ }
7981
+ if (chunks.length === 1)
7982
+ return chunks[0];
7983
+ const bytes = new Uint8Array(total);
7984
+ let offset = 0;
7985
+ for (const chunk of chunks) {
7986
+ bytes.set(chunk, offset);
7987
+ offset += chunk.byteLength;
7988
+ }
7989
+ return bytes;
7990
+ }
7991
+ async function readResponseTextWithTimeout(res, signal, deadline) {
7992
+ return new TextDecoder("utf-8", { fatal: false }).decode(await readResponseBytesWithTimeout(res, signal, deadline));
7993
+ }
7994
+ function timeoutReadableStream(body, signal) {
7995
+ if (!body)
7996
+ return null;
7997
+ const reader = body.getReader();
7998
+ return new ReadableStream({
7999
+ async pull(controller) {
8000
+ try {
8001
+ const { done, value } = await readStreamChunkWithTimeout(reader, signal);
8002
+ if (done) {
8003
+ controller.close();
8004
+ return;
8005
+ }
8006
+ if (value)
8007
+ controller.enqueue(value);
8008
+ } catch (error) {
8009
+ controller.error(error);
8010
+ }
8011
+ },
8012
+ cancel(reason) {
8013
+ return reader.cancel(reason);
8014
+ }
8015
+ });
8016
+ }
7728
8017
  async function dockerCurlFetch(opts) {
7729
8018
  const { args, input } = dockerCurlCommand(opts);
7730
8019
  if (spawnSyncImplIsTestOverride) {
7731
8020
  const proc2 = spawnSyncImpl3("docker", args, {
7732
8021
  encoding: "buffer",
7733
8022
  input,
7734
- timeout: 30000,
8023
+ timeout: s3DockerCurlTimeoutMs,
7735
8024
  stdio: ["pipe", "pipe", "pipe"]
7736
8025
  });
7737
8026
  if ((proc2.status ?? 1) !== 0) {
@@ -7747,10 +8036,10 @@ async function dockerCurlFetch(opts) {
7747
8036
  command: "docker",
7748
8037
  args,
7749
8038
  input,
7750
- timeoutMs: 30000,
8039
+ timeoutMs: s3DockerCurlTimeoutMs,
7751
8040
  signal: opts.signal,
7752
8041
  abortMessage: "S3 HTTP transport aborted",
7753
- timeoutMessage: "docker exec curl timed out"
8042
+ timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`
7754
8043
  });
7755
8044
  if (proc.code !== 0) {
7756
8045
  const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
@@ -7824,73 +8113,77 @@ function parseObjects(xml) {
7824
8113
  };
7825
8114
  }
7826
8115
  function createS3Adapter(config) {
7827
- async function signedFetch(opts) {
7828
- const endpoint = new URL(config.endpoint);
7829
- const { dateStamp, amzDate: requestDate } = amzDate();
7830
- const path = buildPath(opts.bucket, opts.key);
7831
- const query = canonicalQuery(opts.query);
7832
- const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
7833
- const headers = {
7834
- host: endpoint.host,
7835
- "x-amz-content-sha256": EMPTY_SHA256,
7836
- "x-amz-date": requestDate,
7837
- ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
7838
- ...opts.headers || {}
7839
- };
7840
- const signedNames = signedHeadersString(headers);
7841
- const canonicalRequest = [
7842
- opts.method,
7843
- path,
7844
- query,
7845
- canonicalHeaders(headers),
7846
- signedNames,
7847
- EMPTY_SHA256
7848
- ].join(`
8116
+ async function signedFetch(opts, deadline = createS3TransportDeadline(config)) {
8117
+ return guardedS3Transport(opts.signal, (transportSignal) => {
8118
+ const endpoint = new URL(config.endpoint);
8119
+ const { dateStamp, amzDate: requestDate } = amzDate();
8120
+ const path = buildPath(opts.bucket, opts.key);
8121
+ const query = canonicalQuery(opts.query);
8122
+ const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
8123
+ const headers = {
8124
+ host: endpoint.host,
8125
+ "x-amz-content-sha256": EMPTY_SHA256,
8126
+ "x-amz-date": requestDate,
8127
+ ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
8128
+ ...opts.headers || {}
8129
+ };
8130
+ const signedNames = signedHeadersString(headers);
8131
+ const canonicalRequest = [
8132
+ opts.method,
8133
+ path,
8134
+ query,
8135
+ canonicalHeaders(headers),
8136
+ signedNames,
8137
+ EMPTY_SHA256
8138
+ ].join(`
7849
8139
  `);
7850
- const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
7851
- const stringToSign = [
7852
- "AWS4-HMAC-SHA256",
7853
- requestDate,
7854
- scope,
7855
- sha256(canonicalRequest)
7856
- ].join(`
8140
+ const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
8141
+ const stringToSign = [
8142
+ "AWS4-HMAC-SHA256",
8143
+ requestDate,
8144
+ scope,
8145
+ sha256(canonicalRequest)
8146
+ ].join(`
7857
8147
  `);
7858
- const signature = createHmac("sha256", signingKey(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
7859
- const requestHeaders = new Headers;
7860
- for (const [key, value] of Object.entries(headers)) {
7861
- if (key !== "host")
7862
- requestHeaders.set(key, value);
7863
- }
7864
- requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
7865
- if (config.dockerContainerName) {
7866
- if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
7867
- throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8148
+ const signature = createHmac("sha256", signingKey(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
8149
+ const requestHeaders = new Headers;
8150
+ for (const [key, value] of Object.entries(headers)) {
8151
+ if (key !== "host")
8152
+ requestHeaders.set(key, value);
7868
8153
  }
7869
- return dockerCurlFetch({
7870
- containerName: config.dockerContainerName,
8154
+ requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
8155
+ if (config.dockerContainerName) {
8156
+ if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
8157
+ throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8158
+ }
8159
+ return dockerCurlFetch({
8160
+ containerName: config.dockerContainerName,
8161
+ method: opts.method,
8162
+ url,
8163
+ headers: requestHeaders,
8164
+ signal: transportSignal
8165
+ });
8166
+ }
8167
+ return fetch(url, {
7871
8168
  method: opts.method,
7872
- url,
7873
8169
  headers: requestHeaders,
7874
- signal: opts.signal
8170
+ signal: transportSignal
7875
8171
  });
7876
- }
7877
- return fetch(url, {
7878
- method: opts.method,
7879
- headers: requestHeaders,
7880
- signal: opts.signal
7881
- });
8172
+ }, deadline);
7882
8173
  }
7883
- async function textOrThrow(res) {
7884
- const text = await res.text();
8174
+ async function textOrThrow(res, signal, deadline) {
8175
+ const text = await readResponseTextWithTimeout(res, signal, deadline);
7885
8176
  if (!res.ok)
7886
8177
  throw sanitizeS3Error(res.status, text);
7887
8178
  return text;
7888
8179
  }
7889
8180
  async function listBuckets(signal) {
7890
- const xml = await textOrThrow(await signedFetch({ method: "GET", signal }));
8181
+ const deadline = createS3TransportDeadline(config);
8182
+ const xml = await textOrThrow(await signedFetch({ method: "GET", signal }, deadline), signal, deadline);
7891
8183
  return parseBuckets(xml);
7892
8184
  }
7893
8185
  async function listObjects(opts) {
8186
+ const deadline = createS3TransportDeadline(config);
7894
8187
  const xml = await textOrThrow(await signedFetch({
7895
8188
  method: "GET",
7896
8189
  bucket: opts.bucket,
@@ -7901,32 +8194,34 @@ function createS3Adapter(config) {
7901
8194
  ...opts.continuationToken ? { "continuation-token": opts.continuationToken } : {}
7902
8195
  },
7903
8196
  signal: opts.signal
7904
- }));
8197
+ }, deadline), opts.signal, deadline);
7905
8198
  return parseObjects(xml);
7906
8199
  }
7907
8200
  async function headObject(opts) {
8201
+ const deadline = createS3TransportDeadline(config);
7908
8202
  const res = await signedFetch({
7909
8203
  method: "HEAD",
7910
8204
  bucket: opts.bucket,
7911
8205
  key: opts.key,
7912
8206
  signal: opts.signal
7913
- });
8207
+ }, deadline);
7914
8208
  if (!res.ok)
7915
- throw sanitizeS3Error(res.status, await res.text());
8209
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
7916
8210
  return headFromObjectResponse(opts.bucket, opts.key, res);
7917
8211
  }
7918
8212
  async function getObjectText(opts) {
7919
8213
  const maxBytes = Math.min(1024 * 1024, Math.max(1, opts.maxBytes ?? 512 * 1024));
8214
+ const deadline = createS3TransportDeadline(config);
7920
8215
  const res = await signedFetch({
7921
8216
  method: "GET",
7922
8217
  bucket: opts.bucket,
7923
8218
  key: opts.key,
7924
8219
  headers: { range: `bytes=0-${maxBytes - 1}` },
7925
8220
  signal: opts.signal
7926
- });
8221
+ }, deadline);
7927
8222
  if (!res.ok && res.status !== 206)
7928
- throw sanitizeS3Error(res.status, await res.text());
7929
- const bytes = new Uint8Array(await res.arrayBuffer());
8223
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
8224
+ const bytes = await readResponseBytesWithTimeout(res, opts.signal, deadline);
7930
8225
  const head = headFromObjectResponse(opts.bucket, opts.key, res);
7931
8226
  const fullSize = head.sizeBytes;
7932
8227
  return {
@@ -7936,17 +8231,18 @@ function createS3Adapter(config) {
7936
8231
  };
7937
8232
  }
7938
8233
  async function getObjectResponse(opts) {
8234
+ const deadline = createS3TransportDeadline(config);
7939
8235
  const res = await signedFetch({
7940
8236
  method: opts.method,
7941
8237
  bucket: opts.bucket,
7942
8238
  key: opts.key,
7943
8239
  headers: opts.range ? { range: opts.range } : undefined,
7944
8240
  signal: opts.signal
7945
- });
8241
+ }, deadline);
7946
8242
  if (!res.ok && res.status !== 206) {
7947
- throw sanitizeS3Error(res.status, await res.text());
8243
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
7948
8244
  }
7949
- return new Response(opts.method === "HEAD" ? null : res.body, {
8245
+ return new Response(opts.method === "HEAD" ? null : timeoutReadableStream(res.body, opts.signal), {
7950
8246
  status: res.status,
7951
8247
  headers: rawObjectHeaders(opts.key, res)
7952
8248
  });
@@ -7966,6 +8262,9 @@ async function s3ConfigFromDockerInfoAsync(info, signal) {
7966
8262
  const image = info.image?.toLowerCase() || "";
7967
8263
  const minioDefault = image.includes("minio");
7968
8264
  const env = info.env;
8265
+ if (!info.hostPort && minioDefault) {
8266
+ throw new S3HttpError(503, 'MinIO S3 browsing requires a published host port. Add a compose port mapping like "9000:9000" for the MinIO API.');
8267
+ }
7969
8268
  const dockerContainerName = info.hostPort ? undefined : await resolveRunningComposeContainerNameOrThrowAsync(info.serviceName, info.composeDir, signal);
7970
8269
  return {
7971
8270
  endpoint: info.hostPort ? `http://localhost:${info.hostPort}` : `http://127.0.0.1:${info.containerPort}`,
@@ -7982,7 +8281,7 @@ async function openS3ExplorerAsync(info, signal) {
7982
8281
  function isS3HttpError(err) {
7983
8282
  return err instanceof S3HttpError;
7984
8283
  }
7985
- var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
8284
+ var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", DEFAULT_S3_REQUEST_TIMEOUT_MS = 5000, DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS = 30000, s3RequestTimeoutMs, s3DockerCurlTimeoutMs;
7986
8285
  var init_s3 = __esm(() => {
7987
8286
  init_raw_file_headers();
7988
8287
  init_docker_utils();
@@ -7995,6 +8294,8 @@ var init_s3 = __esm(() => {
7995
8294
  this.status = status;
7996
8295
  }
7997
8296
  };
8297
+ s3RequestTimeoutMs = DEFAULT_S3_REQUEST_TIMEOUT_MS;
8298
+ s3DockerCurlTimeoutMs = DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS;
7998
8299
  });
7999
8300
 
8000
8301
  // web-src/server/database/handle-s3.ts
@@ -10889,6 +11190,13 @@ function handleSettings() {
10889
11190
  }
10890
11191
  });
10891
11192
  }
11193
+ function worktreeWatchDirectoryLimitFromEnv() {
11194
+ const raw = process.env.CODE_VIEWER_WORKTREE_WATCH_LIMIT;
11195
+ if (!raw)
11196
+ return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11197
+ const parsed = Number(raw);
11198
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11199
+ }
10892
11200
  function handleFiles2(url) {
10893
11201
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
10894
11202
  if (target !== "worktree" && !verifyTreeRef(target, cwd))
@@ -12237,6 +12545,7 @@ data: ok
12237
12545
  excludeNames: scopeExcludeNames,
12238
12546
  watch,
12239
12547
  initialScanMode: "async",
12548
+ maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
12240
12549
  onUpdate: triggerUpdate,
12241
12550
  onError: (error) => {
12242
12551
  const message = error instanceof Error ? error.message : String(error);