@youtyan/code-viewer 0.1.25 → 0.1.27

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.
@@ -6,16 +6,36 @@ import {
6
6
  constants,
7
7
  existsSync as existsSync3,
8
8
  lstatSync as lstatSync3,
9
+ mkdirSync,
9
10
  openSync,
10
11
  readFileSync as readFileSync2,
11
12
  realpathSync,
13
+ renameSync,
12
14
  statSync,
13
15
  unlinkSync,
14
16
  watch,
15
17
  writeFileSync
16
18
  } from "node:fs";
19
+ import { homedir } from "node:os";
17
20
  import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
18
21
 
22
+ // web-src/directory-name.ts
23
+ function normalizeNewDirectoryName(name) {
24
+ if (typeof name !== "string")
25
+ return null;
26
+ const trimmed = name.trim();
27
+ if (!trimmed || trimmed.length > 180)
28
+ return null;
29
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\x00") || Array.from(trimmed).some((char) => {
30
+ const code = char.charCodeAt(0);
31
+ return code < 32 || code === 127;
32
+ }))
33
+ return null;
34
+ if (trimmed === "." || trimmed === ".." || trimmed.toLowerCase() === ".git")
35
+ return null;
36
+ return trimmed;
37
+ }
38
+
19
39
  // web-src/routes.ts
20
40
  var SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
21
41
  var APP_ENTRY_PATHS = ["/", "/index.html"];
@@ -568,7 +588,13 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
568
588
  return "internal";
569
589
  return omitDirNames.has(name) ? "heavy" : undefined;
570
590
  }
571
- function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames) {
591
+ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
592
+ if (excludeNames.has(name.toLowerCase()))
593
+ return {
594
+ name,
595
+ path: "",
596
+ type: isDirectory ? "tree" : "blob"
597
+ };
572
598
  const entryPath = base ? `${base}/${name}` : name;
573
599
  const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
574
600
  const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
@@ -580,14 +606,15 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames) {
580
606
  children_omitted_reason: omittedReason
581
607
  } : { name, path: entryPath, type };
582
608
  }
583
- function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES) {
609
+ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
584
610
  const base = normalizeTreePath(path);
585
611
  const root = join2(cwd, base);
586
612
  const omitDirNameSet = new Set(omitDirNames);
613
+ const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
587
614
  let directEntries;
588
615
  try {
589
616
  const dirents = readdirSync(root, { withFileTypes: true });
590
- directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet)));
617
+ directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet)).filter((entry) => entry.path));
591
618
  } catch {
592
619
  return [];
593
620
  }
@@ -624,6 +651,8 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
624
651
  return;
625
652
  }
626
653
  for (const entry of entries) {
654
+ if (excludeNameSet.has(entry.name.toLowerCase()))
655
+ continue;
627
656
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
628
657
  const full = join2(dir, entry.name);
629
658
  if (entry.isDirectory()) {
@@ -699,15 +728,12 @@ function combineDirectAndRecursiveFiles(directEntries, fileEntries) {
699
728
  ...fileEntries.filter((entry) => !seen.has(entry.path))
700
729
  ];
701
730
  }
702
- function worktreeFiles(cwd) {
703
- return listTree("worktree", "", cwd, { recursive: true }).entries;
704
- }
705
731
  function listTree(ref, path, cwd, options = {}) {
706
732
  const base = normalizeTreePath(path);
707
733
  if (ref === "worktree") {
708
734
  return {
709
735
  code: 0,
710
- entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
736
+ entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames, options.excludeNames),
711
737
  stderr: ""
712
738
  };
713
739
  }
@@ -1099,17 +1125,19 @@ var GREP_DEFAULT_MAX = 200;
1099
1125
  var GREP_ABSOLUTE_MAX = 500;
1100
1126
  var GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
1101
1127
  var FILE_SEARCH_ABSOLUTE_MAX = 50000;
1128
+ var DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
1102
1129
  function normalizeGrepMax(value) {
1103
1130
  const parsed = Number(value || "");
1104
1131
  if (!Number.isInteger(parsed) || parsed <= 0)
1105
1132
  return GREP_DEFAULT_MAX;
1106
1133
  return Math.min(parsed, GREP_ABSOLUTE_MAX);
1107
1134
  }
1108
- function isSkippableSearchPath(path, omitDirNames = []) {
1135
+ function isSkippableSearchPath(path, omitDirNames = [], excludeNames = []) {
1109
1136
  const omitDirs = new Set(omitDirNames.map((name) => name.toLowerCase()));
1137
+ const excluded = new Set(excludeNames.map((name) => name.toLowerCase()));
1110
1138
  return path.split(/[\\/]+/).some((part) => {
1111
1139
  const lower = part.toLowerCase();
1112
- return lower === ".git" || omitDirs.has(lower);
1140
+ return lower === ".git" || omitDirs.has(lower) || excluded.has(lower);
1113
1141
  });
1114
1142
  }
1115
1143
  function fixedStringLineMatches(path, text, query, max) {
@@ -1142,7 +1170,7 @@ function buildFileSearchList(ref, generation, entries) {
1142
1170
  truncated: entries.length > FILE_SEARCH_ABSOLUTE_MAX
1143
1171
  };
1144
1172
  }
1145
- function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
1173
+ function buildRgArgs(query, max, paths, regex = false, omitDirNames = [], excludeNames = []) {
1146
1174
  const safePaths = paths.length ? paths : ["."];
1147
1175
  const omitGlobs = omitDirNames.flatMap((name) => [
1148
1176
  "--glob",
@@ -1150,6 +1178,12 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
1150
1178
  "--glob",
1151
1179
  `!**/${name}/**`
1152
1180
  ]);
1181
+ const excludeGlobs = excludeNames.flatMap((name) => [
1182
+ "--glob",
1183
+ `!${name}`,
1184
+ "--glob",
1185
+ `!**/${name}`
1186
+ ]);
1153
1187
  const args = [
1154
1188
  "rg",
1155
1189
  "--no-config",
@@ -1164,6 +1198,7 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
1164
1198
  "--max-filesize",
1165
1199
  "2M",
1166
1200
  ...omitGlobs,
1201
+ ...excludeGlobs,
1167
1202
  "-e",
1168
1203
  query,
1169
1204
  "--",
@@ -1173,7 +1208,7 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
1173
1208
  args.splice(8, 0, "--fixed-strings");
1174
1209
  return args;
1175
1210
  }
1176
- function parseRgOutput(stdout, max, omitDirNames = []) {
1211
+ function parseRgOutput(stdout, max, omitDirNames = [], excludeNames = []) {
1177
1212
  const matches = [];
1178
1213
  for (const line of stdout.split(`
1179
1214
  `)) {
@@ -1186,7 +1221,7 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
1186
1221
  const lineNo = Number(parsed[2]);
1187
1222
  const column = Number(parsed[3]);
1188
1223
  const preview = parsed[4];
1189
- if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
1224
+ if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames, excludeNames))
1190
1225
  continue;
1191
1226
  matches.push({
1192
1227
  path,
@@ -1197,12 +1232,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
1197
1232
  }
1198
1233
  return matches;
1199
1234
  }
1200
- function parseGitGrepOutput(stdout, ref, max, omitDirNames = []) {
1235
+ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames = []) {
1201
1236
  const prefix = `${ref}:`;
1202
1237
  const normalized = stdout.split(`
1203
1238
  `).map((line) => line.startsWith(prefix) ? line.slice(prefix.length) : line).join(`
1204
1239
  `);
1205
- return parseRgOutput(normalized, max, omitDirNames);
1240
+ return parseRgOutput(normalized, max, omitDirNames, excludeNames);
1206
1241
  }
1207
1242
 
1208
1243
  // web-src/server/preview.ts
@@ -1218,8 +1253,8 @@ var SIZE_LARGE = 20000;
1218
1253
  var LINE_INDEX_MIN_START = 1e4;
1219
1254
  var LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
1220
1255
  var BLOB_LINE_CACHE_MAX_BYTES = 128 * 1024 * 1024;
1221
- var MAX_UPLOAD_FILE_BYTES = 10 * 1024 * 1024;
1222
- var MAX_UPLOAD_TOTAL_BYTES = 50 * 1024 * 1024;
1256
+ var MAX_UPLOAD_FILE_BYTES = 512 * 1024 * 1024;
1257
+ var MAX_UPLOAD_TOTAL_BYTES = 1024 * 1024 * 1024;
1223
1258
  var MAX_UPLOAD_BODY_BYTES = MAX_UPLOAD_TOTAL_BYTES + 1024 * 1024;
1224
1259
  var MAX_UPLOAD_FILES = 50;
1225
1260
  var SAFE_UPLOAD_EXTENSIONS = new Set([
@@ -1237,7 +1272,19 @@ var SAFE_UPLOAD_EXTENSIONS = new Set([
1237
1272
  ".jpeg",
1238
1273
  ".gif",
1239
1274
  ".webp",
1275
+ ".svg",
1240
1276
  ".pdf",
1277
+ ".mp4",
1278
+ ".mov",
1279
+ ".m4v",
1280
+ ".webm",
1281
+ ".mp3",
1282
+ ".wav",
1283
+ ".m4a",
1284
+ ".aac",
1285
+ ".flac",
1286
+ ".ogg",
1287
+ ".zip",
1241
1288
  ".ts",
1242
1289
  ".tsx",
1243
1290
  ".js",
@@ -1250,11 +1297,11 @@ var generation = 1;
1250
1297
  var cwd = repoRoot(process.cwd()) || process.cwd();
1251
1298
  var cliArgs = DEFAULT_ARGS;
1252
1299
  var listenPort = 0;
1253
- var allowUpload = false;
1254
- var uploadAllowedByCli = false;
1255
1300
  var openAfterStart = false;
1256
1301
  var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
1257
1302
  var scopeOmitDirCliOverride = null;
1303
+ var scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
1304
+ var uploadDisabledByConfig = false;
1258
1305
  var rgAvailableCache = null;
1259
1306
  var enc = new TextEncoder;
1260
1307
  var sseClients = new Set;
@@ -1307,10 +1354,7 @@ Examples:
1307
1354
  listenPort = parsed;
1308
1355
  } else if (arg === "--open") {
1309
1356
  openAfterStart = true;
1310
- } else if (arg === "--allow-upload") {
1311
- allowUpload = true;
1312
- uploadAllowedByCli = true;
1313
- } else if (arg === "--scope-omit-dir") {
1357
+ } else if (arg === "--allow-upload") {} else if (arg === "--scope-omit-dir") {
1314
1358
  const next = process.argv[++i];
1315
1359
  if (!next) {
1316
1360
  console.error("--scope-omit-dir requires a directory name");
@@ -1326,14 +1370,16 @@ Examples:
1326
1370
  }
1327
1371
  if (rest.length)
1328
1372
  cliArgs = rest;
1329
- if (!uploadAllowedByCli)
1330
- allowUpload = loadProjectConfigUploadEnabled();
1331
1373
  const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
1374
+ const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
1375
+ uploadDisabledByConfig = loadProjectConfigUploadDisabled();
1332
1376
  if (scopeOmitDirCliOverride) {
1333
1377
  scopeOmitDirNames = scopeOmitDirCliOverride;
1334
1378
  } else if (configScopeOmitDirs) {
1335
1379
  scopeOmitDirNames = configScopeOmitDirs;
1336
1380
  }
1381
+ if (configScopeExcludeNames)
1382
+ scopeExcludeNames = configScopeExcludeNames;
1337
1383
  }
1338
1384
  function json(data, init = {}) {
1339
1385
  return new Response(JSON.stringify(data), {
@@ -1606,6 +1652,13 @@ function normalizeScopeOmitDirNames(names) {
1606
1652
  ...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 64 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
1607
1653
  ].sort((a, b) => a.localeCompare(b));
1608
1654
  }
1655
+ function normalizeScopeExcludeNames(names) {
1656
+ if (!Array.isArray(names))
1657
+ return [];
1658
+ return [
1659
+ ...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 128 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
1660
+ ].sort((a, b) => a.localeCompare(b));
1661
+ }
1609
1662
  function parseScopeOmitDirNamesQuery(value) {
1610
1663
  const names = value ? value.split(",") : [];
1611
1664
  if (names.length > 100)
@@ -1617,6 +1670,17 @@ function parseScopeOmitDirNamesQuery(value) {
1617
1670
  }
1618
1671
  return normalizeScopeOmitDirNames(names);
1619
1672
  }
1673
+ function parseScopeExcludeNamesQuery(value) {
1674
+ const names = value ? value.split(",") : [];
1675
+ if (names.length > 200)
1676
+ return null;
1677
+ for (const raw of names) {
1678
+ const name = raw.trim();
1679
+ if (!name || name.length > 128 || name.includes("/") || name.includes("\\") || name.includes("\x00") || name === "." || name === ".." || name === ".git")
1680
+ return null;
1681
+ }
1682
+ return normalizeScopeExcludeNames(names);
1683
+ }
1620
1684
  function loadProjectConfig() {
1621
1685
  const full = join4(cwd, ".code-viewer.json");
1622
1686
  if (!existsSync3(full))
@@ -1640,9 +1704,9 @@ function loadProjectConfig() {
1640
1704
  return null;
1641
1705
  }
1642
1706
  }
1643
- function loadProjectConfigUploadEnabled() {
1707
+ function loadProjectConfigUploadDisabled() {
1644
1708
  const config = loadProjectConfig();
1645
- return config?.upload?.enabled === true;
1709
+ return config?.upload?.enabled === false;
1646
1710
  }
1647
1711
  function loadProjectConfigScopeOmitDirs() {
1648
1712
  const config = loadProjectConfig();
@@ -1650,14 +1714,31 @@ function loadProjectConfigScopeOmitDirs() {
1650
1714
  return null;
1651
1715
  return normalizeScopeOmitDirNames(config.scope.omitDirs);
1652
1716
  }
1717
+ function loadProjectConfigScopeExcludeNames() {
1718
+ const config = loadProjectConfig();
1719
+ if (!config?.scope || !Array.isArray(config.scope.excludeNames))
1720
+ return null;
1721
+ return normalizeScopeExcludeNames(config.scope.excludeNames);
1722
+ }
1653
1723
  function scopeOmitDirNamesFromQuery(url) {
1654
1724
  if (!url.searchParams.has("omit_dirs"))
1655
1725
  return scopeOmitDirNames;
1656
1726
  return parseScopeOmitDirNamesQuery(url.searchParams.get("omit_dirs") || "") || scopeOmitDirNames;
1657
1727
  }
1728
+ function scopeExcludeNamesFromQuery(url) {
1729
+ if (!url.searchParams.has("exclude_names"))
1730
+ return scopeExcludeNames;
1731
+ return parseScopeExcludeNamesQuery(url.searchParams.get("exclude_names") || "") || scopeExcludeNames;
1732
+ }
1658
1733
  function invalidScopeOmitDirNamesQuery(url) {
1659
1734
  return url.searchParams.has("omit_dirs") && !parseScopeOmitDirNamesQuery(url.searchParams.get("omit_dirs") || "");
1660
1735
  }
1736
+ function invalidScopeExcludeNamesQuery(url) {
1737
+ return url.searchParams.has("exclude_names") && !parseScopeExcludeNamesQuery(url.searchParams.get("exclude_names") || "");
1738
+ }
1739
+ function isExcludedScopePath(path, excludeNames) {
1740
+ return path.split(/[\\/]+/).some((part) => excludeNames.some((name) => part.toLowerCase() === name.toLowerCase()));
1741
+ }
1661
1742
  function isGitInternalPath(path) {
1662
1743
  return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".git");
1663
1744
  }
@@ -1684,6 +1765,9 @@ function safeWorktreePath(path) {
1684
1765
  return null;
1685
1766
  return realFull;
1686
1767
  }
1768
+ function worktreePath(path) {
1769
+ return join4(cwd, path);
1770
+ }
1687
1771
  function safeOpenWorktreePath(path) {
1688
1772
  if (path === "") {
1689
1773
  try {
@@ -1798,10 +1882,14 @@ function handleTree(url) {
1798
1882
  const recursive = url.searchParams.get("recursive") === "1";
1799
1883
  if (invalidScopeOmitDirNamesQuery(url))
1800
1884
  return text("invalid omit dirs", 400);
1885
+ if (invalidScopeExcludeNamesQuery(url))
1886
+ return text("invalid exclude names", 400);
1887
+ const excludeNames = scopeExcludeNamesFromQuery(url);
1801
1888
  const entries = listTree(target, path, cwd, {
1802
1889
  recursive,
1803
- omitDirNames: scopeOmitDirNamesFromQuery(url)
1804
- }).entries;
1890
+ omitDirNames: scopeOmitDirNamesFromQuery(url),
1891
+ excludeNames
1892
+ }).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
1805
1893
  return json({
1806
1894
  ref: target,
1807
1895
  path,
@@ -1809,7 +1897,7 @@ function handleTree(url) {
1809
1897
  branch: currentBranch(cwd) || undefined,
1810
1898
  entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
1811
1899
  readme: readReadme(target, path),
1812
- upload_enabled: allowUpload && (target === "worktree" || target === "")
1900
+ upload_enabled: !uploadDisabledByConfig && (target === "worktree" || target === "")
1813
1901
  });
1814
1902
  }
1815
1903
  function handleSettings() {
@@ -1818,6 +1906,8 @@ function handleSettings() {
1818
1906
  scope: {
1819
1907
  omit_dirs_effective: scopeOmitDirNames,
1820
1908
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
1909
+ exclude_names_effective: scopeExcludeNames,
1910
+ exclude_names_built_in: DEFAULT_EXCLUDE_NAMES,
1821
1911
  max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT
1822
1912
  }
1823
1913
  });
@@ -1828,22 +1918,26 @@ function handleFiles(url) {
1828
1918
  return text("invalid target", 400);
1829
1919
  if (invalidScopeOmitDirNamesQuery(url))
1830
1920
  return text("invalid omit dirs", 400);
1921
+ if (invalidScopeExcludeNamesQuery(url))
1922
+ return text("invalid exclude names", 400);
1831
1923
  const omitDirNames = scopeOmitDirNamesFromQuery(url);
1832
- const key = `${target || "worktree"}\x00${omitDirNames.join("\x00")}`;
1924
+ const excludeNames = scopeExcludeNamesFromQuery(url);
1925
+ const key = `${target || "worktree"}\x00${omitDirNames.join("\x00")}\x00${excludeNames.join("\x00")}`;
1833
1926
  const cached = fileListCache.get(key);
1834
1927
  if (cached && cached.generation === generation)
1835
1928
  return json(cached.body);
1836
1929
  const ref = target || "worktree";
1837
1930
  const entries = listTree(ref, "", cwd, {
1838
1931
  recursive: true,
1839
- omitDirNames
1840
- }).entries;
1932
+ omitDirNames,
1933
+ excludeNames
1934
+ }).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
1841
1935
  const body = buildFileSearchList(ref, generation, entries);
1842
1936
  fileListCache.set(key, { generation, body });
1843
1937
  return json(body);
1844
1938
  }
1845
- function parseGrepPaths(url, omitDirNames) {
1846
- return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
1939
+ function parseGrepPaths(url, omitDirNames, excludeNames) {
1940
+ return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
1847
1941
  }
1848
1942
  function rgAvailable() {
1849
1943
  if (rgAvailableCache !== null)
@@ -1852,13 +1946,17 @@ function rgAvailable() {
1852
1946
  rgAvailableCache = proc.code === 0;
1853
1947
  return rgAvailableCache;
1854
1948
  }
1855
- function grepWorktreeFallback(query, max, paths, omitDirNames) {
1856
- const candidates = paths.length ? paths : worktreeFiles(cwd).map((entry) => entry.path);
1949
+ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
1950
+ const candidates = paths.length ? paths : listTree("worktree", "", cwd, {
1951
+ recursive: true,
1952
+ omitDirNames,
1953
+ excludeNames
1954
+ }).entries.map((entry) => entry.path);
1857
1955
  const matches = [];
1858
1956
  for (const path of candidates) {
1859
1957
  if (matches.length >= max)
1860
1958
  break;
1861
- if (!safePath(path) || isGitInternalPath(path) || isSkippableSearchPath(path, omitDirNames))
1959
+ if (!safePath(path) || isGitInternalPath(path) || isSkippableSearchPath(path, omitDirNames, excludeNames))
1862
1960
  continue;
1863
1961
  const full = safeWorktreePath(path);
1864
1962
  if (!full)
@@ -1883,13 +1981,13 @@ function grepWorktreeFallback(query, max, paths, omitDirNames) {
1883
1981
  }
1884
1982
  return matches;
1885
1983
  }
1886
- function grepWorktree(query, max, paths, regex, omitDirNames) {
1984
+ function grepWorktree(query, max, paths, regex, omitDirNames, excludeNames) {
1887
1985
  if (rgAvailable()) {
1888
- const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames) && safeWorktreePath(path));
1889
- const args = buildRgArgs(query, max, safePaths, regex, omitDirNames);
1986
+ const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames) && safeWorktreePath(path));
1987
+ const args = buildRgArgs(query, max, safePaths, regex, omitDirNames, excludeNames);
1890
1988
  const proc = runSync(args, cwd, { timeout: 5000 });
1891
1989
  const stdout = proc.stdout;
1892
- const matches2 = parseRgOutput(stdout, max, omitDirNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames) && !!safeWorktreePath(match.path));
1990
+ const matches2 = parseRgOutput(stdout, max, omitDirNames, excludeNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames, excludeNames) && !!safeWorktreePath(match.path));
1893
1991
  return {
1894
1992
  ref: "worktree",
1895
1993
  engine: "rg",
@@ -1904,7 +2002,7 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
1904
2002
  truncated: false,
1905
2003
  matches: []
1906
2004
  };
1907
- const matches = grepWorktreeFallback(query, max, paths, omitDirNames);
2005
+ const matches = grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames);
1908
2006
  return {
1909
2007
  ref: "worktree",
1910
2008
  engine: "fallback",
@@ -1912,8 +2010,8 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
1912
2010
  matches
1913
2011
  };
1914
2012
  }
1915
- function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
1916
- const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
2013
+ function grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames) {
2014
+ const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
1917
2015
  const args = [
1918
2016
  "git",
1919
2017
  "-c",
@@ -1932,7 +2030,7 @@ function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
1932
2030
  ];
1933
2031
  const proc = runSync(args, cwd, { timeout: 5000 });
1934
2032
  const stdout = proc.stdout;
1935
- const matches = parseGitGrepOutput(stdout, ref, max, omitDirNames).slice(0, max);
2033
+ const matches = parseGitGrepOutput(stdout, ref, max, omitDirNames, excludeNames).slice(0, max);
1936
2034
  return { ref, engine: "git", truncated: matches.length >= max, matches };
1937
2035
  }
1938
2036
  function handleGrep(url) {
@@ -1941,8 +2039,11 @@ function handleGrep(url) {
1941
2039
  const max = normalizeGrepMax(url.searchParams.get("max"));
1942
2040
  if (invalidScopeOmitDirNamesQuery(url))
1943
2041
  return text("invalid omit dirs", 400);
2042
+ if (invalidScopeExcludeNamesQuery(url))
2043
+ return text("invalid exclude names", 400);
1944
2044
  const omitDirNames = scopeOmitDirNamesFromQuery(url);
1945
- const paths = parseGrepPaths(url, omitDirNames);
2045
+ const excludeNames = scopeExcludeNamesFromQuery(url);
2046
+ const paths = parseGrepPaths(url, omitDirNames, excludeNames);
1946
2047
  const regex = url.searchParams.get("regex") === "1";
1947
2048
  if (!query.trim())
1948
2049
  return json({
@@ -1952,10 +2053,10 @@ function handleGrep(url) {
1952
2053
  matches: []
1953
2054
  });
1954
2055
  if (ref === "worktree" || ref === "")
1955
- return json(grepWorktree(query, max, paths, regex, omitDirNames));
2056
+ return json(grepWorktree(query, max, paths, regex, omitDirNames, excludeNames));
1956
2057
  if (!verifyTreeRef(ref, cwd))
1957
2058
  return text("invalid target", 400);
1958
- return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
2059
+ return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames));
1959
2060
  }
1960
2061
  function handleRefCommits(url) {
1961
2062
  const query = url.searchParams.get("q") || "";
@@ -2353,24 +2454,26 @@ function isForbiddenUploadName(name) {
2353
2454
  return lower.startsWith(".") || lower === "package.json" || lower === "package-lock.json" || lower === "bun.lock" || lower === "bun.lockb" || lower === "yarn.lock" || lower === "pnpm-lock.yaml" || lower === "makefile" || lower === "dockerfile" || lower.endsWith(".dockerfile") || /^(tsconfig|jsconfig|bunfig|vercel|netlify|wrangler|next|vite|webpack|rollup|esbuild|astro|svelte|tailwind|postcss|babel|prettier|eslint)\./.test(lower) || lower.endsWith(".config.js") || lower.endsWith(".config.jsx") || lower.endsWith(".config.ts") || lower.endsWith(".config.tsx") || lower.endsWith(".config.mjs") || lower.endsWith(".config.cjs") || lower.includes("credential") || lower.includes("secret") || lower.endsWith(".exe") || lower.endsWith(".dll") || lower.endsWith(".dylib") || lower.endsWith(".so") || lower.endsWith(".sh") || lower.endsWith(".bash") || lower.endsWith(".zsh") || lower.endsWith(".fish") || lower.endsWith(".ps1") || lower.endsWith(".bat") || lower.endsWith(".cmd");
2354
2455
  }
2355
2456
  function safeUploadFileName(name) {
2356
- if (!name || name.includes("\x00") || name.includes("/") || name.includes("\\"))
2357
- return null;
2358
- if (name === "." || name === "..")
2457
+ const trimmed = name.trim();
2458
+ if (!trimmed || trimmed.length > 180 || trimmed.includes("\x00") || trimmed.includes("/") || trimmed.includes("\\") || Array.from(trimmed).some((char) => {
2459
+ const code = char.charCodeAt(0);
2460
+ return code < 32 || code === 127;
2461
+ }))
2359
2462
  return null;
2360
- if (!/^[A-Za-z0-9][A-Za-z0-9._ -]{0,180}$/.test(name))
2463
+ if (trimmed === "." || trimmed === "..")
2361
2464
  return null;
2362
- if (isGitInternalPath(name) || isForbiddenUploadName(name))
2465
+ if (isGitInternalPath(trimmed) || isForbiddenUploadName(trimmed))
2363
2466
  return null;
2364
- if (!SAFE_UPLOAD_EXTENSIONS.has(extname(name).toLowerCase()))
2467
+ if (!SAFE_UPLOAD_EXTENSIONS.has(extname(trimmed).toLowerCase()))
2365
2468
  return null;
2366
- return name;
2469
+ return trimmed;
2367
2470
  }
2368
2471
  function uploadOpenFlags() {
2369
2472
  return constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0);
2370
2473
  }
2371
2474
  async function handleUploadFiles(req) {
2372
- if (!allowUpload)
2373
- return text("upload disabled", 403);
2475
+ if (uploadDisabledByConfig)
2476
+ return text("upload disabled by project config", 403);
2374
2477
  if (req.method !== "POST")
2375
2478
  return text("method not allowed", 405);
2376
2479
  if (!sideEffectRequestAllowed(req))
@@ -2468,6 +2571,152 @@ function openOsPath(path) {
2468
2571
  const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
2469
2572
  spawnDetached(cmd);
2470
2573
  }
2574
+ function windowsTrashScript(path) {
2575
+ const quotedPath = path.replace(/'/g, "''");
2576
+ return [
2577
+ "$ErrorActionPreference = 'Stop';",
2578
+ `$path = '${quotedPath}';`,
2579
+ "Add-Type -TypeDefinition @'",
2580
+ "using System;",
2581
+ "using System.Runtime.InteropServices;",
2582
+ "public static class CodeViewerRecycleBin {",
2583
+ " [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
2584
+ " public struct SHFILEOPSTRUCT {",
2585
+ " public IntPtr hwnd;",
2586
+ " public uint wFunc;",
2587
+ " public string pFrom;",
2588
+ " public string pTo;",
2589
+ " public ushort fFlags;",
2590
+ " [MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;",
2591
+ " public IntPtr hNameMappings;",
2592
+ " public string lpszProgressTitle;",
2593
+ " }",
2594
+ ' [DllImport("shell32.dll", CharSet = CharSet.Unicode)]',
2595
+ " private static extern int SHFileOperationW(ref SHFILEOPSTRUCT lpFileOp);",
2596
+ " public static void MoveToRecycleBin(string path) {",
2597
+ " const uint FO_DELETE = 0x0003;",
2598
+ " const ushort FOF_SILENT = 0x0004;",
2599
+ " const ushort FOF_NOCONFIRMATION = 0x0010;",
2600
+ " const ushort FOF_ALLOWUNDO = 0x0040;",
2601
+ " const ushort FOF_NOERRORUI = 0x0400;",
2602
+ " var op = new SHFILEOPSTRUCT {",
2603
+ " hwnd = IntPtr.Zero,",
2604
+ " wFunc = FO_DELETE,",
2605
+ ' pFrom = path + "\\0\\0",',
2606
+ " pTo = null,",
2607
+ " fFlags = (ushort)(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT),",
2608
+ " fAnyOperationsAborted = false,",
2609
+ " hNameMappings = IntPtr.Zero,",
2610
+ " lpszProgressTitle = null",
2611
+ " };",
2612
+ " int result = SHFileOperationW(ref op);",
2613
+ ' if (result != 0) throw new InvalidOperationException("SHFileOperationW failed: " + result);',
2614
+ ' if (op.fAnyOperationsAborted) throw new OperationCanceledException("SHFileOperationW aborted");',
2615
+ " }",
2616
+ "}",
2617
+ "'@;",
2618
+ "[CodeViewerRecycleBin]::MoveToRecycleBin($path);"
2619
+ ].join(" ");
2620
+ }
2621
+ function windowsRestoreTrashScript(originalPath) {
2622
+ const quotedPath = originalPath.replace(/'/g, "''");
2623
+ return [
2624
+ "$ErrorActionPreference = 'Stop';",
2625
+ `$original = '${quotedPath}';`,
2626
+ "$parent = [System.IO.Path]::GetDirectoryName($original);",
2627
+ "$name = [System.IO.Path]::GetFileName($original);",
2628
+ "$shell = New-Object -ComObject Shell.Application;",
2629
+ "$bin = $shell.Namespace(10);",
2630
+ "$restored = $false;",
2631
+ "foreach ($item in $bin.Items()) {",
2632
+ " $deletedFrom = $item.ExtendedProperty('System.Recycle.DeletedFrom');",
2633
+ " if ($item.Name -eq $name -and $deletedFrom -eq $parent) {",
2634
+ " $item.InvokeVerb('ESTORE');",
2635
+ " $restored = $true;",
2636
+ " break;",
2637
+ " }",
2638
+ "}",
2639
+ "if (-not $restored) { throw 'recycle bin item not found'; }"
2640
+ ].join(" ");
2641
+ }
2642
+ function makeUndoId() {
2643
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
2644
+ }
2645
+ function clearMutableCaches() {
2646
+ fileCache.clear();
2647
+ metaCache.clear();
2648
+ fileListCache.clear();
2649
+ }
2650
+ function moveMacPathIntoTrash(path) {
2651
+ const trashDir = join4(homedir(), ".Trash");
2652
+ const base = basename2(path) || "code-viewer-trash-item";
2653
+ const target = join4(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
2654
+ try {
2655
+ mkdirSync(trashDir, { recursive: true });
2656
+ renameSync(path, target);
2657
+ return { ok: true, trashPath: target };
2658
+ } catch (error) {
2659
+ return { ok: false, error: String(error) };
2660
+ }
2661
+ }
2662
+ function movePathToTrash(path) {
2663
+ lstatSync3(path);
2664
+ if (process.platform === "darwin") {
2665
+ return moveMacPathIntoTrash(path);
2666
+ }
2667
+ if (process.platform === "win32") {
2668
+ const res = runSync([
2669
+ "powershell.exe",
2670
+ "-NoProfile",
2671
+ "-NonInteractive",
2672
+ "-ExecutionPolicy",
2673
+ "Bypass",
2674
+ "-Command",
2675
+ windowsTrashScript(path)
2676
+ ], cwd, { timeout: 60000 });
2677
+ return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
2678
+ }
2679
+ return { ok: false, error: "trash unsupported" };
2680
+ }
2681
+ function restoreTrashPath(originalPath, trashPath) {
2682
+ const parent = parentRepoPath(originalPath);
2683
+ const parentFullPath = safeOpenWorktreePath(parent);
2684
+ if (!parentFullPath)
2685
+ return { ok: false, error: "invalid restore target" };
2686
+ const original = worktreePath(originalPath);
2687
+ if (existsSync3(original))
2688
+ return { ok: false, error: "restore target exists" };
2689
+ if (trashPath) {
2690
+ if (process.platform !== "darwin")
2691
+ return { ok: false, error: "invalid trash handle" };
2692
+ if (!existsSync3(trashPath))
2693
+ return { ok: false, error: "trash item not found" };
2694
+ try {
2695
+ const trashRoot = join4(homedir(), ".Trash");
2696
+ const trashRelative = relative(trashRoot, trashPath);
2697
+ if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
2698
+ return { ok: false, error: "invalid trash handle" };
2699
+ mkdirSync(dirname2(original), { recursive: true });
2700
+ renameSync(trashPath, original);
2701
+ return { ok: true };
2702
+ } catch (error) {
2703
+ return { ok: false, error: String(error) };
2704
+ }
2705
+ }
2706
+ if (process.platform === "win32") {
2707
+ const res = runSync([
2708
+ "powershell.exe",
2709
+ "-NoProfile",
2710
+ "-NonInteractive",
2711
+ "-ExecutionPolicy",
2712
+ "Bypass",
2713
+ "-Command",
2714
+ windowsRestoreTrashScript(original)
2715
+ ], cwd, { timeout: 60000 });
2716
+ return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
2717
+ }
2718
+ return { ok: false, error: "undo unavailable for this trash operation" };
2719
+ }
2471
2720
  async function handleOpenPath(req) {
2472
2721
  if (req.method !== "POST")
2473
2722
  return text("method not allowed", 405);
@@ -2508,6 +2757,142 @@ async function handleOpenPath(req) {
2508
2757
  openOsPath(target);
2509
2758
  return json({ ok: true });
2510
2759
  }
2760
+ async function handleTrashPath(req) {
2761
+ if (req.method !== "POST")
2762
+ return text("method not allowed", 405);
2763
+ if (!sideEffectRequestAllowed(req))
2764
+ return text("forbidden", 403);
2765
+ const contentType = req.headers.get("content-type") || "";
2766
+ if (!/^application\/json(?:;|$)/i.test(contentType))
2767
+ return text("unsupported media type", 415);
2768
+ const length = Number(req.headers.get("content-length") || "0");
2769
+ if (length > 1024)
2770
+ return text("payload too large", 413);
2771
+ let body = {};
2772
+ try {
2773
+ const raw = await req.text();
2774
+ if (raw.length > 1024)
2775
+ return text("payload too large", 413);
2776
+ body = JSON.parse(raw);
2777
+ } catch {
2778
+ return text("invalid json", 400);
2779
+ }
2780
+ const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
2781
+ if (!path)
2782
+ return text("invalid path", 400);
2783
+ if (!safeRepoPath(path))
2784
+ return text("invalid path", 400);
2785
+ if (isGitInternalPath(path))
2786
+ return text("forbidden", 403);
2787
+ const originalFullPath = safeWorktreePath(path);
2788
+ if (!originalFullPath)
2789
+ return text("not found", 404);
2790
+ const moved = movePathToTrash(worktreePath(path));
2791
+ if (!moved.ok)
2792
+ return text(moved.error || "trash failed", 500);
2793
+ const undo = {
2794
+ id: makeUndoId(),
2795
+ type: "trash",
2796
+ label: `Restore ${path}`,
2797
+ payload: {
2798
+ original_path: path,
2799
+ trashPath: moved.trashPath
2800
+ }
2801
+ };
2802
+ generation++;
2803
+ clearMutableCaches();
2804
+ sendSse("update");
2805
+ return json({ ok: true, generation, undo });
2806
+ }
2807
+ async function handleCreateDirectory(req) {
2808
+ if (req.method !== "POST")
2809
+ return text("method not allowed", 405);
2810
+ if (!sideEffectRequestAllowed(req))
2811
+ return text("forbidden", 403);
2812
+ const contentType = req.headers.get("content-type") || "";
2813
+ if (!/^application\/json(?:;|$)/i.test(contentType))
2814
+ return text("unsupported media type", 415);
2815
+ const lengthHeader = req.headers.get("content-length");
2816
+ const length = Number(lengthHeader || "0");
2817
+ if (lengthHeader && (!Number.isFinite(length) || length < 0))
2818
+ return text("invalid content length", 400);
2819
+ if (length > 2048)
2820
+ return text("payload too large", 413);
2821
+ let body = {};
2822
+ try {
2823
+ const raw = await req.text();
2824
+ if (raw.length > 2048)
2825
+ return text("payload too large", 413);
2826
+ body = JSON.parse(raw);
2827
+ } catch {
2828
+ return text("invalid json", 400);
2829
+ }
2830
+ const dir = typeof body.dir === "string" ? body.dir.trim().replace(/^\/+|\/+$/g, "") : "";
2831
+ const name = normalizeNewDirectoryName(body.name);
2832
+ if (!safeRepoPath(dir))
2833
+ return text("invalid dir", 400);
2834
+ if (dir && isGitInternalPath(dir))
2835
+ return text("forbidden", 403);
2836
+ if (!name)
2837
+ return text("invalid name", 400);
2838
+ const parent = safeOpenWorktreePath(dir);
2839
+ if (!parent)
2840
+ return text("not found", 404);
2841
+ const stats = statSync(parent);
2842
+ if (!stats.isDirectory())
2843
+ return text("not a directory", 400);
2844
+ const targetPath = dir ? `${dir}/${name}` : name;
2845
+ if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
2846
+ return text("invalid target", 400);
2847
+ const target = join4(parent, name);
2848
+ if (existsSync3(target))
2849
+ return text("already exists", 409);
2850
+ try {
2851
+ mkdirSync(target, { recursive: false });
2852
+ } catch (error) {
2853
+ if (error.code === "EEXIST")
2854
+ return text("already exists", 409);
2855
+ return text("create failed", 500);
2856
+ }
2857
+ generation++;
2858
+ clearMutableCaches();
2859
+ sendSse("update");
2860
+ return json({ ok: true, path: targetPath, generation });
2861
+ }
2862
+ async function handleRestoreTrash(req) {
2863
+ if (req.method !== "POST")
2864
+ return text("method not allowed", 405);
2865
+ if (!sideEffectRequestAllowed(req))
2866
+ return text("forbidden", 403);
2867
+ const contentType = req.headers.get("content-type") || "";
2868
+ if (!/^application\/json(?:;|$)/i.test(contentType))
2869
+ return text("unsupported media type", 415);
2870
+ const length = Number(req.headers.get("content-length") || "0");
2871
+ if (length > 1024)
2872
+ return text("payload too large", 413);
2873
+ let body = {};
2874
+ try {
2875
+ const raw = await req.text();
2876
+ if (raw.length > 1024)
2877
+ return text("payload too large", 413);
2878
+ body = JSON.parse(raw);
2879
+ } catch {
2880
+ return text("invalid json", 400);
2881
+ }
2882
+ const originalPath = typeof body.original_path === "string" ? body.original_path.replace(/^\/+|\/+$/g, "") : "";
2883
+ const trashPath = typeof body.trashPath === "string" ? body.trashPath : "";
2884
+ if (!originalPath || !safeRepoPath(originalPath))
2885
+ return text("invalid restore target", 400);
2886
+ if (isGitInternalPath(originalPath))
2887
+ return text("forbidden", 403);
2888
+ const restored = restoreTrashPath(originalPath, trashPath || undefined);
2889
+ if (!restored.ok)
2890
+ return text(restored.error || "undo failed", 409);
2891
+ generation++;
2892
+ clearMutableCaches();
2893
+ sendSse("update");
2894
+ return json({ ok: true, generation });
2895
+ }
2511
2896
  function sendSse(event, data = "tick") {
2512
2897
  const payload = enc.encode(`event: ${event}
2513
2898
  data: ${data}
@@ -2556,6 +2941,12 @@ var server = await startServer({
2556
2941
  return handleRawFile(req, url);
2557
2942
  if (url.pathname === "/_open_path")
2558
2943
  return handleOpenPath(req);
2944
+ if (url.pathname === "/_trash_path")
2945
+ return handleTrashPath(req);
2946
+ if (url.pathname === "/_restore_trash")
2947
+ return handleRestoreTrash(req);
2948
+ if (url.pathname === "/_create_directory")
2949
+ return handleCreateDirectory(req);
2559
2950
  if (url.pathname === "/_upload_files")
2560
2951
  return handleUploadFiles(req);
2561
2952
  if (url.pathname === "/_refs")
@@ -2564,9 +2955,7 @@ var server = await startServer({
2564
2955
  if (!sideEffectRequestAllowed(req))
2565
2956
  return text("forbidden", 403);
2566
2957
  generation++;
2567
- fileCache.clear();
2568
- metaCache.clear();
2569
- fileListCache.clear();
2958
+ clearMutableCaches();
2570
2959
  sendSse("update");
2571
2960
  return json({ ok: true, generation });
2572
2961
  }