@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.
- package/README.md +7 -13
- package/dist/code-viewer.js +449 -60
- package/package.json +1 -1
- package/web/app.js +594 -49
- package/web/index.html +5 -2
- package/web/style.css +144 -12
package/dist/code-viewer.js
CHANGED
|
@@ -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 =
|
|
1222
|
-
var MAX_UPLOAD_TOTAL_BYTES =
|
|
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
|
|
1707
|
+
function loadProjectConfigUploadDisabled() {
|
|
1644
1708
|
const config = loadProjectConfig();
|
|
1645
|
-
return config?.upload?.enabled ===
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 :
|
|
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
|
|
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
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
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 (
|
|
2463
|
+
if (trimmed === "." || trimmed === "..")
|
|
2361
2464
|
return null;
|
|
2362
|
-
if (isGitInternalPath(
|
|
2465
|
+
if (isGitInternalPath(trimmed) || isForbiddenUploadName(trimmed))
|
|
2363
2466
|
return null;
|
|
2364
|
-
if (!SAFE_UPLOAD_EXTENSIONS.has(extname(
|
|
2467
|
+
if (!SAFE_UPLOAD_EXTENSIONS.has(extname(trimmed).toLowerCase()))
|
|
2365
2468
|
return null;
|
|
2366
|
-
return
|
|
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 (
|
|
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
|
-
|
|
2568
|
-
metaCache.clear();
|
|
2569
|
-
fileListCache.clear();
|
|
2958
|
+
clearMutableCaches();
|
|
2570
2959
|
sendSse("update");
|
|
2571
2960
|
return json({ ok: true, generation });
|
|
2572
2961
|
}
|