@youtyan/code-viewer 0.2.5 → 0.2.7
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/dist/code-viewer.js +1218 -345
- package/package.json +1 -1
- package/web/app.js +3313 -3031
package/dist/code-viewer.js
CHANGED
|
@@ -17,14 +17,97 @@ var __export = (target, all) => {
|
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
|
+
// web-src/server/json-store.ts
|
|
21
|
+
import { randomBytes } from "node:crypto";
|
|
22
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
23
|
+
import { dirname } from "node:path";
|
|
24
|
+
function isEnoent(err) {
|
|
25
|
+
return err?.code === "ENOENT";
|
|
26
|
+
}
|
|
27
|
+
function tmpPath(file) {
|
|
28
|
+
return `${file}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
29
|
+
}
|
|
30
|
+
async function backupInvalidFile(file, suffix) {
|
|
31
|
+
try {
|
|
32
|
+
await rename(file, `${file}.${suffix}-${Date.now()}`);
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
function createJsonFileStore(options) {
|
|
36
|
+
const queues = new Map;
|
|
37
|
+
const backupSuffix = options.backupSuffix ?? "corrupt";
|
|
38
|
+
const serialize = options.serialize ?? ((state) => `${JSON.stringify(state, null, 2)}
|
|
39
|
+
`);
|
|
40
|
+
async function loadUnqueued(root) {
|
|
41
|
+
const file = options.filePath(root);
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = await readFile(file, "utf8");
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (isEnoent(err))
|
|
47
|
+
return options.empty();
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return options.sanitize(JSON.parse(raw));
|
|
52
|
+
} catch {
|
|
53
|
+
await backupInvalidFile(file, backupSuffix);
|
|
54
|
+
return options.empty();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function saveUnqueued(root, state) {
|
|
58
|
+
const file = options.filePath(root);
|
|
59
|
+
const normalized = options.sanitize(state);
|
|
60
|
+
const content = serialize(normalized);
|
|
61
|
+
if (options.maxBytes !== undefined && Buffer.byteLength(content, "utf8") > options.maxBytes) {
|
|
62
|
+
throw new Error(options.sizeErrorMessage ?? "JSON state too large");
|
|
63
|
+
}
|
|
64
|
+
await mkdir(dirname(file), { recursive: true });
|
|
65
|
+
const tmp = tmpPath(file);
|
|
66
|
+
await writeFile(tmp, content, "utf8");
|
|
67
|
+
await rename(tmp, file);
|
|
68
|
+
}
|
|
69
|
+
async function load(root) {
|
|
70
|
+
const pendingWrite = queues.get(options.filePath(root));
|
|
71
|
+
if (pendingWrite)
|
|
72
|
+
await pendingWrite.catch(() => {});
|
|
73
|
+
return loadUnqueued(root);
|
|
74
|
+
}
|
|
75
|
+
async function save(root, state) {
|
|
76
|
+
const file = options.filePath(root);
|
|
77
|
+
const previous = queues.get(file) ?? Promise.resolve();
|
|
78
|
+
const run = previous.catch(() => {}).then(() => saveUnqueued(root, state));
|
|
79
|
+
const queued = run.then(() => {}, () => {});
|
|
80
|
+
queues.set(file, queued);
|
|
81
|
+
try {
|
|
82
|
+
await run;
|
|
83
|
+
} finally {
|
|
84
|
+
if (queues.get(file) === queued)
|
|
85
|
+
queues.delete(file);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function update(root, updater) {
|
|
89
|
+
const file = options.filePath(root);
|
|
90
|
+
const previous = queues.get(file) ?? Promise.resolve();
|
|
91
|
+
const run = previous.catch(() => {}).then(async () => {
|
|
92
|
+
const current = await loadUnqueued(root);
|
|
93
|
+
const updated = await updater(current);
|
|
94
|
+
await saveUnqueued(root, updated.state);
|
|
95
|
+
return updated.result;
|
|
96
|
+
});
|
|
97
|
+
const queued = run.then(() => {}, () => {});
|
|
98
|
+
queues.set(file, queued);
|
|
99
|
+
try {
|
|
100
|
+
return await run;
|
|
101
|
+
} finally {
|
|
102
|
+
if (queues.get(file) === queued)
|
|
103
|
+
queues.delete(file);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { load, save, update };
|
|
107
|
+
}
|
|
108
|
+
var init_json_store = () => {};
|
|
109
|
+
|
|
20
110
|
// web-src/server/annotations.ts
|
|
21
|
-
import {
|
|
22
|
-
existsSync,
|
|
23
|
-
mkdirSync,
|
|
24
|
-
readFileSync,
|
|
25
|
-
renameSync,
|
|
26
|
-
writeFileSync
|
|
27
|
-
} from "node:fs";
|
|
28
111
|
import { join } from "node:path";
|
|
29
112
|
function annotationsFilePath(root) {
|
|
30
113
|
return join(root, CODE_VIEWER_DIR, ANNOTATIONS_FILE_NAME);
|
|
@@ -242,24 +325,11 @@ function normalizeAnnotationsState(raw) {
|
|
|
242
325
|
sessions: sessions.map(normalizeSession).filter((session) => session !== null)
|
|
243
326
|
};
|
|
244
327
|
}
|
|
245
|
-
function loadAnnotationsState(root) {
|
|
246
|
-
|
|
247
|
-
if (!existsSync(file))
|
|
248
|
-
return emptyAnnotationsState();
|
|
249
|
-
try {
|
|
250
|
-
return normalizeAnnotationsState(JSON.parse(readFileSync(file, "utf8")));
|
|
251
|
-
} catch {
|
|
252
|
-
return emptyAnnotationsState();
|
|
253
|
-
}
|
|
328
|
+
async function loadAnnotationsState(root) {
|
|
329
|
+
return annotationsStore.load(root);
|
|
254
330
|
}
|
|
255
|
-
function saveAnnotationsState(root, state) {
|
|
256
|
-
|
|
257
|
-
mkdirSync(dir, { recursive: true });
|
|
258
|
-
const file = annotationsFilePath(root);
|
|
259
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
260
|
-
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}
|
|
261
|
-
`, "utf8");
|
|
262
|
-
renameSync(tmp, file);
|
|
331
|
+
async function saveAnnotationsState(root, state) {
|
|
332
|
+
return annotationsStore.save(root, state);
|
|
263
333
|
}
|
|
264
334
|
function startAnnotationSession(state, title, now, id = makeAnnotationId("s")) {
|
|
265
335
|
const session = {
|
|
@@ -484,9 +554,18 @@ function deleteAnnotationById(state, id) {
|
|
|
484
554
|
}
|
|
485
555
|
return { state, removed: null };
|
|
486
556
|
}
|
|
487
|
-
var CODE_VIEWER_DIR = ".code-viewer", ANNOTATIONS_FILE_NAME = "annotations.json", ANNOTATION_BODY_MAX_BYTES, ANNOTATION_TITLE_MAX_CHARS = 300;
|
|
557
|
+
var CODE_VIEWER_DIR = ".code-viewer", ANNOTATIONS_FILE_NAME = "annotations.json", ANNOTATION_BODY_MAX_BYTES, ANNOTATION_TITLE_MAX_CHARS = 300, MAX_ANNOTATIONS_JSON_BYTES = 5000000, annotationsStore;
|
|
488
558
|
var init_annotations = __esm(() => {
|
|
559
|
+
init_json_store();
|
|
489
560
|
ANNOTATION_BODY_MAX_BYTES = 64 * 1024;
|
|
561
|
+
annotationsStore = createJsonFileStore({
|
|
562
|
+
filePath: annotationsFilePath,
|
|
563
|
+
empty: emptyAnnotationsState,
|
|
564
|
+
sanitize: normalizeAnnotationsState,
|
|
565
|
+
maxBytes: MAX_ANNOTATIONS_JSON_BYTES,
|
|
566
|
+
backupSuffix: "corrupt",
|
|
567
|
+
sizeErrorMessage: "annotations state too large"
|
|
568
|
+
});
|
|
490
569
|
});
|
|
491
570
|
|
|
492
571
|
// web-src/server/runtime.ts
|
|
@@ -673,10 +752,10 @@ var init_runtime = () => {};
|
|
|
673
752
|
|
|
674
753
|
// web-src/server/git.ts
|
|
675
754
|
import {
|
|
676
|
-
existsSync
|
|
755
|
+
existsSync,
|
|
677
756
|
lstatSync,
|
|
678
757
|
readdirSync,
|
|
679
|
-
readFileSync
|
|
758
|
+
readFileSync,
|
|
680
759
|
statSync
|
|
681
760
|
} from "node:fs";
|
|
682
761
|
import { join as join2 } from "node:path";
|
|
@@ -1120,7 +1199,7 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
|
|
|
1120
1199
|
return omitDirNames.has(name) ? "heavy" : undefined;
|
|
1121
1200
|
}
|
|
1122
1201
|
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
|
|
1123
|
-
if (excludeNames.has(name.toLowerCase())
|
|
1202
|
+
if (excludeNames.has(name.toLowerCase()))
|
|
1124
1203
|
return {
|
|
1125
1204
|
name,
|
|
1126
1205
|
path: "",
|
|
@@ -1182,7 +1261,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
1182
1261
|
return;
|
|
1183
1262
|
}
|
|
1184
1263
|
for (const entry of entries) {
|
|
1185
|
-
if (excludeNameSet.has(entry.name.toLowerCase())
|
|
1264
|
+
if (excludeNameSet.has(entry.name.toLowerCase()))
|
|
1186
1265
|
continue;
|
|
1187
1266
|
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1188
1267
|
const full = join2(dir, entry.name);
|
|
@@ -1287,12 +1366,12 @@ function untrackedMeta(cwd) {
|
|
|
1287
1366
|
let lines = 0;
|
|
1288
1367
|
let fileExists = false;
|
|
1289
1368
|
try {
|
|
1290
|
-
fileExists =
|
|
1369
|
+
fileExists = existsSync(full) && statSync(full).isFile();
|
|
1291
1370
|
} catch {
|
|
1292
1371
|
fileExists = false;
|
|
1293
1372
|
}
|
|
1294
1373
|
if (fileExists) {
|
|
1295
|
-
const data =
|
|
1374
|
+
const data = readFileSync(full);
|
|
1296
1375
|
const probe = data.subarray(0, 8192);
|
|
1297
1376
|
binary = probe.includes(0);
|
|
1298
1377
|
if (!binary)
|
|
@@ -1458,6 +1537,9 @@ var init_git = __esm(() => {
|
|
|
1458
1537
|
"vendor",
|
|
1459
1538
|
".cache",
|
|
1460
1539
|
"coverage",
|
|
1540
|
+
"tmp",
|
|
1541
|
+
"log",
|
|
1542
|
+
"storage",
|
|
1461
1543
|
"DerivedData",
|
|
1462
1544
|
"Pods",
|
|
1463
1545
|
"bin",
|
|
@@ -1468,11 +1550,11 @@ var init_git = __esm(() => {
|
|
|
1468
1550
|
// web-src/server/server-registry.ts
|
|
1469
1551
|
import { createHash } from "node:crypto";
|
|
1470
1552
|
import {
|
|
1471
|
-
existsSync as
|
|
1472
|
-
mkdirSync
|
|
1473
|
-
readFileSync as
|
|
1553
|
+
existsSync as existsSync2,
|
|
1554
|
+
mkdirSync,
|
|
1555
|
+
readFileSync as readFileSync2,
|
|
1474
1556
|
unlinkSync,
|
|
1475
|
-
writeFileSync
|
|
1557
|
+
writeFileSync
|
|
1476
1558
|
} from "node:fs";
|
|
1477
1559
|
import { homedir } from "node:os";
|
|
1478
1560
|
import { join as join3 } from "node:path";
|
|
@@ -1485,17 +1567,17 @@ function serverRegistryFilePath(root) {
|
|
|
1485
1567
|
}
|
|
1486
1568
|
function writeServerRegistry(entry) {
|
|
1487
1569
|
try {
|
|
1488
|
-
|
|
1489
|
-
|
|
1570
|
+
mkdirSync(registryDir(), { recursive: true });
|
|
1571
|
+
writeFileSync(serverRegistryFilePath(entry.root), `${JSON.stringify(entry, null, 2)}
|
|
1490
1572
|
`, "utf8");
|
|
1491
1573
|
} catch {}
|
|
1492
1574
|
}
|
|
1493
1575
|
function readServerRegistry(root) {
|
|
1494
1576
|
const file = serverRegistryFilePath(root);
|
|
1495
|
-
if (!
|
|
1577
|
+
if (!existsSync2(file))
|
|
1496
1578
|
return null;
|
|
1497
1579
|
try {
|
|
1498
|
-
const raw = JSON.parse(
|
|
1580
|
+
const raw = JSON.parse(readFileSync2(file, "utf8"));
|
|
1499
1581
|
if (!raw || typeof raw !== "object")
|
|
1500
1582
|
return null;
|
|
1501
1583
|
const entry = raw;
|
|
@@ -1529,7 +1611,7 @@ __export(exports_annotate_cli, {
|
|
|
1529
1611
|
ANNOTATE_HELP: () => ANNOTATE_HELP,
|
|
1530
1612
|
ANNOTATE_AGENT_HELP: () => ANNOTATE_AGENT_HELP
|
|
1531
1613
|
});
|
|
1532
|
-
import { readFileSync as
|
|
1614
|
+
import { readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
1533
1615
|
function takeValue(argv, index, flag) {
|
|
1534
1616
|
const value = argv[index + 1];
|
|
1535
1617
|
if (value === undefined)
|
|
@@ -1958,7 +2040,7 @@ async function annotationBodyFromCommand(command) {
|
|
|
1958
2040
|
let body = command.body;
|
|
1959
2041
|
if (body === undefined && command.bodyFile !== undefined) {
|
|
1960
2042
|
try {
|
|
1961
|
-
body =
|
|
2043
|
+
body = readFileSync3(command.bodyFile, "utf8");
|
|
1962
2044
|
} catch {
|
|
1963
2045
|
console.error(`could not read --body-file: ${command.bodyFile}`);
|
|
1964
2046
|
process.exit(1);
|
|
@@ -2026,7 +2108,7 @@ async function runAnnotateCli(argv) {
|
|
|
2026
2108
|
let sql = command.sql;
|
|
2027
2109
|
if (sql === undefined && command.sqlFile !== undefined) {
|
|
2028
2110
|
try {
|
|
2029
|
-
sql =
|
|
2111
|
+
sql = readFileSync3(command.sqlFile, "utf8");
|
|
2030
2112
|
} catch {
|
|
2031
2113
|
console.error(`could not read --sql-file: ${command.sqlFile}`);
|
|
2032
2114
|
process.exit(1);
|
|
@@ -2095,7 +2177,7 @@ async function runAnnotateCli(argv) {
|
|
|
2095
2177
|
if (command.kind === "edit") {
|
|
2096
2178
|
let bodyText = command.body;
|
|
2097
2179
|
if (command.bodyFile !== undefined)
|
|
2098
|
-
bodyText =
|
|
2180
|
+
bodyText = readFileSync3(command.bodyFile, "utf8");
|
|
2099
2181
|
if (bodyText === undefined) {
|
|
2100
2182
|
const stdin = await readStdin();
|
|
2101
2183
|
if (stdin.trim())
|
|
@@ -2653,16 +2735,16 @@ var init_query_cli = __esm(() => {
|
|
|
2653
2735
|
});
|
|
2654
2736
|
|
|
2655
2737
|
// web-src/server/root.ts
|
|
2656
|
-
import { existsSync as
|
|
2657
|
-
import { dirname, join as join4, normalize } from "node:path";
|
|
2738
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
2739
|
+
import { dirname as dirname2, join as join4, normalize } from "node:path";
|
|
2658
2740
|
import { fileURLToPath } from "node:url";
|
|
2659
2741
|
function findRoot(start) {
|
|
2660
2742
|
let current = start;
|
|
2661
2743
|
for (let i = 0;i < 5; i++) {
|
|
2662
|
-
if (
|
|
2744
|
+
if (existsSync3(join4(current, "package.json")) && existsSync3(join4(current, "web"))) {
|
|
2663
2745
|
return normalize(current);
|
|
2664
2746
|
}
|
|
2665
|
-
const parent =
|
|
2747
|
+
const parent = dirname2(current);
|
|
2666
2748
|
if (parent === current)
|
|
2667
2749
|
break;
|
|
2668
2750
|
current = parent;
|
|
@@ -2671,7 +2753,7 @@ function findRoot(start) {
|
|
|
2671
2753
|
}
|
|
2672
2754
|
var ROOT;
|
|
2673
2755
|
var init_root = __esm(() => {
|
|
2674
|
-
ROOT = findRoot(
|
|
2756
|
+
ROOT = findRoot(dirname2(fileURLToPath(import.meta.url)));
|
|
2675
2757
|
});
|
|
2676
2758
|
|
|
2677
2759
|
// web-src/server/skill-cli.ts
|
|
@@ -2683,7 +2765,7 @@ __export(exports_skill_cli, {
|
|
|
2683
2765
|
SKILL_HELP: () => SKILL_HELP,
|
|
2684
2766
|
AGENT_SKILL_DIRS: () => AGENT_SKILL_DIRS
|
|
2685
2767
|
});
|
|
2686
|
-
import { cpSync, existsSync as
|
|
2768
|
+
import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2687
2769
|
import { homedir as homedir2 } from "node:os";
|
|
2688
2770
|
import { join as join5, resolve } from "node:path";
|
|
2689
2771
|
function parseAgentList(value) {
|
|
@@ -2740,7 +2822,7 @@ function parseSkillArgs(argv) {
|
|
|
2740
2822
|
return { ok: true, args: { kind: "install", agents, global, cwd } };
|
|
2741
2823
|
}
|
|
2742
2824
|
function installSkill(args, deps) {
|
|
2743
|
-
if (!
|
|
2825
|
+
if (!existsSync4(join5(deps.sourceDir, "SKILL.md"))) {
|
|
2744
2826
|
return {
|
|
2745
2827
|
ok: false,
|
|
2746
2828
|
error: `bundled skill not found at ${deps.sourceDir}`
|
|
@@ -2750,9 +2832,9 @@ function installSkill(args, deps) {
|
|
|
2750
2832
|
const results = [];
|
|
2751
2833
|
for (const agent of args.agents) {
|
|
2752
2834
|
const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", SKILL_NAME);
|
|
2753
|
-
const action =
|
|
2835
|
+
const action = existsSync4(target) ? "updated" : "installed";
|
|
2754
2836
|
try {
|
|
2755
|
-
|
|
2837
|
+
mkdirSync2(target, { recursive: true });
|
|
2756
2838
|
cpSync(deps.sourceDir, target, { recursive: true });
|
|
2757
2839
|
} catch (error) {
|
|
2758
2840
|
return { ok: false, error: String(error) };
|
|
@@ -3681,14 +3763,23 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3681
3763
|
const setTimer = options.setTimeoutFn || setTimeout;
|
|
3682
3764
|
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
3683
3765
|
const debounceMs = options.debounceMs ?? 250;
|
|
3766
|
+
const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
|
|
3684
3767
|
const watchers = new Map;
|
|
3685
3768
|
const signatures = new Map;
|
|
3686
3769
|
const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
|
|
3687
3770
|
const initialScanQueue = [];
|
|
3688
3771
|
let initialScanTimer = null;
|
|
3772
|
+
const pendingPathInspections = new Map;
|
|
3773
|
+
let pathInspectionTimer = null;
|
|
3689
3774
|
let timer = null;
|
|
3690
3775
|
const pendingChangedPaths = new Set;
|
|
3776
|
+
let watchLimitReported = false;
|
|
3691
3777
|
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
3778
|
+
const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
|
|
3779
|
+
const ignoredDirectory = (dir) => {
|
|
3780
|
+
const rel = directoryRelativePath(dir);
|
|
3781
|
+
return Boolean(rel && ignored(rel));
|
|
3782
|
+
};
|
|
3692
3783
|
const scheduleUpdate = (changedPath) => {
|
|
3693
3784
|
if (changedPath)
|
|
3694
3785
|
pendingChangedPaths.add(changedPath);
|
|
@@ -3701,6 +3792,12 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3701
3792
|
options.onUpdate(paths);
|
|
3702
3793
|
}, debounceMs);
|
|
3703
3794
|
};
|
|
3795
|
+
const reportWatchLimit = () => {
|
|
3796
|
+
if (watchLimitReported)
|
|
3797
|
+
return;
|
|
3798
|
+
watchLimitReported = true;
|
|
3799
|
+
options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
|
|
3800
|
+
};
|
|
3704
3801
|
const closeSubtree = (dir) => {
|
|
3705
3802
|
for (const [watchedDir, watcher] of [...watchers]) {
|
|
3706
3803
|
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
@@ -3717,7 +3814,12 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3717
3814
|
clearTimer(initialScanTimer);
|
|
3718
3815
|
initialScanTimer = null;
|
|
3719
3816
|
}
|
|
3817
|
+
if (pathInspectionTimer) {
|
|
3818
|
+
clearTimer(pathInspectionTimer);
|
|
3819
|
+
pathInspectionTimer = null;
|
|
3820
|
+
}
|
|
3720
3821
|
initialScanQueue.length = 0;
|
|
3822
|
+
pendingPathInspections.clear();
|
|
3721
3823
|
for (const watcher of [...watchers.values()]) {
|
|
3722
3824
|
try {
|
|
3723
3825
|
watcher.close?.();
|
|
@@ -3738,27 +3840,82 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3738
3840
|
for (const entry of entries) {
|
|
3739
3841
|
if (!entry.isDirectory())
|
|
3740
3842
|
continue;
|
|
3741
|
-
|
|
3843
|
+
const child = join7(dir, entry.name);
|
|
3844
|
+
if (ignoredDirectory(child))
|
|
3845
|
+
continue;
|
|
3846
|
+
children.push(child);
|
|
3742
3847
|
}
|
|
3743
3848
|
return children;
|
|
3744
3849
|
};
|
|
3745
3850
|
const processInitialScanQueue = () => {
|
|
3746
3851
|
initialScanTimer = null;
|
|
3852
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3853
|
+
reportWatchLimit();
|
|
3854
|
+
initialScanQueue.length = 0;
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3747
3857
|
const next = initialScanQueue.shift();
|
|
3748
3858
|
if (next)
|
|
3749
3859
|
watchDirectory(next, true);
|
|
3860
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3861
|
+
reportWatchLimit();
|
|
3862
|
+
initialScanQueue.length = 0;
|
|
3863
|
+
}
|
|
3750
3864
|
if (initialScanQueue.length)
|
|
3751
3865
|
initialScanTimer = setTimer(processInitialScanQueue, 50);
|
|
3752
3866
|
};
|
|
3753
3867
|
const queueInitialChildren = (dir) => {
|
|
3754
|
-
|
|
3868
|
+
const remaining = maxWatchedDirectories - watchers.size;
|
|
3869
|
+
if (remaining <= 0) {
|
|
3870
|
+
reportWatchLimit();
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
const children = readChildDirectories(dir);
|
|
3874
|
+
if (children.length > remaining)
|
|
3875
|
+
reportWatchLimit();
|
|
3876
|
+
initialScanQueue.push(...children.slice(0, remaining));
|
|
3755
3877
|
if (!initialScanTimer)
|
|
3756
3878
|
initialScanTimer = setTimer(processInitialScanQueue, 5000);
|
|
3757
3879
|
};
|
|
3880
|
+
const processChangedPath = (changed, fullChangedPath) => {
|
|
3881
|
+
const known = watchers.has(fullChangedPath);
|
|
3882
|
+
if (isDirectory(fullChangedPath)) {
|
|
3883
|
+
if (known) {
|
|
3884
|
+
const signature = directorySignature(fullChangedPath);
|
|
3885
|
+
if (signature && signature !== signatures.get(fullChangedPath)) {
|
|
3886
|
+
closeSubtree(fullChangedPath);
|
|
3887
|
+
watchDirectory(fullChangedPath, initialScanAsync);
|
|
3888
|
+
}
|
|
3889
|
+
scheduleUpdate(changed);
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
watchDirectory(fullChangedPath, initialScanAsync);
|
|
3893
|
+
} else if (known) {
|
|
3894
|
+
closeSubtree(fullChangedPath);
|
|
3895
|
+
}
|
|
3896
|
+
scheduleUpdate(changed);
|
|
3897
|
+
};
|
|
3898
|
+
const processPathInspections = () => {
|
|
3899
|
+
pathInspectionTimer = null;
|
|
3900
|
+
const entries = [...pendingPathInspections];
|
|
3901
|
+
pendingPathInspections.clear();
|
|
3902
|
+
for (const [changed, fullChangedPath] of entries) {
|
|
3903
|
+
processChangedPath(changed, fullChangedPath);
|
|
3904
|
+
}
|
|
3905
|
+
};
|
|
3906
|
+
const queuePathInspection = (changed, fullChangedPath) => {
|
|
3907
|
+
pendingPathInspections.set(changed, fullChangedPath);
|
|
3908
|
+
if (!pathInspectionTimer)
|
|
3909
|
+
pathInspectionTimer = setTimer(processPathInspections, 25);
|
|
3910
|
+
};
|
|
3758
3911
|
const watchDirectory = (dir, initialScan = false) => {
|
|
3759
3912
|
if (watchers.has(dir))
|
|
3760
3913
|
return;
|
|
3761
|
-
|
|
3914
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3915
|
+
reportWatchLimit();
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
const rel = directoryRelativePath(dir);
|
|
3762
3919
|
if (rel && ignored(rel))
|
|
3763
3920
|
return;
|
|
3764
3921
|
try {
|
|
@@ -3773,22 +3930,11 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3773
3930
|
const fullChangedPath = join7(options.root, changed);
|
|
3774
3931
|
if (!isInsideRoot(options.root, fullChangedPath))
|
|
3775
3932
|
return;
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
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);
|
|
3933
|
+
if (initialScanAsync) {
|
|
3934
|
+
queuePathInspection(changed, fullChangedPath);
|
|
3935
|
+
return;
|
|
3790
3936
|
}
|
|
3791
|
-
|
|
3937
|
+
processChangedPath(changed, fullChangedPath);
|
|
3792
3938
|
}) || {};
|
|
3793
3939
|
watchers.set(dir, watcher);
|
|
3794
3940
|
const signature = directorySignature(dir);
|
|
@@ -3814,12 +3960,17 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3814
3960
|
queueInitialChildren(dir);
|
|
3815
3961
|
return;
|
|
3816
3962
|
}
|
|
3963
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3964
|
+
reportWatchLimit();
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3817
3967
|
for (const child of readChildDirectories(dir))
|
|
3818
3968
|
watchDirectory(child);
|
|
3819
3969
|
};
|
|
3820
3970
|
watchDirectory(options.root, true);
|
|
3821
3971
|
return { started: watchers.size > 0, close: closeAll };
|
|
3822
3972
|
}
|
|
3973
|
+
var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
|
|
3823
3974
|
var init_worktree_watcher = __esm(() => {
|
|
3824
3975
|
init_search();
|
|
3825
3976
|
});
|
|
@@ -3851,6 +4002,366 @@ function makeId(prefix) {
|
|
|
3851
4002
|
return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
3852
4003
|
}
|
|
3853
4004
|
|
|
4005
|
+
// web-src/server/state-store.ts
|
|
4006
|
+
import { join as join8 } from "node:path";
|
|
4007
|
+
function codeViewerPath(root, fileName) {
|
|
4008
|
+
return join8(root, CODE_VIEWER_DIR2, fileName);
|
|
4009
|
+
}
|
|
4010
|
+
function isRecord(value) {
|
|
4011
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4012
|
+
}
|
|
4013
|
+
function optionalString(value, maxLen) {
|
|
4014
|
+
if (typeof value !== "string")
|
|
4015
|
+
return;
|
|
4016
|
+
if (value.length === 0 || value.length > maxLen)
|
|
4017
|
+
return;
|
|
4018
|
+
if (value.includes("\x00"))
|
|
4019
|
+
return;
|
|
4020
|
+
return value;
|
|
4021
|
+
}
|
|
4022
|
+
function optionalBoolean(value) {
|
|
4023
|
+
return typeof value === "boolean" ? value : undefined;
|
|
4024
|
+
}
|
|
4025
|
+
function optionalNumber(value, min, max) {
|
|
4026
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
4027
|
+
return;
|
|
4028
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
4029
|
+
}
|
|
4030
|
+
function optionalFloat(value, min, max) {
|
|
4031
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
4032
|
+
return;
|
|
4033
|
+
return Math.max(min, Math.min(max, value));
|
|
4034
|
+
}
|
|
4035
|
+
function optionalFontSize(value) {
|
|
4036
|
+
return value === "compact" || value === "regular" || value === "large" || value === "xlarge" ? value : undefined;
|
|
4037
|
+
}
|
|
4038
|
+
function normalizeStringList(value, options) {
|
|
4039
|
+
if (!Array.isArray(value))
|
|
4040
|
+
return;
|
|
4041
|
+
const out = [];
|
|
4042
|
+
const seen = new Set;
|
|
4043
|
+
const items = options.keepLast ? [...value].reverse() : value;
|
|
4044
|
+
for (const item of items) {
|
|
4045
|
+
if (out.length >= options.maxItems)
|
|
4046
|
+
break;
|
|
4047
|
+
if (typeof item !== "string")
|
|
4048
|
+
continue;
|
|
4049
|
+
const name = item.trim();
|
|
4050
|
+
if (!name || name.length > options.maxLen || name.includes("\x00"))
|
|
4051
|
+
continue;
|
|
4052
|
+
if (options.pathSafe && (name.includes("/") || name.includes("\\") || name === "." || name === ".." || name === ".git")) {
|
|
4053
|
+
continue;
|
|
4054
|
+
}
|
|
4055
|
+
if (seen.has(name))
|
|
4056
|
+
continue;
|
|
4057
|
+
seen.add(name);
|
|
4058
|
+
out.push(name);
|
|
4059
|
+
}
|
|
4060
|
+
if (options.keepLast)
|
|
4061
|
+
out.reverse();
|
|
4062
|
+
return options.sort === false ? out : out.sort((a, b) => a.localeCompare(b));
|
|
4063
|
+
}
|
|
4064
|
+
function emptySettings() {
|
|
4065
|
+
return { version: 1 };
|
|
4066
|
+
}
|
|
4067
|
+
function emptyViewState() {
|
|
4068
|
+
return { version: 1, collapsedDirs: [], viewedFiles: [] };
|
|
4069
|
+
}
|
|
4070
|
+
function emptyDbUiState() {
|
|
4071
|
+
return { version: 1, columnWidths: {} };
|
|
4072
|
+
}
|
|
4073
|
+
function sanitizeSettings(raw) {
|
|
4074
|
+
if (!isRecord(raw))
|
|
4075
|
+
return emptySettings();
|
|
4076
|
+
const out = { version: 1 };
|
|
4077
|
+
if (raw.layout === "side-by-side" || raw.layout === "line-by-line")
|
|
4078
|
+
out.layout = raw.layout;
|
|
4079
|
+
if (raw.theme === "light" || raw.theme === "dark")
|
|
4080
|
+
out.theme = raw.theme;
|
|
4081
|
+
if (raw.language === "en" || raw.language === "ja")
|
|
4082
|
+
out.language = raw.language;
|
|
4083
|
+
if (raw.sidebarView === "tree" || raw.sidebarView === "flat")
|
|
4084
|
+
out.sidebarView = raw.sidebarView;
|
|
4085
|
+
const sidebarWidth = optionalNumber(raw.sidebarWidth, 180, 900);
|
|
4086
|
+
if (sidebarWidth !== undefined)
|
|
4087
|
+
out.sidebarWidth = sidebarWidth;
|
|
4088
|
+
const historyWidth = optionalNumber(raw.historyWidth, 220, 640);
|
|
4089
|
+
if (historyWidth !== undefined)
|
|
4090
|
+
out.historyWidth = historyWidth;
|
|
4091
|
+
const sidebarHidden = optionalBoolean(raw.sidebarHidden);
|
|
4092
|
+
if (sidebarHidden !== undefined)
|
|
4093
|
+
out.sidebarHidden = sidebarHidden;
|
|
4094
|
+
const sidebarFontSize = optionalFontSize(raw.sidebarFontSize);
|
|
4095
|
+
if (sidebarFontSize)
|
|
4096
|
+
out.sidebarFontSize = sidebarFontSize;
|
|
4097
|
+
const codeFontSize = optionalFontSize(raw.codeFontSize);
|
|
4098
|
+
if (codeFontSize)
|
|
4099
|
+
out.codeFontSize = codeFontSize;
|
|
4100
|
+
const syntaxHighlight = optionalBoolean(raw.syntaxHighlight);
|
|
4101
|
+
if (syntaxHighlight !== undefined)
|
|
4102
|
+
out.syntaxHighlight = syntaxHighlight;
|
|
4103
|
+
const autoUpdate = optionalBoolean(raw.autoUpdate);
|
|
4104
|
+
if (autoUpdate !== undefined)
|
|
4105
|
+
out.autoUpdate = autoUpdate;
|
|
4106
|
+
const queryHistoryPanelWidth = optionalNumber(raw.queryHistoryPanelWidth, 280, 800);
|
|
4107
|
+
if (queryHistoryPanelWidth !== undefined)
|
|
4108
|
+
out.queryHistoryPanelWidth = queryHistoryPanelWidth;
|
|
4109
|
+
const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
|
|
4110
|
+
if (annotationPanelOpen !== undefined)
|
|
4111
|
+
out.annotationPanelOpen = annotationPanelOpen;
|
|
4112
|
+
const annotationFollow = optionalBoolean(raw.annotationFollow);
|
|
4113
|
+
if (annotationFollow !== undefined)
|
|
4114
|
+
out.annotationFollow = annotationFollow;
|
|
4115
|
+
const annotationMuted = optionalBoolean(raw.annotationMuted);
|
|
4116
|
+
if (annotationMuted !== undefined)
|
|
4117
|
+
out.annotationMuted = annotationMuted;
|
|
4118
|
+
const annotationRate = optionalFloat(raw.annotationRate, 0.5, 2);
|
|
4119
|
+
if (annotationRate !== undefined)
|
|
4120
|
+
out.annotationRate = annotationRate;
|
|
4121
|
+
const ignoreWhitespace = optionalBoolean(raw.ignoreWhitespace);
|
|
4122
|
+
if (ignoreWhitespace !== undefined)
|
|
4123
|
+
out.ignoreWhitespace = ignoreWhitespace;
|
|
4124
|
+
const hideTests = optionalBoolean(raw.hideTests);
|
|
4125
|
+
if (hideTests !== undefined)
|
|
4126
|
+
out.hideTests = hideTests;
|
|
4127
|
+
const scopeOmitDirs = normalizeStringList(raw.scopeOmitDirs, {
|
|
4128
|
+
maxItems: 100,
|
|
4129
|
+
maxLen: 64,
|
|
4130
|
+
pathSafe: true
|
|
4131
|
+
});
|
|
4132
|
+
if (scopeOmitDirs)
|
|
4133
|
+
out.scopeOmitDirs = scopeOmitDirs;
|
|
4134
|
+
const scopeExcludeNames = normalizeStringList(raw.scopeExcludeNames, {
|
|
4135
|
+
maxItems: 200,
|
|
4136
|
+
maxLen: 128,
|
|
4137
|
+
pathSafe: true
|
|
4138
|
+
});
|
|
4139
|
+
if (scopeExcludeNames)
|
|
4140
|
+
out.scopeExcludeNames = scopeExcludeNames;
|
|
4141
|
+
if (isRecord(raw.range)) {
|
|
4142
|
+
const from = optionalString(raw.range.from, MAX_REF_LEN);
|
|
4143
|
+
const to = optionalString(raw.range.to, MAX_REF_LEN);
|
|
4144
|
+
if (from && to)
|
|
4145
|
+
out.range = { from, to };
|
|
4146
|
+
}
|
|
4147
|
+
return out;
|
|
4148
|
+
}
|
|
4149
|
+
function mergeSettings(current, patch) {
|
|
4150
|
+
if (!isRecord(patch))
|
|
4151
|
+
return current;
|
|
4152
|
+
const raw = { ...current };
|
|
4153
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
4154
|
+
if (key === "version")
|
|
4155
|
+
continue;
|
|
4156
|
+
if (value === null)
|
|
4157
|
+
delete raw[key];
|
|
4158
|
+
else
|
|
4159
|
+
raw[key] = value;
|
|
4160
|
+
}
|
|
4161
|
+
return sanitizeSettings({ ...raw, version: 1 });
|
|
4162
|
+
}
|
|
4163
|
+
function sanitizeViewState(raw) {
|
|
4164
|
+
if (!isRecord(raw))
|
|
4165
|
+
return emptyViewState();
|
|
4166
|
+
return {
|
|
4167
|
+
version: 1,
|
|
4168
|
+
collapsedDirs: normalizeStringList(raw.collapsedDirs, {
|
|
4169
|
+
maxItems: MAX_VIEW_ITEMS,
|
|
4170
|
+
maxLen: MAX_KEY_LEN,
|
|
4171
|
+
keepLast: true,
|
|
4172
|
+
sort: false
|
|
4173
|
+
}) ?? [],
|
|
4174
|
+
viewedFiles: normalizeStringList(raw.viewedFiles, {
|
|
4175
|
+
maxItems: MAX_VIEW_ITEMS,
|
|
4176
|
+
maxLen: MAX_KEY_LEN,
|
|
4177
|
+
keepLast: true,
|
|
4178
|
+
sort: false
|
|
4179
|
+
}) ?? []
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
function mergeViewState(current, patch) {
|
|
4183
|
+
if (!isRecord(patch))
|
|
4184
|
+
return current;
|
|
4185
|
+
const base = sanitizeViewState({ ...current, version: 1 });
|
|
4186
|
+
const collapsedDirs = new Set(base.collapsedDirs);
|
|
4187
|
+
const viewedFiles = new Set(base.viewedFiles);
|
|
4188
|
+
const addedCollapsedDirs = normalizeStringList(patch.addedCollapsedDirs, {
|
|
4189
|
+
maxItems: MAX_VIEW_ITEMS,
|
|
4190
|
+
maxLen: MAX_KEY_LEN,
|
|
4191
|
+
keepLast: true,
|
|
4192
|
+
sort: false
|
|
4193
|
+
});
|
|
4194
|
+
for (const path of addedCollapsedDirs || [])
|
|
4195
|
+
collapsedDirs.add(path);
|
|
4196
|
+
const removedCollapsedDirs = normalizeStringList(patch.removedCollapsedDirs, {
|
|
4197
|
+
maxItems: MAX_VIEW_ITEMS,
|
|
4198
|
+
maxLen: MAX_KEY_LEN,
|
|
4199
|
+
sort: false
|
|
4200
|
+
});
|
|
4201
|
+
for (const path of removedCollapsedDirs || [])
|
|
4202
|
+
collapsedDirs.delete(path);
|
|
4203
|
+
const addedViewedFiles = normalizeStringList(patch.addedViewedFiles, {
|
|
4204
|
+
maxItems: MAX_VIEW_ITEMS,
|
|
4205
|
+
maxLen: MAX_KEY_LEN,
|
|
4206
|
+
keepLast: true,
|
|
4207
|
+
sort: false
|
|
4208
|
+
});
|
|
4209
|
+
for (const path of addedViewedFiles || [])
|
|
4210
|
+
viewedFiles.add(path);
|
|
4211
|
+
const removedViewedFiles = normalizeStringList(patch.removedViewedFiles, {
|
|
4212
|
+
maxItems: MAX_VIEW_ITEMS,
|
|
4213
|
+
maxLen: MAX_KEY_LEN,
|
|
4214
|
+
sort: false
|
|
4215
|
+
});
|
|
4216
|
+
for (const path of removedViewedFiles || [])
|
|
4217
|
+
viewedFiles.delete(path);
|
|
4218
|
+
return sanitizeViewState({
|
|
4219
|
+
version: 1,
|
|
4220
|
+
collapsedDirs: [...collapsedDirs],
|
|
4221
|
+
viewedFiles: [...viewedFiles]
|
|
4222
|
+
});
|
|
4223
|
+
}
|
|
4224
|
+
function safeObjectKey(value) {
|
|
4225
|
+
if (!value || value.length > MAX_KEY_LEN || value.includes("\x00"))
|
|
4226
|
+
return null;
|
|
4227
|
+
return value;
|
|
4228
|
+
}
|
|
4229
|
+
function sanitizeDbUiState(raw) {
|
|
4230
|
+
if (!isRecord(raw) || !isRecord(raw.columnWidths))
|
|
4231
|
+
return emptyDbUiState();
|
|
4232
|
+
const columnWidths = {};
|
|
4233
|
+
let dbCount = 0;
|
|
4234
|
+
for (const [dbIdRaw, tablesRaw] of Object.entries(raw.columnWidths)) {
|
|
4235
|
+
if (dbCount >= MAX_DB_UI_DBS)
|
|
4236
|
+
break;
|
|
4237
|
+
const dbId = safeObjectKey(dbIdRaw);
|
|
4238
|
+
if (!dbId || !isRecord(tablesRaw))
|
|
4239
|
+
continue;
|
|
4240
|
+
const tables = {};
|
|
4241
|
+
let tableCount = 0;
|
|
4242
|
+
for (const [tableRaw, columnsRaw] of Object.entries(tablesRaw)) {
|
|
4243
|
+
if (tableCount >= MAX_DB_UI_TABLES)
|
|
4244
|
+
break;
|
|
4245
|
+
const table = safeObjectKey(tableRaw);
|
|
4246
|
+
if (!table || !isRecord(columnsRaw))
|
|
4247
|
+
continue;
|
|
4248
|
+
const columns = {};
|
|
4249
|
+
let columnCount = 0;
|
|
4250
|
+
for (const [columnRaw, widthRaw] of Object.entries(columnsRaw)) {
|
|
4251
|
+
if (columnCount >= MAX_DB_UI_COLUMNS)
|
|
4252
|
+
break;
|
|
4253
|
+
const column = safeObjectKey(columnRaw);
|
|
4254
|
+
const width = optionalNumber(widthRaw, 60, 1200);
|
|
4255
|
+
if (!column || width === undefined)
|
|
4256
|
+
continue;
|
|
4257
|
+
columns[column] = width;
|
|
4258
|
+
columnCount++;
|
|
4259
|
+
}
|
|
4260
|
+
if (Object.keys(columns).length === 0)
|
|
4261
|
+
continue;
|
|
4262
|
+
tables[table] = columns;
|
|
4263
|
+
tableCount++;
|
|
4264
|
+
}
|
|
4265
|
+
if (Object.keys(tables).length === 0)
|
|
4266
|
+
continue;
|
|
4267
|
+
columnWidths[dbId] = tables;
|
|
4268
|
+
dbCount++;
|
|
4269
|
+
}
|
|
4270
|
+
return { version: 1, columnWidths };
|
|
4271
|
+
}
|
|
4272
|
+
function mergeDbUiState(current, patch) {
|
|
4273
|
+
if (!isRecord(patch))
|
|
4274
|
+
return current;
|
|
4275
|
+
if (!isRecord(patch.columnWidths)) {
|
|
4276
|
+
return sanitizeDbUiState({ ...current, ...patch, version: 1 });
|
|
4277
|
+
}
|
|
4278
|
+
const columnWidths = {
|
|
4279
|
+
...current.columnWidths
|
|
4280
|
+
};
|
|
4281
|
+
for (const [dbId, tablesRaw] of Object.entries(patch.columnWidths)) {
|
|
4282
|
+
if (tablesRaw === null) {
|
|
4283
|
+
delete columnWidths[dbId];
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
if (!isRecord(tablesRaw))
|
|
4287
|
+
continue;
|
|
4288
|
+
const tables = { ...columnWidths[dbId] || {} };
|
|
4289
|
+
for (const [table, columnsRaw] of Object.entries(tablesRaw)) {
|
|
4290
|
+
if (columnsRaw === null) {
|
|
4291
|
+
delete tables[table];
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
if (!isRecord(columnsRaw))
|
|
4295
|
+
continue;
|
|
4296
|
+
const columns = { ...tables[table] || {} };
|
|
4297
|
+
for (const [column, widthRaw] of Object.entries(columnsRaw)) {
|
|
4298
|
+
if (widthRaw === null)
|
|
4299
|
+
delete columns[column];
|
|
4300
|
+
else
|
|
4301
|
+
columns[column] = widthRaw;
|
|
4302
|
+
}
|
|
4303
|
+
tables[table] = columns;
|
|
4304
|
+
}
|
|
4305
|
+
columnWidths[dbId] = tables;
|
|
4306
|
+
}
|
|
4307
|
+
return sanitizeDbUiState({ ...current, ...patch, columnWidths, version: 1 });
|
|
4308
|
+
}
|
|
4309
|
+
async function loadAppSettingsState(root) {
|
|
4310
|
+
return settingsStore.load(root);
|
|
4311
|
+
}
|
|
4312
|
+
async function patchAppSettingsState(root, patch) {
|
|
4313
|
+
return settingsStore.update(root, (state) => {
|
|
4314
|
+
const next = mergeSettings(state, patch);
|
|
4315
|
+
return { state: next, result: next };
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
async function loadViewState(root) {
|
|
4319
|
+
return viewStateStore.load(root);
|
|
4320
|
+
}
|
|
4321
|
+
async function patchViewState(root, patch) {
|
|
4322
|
+
return viewStateStore.update(root, (state) => {
|
|
4323
|
+
const next = mergeViewState(state, patch);
|
|
4324
|
+
return { state: next, result: next };
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
async function loadDbUiState(root) {
|
|
4328
|
+
return dbUiStore.load(root);
|
|
4329
|
+
}
|
|
4330
|
+
async function patchDbUiState(root, patch) {
|
|
4331
|
+
return dbUiStore.update(root, (state) => {
|
|
4332
|
+
const next = mergeDbUiState(state, patch);
|
|
4333
|
+
return { state: next, result: next };
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, settingsStore, viewStateStore, dbUiStore;
|
|
4337
|
+
var init_state_store = __esm(() => {
|
|
4338
|
+
init_json_store();
|
|
4339
|
+
settingsStore = createJsonFileStore({
|
|
4340
|
+
filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
|
|
4341
|
+
empty: emptySettings,
|
|
4342
|
+
sanitize: sanitizeSettings,
|
|
4343
|
+
maxBytes: MAX_SETTINGS_BYTES,
|
|
4344
|
+
backupSuffix: "corrupt",
|
|
4345
|
+
sizeErrorMessage: "settings state too large"
|
|
4346
|
+
});
|
|
4347
|
+
viewStateStore = createJsonFileStore({
|
|
4348
|
+
filePath: (root) => codeViewerPath(root, VIEW_STATE_FILE_NAME),
|
|
4349
|
+
empty: emptyViewState,
|
|
4350
|
+
sanitize: sanitizeViewState,
|
|
4351
|
+
maxBytes: MAX_VIEW_STATE_BYTES,
|
|
4352
|
+
backupSuffix: "corrupt",
|
|
4353
|
+
sizeErrorMessage: "view state too large"
|
|
4354
|
+
});
|
|
4355
|
+
dbUiStore = createJsonFileStore({
|
|
4356
|
+
filePath: (root) => codeViewerPath(root, DB_UI_FILE_NAME),
|
|
4357
|
+
empty: emptyDbUiState,
|
|
4358
|
+
sanitize: sanitizeDbUiState,
|
|
4359
|
+
maxBytes: MAX_DB_UI_BYTES,
|
|
4360
|
+
backupSuffix: "corrupt",
|
|
4361
|
+
sizeErrorMessage: "db UI state too large"
|
|
4362
|
+
});
|
|
4363
|
+
});
|
|
4364
|
+
|
|
3854
4365
|
// web-src/server/database/adapters/abort.ts
|
|
3855
4366
|
function abortError(message = "operation aborted") {
|
|
3856
4367
|
const err = new Error(message);
|
|
@@ -5554,14 +6065,14 @@ var init_connection_pool = __esm(() => {
|
|
|
5554
6065
|
// web-src/server/database/discovery.ts
|
|
5555
6066
|
import {
|
|
5556
6067
|
closeSync,
|
|
5557
|
-
existsSync as
|
|
6068
|
+
existsSync as existsSync5,
|
|
5558
6069
|
openSync,
|
|
5559
6070
|
readSync,
|
|
5560
6071
|
realpathSync as realpathSync3,
|
|
5561
6072
|
statSync as statSync2
|
|
5562
6073
|
} from "node:fs";
|
|
5563
|
-
import { lstat, open, readdir, readFile, stat } from "node:fs/promises";
|
|
5564
|
-
import { basename as basename2, join as
|
|
6074
|
+
import { lstat, open, readdir, readFile as readFile2, stat } from "node:fs/promises";
|
|
6075
|
+
import { basename as basename2, join as join9, relative as relative2 } from "node:path";
|
|
5565
6076
|
function isSqliteFile(fullPath) {
|
|
5566
6077
|
try {
|
|
5567
6078
|
const stat2 = statSync2(fullPath);
|
|
@@ -5632,7 +6143,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
|
|
|
5632
6143
|
return;
|
|
5633
6144
|
if (omitSet.has(entry.toLowerCase()))
|
|
5634
6145
|
continue;
|
|
5635
|
-
const full =
|
|
6146
|
+
const full = join9(dir, entry);
|
|
5636
6147
|
let entryStat;
|
|
5637
6148
|
try {
|
|
5638
6149
|
entryStat = await lstat(full);
|
|
@@ -5676,8 +6187,8 @@ function validateDbPath(cwd, dbPath) {
|
|
|
5676
6187
|
const parts = dbPath.split(/[\\/]+/);
|
|
5677
6188
|
if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
|
|
5678
6189
|
return null;
|
|
5679
|
-
const full =
|
|
5680
|
-
if (!
|
|
6190
|
+
const full = join9(cwd, dbPath);
|
|
6191
|
+
if (!existsSync5(full))
|
|
5681
6192
|
return null;
|
|
5682
6193
|
let realCwd;
|
|
5683
6194
|
let realFull;
|
|
@@ -5813,7 +6324,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
|
|
|
5813
6324
|
}
|
|
5814
6325
|
async function readDotenvAsync(composeDir) {
|
|
5815
6326
|
try {
|
|
5816
|
-
const content = await
|
|
6327
|
+
const content = await readFile2(join9(composeDir, ".env"), "utf-8");
|
|
5817
6328
|
return parseDotenvContent(content);
|
|
5818
6329
|
} catch {
|
|
5819
6330
|
return {};
|
|
@@ -5855,7 +6366,7 @@ function parseComposeEnv(serviceBlock, composeDirEnv = {}) {
|
|
|
5855
6366
|
}
|
|
5856
6367
|
return env;
|
|
5857
6368
|
}
|
|
5858
|
-
function parseComposePortMappings(serviceBlock) {
|
|
6369
|
+
function parseComposePortMappings(serviceBlock, composeDirEnv = {}) {
|
|
5859
6370
|
const portsMatch = serviceBlock.match(/^[ \t]+ports:\s*\n((?:[ \t]+- [^\n]+\n?)*)/m);
|
|
5860
6371
|
if (!portsMatch)
|
|
5861
6372
|
return [];
|
|
@@ -5865,26 +6376,45 @@ function parseComposePortMappings(serviceBlock) {
|
|
|
5865
6376
|
const trimmed = line.trim();
|
|
5866
6377
|
if (!trimmed.startsWith("-"))
|
|
5867
6378
|
continue;
|
|
5868
|
-
const value = trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim();
|
|
6379
|
+
const value = resolveEnvValue(trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim(), composeDirEnv);
|
|
5869
6380
|
if (!value || value.includes("target:"))
|
|
5870
6381
|
continue;
|
|
5871
6382
|
const parts = value.split(":");
|
|
5872
6383
|
const container = parts.pop()?.trim() || "";
|
|
5873
6384
|
const host = parts.pop()?.trim() || "";
|
|
5874
|
-
|
|
6385
|
+
const containerPorts = expandPortRange(container);
|
|
6386
|
+
const hostPorts = host ? expandPortRange(host) : [];
|
|
6387
|
+
if (!containerPorts)
|
|
5875
6388
|
continue;
|
|
5876
|
-
if (host &&
|
|
6389
|
+
if (host && (!hostPorts || hostPorts.length !== containerPorts.length)) {
|
|
5877
6390
|
continue;
|
|
5878
|
-
|
|
6391
|
+
}
|
|
6392
|
+
for (let i = 0;i < containerPorts.length; i++) {
|
|
6393
|
+
mappings.push({
|
|
6394
|
+
host: hostPorts ? (hostPorts[i] ?? "").toString() : "",
|
|
6395
|
+
container: containerPorts[i].toString()
|
|
6396
|
+
});
|
|
6397
|
+
}
|
|
5879
6398
|
}
|
|
5880
6399
|
return mappings;
|
|
5881
6400
|
}
|
|
5882
|
-
function
|
|
5883
|
-
const
|
|
6401
|
+
function expandPortRange(value) {
|
|
6402
|
+
const match = value.match(/^(\d+)(?:-(\d+))?$/);
|
|
6403
|
+
if (!match)
|
|
6404
|
+
return null;
|
|
6405
|
+
const start = Number(match[1]);
|
|
6406
|
+
const end = Number(match[2] || match[1]);
|
|
6407
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end > 65535 || end < start) {
|
|
6408
|
+
return null;
|
|
6409
|
+
}
|
|
6410
|
+
return Array.from({ length: end - start + 1 }, (_, idx) => start + idx);
|
|
6411
|
+
}
|
|
6412
|
+
function parseComposePorts(serviceBlock, composeDirEnv = {}) {
|
|
6413
|
+
const first = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.host);
|
|
5884
6414
|
return first?.host || null;
|
|
5885
6415
|
}
|
|
5886
|
-
function parseComposeHostPortForContainer(serviceBlock, containerPort) {
|
|
5887
|
-
const found = parseComposePortMappings(serviceBlock).find((m) => m.container === containerPort && m.host);
|
|
6416
|
+
function parseComposeHostPortForContainer(serviceBlock, containerPort, composeDirEnv = {}) {
|
|
6417
|
+
const found = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.container === containerPort && m.host);
|
|
5888
6418
|
return found?.host || null;
|
|
5889
6419
|
}
|
|
5890
6420
|
function parseComposeContainerPort(serviceBlock) {
|
|
@@ -5935,7 +6465,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
|
|
|
5935
6465
|
continue;
|
|
5936
6466
|
const defaultPort = defaultPortFor(kind, image, env);
|
|
5937
6467
|
const serviceContainerPort = kind === "s3" ? defaultPort : containerPort || defaultPort;
|
|
5938
|
-
const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort) || parseComposeHostPortForContainer(svcBlock, defaultPort) || parseComposePorts(svcBlock);
|
|
6468
|
+
const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort, composeDirEnv) || parseComposeHostPortForContainer(svcBlock, defaultPort, composeDirEnv) || parseComposePorts(svcBlock, composeDirEnv);
|
|
5939
6469
|
const hostPort = kind === "s3" ? publishedHostPort || undefined : publishedHostPort || defaultPort;
|
|
5940
6470
|
const imageLabel = image ?? `build:${kind}`;
|
|
5941
6471
|
const id = isRoot ? `docker:${svc.name}` : `docker:${svc.name}@${encodeURIComponent(relDirSlash)}`;
|
|
@@ -5968,7 +6498,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
|
|
|
5968
6498
|
async function parseComposeFileAsync(filepath, composeDir, cwd, results) {
|
|
5969
6499
|
let content;
|
|
5970
6500
|
try {
|
|
5971
|
-
content = await
|
|
6501
|
+
content = await readFile2(filepath, "utf-8");
|
|
5972
6502
|
} catch {
|
|
5973
6503
|
return;
|
|
5974
6504
|
}
|
|
@@ -6014,7 +6544,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
|
|
|
6014
6544
|
if (depth > MAX_SCAN_DEPTH)
|
|
6015
6545
|
return;
|
|
6016
6546
|
for (const filename of COMPOSE_FILENAMES) {
|
|
6017
|
-
const filepath =
|
|
6547
|
+
const filepath = join9(dir, filename);
|
|
6018
6548
|
if (await pathExistsAsync(filepath)) {
|
|
6019
6549
|
await parseComposeFileAsync(filepath, dir, cwd, results);
|
|
6020
6550
|
break;
|
|
@@ -6037,7 +6567,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
|
|
|
6037
6567
|
return;
|
|
6038
6568
|
if (omitSet.has(entry.toLowerCase()))
|
|
6039
6569
|
continue;
|
|
6040
|
-
const full =
|
|
6570
|
+
const full = join9(dir, entry);
|
|
6041
6571
|
let entryStat;
|
|
6042
6572
|
try {
|
|
6043
6573
|
entryStat = await lstat(full);
|
|
@@ -6672,6 +7202,67 @@ function textError(message, status) {
|
|
|
6672
7202
|
}
|
|
6673
7203
|
});
|
|
6674
7204
|
}
|
|
7205
|
+
async function jsonLoadResponse(load, logPrefix, errorMessage) {
|
|
7206
|
+
try {
|
|
7207
|
+
return json(await load());
|
|
7208
|
+
} catch (err) {
|
|
7209
|
+
console.error(`[code-viewer] ${logPrefix} error:`, err);
|
|
7210
|
+
return textError(errorMessage, 500);
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
async function parseBoundedJsonBody(req, maxBytes, tooLargeMessage) {
|
|
7214
|
+
const contentType = req.headers.get("content-type") || "";
|
|
7215
|
+
if (!contentType.toLowerCase().startsWith("application/json")) {
|
|
7216
|
+
return textError("unsupported media type", 415);
|
|
7217
|
+
}
|
|
7218
|
+
const contentLength = Number(req.headers.get("content-length") || "0");
|
|
7219
|
+
if (contentLength > maxBytes)
|
|
7220
|
+
return textError(tooLargeMessage, 413);
|
|
7221
|
+
try {
|
|
7222
|
+
const raw = await req.text();
|
|
7223
|
+
if (Buffer.byteLength(raw, "utf8") > maxBytes) {
|
|
7224
|
+
return textError(tooLargeMessage, 413);
|
|
7225
|
+
}
|
|
7226
|
+
return JSON.parse(raw);
|
|
7227
|
+
} catch {
|
|
7228
|
+
return textError("invalid JSON body", 400);
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
function waitForCallerAbort(promise, signal, message) {
|
|
7232
|
+
if (!signal)
|
|
7233
|
+
return promise;
|
|
7234
|
+
if (signal.aborted)
|
|
7235
|
+
return Promise.reject(abortError(message));
|
|
7236
|
+
return new Promise((resolve2, reject) => {
|
|
7237
|
+
let settled = false;
|
|
7238
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
7239
|
+
const onAbort = () => {
|
|
7240
|
+
if (settled)
|
|
7241
|
+
return;
|
|
7242
|
+
settled = true;
|
|
7243
|
+
cleanup();
|
|
7244
|
+
reject(abortError(message));
|
|
7245
|
+
};
|
|
7246
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7247
|
+
promise.then((value) => {
|
|
7248
|
+
if (settled)
|
|
7249
|
+
return;
|
|
7250
|
+
settled = true;
|
|
7251
|
+
cleanup();
|
|
7252
|
+
resolve2(value);
|
|
7253
|
+
}, (err) => {
|
|
7254
|
+
if (settled)
|
|
7255
|
+
return;
|
|
7256
|
+
settled = true;
|
|
7257
|
+
cleanup();
|
|
7258
|
+
reject(err);
|
|
7259
|
+
});
|
|
7260
|
+
});
|
|
7261
|
+
}
|
|
7262
|
+
function isFilesystemAccessError(err) {
|
|
7263
|
+
const code = err?.code;
|
|
7264
|
+
return code === "EACCES" || code === "EBUSY" || code === "EIO" || code === "EISDIR" || code === "ENOSPC" || code === "ENOTDIR" || code === "EPERM" || code === "EROFS";
|
|
7265
|
+
}
|
|
6675
7266
|
async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omitDirNames, signal) {
|
|
6676
7267
|
if (!dbParam)
|
|
6677
7268
|
return textError("missing db parameter", 400);
|
|
@@ -6681,10 +7272,26 @@ async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omi
|
|
|
6681
7272
|
const parsed = parseDockerDbId(dbParam);
|
|
6682
7273
|
if (!parsed)
|
|
6683
7274
|
return textError("invalid docker db id", 400);
|
|
6684
|
-
|
|
7275
|
+
let info;
|
|
7276
|
+
try {
|
|
7277
|
+
info = await findDockerServiceByDbIdAsync(cwd, dbParam, kind, omitDirNames, signal);
|
|
7278
|
+
} catch (err) {
|
|
7279
|
+
if (isAbortLikeError(err, signal)) {
|
|
7280
|
+
return textError(`${kind} lookup aborted`, 503);
|
|
7281
|
+
}
|
|
7282
|
+
throw err;
|
|
7283
|
+
}
|
|
6685
7284
|
if (!info)
|
|
6686
7285
|
return textError(`${kind} service not found`, 404);
|
|
6687
|
-
|
|
7286
|
+
let explorer;
|
|
7287
|
+
try {
|
|
7288
|
+
explorer = await waitForCallerAbort(cache.getOrOpenAsync(dbParam, () => openFn(info)), signal, `${kind} open aborted`);
|
|
7289
|
+
} catch (err) {
|
|
7290
|
+
if (isAbortLikeError(err, signal)) {
|
|
7291
|
+
return textError(`${kind} open aborted`, 503);
|
|
7292
|
+
}
|
|
7293
|
+
throw err;
|
|
7294
|
+
}
|
|
6688
7295
|
return { dbId: dbParam, explorer };
|
|
6689
7296
|
}
|
|
6690
7297
|
async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
|
|
@@ -6722,6 +7329,9 @@ function handleError(prefix, action, err) {
|
|
|
6722
7329
|
if (isDockerComposeServiceUnavailableError(err)) {
|
|
6723
7330
|
return textError(message, err.status);
|
|
6724
7331
|
}
|
|
7332
|
+
if (isFilesystemAccessError(err)) {
|
|
7333
|
+
return textError(`failed to ${action}`, 500);
|
|
7334
|
+
}
|
|
6725
7335
|
return textError(`failed to ${action}: ${message}`, 500);
|
|
6726
7336
|
}
|
|
6727
7337
|
var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS;
|
|
@@ -7591,6 +8201,31 @@ function s3ObjectName(key) {
|
|
|
7591
8201
|
// web-src/server/database/adapters/s3.ts
|
|
7592
8202
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
7593
8203
|
import { createHash as createHash4, createHmac } from "node:crypto";
|
|
8204
|
+
function createS3RequestDeadline() {
|
|
8205
|
+
const timeoutMs = s3RequestTimeoutMs;
|
|
8206
|
+
return {
|
|
8207
|
+
expiresAt: Date.now() + timeoutMs,
|
|
8208
|
+
timeoutMs
|
|
8209
|
+
};
|
|
8210
|
+
}
|
|
8211
|
+
function createS3DockerCurlDeadline() {
|
|
8212
|
+
const timeoutMs = s3DockerCurlTimeoutMs;
|
|
8213
|
+
return {
|
|
8214
|
+
expiresAt: Date.now() + timeoutMs,
|
|
8215
|
+
timeoutMs
|
|
8216
|
+
};
|
|
8217
|
+
}
|
|
8218
|
+
function createS3TransportDeadline(config) {
|
|
8219
|
+
return config.dockerContainerName ? createS3DockerCurlDeadline() : createS3RequestDeadline();
|
|
8220
|
+
}
|
|
8221
|
+
function s3TimeoutError(deadline) {
|
|
8222
|
+
return new S3HttpError(503, `S3 request timed out after ${deadline?.timeoutMs ?? s3RequestTimeoutMs}ms`);
|
|
8223
|
+
}
|
|
8224
|
+
function remainingS3TimeoutMs(deadline) {
|
|
8225
|
+
if (!deadline)
|
|
8226
|
+
return s3RequestTimeoutMs;
|
|
8227
|
+
return Math.max(0, deadline.expiresAt - Date.now());
|
|
8228
|
+
}
|
|
7594
8229
|
function hmac(key, value) {
|
|
7595
8230
|
return createHmac("sha256", key).update(value, "utf8").digest();
|
|
7596
8231
|
}
|
|
@@ -7725,13 +8360,139 @@ function dockerCurlCommand(opts) {
|
|
|
7725
8360
|
input: Buffer.from(curlHeaderConfig(opts.headers), "utf8")
|
|
7726
8361
|
};
|
|
7727
8362
|
}
|
|
8363
|
+
function guardedS3Transport(signal, operation, deadline) {
|
|
8364
|
+
if (signal?.aborted) {
|
|
8365
|
+
return Promise.reject(new S3HttpError(503, "S3 HTTP transport aborted"));
|
|
8366
|
+
}
|
|
8367
|
+
const timeoutMs = remainingS3TimeoutMs(deadline);
|
|
8368
|
+
if (timeoutMs <= 0) {
|
|
8369
|
+
return Promise.reject(s3TimeoutError(deadline));
|
|
8370
|
+
}
|
|
8371
|
+
const controller = new AbortController;
|
|
8372
|
+
let timedOut = false;
|
|
8373
|
+
let settled = false;
|
|
8374
|
+
let timer;
|
|
8375
|
+
let cleanupParent = () => {};
|
|
8376
|
+
const abort = (err, reject) => {
|
|
8377
|
+
if (settled)
|
|
8378
|
+
return;
|
|
8379
|
+
settled = true;
|
|
8380
|
+
controller.abort(err);
|
|
8381
|
+
reject(err);
|
|
8382
|
+
};
|
|
8383
|
+
const guarded = new Promise((resolve2, reject) => {
|
|
8384
|
+
const onParentAbort = () => abort(new S3HttpError(503, "S3 HTTP transport aborted"), reject);
|
|
8385
|
+
if (signal) {
|
|
8386
|
+
signal.addEventListener("abort", onParentAbort, { once: true });
|
|
8387
|
+
cleanupParent = () => signal.removeEventListener("abort", onParentAbort);
|
|
8388
|
+
}
|
|
8389
|
+
timer = setTimeout(() => {
|
|
8390
|
+
timedOut = true;
|
|
8391
|
+
abort(s3TimeoutError(deadline), reject);
|
|
8392
|
+
}, timeoutMs);
|
|
8393
|
+
operation(controller.signal).then((value) => {
|
|
8394
|
+
if (settled)
|
|
8395
|
+
return;
|
|
8396
|
+
settled = true;
|
|
8397
|
+
resolve2(value);
|
|
8398
|
+
}, (err) => {
|
|
8399
|
+
if (settled)
|
|
8400
|
+
return;
|
|
8401
|
+
settled = true;
|
|
8402
|
+
if (timedOut) {
|
|
8403
|
+
reject(s3TimeoutError(deadline));
|
|
8404
|
+
} else if (signal?.aborted) {
|
|
8405
|
+
reject(new S3HttpError(503, "S3 HTTP transport aborted"));
|
|
8406
|
+
} else {
|
|
8407
|
+
reject(err);
|
|
8408
|
+
}
|
|
8409
|
+
});
|
|
8410
|
+
});
|
|
8411
|
+
return guarded.finally(() => {
|
|
8412
|
+
if (timer)
|
|
8413
|
+
clearTimeout(timer);
|
|
8414
|
+
cleanupParent();
|
|
8415
|
+
});
|
|
8416
|
+
}
|
|
8417
|
+
async function readStreamChunkWithTimeout(reader, signal, deadline) {
|
|
8418
|
+
return guardedS3Transport(signal, (transportSignal) => {
|
|
8419
|
+
const cancelRead = () => {
|
|
8420
|
+
reader.cancel(transportSignal.reason).catch(() => {});
|
|
8421
|
+
};
|
|
8422
|
+
if (transportSignal.aborted) {
|
|
8423
|
+
cancelRead();
|
|
8424
|
+
} else {
|
|
8425
|
+
transportSignal.addEventListener("abort", cancelRead, { once: true });
|
|
8426
|
+
}
|
|
8427
|
+
return reader.read().finally(() => {
|
|
8428
|
+
transportSignal.removeEventListener("abort", cancelRead);
|
|
8429
|
+
});
|
|
8430
|
+
}, deadline);
|
|
8431
|
+
}
|
|
8432
|
+
async function readResponseBytesWithTimeout(res, signal, deadline) {
|
|
8433
|
+
if (!res.body)
|
|
8434
|
+
return new Uint8Array;
|
|
8435
|
+
const reader = res.body.getReader();
|
|
8436
|
+
const chunks = [];
|
|
8437
|
+
let total = 0;
|
|
8438
|
+
try {
|
|
8439
|
+
for (;; ) {
|
|
8440
|
+
const { done, value } = await readStreamChunkWithTimeout(reader, signal, deadline);
|
|
8441
|
+
if (done)
|
|
8442
|
+
break;
|
|
8443
|
+
if (!value?.byteLength)
|
|
8444
|
+
continue;
|
|
8445
|
+
chunks.push(value);
|
|
8446
|
+
total += value.byteLength;
|
|
8447
|
+
}
|
|
8448
|
+
} finally {
|
|
8449
|
+
try {
|
|
8450
|
+
reader.releaseLock();
|
|
8451
|
+
} catch {}
|
|
8452
|
+
}
|
|
8453
|
+
if (chunks.length === 1)
|
|
8454
|
+
return chunks[0];
|
|
8455
|
+
const bytes = new Uint8Array(total);
|
|
8456
|
+
let offset = 0;
|
|
8457
|
+
for (const chunk of chunks) {
|
|
8458
|
+
bytes.set(chunk, offset);
|
|
8459
|
+
offset += chunk.byteLength;
|
|
8460
|
+
}
|
|
8461
|
+
return bytes;
|
|
8462
|
+
}
|
|
8463
|
+
async function readResponseTextWithTimeout(res, signal, deadline) {
|
|
8464
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(await readResponseBytesWithTimeout(res, signal, deadline));
|
|
8465
|
+
}
|
|
8466
|
+
function timeoutReadableStream(body, signal) {
|
|
8467
|
+
if (!body)
|
|
8468
|
+
return null;
|
|
8469
|
+
const reader = body.getReader();
|
|
8470
|
+
return new ReadableStream({
|
|
8471
|
+
async pull(controller) {
|
|
8472
|
+
try {
|
|
8473
|
+
const { done, value } = await readStreamChunkWithTimeout(reader, signal);
|
|
8474
|
+
if (done) {
|
|
8475
|
+
controller.close();
|
|
8476
|
+
return;
|
|
8477
|
+
}
|
|
8478
|
+
if (value)
|
|
8479
|
+
controller.enqueue(value);
|
|
8480
|
+
} catch (error) {
|
|
8481
|
+
controller.error(error);
|
|
8482
|
+
}
|
|
8483
|
+
},
|
|
8484
|
+
cancel(reason) {
|
|
8485
|
+
return reader.cancel(reason);
|
|
8486
|
+
}
|
|
8487
|
+
});
|
|
8488
|
+
}
|
|
7728
8489
|
async function dockerCurlFetch(opts) {
|
|
7729
8490
|
const { args, input } = dockerCurlCommand(opts);
|
|
7730
8491
|
if (spawnSyncImplIsTestOverride) {
|
|
7731
8492
|
const proc2 = spawnSyncImpl3("docker", args, {
|
|
7732
8493
|
encoding: "buffer",
|
|
7733
8494
|
input,
|
|
7734
|
-
timeout:
|
|
8495
|
+
timeout: s3DockerCurlTimeoutMs,
|
|
7735
8496
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7736
8497
|
});
|
|
7737
8498
|
if ((proc2.status ?? 1) !== 0) {
|
|
@@ -7747,10 +8508,10 @@ async function dockerCurlFetch(opts) {
|
|
|
7747
8508
|
command: "docker",
|
|
7748
8509
|
args,
|
|
7749
8510
|
input,
|
|
7750
|
-
timeoutMs:
|
|
8511
|
+
timeoutMs: s3DockerCurlTimeoutMs,
|
|
7751
8512
|
signal: opts.signal,
|
|
7752
8513
|
abortMessage: "S3 HTTP transport aborted",
|
|
7753
|
-
timeoutMessage:
|
|
8514
|
+
timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`
|
|
7754
8515
|
});
|
|
7755
8516
|
if (proc.code !== 0) {
|
|
7756
8517
|
const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
|
|
@@ -7824,73 +8585,77 @@ function parseObjects(xml) {
|
|
|
7824
8585
|
};
|
|
7825
8586
|
}
|
|
7826
8587
|
function createS3Adapter(config) {
|
|
7827
|
-
async function signedFetch(opts) {
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
8588
|
+
async function signedFetch(opts, deadline = createS3TransportDeadline(config)) {
|
|
8589
|
+
return guardedS3Transport(opts.signal, (transportSignal) => {
|
|
8590
|
+
const endpoint = new URL(config.endpoint);
|
|
8591
|
+
const { dateStamp, amzDate: requestDate } = amzDate();
|
|
8592
|
+
const path = buildPath(opts.bucket, opts.key);
|
|
8593
|
+
const query = canonicalQuery(opts.query);
|
|
8594
|
+
const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
8595
|
+
const headers = {
|
|
8596
|
+
host: endpoint.host,
|
|
8597
|
+
"x-amz-content-sha256": EMPTY_SHA256,
|
|
8598
|
+
"x-amz-date": requestDate,
|
|
8599
|
+
...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
|
|
8600
|
+
...opts.headers || {}
|
|
8601
|
+
};
|
|
8602
|
+
const signedNames = signedHeadersString(headers);
|
|
8603
|
+
const canonicalRequest = [
|
|
8604
|
+
opts.method,
|
|
8605
|
+
path,
|
|
8606
|
+
query,
|
|
8607
|
+
canonicalHeaders(headers),
|
|
8608
|
+
signedNames,
|
|
8609
|
+
EMPTY_SHA256
|
|
8610
|
+
].join(`
|
|
7849
8611
|
`);
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
8612
|
+
const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
|
|
8613
|
+
const stringToSign = [
|
|
8614
|
+
"AWS4-HMAC-SHA256",
|
|
8615
|
+
requestDate,
|
|
8616
|
+
scope,
|
|
8617
|
+
sha256(canonicalRequest)
|
|
8618
|
+
].join(`
|
|
7857
8619
|
`);
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
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");
|
|
8620
|
+
const signature = createHmac("sha256", signingKey(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
|
|
8621
|
+
const requestHeaders = new Headers;
|
|
8622
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
8623
|
+
if (key !== "host")
|
|
8624
|
+
requestHeaders.set(key, value);
|
|
7868
8625
|
}
|
|
7869
|
-
|
|
7870
|
-
|
|
8626
|
+
requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
|
|
8627
|
+
if (config.dockerContainerName) {
|
|
8628
|
+
if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
|
|
8629
|
+
throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
|
|
8630
|
+
}
|
|
8631
|
+
return dockerCurlFetch({
|
|
8632
|
+
containerName: config.dockerContainerName,
|
|
8633
|
+
method: opts.method,
|
|
8634
|
+
url,
|
|
8635
|
+
headers: requestHeaders,
|
|
8636
|
+
signal: transportSignal
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
return fetch(url, {
|
|
7871
8640
|
method: opts.method,
|
|
7872
|
-
url,
|
|
7873
8641
|
headers: requestHeaders,
|
|
7874
|
-
signal:
|
|
8642
|
+
signal: transportSignal
|
|
7875
8643
|
});
|
|
7876
|
-
}
|
|
7877
|
-
return fetch(url, {
|
|
7878
|
-
method: opts.method,
|
|
7879
|
-
headers: requestHeaders,
|
|
7880
|
-
signal: opts.signal
|
|
7881
|
-
});
|
|
8644
|
+
}, deadline);
|
|
7882
8645
|
}
|
|
7883
|
-
async function textOrThrow(res) {
|
|
7884
|
-
const text = await res
|
|
8646
|
+
async function textOrThrow(res, signal, deadline) {
|
|
8647
|
+
const text = await readResponseTextWithTimeout(res, signal, deadline);
|
|
7885
8648
|
if (!res.ok)
|
|
7886
8649
|
throw sanitizeS3Error(res.status, text);
|
|
7887
8650
|
return text;
|
|
7888
8651
|
}
|
|
7889
8652
|
async function listBuckets(signal) {
|
|
7890
|
-
const
|
|
8653
|
+
const deadline = createS3TransportDeadline(config);
|
|
8654
|
+
const xml = await textOrThrow(await signedFetch({ method: "GET", signal }, deadline), signal, deadline);
|
|
7891
8655
|
return parseBuckets(xml);
|
|
7892
8656
|
}
|
|
7893
8657
|
async function listObjects(opts) {
|
|
8658
|
+
const deadline = createS3TransportDeadline(config);
|
|
7894
8659
|
const xml = await textOrThrow(await signedFetch({
|
|
7895
8660
|
method: "GET",
|
|
7896
8661
|
bucket: opts.bucket,
|
|
@@ -7901,32 +8666,34 @@ function createS3Adapter(config) {
|
|
|
7901
8666
|
...opts.continuationToken ? { "continuation-token": opts.continuationToken } : {}
|
|
7902
8667
|
},
|
|
7903
8668
|
signal: opts.signal
|
|
7904
|
-
}));
|
|
8669
|
+
}, deadline), opts.signal, deadline);
|
|
7905
8670
|
return parseObjects(xml);
|
|
7906
8671
|
}
|
|
7907
8672
|
async function headObject(opts) {
|
|
8673
|
+
const deadline = createS3TransportDeadline(config);
|
|
7908
8674
|
const res = await signedFetch({
|
|
7909
8675
|
method: "HEAD",
|
|
7910
8676
|
bucket: opts.bucket,
|
|
7911
8677
|
key: opts.key,
|
|
7912
8678
|
signal: opts.signal
|
|
7913
|
-
});
|
|
8679
|
+
}, deadline);
|
|
7914
8680
|
if (!res.ok)
|
|
7915
|
-
throw sanitizeS3Error(res.status, await res.
|
|
8681
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
7916
8682
|
return headFromObjectResponse(opts.bucket, opts.key, res);
|
|
7917
8683
|
}
|
|
7918
8684
|
async function getObjectText(opts) {
|
|
7919
8685
|
const maxBytes = Math.min(1024 * 1024, Math.max(1, opts.maxBytes ?? 512 * 1024));
|
|
8686
|
+
const deadline = createS3TransportDeadline(config);
|
|
7920
8687
|
const res = await signedFetch({
|
|
7921
8688
|
method: "GET",
|
|
7922
8689
|
bucket: opts.bucket,
|
|
7923
8690
|
key: opts.key,
|
|
7924
8691
|
headers: { range: `bytes=0-${maxBytes - 1}` },
|
|
7925
8692
|
signal: opts.signal
|
|
7926
|
-
});
|
|
8693
|
+
}, deadline);
|
|
7927
8694
|
if (!res.ok && res.status !== 206)
|
|
7928
|
-
throw sanitizeS3Error(res.status, await res.
|
|
7929
|
-
const bytes =
|
|
8695
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
8696
|
+
const bytes = await readResponseBytesWithTimeout(res, opts.signal, deadline);
|
|
7930
8697
|
const head = headFromObjectResponse(opts.bucket, opts.key, res);
|
|
7931
8698
|
const fullSize = head.sizeBytes;
|
|
7932
8699
|
return {
|
|
@@ -7936,17 +8703,18 @@ function createS3Adapter(config) {
|
|
|
7936
8703
|
};
|
|
7937
8704
|
}
|
|
7938
8705
|
async function getObjectResponse(opts) {
|
|
8706
|
+
const deadline = createS3TransportDeadline(config);
|
|
7939
8707
|
const res = await signedFetch({
|
|
7940
8708
|
method: opts.method,
|
|
7941
8709
|
bucket: opts.bucket,
|
|
7942
8710
|
key: opts.key,
|
|
7943
8711
|
headers: opts.range ? { range: opts.range } : undefined,
|
|
7944
8712
|
signal: opts.signal
|
|
7945
|
-
});
|
|
8713
|
+
}, deadline);
|
|
7946
8714
|
if (!res.ok && res.status !== 206) {
|
|
7947
|
-
throw sanitizeS3Error(res.status, await res.
|
|
8715
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
7948
8716
|
}
|
|
7949
|
-
return new Response(opts.method === "HEAD" ? null : res.body, {
|
|
8717
|
+
return new Response(opts.method === "HEAD" ? null : timeoutReadableStream(res.body, opts.signal), {
|
|
7950
8718
|
status: res.status,
|
|
7951
8719
|
headers: rawObjectHeaders(opts.key, res)
|
|
7952
8720
|
});
|
|
@@ -7966,6 +8734,9 @@ async function s3ConfigFromDockerInfoAsync(info, signal) {
|
|
|
7966
8734
|
const image = info.image?.toLowerCase() || "";
|
|
7967
8735
|
const minioDefault = image.includes("minio");
|
|
7968
8736
|
const env = info.env;
|
|
8737
|
+
if (!info.hostPort && minioDefault) {
|
|
8738
|
+
throw new S3HttpError(503, 'MinIO S3 browsing requires a published host port. Add a compose port mapping like "9000:9000" for the MinIO API.');
|
|
8739
|
+
}
|
|
7969
8740
|
const dockerContainerName = info.hostPort ? undefined : await resolveRunningComposeContainerNameOrThrowAsync(info.serviceName, info.composeDir, signal);
|
|
7970
8741
|
return {
|
|
7971
8742
|
endpoint: info.hostPort ? `http://localhost:${info.hostPort}` : `http://127.0.0.1:${info.containerPort}`,
|
|
@@ -7982,7 +8753,7 @@ async function openS3ExplorerAsync(info, signal) {
|
|
|
7982
8753
|
function isS3HttpError(err) {
|
|
7983
8754
|
return err instanceof S3HttpError;
|
|
7984
8755
|
}
|
|
7985
|
-
var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
8756
|
+
var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", DEFAULT_S3_REQUEST_TIMEOUT_MS = 5000, DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS = 30000, s3RequestTimeoutMs, s3DockerCurlTimeoutMs;
|
|
7986
8757
|
var init_s3 = __esm(() => {
|
|
7987
8758
|
init_raw_file_headers();
|
|
7988
8759
|
init_docker_utils();
|
|
@@ -7995,6 +8766,8 @@ var init_s3 = __esm(() => {
|
|
|
7995
8766
|
this.status = status;
|
|
7996
8767
|
}
|
|
7997
8768
|
};
|
|
8769
|
+
s3RequestTimeoutMs = DEFAULT_S3_REQUEST_TIMEOUT_MS;
|
|
8770
|
+
s3DockerCurlTimeoutMs = DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS;
|
|
7998
8771
|
});
|
|
7999
8772
|
|
|
8000
8773
|
// web-src/server/database/handle-s3.ts
|
|
@@ -8356,13 +9129,9 @@ var init_handle_s3 = __esm(() => {
|
|
|
8356
9129
|
});
|
|
8357
9130
|
|
|
8358
9131
|
// web-src/server/database/query-history.ts
|
|
8359
|
-
import {
|
|
8360
|
-
import { join as join9 } from "node:path";
|
|
9132
|
+
import { join as join10 } from "node:path";
|
|
8361
9133
|
function historyFilePath(root) {
|
|
8362
|
-
return
|
|
8363
|
-
}
|
|
8364
|
-
function isEnoent(err) {
|
|
8365
|
-
return err?.code === "ENOENT";
|
|
9134
|
+
return join10(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
|
|
8366
9135
|
}
|
|
8367
9136
|
function emptyState() {
|
|
8368
9137
|
return { version: 1, entries: [] };
|
|
@@ -8381,69 +9150,86 @@ function serializeHistoryState(state) {
|
|
|
8381
9150
|
}
|
|
8382
9151
|
return content;
|
|
8383
9152
|
}
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
9153
|
+
function optionalString2(value, maxLen) {
|
|
9154
|
+
if (typeof value !== "string")
|
|
9155
|
+
return;
|
|
9156
|
+
if (!value || value.length > maxLen || value.includes("\x00"))
|
|
9157
|
+
return;
|
|
9158
|
+
return value;
|
|
8388
9159
|
}
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
9160
|
+
function finiteNumber(value, min = 0) {
|
|
9161
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
9162
|
+
return;
|
|
9163
|
+
return Math.max(min, Math.round(value));
|
|
9164
|
+
}
|
|
9165
|
+
function sanitizeDbValue(value) {
|
|
9166
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
9167
|
+
return value;
|
|
8393
9168
|
}
|
|
8394
|
-
return
|
|
9169
|
+
return null;
|
|
8395
9170
|
}
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
9171
|
+
function sanitizeRows(raw) {
|
|
9172
|
+
if (!Array.isArray(raw))
|
|
9173
|
+
return [];
|
|
9174
|
+
return raw.slice(0, MAX_PREVIEW_ROWS).map((row) => {
|
|
9175
|
+
if (!Array.isArray(row))
|
|
9176
|
+
return [];
|
|
9177
|
+
return row.map(sanitizeDbValue);
|
|
9178
|
+
});
|
|
9179
|
+
}
|
|
9180
|
+
function sanitizeEntry(raw) {
|
|
9181
|
+
if (!raw || typeof raw !== "object")
|
|
9182
|
+
return null;
|
|
9183
|
+
const entry = raw;
|
|
9184
|
+
const id = optionalString2(entry.id, MAX_ID_LEN);
|
|
9185
|
+
const dbId = optionalString2(entry.dbId, MAX_DB_ID_LEN);
|
|
9186
|
+
const sql = optionalString2(entry.sql, MAX_SQL_LEN);
|
|
9187
|
+
if (!id || !dbId || !sql)
|
|
9188
|
+
return null;
|
|
9189
|
+
const columns = Array.isArray(entry.columns) ? entry.columns.filter((col) => typeof col === "string" && !!col).map((col) => col.slice(0, MAX_COLUMN_LEN)).slice(0, MAX_COLUMNS) : [];
|
|
9190
|
+
const rowsPreview = sanitizeRows(entry.rowsPreview);
|
|
9191
|
+
const schema = optionalString2(entry.schema, MAX_SCHEMA_LEN);
|
|
9192
|
+
const title = optionalString2(entry.title, MAX_TEXT_LEN);
|
|
9193
|
+
const body = optionalString2(entry.body, MAX_TEXT_LEN);
|
|
9194
|
+
return {
|
|
9195
|
+
id,
|
|
9196
|
+
dbId,
|
|
9197
|
+
...schema ? { schema } : {},
|
|
9198
|
+
sql,
|
|
9199
|
+
...title ? { title } : {},
|
|
9200
|
+
...body ? { body } : {},
|
|
9201
|
+
columns,
|
|
9202
|
+
rowsPreview,
|
|
9203
|
+
rowCount: finiteNumber(entry.rowCount) ?? rowsPreview.length,
|
|
9204
|
+
savedRows: finiteNumber(entry.savedRows) ?? rowsPreview.length,
|
|
9205
|
+
truncated: typeof entry.truncated === "boolean" ? entry.truncated : false,
|
|
9206
|
+
elapsedMs: finiteNumber(entry.elapsedMs) ?? 0,
|
|
9207
|
+
executedAt: optionalString2(entry.executedAt, 64) ?? new Date(0).toISOString(),
|
|
9208
|
+
executedBy: entry.executedBy === "ai" ? "ai" : "user",
|
|
9209
|
+
source: entry.source === "cli" ? "cli" : "browser"
|
|
9210
|
+
};
|
|
9211
|
+
}
|
|
9212
|
+
function sanitizeHistoryState(raw) {
|
|
9213
|
+
if (!raw || typeof raw !== "object")
|
|
8405
9214
|
return emptyState();
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
const parsed = JSON.parse(raw);
|
|
8409
|
-
if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
8410
|
-
await backupCorruptHistoryFileAsync(file);
|
|
8411
|
-
return emptyState();
|
|
8412
|
-
}
|
|
8413
|
-
return parsed;
|
|
8414
|
-
} catch (err) {
|
|
8415
|
-
if (isEnoent(err))
|
|
8416
|
-
return emptyState();
|
|
8417
|
-
await backupCorruptHistoryFileAsync(file);
|
|
9215
|
+
const entriesRaw = raw.entries;
|
|
9216
|
+
if (!Array.isArray(entriesRaw))
|
|
8418
9217
|
return emptyState();
|
|
9218
|
+
const entries = [];
|
|
9219
|
+
for (const entry of entriesRaw) {
|
|
9220
|
+
if (entries.length >= MAX_ENTRIES2)
|
|
9221
|
+
break;
|
|
9222
|
+
const normalized = sanitizeEntry(entry);
|
|
9223
|
+
if (normalized)
|
|
9224
|
+
entries.push(normalized);
|
|
8419
9225
|
}
|
|
9226
|
+
return { version: 1, entries };
|
|
8420
9227
|
}
|
|
8421
|
-
async function
|
|
8422
|
-
|
|
8423
|
-
await mkdir(dir, { recursive: true });
|
|
8424
|
-
const file = historyFilePath(cwd);
|
|
8425
|
-
const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
8426
|
-
const content = serializeHistoryState(state);
|
|
8427
|
-
await writeFile(tmp, content, "utf8");
|
|
8428
|
-
await rename(tmp, file);
|
|
9228
|
+
async function loadQueryHistoryAsync(cwd) {
|
|
9229
|
+
return historyStore.load(cwd);
|
|
8429
9230
|
}
|
|
8430
9231
|
async function updateQueryHistoryAsync(cwd, updater) {
|
|
8431
|
-
|
|
8432
|
-
const run2 = previous.catch(() => {}).then(async () => {
|
|
8433
|
-
const current = await loadQueryHistoryAsyncUnqueued(cwd);
|
|
8434
|
-
const updated = await updater(current);
|
|
8435
|
-
await saveQueryHistoryAsync(cwd, updated.state);
|
|
8436
|
-
return updated.result;
|
|
8437
|
-
});
|
|
8438
|
-
const queued = run2.then(() => {}, () => {});
|
|
8439
|
-
historyWriteQueues.set(cwd, queued);
|
|
8440
|
-
try {
|
|
8441
|
-
return await run2;
|
|
8442
|
-
} finally {
|
|
8443
|
-
if (historyWriteQueues.get(cwd) === queued) {
|
|
8444
|
-
historyWriteQueues.delete(cwd);
|
|
8445
|
-
}
|
|
8446
|
-
}
|
|
9232
|
+
return historyStore.update(cwd, updater);
|
|
8447
9233
|
}
|
|
8448
9234
|
function clampPreviewRows(rows) {
|
|
8449
9235
|
return rows.slice(0, MAX_PREVIEW_ROWS);
|
|
@@ -8479,15 +9265,23 @@ function clearQueryHistory(state, dbId, schema) {
|
|
|
8479
9265
|
})
|
|
8480
9266
|
};
|
|
8481
9267
|
}
|
|
8482
|
-
var
|
|
9268
|
+
var CODE_VIEWER_DIR3 = ".code-viewer", HISTORY_FILE_NAME = "query-history.json", MAX_ENTRIES2 = 200, MAX_PREVIEW_ROWS = 100, MAX_JSON_BYTES = 1e6, MAX_ID_LEN = 128, MAX_DB_ID_LEN = 2048, MAX_SCHEMA_LEN = 512, MAX_SQL_LEN = 64000, MAX_TEXT_LEN = 64000, MAX_COLUMN_LEN = 512, MAX_COLUMNS = 500, historyStore;
|
|
8483
9269
|
var init_query_history = __esm(() => {
|
|
8484
|
-
|
|
9270
|
+
init_json_store();
|
|
9271
|
+
historyStore = createJsonFileStore({
|
|
9272
|
+
filePath: historyFilePath,
|
|
9273
|
+
empty: emptyState,
|
|
9274
|
+
sanitize: sanitizeHistoryState,
|
|
9275
|
+
maxBytes: MAX_JSON_BYTES,
|
|
9276
|
+
backupSuffix: "corrupt",
|
|
9277
|
+
serialize: serializeHistoryState
|
|
9278
|
+
});
|
|
8485
9279
|
});
|
|
8486
9280
|
|
|
8487
9281
|
// web-src/server/database/snapshot-store.ts
|
|
8488
|
-
import { createHash as createHash5, randomBytes } from "node:crypto";
|
|
8489
|
-
import { mkdirSync as
|
|
8490
|
-
import { join as
|
|
9282
|
+
import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
|
|
9283
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9284
|
+
import { join as join11 } from "node:path";
|
|
8491
9285
|
async function getSqliteClass2() {
|
|
8492
9286
|
if (cachedDbClass2)
|
|
8493
9287
|
return cachedDbClass2;
|
|
@@ -8504,7 +9298,7 @@ async function getSqliteClass2() {
|
|
|
8504
9298
|
throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
|
|
8505
9299
|
}
|
|
8506
9300
|
async function getStoreDb(cwd) {
|
|
8507
|
-
const dbPath =
|
|
9301
|
+
const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
|
|
8508
9302
|
if (storeDb && storeDbPath === dbPath)
|
|
8509
9303
|
return storeDb;
|
|
8510
9304
|
if (storeDb) {
|
|
@@ -8512,7 +9306,7 @@ async function getStoreDb(cwd) {
|
|
|
8512
9306
|
storeDb.close();
|
|
8513
9307
|
} catch {}
|
|
8514
9308
|
}
|
|
8515
|
-
|
|
9309
|
+
mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
|
|
8516
9310
|
const DbClass = await getSqliteClass2();
|
|
8517
9311
|
storeDb = new DbClass(dbPath);
|
|
8518
9312
|
storeDbPath = dbPath;
|
|
@@ -8525,7 +9319,7 @@ async function getStoreDb(cwd) {
|
|
|
8525
9319
|
return storeDb;
|
|
8526
9320
|
}
|
|
8527
9321
|
function makeId2(prefix) {
|
|
8528
|
-
return `${prefix}-${
|
|
9322
|
+
return `${prefix}-${randomBytes2(8).toString("hex")}`;
|
|
8529
9323
|
}
|
|
8530
9324
|
function hashPayload(payloadJson) {
|
|
8531
9325
|
return createHash5("sha256").update(payloadJson).digest("hex");
|
|
@@ -8772,7 +9566,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
8772
9566
|
});
|
|
8773
9567
|
return { rows, total };
|
|
8774
9568
|
}
|
|
8775
|
-
var
|
|
9569
|
+
var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
|
|
8776
9570
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
8777
9571
|
id TEXT PRIMARY KEY,
|
|
8778
9572
|
db_id TEXT NOT NULL,
|
|
@@ -8869,13 +9663,9 @@ var init_snapshot_runner = __esm(() => {
|
|
|
8869
9663
|
});
|
|
8870
9664
|
|
|
8871
9665
|
// web-src/server/database/tabs-store.ts
|
|
8872
|
-
import {
|
|
8873
|
-
import { join as join11 } from "node:path";
|
|
9666
|
+
import { join as join12 } from "node:path";
|
|
8874
9667
|
function tabsFilePath(root) {
|
|
8875
|
-
return
|
|
8876
|
-
}
|
|
8877
|
-
function isEnoent2(err) {
|
|
8878
|
-
return err?.code === "ENOENT";
|
|
9668
|
+
return join12(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
|
|
8879
9669
|
}
|
|
8880
9670
|
function emptyState2() {
|
|
8881
9671
|
return { version: 1, tabs: [], activeTabId: null };
|
|
@@ -8943,6 +9733,31 @@ function sanitizeEs(v) {
|
|
|
8943
9733
|
return;
|
|
8944
9734
|
return out;
|
|
8945
9735
|
}
|
|
9736
|
+
function sanitizeS3(v) {
|
|
9737
|
+
if (!v || typeof v !== "object")
|
|
9738
|
+
return;
|
|
9739
|
+
const r = v;
|
|
9740
|
+
const out = {};
|
|
9741
|
+
const bucket = sanitizeOptionalString(r.bucket, MAX_S3_BUCKET_LEN);
|
|
9742
|
+
if (bucket !== undefined)
|
|
9743
|
+
out.bucket = bucket;
|
|
9744
|
+
const prefix = sanitizeOptionalString(r.prefix, MAX_S3_KEY_LEN);
|
|
9745
|
+
if (prefix !== undefined)
|
|
9746
|
+
out.prefix = prefix;
|
|
9747
|
+
const query = sanitizeOptionalString(r.query, MAX_S3_QUERY_LEN);
|
|
9748
|
+
if (query !== undefined)
|
|
9749
|
+
out.query = query;
|
|
9750
|
+
if (r.mode === "prefix" || r.mode === "contains")
|
|
9751
|
+
out.mode = r.mode;
|
|
9752
|
+
if (r.sort === "key-asc" || r.sort === "updated-desc")
|
|
9753
|
+
out.sort = r.sort;
|
|
9754
|
+
const key = sanitizeOptionalString(r.key, MAX_S3_KEY_LEN);
|
|
9755
|
+
if (key !== undefined)
|
|
9756
|
+
out.key = key;
|
|
9757
|
+
if (out.bucket === undefined && out.prefix === undefined && out.query === undefined && out.mode === undefined && out.sort === undefined && out.key === undefined)
|
|
9758
|
+
return;
|
|
9759
|
+
return out;
|
|
9760
|
+
}
|
|
8946
9761
|
function sanitize(input) {
|
|
8947
9762
|
if (!input || typeof input !== "object")
|
|
8948
9763
|
return emptyState2();
|
|
@@ -8964,12 +9779,15 @@ function sanitize(input) {
|
|
|
8964
9779
|
if (seenIds.has(id))
|
|
8965
9780
|
continue;
|
|
8966
9781
|
seenIds.add(id);
|
|
8967
|
-
const dbId = sanitizeOptionalString(tab.dbId,
|
|
9782
|
+
const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN2) ?? null;
|
|
8968
9783
|
if (isToolInternalDbId(dbId))
|
|
8969
9784
|
continue;
|
|
9785
|
+
const schema = sanitizeOptionalString(tab.schema, MAX_SCHEMA_NAME_LEN);
|
|
8970
9786
|
const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
|
|
8971
9787
|
const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
|
|
8972
9788
|
const out = { id, dbId, table, view };
|
|
9789
|
+
if (schema !== undefined)
|
|
9790
|
+
out.schema = schema;
|
|
8973
9791
|
const sqlDraft = sanitizeOptionalString(tab.sqlDraft, MAX_SQL_DRAFT_LEN);
|
|
8974
9792
|
if (sqlDraft !== undefined)
|
|
8975
9793
|
out.sqlDraft = sqlDraft;
|
|
@@ -8987,6 +9805,9 @@ function sanitize(input) {
|
|
|
8987
9805
|
const es = sanitizeEs(tab.es);
|
|
8988
9806
|
if (es !== undefined)
|
|
8989
9807
|
out.es = es;
|
|
9808
|
+
const s3 = sanitizeS3(tab.s3);
|
|
9809
|
+
if (s3 !== undefined)
|
|
9810
|
+
out.s3 = s3;
|
|
8990
9811
|
tabs.push(out);
|
|
8991
9812
|
}
|
|
8992
9813
|
let activeTabId = sanitizeOptionalString(obj.activeTabId, MAX_TAB_ID_LEN) ?? null;
|
|
@@ -8996,69 +9817,14 @@ function sanitize(input) {
|
|
|
8996
9817
|
return { version: 1, tabs, activeTabId };
|
|
8997
9818
|
}
|
|
8998
9819
|
async function loadTabsAsync(cwd) {
|
|
8999
|
-
|
|
9000
|
-
if (pendingWrite) {
|
|
9001
|
-
await pendingWrite.catch(() => {});
|
|
9002
|
-
}
|
|
9003
|
-
const file = tabsFilePath(cwd);
|
|
9004
|
-
let raw;
|
|
9005
|
-
try {
|
|
9006
|
-
raw = await readFile3(file, "utf8");
|
|
9007
|
-
} catch (err) {
|
|
9008
|
-
if (isEnoent2(err))
|
|
9009
|
-
return emptyState2();
|
|
9010
|
-
await backupInvalidTabsFileAsync(file);
|
|
9011
|
-
return emptyState2();
|
|
9012
|
-
}
|
|
9013
|
-
try {
|
|
9014
|
-
const parsed = JSON.parse(raw);
|
|
9015
|
-
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
|
|
9016
|
-
await backupInvalidTabsFileAsync(file);
|
|
9017
|
-
return emptyState2();
|
|
9018
|
-
}
|
|
9019
|
-
return sanitize(parsed);
|
|
9020
|
-
} catch (err) {
|
|
9021
|
-
if (isEnoent2(err))
|
|
9022
|
-
return emptyState2();
|
|
9023
|
-
await backupInvalidTabsFileAsync(file);
|
|
9024
|
-
return emptyState2();
|
|
9025
|
-
}
|
|
9026
|
-
}
|
|
9027
|
-
async function backupInvalidTabsFileAsync(file) {
|
|
9028
|
-
try {
|
|
9029
|
-
await rename2(file, `${file}.bak-${Date.now()}`);
|
|
9030
|
-
} catch {}
|
|
9031
|
-
}
|
|
9032
|
-
async function saveTabsAsyncUnqueued(cwd, state) {
|
|
9033
|
-
const normalized = sanitize(state);
|
|
9034
|
-
const dir = join11(cwd, CODE_VIEWER_DIR4);
|
|
9035
|
-
await mkdir2(dir, { recursive: true });
|
|
9036
|
-
const file = tabsFilePath(cwd);
|
|
9037
|
-
const tmp = `${file}.${makeId(`tmp-${process.pid}`)}`;
|
|
9038
|
-
const content = `${JSON.stringify(normalized, null, 2)}
|
|
9039
|
-
`;
|
|
9040
|
-
if (Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES2) {
|
|
9041
|
-
throw new Error("tabs state too large");
|
|
9042
|
-
}
|
|
9043
|
-
await writeFile2(tmp, content, "utf8");
|
|
9044
|
-
await rename2(tmp, file);
|
|
9820
|
+
return tabsStore.load(cwd);
|
|
9045
9821
|
}
|
|
9046
9822
|
async function saveTabsAsync(cwd, state) {
|
|
9047
|
-
|
|
9048
|
-
const run2 = previous.catch(() => {}).then(() => saveTabsAsyncUnqueued(cwd, state));
|
|
9049
|
-
const queued = run2.then(() => {}, () => {});
|
|
9050
|
-
tabsWriteQueues.set(cwd, queued);
|
|
9051
|
-
try {
|
|
9052
|
-
await run2;
|
|
9053
|
-
} finally {
|
|
9054
|
-
if (tabsWriteQueues.get(cwd) === queued) {
|
|
9055
|
-
tabsWriteQueues.delete(cwd);
|
|
9056
|
-
}
|
|
9057
|
-
}
|
|
9823
|
+
return tabsStore.save(cwd, state);
|
|
9058
9824
|
}
|
|
9059
|
-
var
|
|
9825
|
+
var CODE_VIEWER_DIR5 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES2 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN2 = 2048, MAX_SCHEMA_NAME_LEN = 512, MAX_TABLE_NAME_LEN = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, MAX_S3_BUCKET_LEN = 256, MAX_S3_KEY_LEN = 2048, MAX_S3_QUERY_LEN = 2048, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS, tabsStore;
|
|
9060
9826
|
var init_tabs_store = __esm(() => {
|
|
9061
|
-
|
|
9827
|
+
init_json_store();
|
|
9062
9828
|
VALID_VIEWS = new Set([
|
|
9063
9829
|
"data",
|
|
9064
9830
|
"query",
|
|
@@ -9067,6 +9833,14 @@ var init_tabs_store = __esm(() => {
|
|
|
9067
9833
|
"search",
|
|
9068
9834
|
"snapshot"
|
|
9069
9835
|
]);
|
|
9836
|
+
tabsStore = createJsonFileStore({
|
|
9837
|
+
filePath: tabsFilePath,
|
|
9838
|
+
empty: emptyState2,
|
|
9839
|
+
sanitize,
|
|
9840
|
+
maxBytes: MAX_JSON_BYTES2,
|
|
9841
|
+
backupSuffix: "bak",
|
|
9842
|
+
sizeErrorMessage: "tabs state too large"
|
|
9843
|
+
});
|
|
9070
9844
|
});
|
|
9071
9845
|
|
|
9072
9846
|
// web-src/server/database/handle.ts
|
|
@@ -9095,7 +9869,7 @@ function sanitizeFilename(name) {
|
|
|
9095
9869
|
function normalizeSchemaParam(value) {
|
|
9096
9870
|
if (value === undefined || value === null || value === "")
|
|
9097
9871
|
return;
|
|
9098
|
-
if (value.length >
|
|
9872
|
+
if (value.length > MAX_SCHEMA_NAME_LEN2) {
|
|
9099
9873
|
return textError("invalid schema parameter", 400);
|
|
9100
9874
|
}
|
|
9101
9875
|
if (hasControlCharacter(value)) {
|
|
@@ -10054,6 +10828,23 @@ async function handleTabsPut(cwd, req) {
|
|
|
10054
10828
|
return textError(`failed to save tabs: ${message}`, 500);
|
|
10055
10829
|
}
|
|
10056
10830
|
}
|
|
10831
|
+
async function handleDbUiGet(cwd) {
|
|
10832
|
+
return jsonLoadResponse(() => loadDbUiState(cwd), "db UI", "failed to load db UI state");
|
|
10833
|
+
}
|
|
10834
|
+
async function handleDbUiPatch(cwd, req) {
|
|
10835
|
+
const body = await parseBoundedJsonBody(req, MAX_DB_UI_BODY_BYTES, "db UI body too large");
|
|
10836
|
+
if (body instanceof Response)
|
|
10837
|
+
return body;
|
|
10838
|
+
try {
|
|
10839
|
+
return json(await patchDbUiState(cwd, body));
|
|
10840
|
+
} catch (err) {
|
|
10841
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10842
|
+
if (message === "db UI state too large")
|
|
10843
|
+
return textError(message, 413);
|
|
10844
|
+
console.error("[code-viewer] db UI error:", err);
|
|
10845
|
+
return textError("failed to save db UI state", 500);
|
|
10846
|
+
}
|
|
10847
|
+
}
|
|
10057
10848
|
async function handleClose(cwd, req, omitDirNames) {
|
|
10058
10849
|
const body = await parsePostJsonBody(req);
|
|
10059
10850
|
if (body instanceof Response)
|
|
@@ -10216,11 +11007,17 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
|
|
|
10216
11007
|
methods: ["GET", "PUT", "POST"],
|
|
10217
11008
|
sideEffect: (m) => m !== "GET",
|
|
10218
11009
|
handler: () => method === "GET" ? handleTabsGet(cwd) : handleTabsPut(cwd, req)
|
|
11010
|
+
},
|
|
11011
|
+
"/_db/ui": {
|
|
11012
|
+
methods: ["GET", "PATCH"],
|
|
11013
|
+
sideEffect: (m) => m !== "GET",
|
|
11014
|
+
handler: () => method === "GET" ? handleDbUiGet(cwd) : handleDbUiPatch(cwd, req)
|
|
10219
11015
|
}
|
|
10220
11016
|
}, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
|
|
10221
11017
|
}
|
|
10222
|
-
var initialized = false, dockerAdapterCache,
|
|
11018
|
+
var initialized = false, dockerAdapterCache, MAX_SCHEMA_NAME_LEN2 = 1024, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_DB_UI_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
|
|
10223
11019
|
var init_handle = __esm(() => {
|
|
11020
|
+
init_state_store();
|
|
10224
11021
|
init_docker();
|
|
10225
11022
|
init_docker_utils();
|
|
10226
11023
|
init_sqlite();
|
|
@@ -10274,25 +11071,87 @@ var init_handle = __esm(() => {
|
|
|
10274
11071
|
};
|
|
10275
11072
|
});
|
|
10276
11073
|
|
|
11074
|
+
// web-src/server/state-route.ts
|
|
11075
|
+
var exports_state_route = {};
|
|
11076
|
+
__export(exports_state_route, {
|
|
11077
|
+
handleStateRoute: () => handleStateRoute
|
|
11078
|
+
});
|
|
11079
|
+
async function parseJsonBody(req) {
|
|
11080
|
+
return parseBoundedJsonBody(req, MAX_STATE_PATCH_BODY_BYTES, "state body too large");
|
|
11081
|
+
}
|
|
11082
|
+
async function handleSettingsGet(cwd) {
|
|
11083
|
+
return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
|
|
11084
|
+
}
|
|
11085
|
+
async function handleSettingsPatch(cwd, req) {
|
|
11086
|
+
const body = await parseJsonBody(req);
|
|
11087
|
+
if (body instanceof Response)
|
|
11088
|
+
return body;
|
|
11089
|
+
try {
|
|
11090
|
+
return json(await patchAppSettingsState(cwd, body));
|
|
11091
|
+
} catch (err) {
|
|
11092
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11093
|
+
if (message === "settings state too large")
|
|
11094
|
+
return textError(message, 413);
|
|
11095
|
+
console.error("[code-viewer] state error:", err);
|
|
11096
|
+
return textError("failed to save settings state", 500);
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
async function handleViewGet(cwd) {
|
|
11100
|
+
return jsonLoadResponse(() => loadViewState(cwd), "state", "failed to load view state");
|
|
11101
|
+
}
|
|
11102
|
+
async function handleViewPatch(cwd, req) {
|
|
11103
|
+
const body = await parseJsonBody(req);
|
|
11104
|
+
if (body instanceof Response)
|
|
11105
|
+
return body;
|
|
11106
|
+
try {
|
|
11107
|
+
return json(await patchViewState(cwd, body));
|
|
11108
|
+
} catch (err) {
|
|
11109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11110
|
+
if (message === "view state too large")
|
|
11111
|
+
return textError(message, 413);
|
|
11112
|
+
console.error("[code-viewer] state error:", err);
|
|
11113
|
+
return textError("failed to save view state", 500);
|
|
11114
|
+
}
|
|
11115
|
+
}
|
|
11116
|
+
async function handleStateRoute(req, url, cwd, sideEffectAllowed) {
|
|
11117
|
+
return dispatchRoutes(req, url, {
|
|
11118
|
+
"/_state/settings": {
|
|
11119
|
+
methods: ["GET", "PATCH"],
|
|
11120
|
+
sideEffect: (method) => method !== "GET",
|
|
11121
|
+
handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req)
|
|
11122
|
+
},
|
|
11123
|
+
"/_state/view": {
|
|
11124
|
+
methods: ["GET", "PATCH"],
|
|
11125
|
+
sideEffect: (method) => method !== "GET",
|
|
11126
|
+
handler: () => req.method === "GET" ? handleViewGet(cwd) : handleViewPatch(cwd, req)
|
|
11127
|
+
}
|
|
11128
|
+
}, sideEffectAllowed, (res) => res, (err) => handleError("state", "handle state request", err));
|
|
11129
|
+
}
|
|
11130
|
+
var MAX_STATE_PATCH_BODY_BYTES = 1e6;
|
|
11131
|
+
var init_state_route = __esm(() => {
|
|
11132
|
+
init_handle_shared();
|
|
11133
|
+
init_state_store();
|
|
11134
|
+
});
|
|
11135
|
+
|
|
10277
11136
|
// web-src/server/preview.ts
|
|
10278
11137
|
var exports_preview = {};
|
|
10279
11138
|
import {
|
|
10280
11139
|
closeSync as closeSync2,
|
|
10281
11140
|
constants,
|
|
10282
|
-
existsSync as
|
|
11141
|
+
existsSync as existsSync6,
|
|
10283
11142
|
lstatSync as lstatSync4,
|
|
10284
|
-
mkdirSync as
|
|
11143
|
+
mkdirSync as mkdirSync4,
|
|
10285
11144
|
openSync as openSync2,
|
|
10286
|
-
readFileSync as
|
|
11145
|
+
readFileSync as readFileSync4,
|
|
10287
11146
|
realpathSync as realpathSync4,
|
|
10288
|
-
renameSync
|
|
11147
|
+
renameSync,
|
|
10289
11148
|
statSync as statSync3,
|
|
10290
11149
|
unlinkSync as unlinkSync2,
|
|
10291
11150
|
watch,
|
|
10292
|
-
writeFileSync as
|
|
11151
|
+
writeFileSync as writeFileSync2
|
|
10293
11152
|
} from "node:fs";
|
|
10294
11153
|
import { homedir as homedir3 } from "node:os";
|
|
10295
|
-
import { basename as basename3, dirname as
|
|
11154
|
+
import { basename as basename3, dirname as dirname3, extname as extname2, join as join13, relative as relative3 } from "node:path";
|
|
10296
11155
|
function parseCli() {
|
|
10297
11156
|
const rest = [];
|
|
10298
11157
|
for (let i = 2;i < process.argv.length; i++) {
|
|
@@ -10433,10 +11292,10 @@ function staticFile(pathname) {
|
|
|
10433
11292
|
const spec = map[pathname];
|
|
10434
11293
|
if (!spec)
|
|
10435
11294
|
return null;
|
|
10436
|
-
const full =
|
|
10437
|
-
if (!
|
|
11295
|
+
const full = join13(WEB_ROOT, spec[0]);
|
|
11296
|
+
if (!existsSync6(full))
|
|
10438
11297
|
return text("not found", 404);
|
|
10439
|
-
return new Response(
|
|
11298
|
+
return new Response(readFileSync4(full), {
|
|
10440
11299
|
headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
|
|
10441
11300
|
});
|
|
10442
11301
|
}
|
|
@@ -10667,8 +11526,8 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
10667
11526
|
return normalizeScopeExcludeNames(names);
|
|
10668
11527
|
}
|
|
10669
11528
|
function loadProjectConfig() {
|
|
10670
|
-
const full =
|
|
10671
|
-
if (!
|
|
11529
|
+
const full = join13(cwd, ".code-viewer.json");
|
|
11530
|
+
if (!existsSync6(full))
|
|
10672
11531
|
return null;
|
|
10673
11532
|
let realCwd;
|
|
10674
11533
|
let realConfig;
|
|
@@ -10678,10 +11537,10 @@ function loadProjectConfig() {
|
|
|
10678
11537
|
} catch {
|
|
10679
11538
|
return null;
|
|
10680
11539
|
}
|
|
10681
|
-
if (
|
|
11540
|
+
if (dirname3(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
|
|
10682
11541
|
return null;
|
|
10683
11542
|
try {
|
|
10684
|
-
const parsed = JSON.parse(
|
|
11543
|
+
const parsed = JSON.parse(readFileSync4(realConfig, "utf8"));
|
|
10685
11544
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
|
|
10686
11545
|
return null;
|
|
10687
11546
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
@@ -10732,8 +11591,8 @@ function safeWorktreePath(path) {
|
|
|
10732
11591
|
return null;
|
|
10733
11592
|
if (isGitInternalPath(path))
|
|
10734
11593
|
return null;
|
|
10735
|
-
const full =
|
|
10736
|
-
if (!
|
|
11594
|
+
const full = join13(cwd, path);
|
|
11595
|
+
if (!existsSync6(full))
|
|
10737
11596
|
return null;
|
|
10738
11597
|
let realCwd;
|
|
10739
11598
|
let realFull;
|
|
@@ -10751,7 +11610,7 @@ function safeWorktreePath(path) {
|
|
|
10751
11610
|
return realFull;
|
|
10752
11611
|
}
|
|
10753
11612
|
function worktreePath(path) {
|
|
10754
|
-
return
|
|
11613
|
+
return join13(cwd, path);
|
|
10755
11614
|
}
|
|
10756
11615
|
function safeOpenWorktreePath(path) {
|
|
10757
11616
|
if (path === "") {
|
|
@@ -10767,7 +11626,7 @@ function safeOpenWorktreePath(path) {
|
|
|
10767
11626
|
return safeWorktreePath(path);
|
|
10768
11627
|
}
|
|
10769
11628
|
function parentRepoPath(path) {
|
|
10770
|
-
const parent =
|
|
11629
|
+
const parent = dirname3(path);
|
|
10771
11630
|
return parent === "." ? "" : parent;
|
|
10772
11631
|
}
|
|
10773
11632
|
function isoDate(ms) {
|
|
@@ -10834,7 +11693,7 @@ function readReadme(target, dirPath) {
|
|
|
10834
11693
|
if (!full)
|
|
10835
11694
|
continue;
|
|
10836
11695
|
try {
|
|
10837
|
-
return { path, text:
|
|
11696
|
+
return { path, text: readFileSync4(full, "utf8") };
|
|
10838
11697
|
} catch {
|
|
10839
11698
|
continue;
|
|
10840
11699
|
}
|
|
@@ -10889,6 +11748,13 @@ function handleSettings() {
|
|
|
10889
11748
|
}
|
|
10890
11749
|
});
|
|
10891
11750
|
}
|
|
11751
|
+
function worktreeWatchDirectoryLimitFromEnv() {
|
|
11752
|
+
const raw = process.env.CODE_VIEWER_WORKTREE_WATCH_LIMIT;
|
|
11753
|
+
if (!raw)
|
|
11754
|
+
return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
|
|
11755
|
+
const parsed = Number(raw);
|
|
11756
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
|
|
11757
|
+
}
|
|
10892
11758
|
function handleFiles2(url) {
|
|
10893
11759
|
const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
|
|
10894
11760
|
if (target !== "worktree" && !verifyTreeRef(target, cwd))
|
|
@@ -10948,7 +11814,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
|
10948
11814
|
continue;
|
|
10949
11815
|
let data;
|
|
10950
11816
|
try {
|
|
10951
|
-
data =
|
|
11817
|
+
data = readFileSync4(full);
|
|
10952
11818
|
} catch {
|
|
10953
11819
|
continue;
|
|
10954
11820
|
}
|
|
@@ -11484,10 +12350,10 @@ async function handleUploadFiles(req) {
|
|
|
11484
12350
|
total += file.size;
|
|
11485
12351
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
11486
12352
|
return text("upload too large", 413);
|
|
11487
|
-
const target =
|
|
11488
|
-
if (relative3(realDir,
|
|
12353
|
+
const target = join13(realDir, safeName);
|
|
12354
|
+
if (relative3(realDir, dirname3(target)) !== "")
|
|
11489
12355
|
return text("invalid filename", 400);
|
|
11490
|
-
if (
|
|
12356
|
+
if (existsSync6(target))
|
|
11491
12357
|
return text("file exists", 409);
|
|
11492
12358
|
uploads.push({ file, name: safeName, target });
|
|
11493
12359
|
}
|
|
@@ -11496,7 +12362,7 @@ async function handleUploadFiles(req) {
|
|
|
11496
12362
|
for (const upload of uploads) {
|
|
11497
12363
|
const fd = openSync2(upload.target, uploadOpenFlags(), 420);
|
|
11498
12364
|
try {
|
|
11499
|
-
|
|
12365
|
+
writeFileSync2(fd, new Uint8Array(await upload.file.arrayBuffer()));
|
|
11500
12366
|
} finally {
|
|
11501
12367
|
closeSync2(fd);
|
|
11502
12368
|
}
|
|
@@ -11606,12 +12472,12 @@ function triggerUpdate(changedPaths) {
|
|
|
11606
12472
|
sendSse("update", data);
|
|
11607
12473
|
}
|
|
11608
12474
|
function moveMacPathIntoTrash(path) {
|
|
11609
|
-
const trashDir =
|
|
12475
|
+
const trashDir = join13(homedir3(), ".Trash");
|
|
11610
12476
|
const base = basename3(path) || "code-viewer-trash-item";
|
|
11611
|
-
const target =
|
|
12477
|
+
const target = join13(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
11612
12478
|
try {
|
|
11613
|
-
|
|
11614
|
-
|
|
12479
|
+
mkdirSync4(trashDir, { recursive: true });
|
|
12480
|
+
renameSync(path, target);
|
|
11615
12481
|
return { ok: true, trashPath: target };
|
|
11616
12482
|
} catch (error) {
|
|
11617
12483
|
return { ok: false, error: String(error) };
|
|
@@ -11642,20 +12508,20 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
11642
12508
|
if (!parentFullPath)
|
|
11643
12509
|
return { ok: false, error: "invalid restore target" };
|
|
11644
12510
|
const original = worktreePath(originalPath);
|
|
11645
|
-
if (
|
|
12511
|
+
if (existsSync6(original))
|
|
11646
12512
|
return { ok: false, error: "restore target exists" };
|
|
11647
12513
|
if (trashPath) {
|
|
11648
12514
|
if (process.platform !== "darwin")
|
|
11649
12515
|
return { ok: false, error: "invalid trash handle" };
|
|
11650
|
-
if (!
|
|
12516
|
+
if (!existsSync6(trashPath))
|
|
11651
12517
|
return { ok: false, error: "trash item not found" };
|
|
11652
12518
|
try {
|
|
11653
|
-
const trashRoot =
|
|
12519
|
+
const trashRoot = join13(homedir3(), ".Trash");
|
|
11654
12520
|
const trashRelative = relative3(trashRoot, trashPath);
|
|
11655
12521
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
11656
12522
|
return { ok: false, error: "invalid trash handle" };
|
|
11657
|
-
|
|
11658
|
-
|
|
12523
|
+
mkdirSync4(dirname3(original), { recursive: true });
|
|
12524
|
+
renameSync(trashPath, original);
|
|
11659
12525
|
return { ok: true };
|
|
11660
12526
|
} catch (error) {
|
|
11661
12527
|
return { ok: false, error: String(error) };
|
|
@@ -11800,11 +12666,11 @@ async function handleCreateDirectory(req) {
|
|
|
11800
12666
|
const targetPath = dir ? `${dir}/${name}` : name;
|
|
11801
12667
|
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
11802
12668
|
return text("invalid target", 400);
|
|
11803
|
-
const target =
|
|
11804
|
-
if (
|
|
12669
|
+
const target = join13(parent, name);
|
|
12670
|
+
if (existsSync6(target))
|
|
11805
12671
|
return text("already exists", 409);
|
|
11806
12672
|
try {
|
|
11807
|
-
|
|
12673
|
+
mkdirSync4(target, { recursive: false });
|
|
11808
12674
|
} catch (error) {
|
|
11809
12675
|
if (error.code === "EEXIST")
|
|
11810
12676
|
return text("already exists", 409);
|
|
@@ -11850,7 +12716,7 @@ function annotationSse(kind, sessionId, entryId) {
|
|
|
11850
12716
|
}
|
|
11851
12717
|
async function handleAnnotations(req) {
|
|
11852
12718
|
if (req.method === "GET")
|
|
11853
|
-
return json2(loadAnnotationsState(cwd));
|
|
12719
|
+
return json2(await loadAnnotationsState(cwd));
|
|
11854
12720
|
if (req.method !== "POST")
|
|
11855
12721
|
return text("method not allowed", 405);
|
|
11856
12722
|
if (!sideEffectRequestAllowed(req))
|
|
@@ -11874,8 +12740,8 @@ async function handleAnnotations(req) {
|
|
|
11874
12740
|
const action = body.action;
|
|
11875
12741
|
if (action === "start") {
|
|
11876
12742
|
const title = typeof body.title === "string" ? body.title : "";
|
|
11877
|
-
const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
|
|
11878
|
-
saveAnnotationsState(cwd, started.state);
|
|
12743
|
+
const started = startAnnotationSession(await loadAnnotationsState(cwd), title, new Date().toISOString());
|
|
12744
|
+
await saveAnnotationsState(cwd, started.state);
|
|
11879
12745
|
annotationSse("start", started.session.id);
|
|
11880
12746
|
return json2({ ok: true, session: started.session });
|
|
11881
12747
|
}
|
|
@@ -11893,7 +12759,7 @@ async function handleAnnotations(req) {
|
|
|
11893
12759
|
if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
|
|
11894
12760
|
return text("forbidden", 403);
|
|
11895
12761
|
}
|
|
11896
|
-
const result = addAnnotationEntry(loadAnnotationsState(cwd), {
|
|
12762
|
+
const result = addAnnotationEntry(await loadAnnotationsState(cwd), {
|
|
11897
12763
|
session_id: typeof body.session_id === "string" ? body.session_id : undefined,
|
|
11898
12764
|
session_title: typeof body.session_title === "string" ? body.session_title : undefined,
|
|
11899
12765
|
path,
|
|
@@ -11908,7 +12774,7 @@ async function handleAnnotations(req) {
|
|
|
11908
12774
|
}, new Date().toISOString());
|
|
11909
12775
|
if (result.ok === false)
|
|
11910
12776
|
return text(result.error, 400);
|
|
11911
|
-
saveAnnotationsState(cwd, result.state);
|
|
12777
|
+
await saveAnnotationsState(cwd, result.state);
|
|
11912
12778
|
annotationSse("add", result.session.id, result.entry.id);
|
|
11913
12779
|
return json2({
|
|
11914
12780
|
ok: true,
|
|
@@ -11922,14 +12788,14 @@ async function handleAnnotations(req) {
|
|
|
11922
12788
|
const id = typeof body.id === "string" ? body.id : "";
|
|
11923
12789
|
if (!id)
|
|
11924
12790
|
return text("invalid id", 400);
|
|
11925
|
-
const result = moveAnnotationEntry(loadAnnotationsState(cwd), id, {
|
|
12791
|
+
const result = moveAnnotationEntry(await loadAnnotationsState(cwd), id, {
|
|
11926
12792
|
before_id: typeof body.before_id === "string" ? body.before_id : undefined,
|
|
11927
12793
|
after_id: typeof body.after_id === "string" ? body.after_id : undefined,
|
|
11928
12794
|
position: typeof body.position === "number" ? body.position : undefined
|
|
11929
12795
|
});
|
|
11930
12796
|
if (result.ok === false)
|
|
11931
12797
|
return text(result.error, 400);
|
|
11932
|
-
saveAnnotationsState(cwd, result.state);
|
|
12798
|
+
await saveAnnotationsState(cwd, result.state);
|
|
11933
12799
|
annotationSse("update", result.session.id, result.entry.id);
|
|
11934
12800
|
return json2({
|
|
11935
12801
|
ok: true,
|
|
@@ -11941,9 +12807,9 @@ async function handleAnnotations(req) {
|
|
|
11941
12807
|
const id = typeof body.id === "string" ? body.id : "";
|
|
11942
12808
|
if (!id)
|
|
11943
12809
|
return text("invalid id", 400);
|
|
11944
|
-
const result = deleteAnnotationById(loadAnnotationsState(cwd), id);
|
|
12810
|
+
const result = deleteAnnotationById(await loadAnnotationsState(cwd), id);
|
|
11945
12811
|
if (result.removed) {
|
|
11946
|
-
saveAnnotationsState(cwd, result.state);
|
|
12812
|
+
await saveAnnotationsState(cwd, result.state);
|
|
11947
12813
|
annotationSse("delete");
|
|
11948
12814
|
}
|
|
11949
12815
|
return json2({ ok: true, removed: result.removed });
|
|
@@ -11953,10 +12819,10 @@ async function handleAnnotations(req) {
|
|
|
11953
12819
|
const title = typeof body.title === "string" ? body.title : "";
|
|
11954
12820
|
if (!id)
|
|
11955
12821
|
return text("invalid id", 400);
|
|
11956
|
-
const result = renameAnnotationSession(loadAnnotationsState(cwd), id, title);
|
|
12822
|
+
const result = renameAnnotationSession(await loadAnnotationsState(cwd), id, title);
|
|
11957
12823
|
if (!result.renamed)
|
|
11958
12824
|
return text("session not found", 404);
|
|
11959
|
-
saveAnnotationsState(cwd, result.state);
|
|
12825
|
+
await saveAnnotationsState(cwd, result.state);
|
|
11960
12826
|
annotationSse("update", id);
|
|
11961
12827
|
return json2({ ok: true });
|
|
11962
12828
|
}
|
|
@@ -11964,18 +12830,18 @@ async function handleAnnotations(req) {
|
|
|
11964
12830
|
const id = typeof body.id === "string" ? body.id : "";
|
|
11965
12831
|
if (!id)
|
|
11966
12832
|
return text("invalid id", 400);
|
|
11967
|
-
const result = updateAnnotationEntry(loadAnnotationsState(cwd), id, {
|
|
12833
|
+
const result = updateAnnotationEntry(await loadAnnotationsState(cwd), id, {
|
|
11968
12834
|
title: typeof body.title === "string" ? body.title : undefined,
|
|
11969
12835
|
body: typeof body.body === "string" ? body.body : undefined
|
|
11970
12836
|
});
|
|
11971
12837
|
if (result.ok === false)
|
|
11972
12838
|
return text(result.error, 400);
|
|
11973
|
-
saveAnnotationsState(cwd, result.state);
|
|
12839
|
+
await saveAnnotationsState(cwd, result.state);
|
|
11974
12840
|
annotationSse("update", undefined, id);
|
|
11975
12841
|
return json2({ ok: true, entry: result.entry });
|
|
11976
12842
|
}
|
|
11977
12843
|
if (action === "clear") {
|
|
11978
|
-
saveAnnotationsState(cwd, emptyAnnotationsState());
|
|
12844
|
+
await saveAnnotationsState(cwd, emptyAnnotationsState());
|
|
11979
12845
|
annotationSse("clear");
|
|
11980
12846
|
return json2({ ok: true });
|
|
11981
12847
|
}
|
|
@@ -12044,8 +12910,8 @@ var init_preview = __esm(async () => {
|
|
|
12044
12910
|
init_search();
|
|
12045
12911
|
init_server_registry();
|
|
12046
12912
|
init_worktree_watcher();
|
|
12047
|
-
WEB_ROOT =
|
|
12048
|
-
VERSION = JSON.parse(
|
|
12913
|
+
WEB_ROOT = join13(ROOT, "web");
|
|
12914
|
+
VERSION = JSON.parse(readFileSync4(join13(ROOT, "package.json"), "utf8")).version;
|
|
12049
12915
|
DEFAULT_ARGS = ["HEAD"];
|
|
12050
12916
|
WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
|
|
12051
12917
|
LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
@@ -12149,6 +13015,12 @@ var init_preview = __esm(async () => {
|
|
|
12149
13015
|
if (dbResponse)
|
|
12150
13016
|
return dbResponse;
|
|
12151
13017
|
}
|
|
13018
|
+
if (url.pathname.startsWith("/_state/")) {
|
|
13019
|
+
const { handleStateRoute: handleStateRoute2 } = await Promise.resolve().then(() => (init_state_route(), exports_state_route));
|
|
13020
|
+
const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed);
|
|
13021
|
+
if (stateResponse)
|
|
13022
|
+
return stateResponse;
|
|
13023
|
+
}
|
|
12152
13024
|
if (url.pathname === "/_annotations")
|
|
12153
13025
|
return handleAnnotations(req);
|
|
12154
13026
|
if (url.pathname === "/_refs")
|
|
@@ -12237,6 +13109,7 @@ data: ok
|
|
|
12237
13109
|
excludeNames: scopeExcludeNames,
|
|
12238
13110
|
watch,
|
|
12239
13111
|
initialScanMode: "async",
|
|
13112
|
+
maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
|
|
12240
13113
|
onUpdate: triggerUpdate,
|
|
12241
13114
|
onError: (error) => {
|
|
12242
13115
|
const message = error instanceof Error ? error.message : String(error);
|