termdock 1.4.55 → 1.4.57
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/client/assets/ModelPreview-Cc96uOV4.js +4108 -0
- package/dist/client/assets/{abnfDiagram-VRR7QNED-v9KOyvUA.js → abnfDiagram-VRR7QNED-DZYgxd_m.js} +1 -1
- package/dist/client/assets/{arc-Dokn57ft.js → arc-CUTAoWR1.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B2_2J4-w.js → architectureDiagram-ZJ3FMSHR-CaRSesi9.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-DFl0I-w9.js → blockDiagram-677ZJIJ3-D5XDdY80.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-Bo1Hy5ly.js → c4Diagram-LMCZKHZV-Bv99p2gv.js} +1 -1
- package/dist/client/assets/channel-BgCPMo_A.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-DVTvlK-j.js → chunk-2Q5K7J3B-VVyNmkmL.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-NdC1yaOj.js → chunk-32BRIVSS-EAEC1KDa.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-ZRKNZVGo.js → chunk-5VM5RSS4-DdF3_HPI.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-CzhNQmEu.js → chunk-EX3LRPZG-CAE4jduk.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-BL76uLj3.js → chunk-JWPE2WC7-BO8M7yBD.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-ZNx7ZEmO.js → chunk-MOJQB5TN-Bx9n-q1Y.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-xxcXIEwu.js → chunk-RYQCIY6F-DUMAVoqS.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-Bg_lp0y3.js → chunk-V7JOEXUC-Df8pttJR.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-C93CSXty.js → chunk-VR4S4FIN-B6B0MzBC.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-DHn8Vn3c.js → chunk-XXDRQBXY-BMpgnm2D.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-DXBrcRht.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-DXBrcRht.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-Baif6bSx.js → cose-bilkent-JH36ORCC-CLynu2qg.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-DryF4Xhm.js → cynefin-VYW2F7L2-BLva0hv1.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-BdlUIn8W.js → cynefinDiagram-TSTJHNR4-Cu0GsUas.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-C-D019df.js → dagre-VKFMJZFB-D4NGJ5mV.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-DgdPi3di.js → diagram-FQU43EPY-QcK_o6qr.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-CZ-lA51n.js → diagram-G47NLZAW-Dyg4THNX.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-A698GPm8.js → diagram-NH7WQ7WH-0uQxu1x8.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DR695wJg.js → diagram-OA4YK3LP-GiXoX0mz.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-jNiC3Mji.js → diagram-WEI45ONY-CUgxdG_h.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DV-zX_Gy.js → ebnfDiagram-CCIWWBDH-CXMOFJem.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-q8tX38xm.js → erDiagram-Q63AITRT-BuwqDEKI.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-ChsZYCRf.js → flowDiagram-23GEKE2U-4_4ENI5h.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-BqhsmAM7.js → ganttDiagram-NO4QXBWP-CQrNj7GA.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BnUGLA-M.js → gitGraphDiagram-IHSO6WYX-DwLebu8y.js} +1 -1
- package/dist/client/assets/index-B-ASvk_D.css +1 -0
- package/dist/client/assets/index-C_4nBgHt.js +438 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-DB2TpJYj.js → infoDiagram-FWYZ7A6U-CXxo6ay4.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-Bt3UPJD3.js → ishikawaDiagram-FXEZZL3T-Dm6F00o3.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-DiW3ZF-j.js → journeyDiagram-5HDEW3XC-D0IivWuB.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-DIqWSzdn.js → kanban-definition-HUTT4EX6-DSUdfCUg.js} +1 -1
- package/dist/client/assets/{linear-DseQ2yoz.js → linear-80DX1B10.js} +1 -1
- package/dist/client/assets/{mermaid.core-p5MzsEWg.js → mermaid.core-DwdnFhTj.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-Bcr8vE-x.js → mindmap-definition-LN4V7U3C-DBRmYihp.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-BIgxPBoQ.js → pegDiagram-2B236MQR-Cn6RnMqR.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-CY4Lg0At.js → pieDiagram-ENE6RG2P-DyXQNB8P.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CIBEQHQ4.js → quadrantDiagram-ABIIQ3AL-IdtNQ0W_.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-Caq1SbrK.js → railroadDiagram-RFXS5EU6-DcQPaOmc.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-B6ZlT1wV.js → requirementDiagram-TGXJPOKE-DSFn8fnf.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-B1f4JOr-.js → sankeyDiagram-HTMAVEWB-CxU-S3Dj.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-CB12eCRf.js → sequenceDiagram-DBY2YBRQ-K6Q-rsSo.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-BTKjMjPw.js → sizeCapture-X5ZJPWSS-D4Bt9ZVt.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-BwoS0_yj.js → stateDiagram-2N3HPSRC-CINQ6i8C.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-DM_9UWsD.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-CyY7nmOK.js → swimlanes-5IMT3BWC-CIF_FKrE.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI76vK0H.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-DuJhrZL3.js → timeline-definition-FHXFAJF6-zvFZjTxi.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-QY6zAcOu.js → vennDiagram-L72KCM5P-X7O88mEN.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-tCRSKFnO.js → wardleyDiagram-EHGQE667-CMgPIEJC.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-hrmaVtk8.js → xychartDiagram-FW5EYKEG-C6DhA7s3.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/routes/filesystem.js +402 -35
- package/package.json +3 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/channel-CMy1yWmT.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CWL2Kj7x.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CWL2Kj7x.js +0 -1
- package/dist/client/assets/index-7oItXRxL.js +0 -436
- package/dist/client/assets/index-BxDTdORQ.css +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CPTGG4BJ.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-BsEBNolb.js +0 -8
|
@@ -13,6 +13,8 @@ import { inspectBinaryFile } from '../utils/binaryFile.js';
|
|
|
13
13
|
const router = Router();
|
|
14
14
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
15
15
|
const MAX_IMAGE_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
|
|
16
|
+
const MAX_HTML_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
|
|
17
|
+
const MAX_VIDEO_PREVIEW_SIZE = 4 * 1024 * 1024 * 1024; // 4GB
|
|
16
18
|
const MAX_DOWNLOAD_SIZE = 200 * 1024 * 1024; // 200MB
|
|
17
19
|
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024; // 100MB per file
|
|
18
20
|
const MAX_UPLOAD_FILES = 50; // max 50 files per upload
|
|
@@ -44,6 +46,128 @@ const RESTORE_CONFIRM_PHRASES = new Set(['丢弃改动', 'discard changes']);
|
|
|
44
46
|
const FS_IO_LOG_NAME = 'fs-io.log';
|
|
45
47
|
const activeDiffSlots = new Map();
|
|
46
48
|
const activeIoSlots = new Map();
|
|
49
|
+
const sharedWatchRegistry = new Map();
|
|
50
|
+
const SHARED_WATCH_EVICT_DELAY_MS = 60_000;
|
|
51
|
+
function sharedWatchCallback(rootPath, entry) {
|
|
52
|
+
return (error, events) => {
|
|
53
|
+
if (entry.failure)
|
|
54
|
+
return;
|
|
55
|
+
if (error) {
|
|
56
|
+
entry.failure = error.message || 'watch-error';
|
|
57
|
+
const reason = entry.failure;
|
|
58
|
+
for (const client of entry.clients) {
|
|
59
|
+
if (!client.closed)
|
|
60
|
+
client.enqueue({ type: 'rescan-required', path: rootPath, reason });
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const event of events) {
|
|
65
|
+
const changedPath = path.resolve(event.path);
|
|
66
|
+
if (!isPathInside(rootPath, changedPath) || isIgnoredWatchPath(rootPath, changedPath))
|
|
67
|
+
continue;
|
|
68
|
+
if (event.type === 'delete') {
|
|
69
|
+
for (const client of entry.clients) {
|
|
70
|
+
if (!client.closed)
|
|
71
|
+
client.enqueue({ type: 'deleted', path: changedPath });
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
fs.promises.lstat(changedPath)
|
|
76
|
+
.then((stat) => {
|
|
77
|
+
const payload = {
|
|
78
|
+
type: event.type === 'create' ? 'created' : 'updated',
|
|
79
|
+
path: changedPath,
|
|
80
|
+
entry: toFileEntry(changedPath, stat),
|
|
81
|
+
};
|
|
82
|
+
for (const client of entry.clients) {
|
|
83
|
+
if (!client.closed)
|
|
84
|
+
client.enqueue(payload);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.catch(() => {
|
|
88
|
+
for (const client of entry.clients) {
|
|
89
|
+
if (!client.closed)
|
|
90
|
+
client.enqueue({ type: 'deleted', path: changedPath });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function acquireSharedWatch(rootPath, client) {
|
|
97
|
+
let entry = sharedWatchRegistry.get(rootPath);
|
|
98
|
+
if (!entry) {
|
|
99
|
+
entry = {
|
|
100
|
+
refs: 0,
|
|
101
|
+
subscription: null,
|
|
102
|
+
failure: null,
|
|
103
|
+
clients: new Set(),
|
|
104
|
+
inFlight: null,
|
|
105
|
+
evictTimer: null,
|
|
106
|
+
};
|
|
107
|
+
sharedWatchRegistry.set(rootPath, entry);
|
|
108
|
+
}
|
|
109
|
+
if (entry.evictTimer) {
|
|
110
|
+
clearTimeout(entry.evictTimer);
|
|
111
|
+
entry.evictTimer = null;
|
|
112
|
+
}
|
|
113
|
+
entry.refs += 1;
|
|
114
|
+
entry.clients.add(client);
|
|
115
|
+
if (entry.failure)
|
|
116
|
+
return null;
|
|
117
|
+
if (entry.subscription)
|
|
118
|
+
return rootPath;
|
|
119
|
+
if (!entry.inFlight) {
|
|
120
|
+
entry.inFlight = watcher.subscribe(rootPath, sharedWatchCallback(rootPath, entry), {
|
|
121
|
+
ignore: Array.from(WATCH_IGNORED_NAMES).map((name) => `**/${name}/**`),
|
|
122
|
+
})
|
|
123
|
+
.then((subscription) => {
|
|
124
|
+
const current = sharedWatchRegistry.get(rootPath);
|
|
125
|
+
if (!current || current.refs === 0) {
|
|
126
|
+
// Nobody is watching anymore (client disconnected mid-scan): free
|
|
127
|
+
// the freshly built subscription instead of leaking its DirTree.
|
|
128
|
+
void subscription.unsubscribe().catch(() => undefined);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
current.subscription = subscription;
|
|
132
|
+
return subscription;
|
|
133
|
+
})
|
|
134
|
+
.catch((error) => {
|
|
135
|
+
const current = sharedWatchRegistry.get(rootPath);
|
|
136
|
+
if (current)
|
|
137
|
+
current.failure = error instanceof Error ? error.message : String(error);
|
|
138
|
+
return null;
|
|
139
|
+
})
|
|
140
|
+
.finally(() => {
|
|
141
|
+
const current = sharedWatchRegistry.get(rootPath);
|
|
142
|
+
if (current)
|
|
143
|
+
current.inFlight = null;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
await entry.inFlight;
|
|
147
|
+
const current = sharedWatchRegistry.get(rootPath);
|
|
148
|
+
return current && !current.failure ? rootPath : null;
|
|
149
|
+
}
|
|
150
|
+
function releaseSharedWatch(rootPath, client) {
|
|
151
|
+
client.closed = true;
|
|
152
|
+
const entry = sharedWatchRegistry.get(rootPath);
|
|
153
|
+
if (!entry)
|
|
154
|
+
return;
|
|
155
|
+
entry.clients.delete(client);
|
|
156
|
+
entry.refs -= 1;
|
|
157
|
+
if (entry.refs > 0)
|
|
158
|
+
return;
|
|
159
|
+
if (entry.evictTimer)
|
|
160
|
+
clearTimeout(entry.evictTimer);
|
|
161
|
+
entry.evictTimer = setTimeout(() => {
|
|
162
|
+
const current = sharedWatchRegistry.get(rootPath);
|
|
163
|
+
if (!current || current.refs > 0)
|
|
164
|
+
return;
|
|
165
|
+
sharedWatchRegistry.delete(rootPath);
|
|
166
|
+
void current.subscription?.unsubscribe().catch(() => undefined);
|
|
167
|
+
current.subscription = null;
|
|
168
|
+
}, SHARED_WATCH_EVICT_DELAY_MS);
|
|
169
|
+
entry.evictTimer.unref?.();
|
|
170
|
+
}
|
|
47
171
|
const untrackedJobs = new Map();
|
|
48
172
|
const nestedGitRootsCache = new Map();
|
|
49
173
|
const gitActionJobs = new Map();
|
|
@@ -287,6 +411,88 @@ const IMAGE_MIME_BY_EXT = {
|
|
|
287
411
|
function getImageMimeType(filePath) {
|
|
288
412
|
return IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
|
|
289
413
|
}
|
|
414
|
+
// Video files play in the right sidebar through the /video route, which
|
|
415
|
+
// streams with HTTP Range support so the browser <video> element can seek
|
|
416
|
+
// without loading the whole file into memory. Keep the map to formats the
|
|
417
|
+
// browser can actually decode (mkv/avi intentionally excluded).
|
|
418
|
+
const VIDEO_MIME_BY_EXT = {
|
|
419
|
+
'.mp4': 'video/mp4',
|
|
420
|
+
'.m4v': 'video/x-m4v',
|
|
421
|
+
'.webm': 'video/webm',
|
|
422
|
+
'.mov': 'video/quicktime',
|
|
423
|
+
'.ogv': 'video/ogg',
|
|
424
|
+
'.mpeg': 'video/mpeg',
|
|
425
|
+
'.mpg': 'video/mpeg',
|
|
426
|
+
};
|
|
427
|
+
function getVideoMimeType(filePath) {
|
|
428
|
+
return VIDEO_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
|
|
429
|
+
}
|
|
430
|
+
// Single-range `Range: bytes=start-end` / `bytes=-N` parsing. Malformed or
|
|
431
|
+
// multi-range headers fall back to a full 200 response, which browsers accept
|
|
432
|
+
// (they just cannot seek until the file is buffered).
|
|
433
|
+
function parseVideoRange(rangeHeader, total) {
|
|
434
|
+
if (!rangeHeader || total <= 0)
|
|
435
|
+
return { kind: 'full' };
|
|
436
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
|
437
|
+
if (!match)
|
|
438
|
+
return { kind: 'full' };
|
|
439
|
+
const rawStart = match[1];
|
|
440
|
+
const rawEnd = match[2];
|
|
441
|
+
if (rawStart === '' && rawEnd === '')
|
|
442
|
+
return { kind: 'full' };
|
|
443
|
+
if (rawStart === '') {
|
|
444
|
+
const suffixLength = Number(rawEnd);
|
|
445
|
+
if (!Number.isInteger(suffixLength) || suffixLength <= 0)
|
|
446
|
+
return { kind: 'unsatisfiable' };
|
|
447
|
+
return { kind: 'range', start: Math.max(0, total - suffixLength), end: total - 1 };
|
|
448
|
+
}
|
|
449
|
+
const start = Number(rawStart);
|
|
450
|
+
if (!Number.isInteger(start) || start < 0 || start >= total)
|
|
451
|
+
return { kind: 'unsatisfiable' };
|
|
452
|
+
const end = rawEnd === '' ? total - 1 : Math.min(Number(rawEnd), total - 1);
|
|
453
|
+
if (!Number.isInteger(end) || end < start)
|
|
454
|
+
return { kind: 'unsatisfiable' };
|
|
455
|
+
return { kind: 'range', start, end };
|
|
456
|
+
}
|
|
457
|
+
// MIME types for the right-sidebar HTML preview route. The route mirrors the
|
|
458
|
+
// absolute filesystem path in the URL, so every relative resource referenced
|
|
459
|
+
// by a previewed document (css/js/fonts/images/...) is served by the same
|
|
460
|
+
// route with a type the browser can render.
|
|
461
|
+
const HTML_PREVIEW_MIME_BY_EXT = {
|
|
462
|
+
'.html': 'text/html; charset=utf-8',
|
|
463
|
+
'.htm': 'text/html; charset=utf-8',
|
|
464
|
+
'.css': 'text/css; charset=utf-8',
|
|
465
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
466
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
467
|
+
'.json': 'application/json; charset=utf-8',
|
|
468
|
+
'.map': 'application/json; charset=utf-8',
|
|
469
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
470
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
471
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
472
|
+
'.svg': 'image/svg+xml',
|
|
473
|
+
'.png': 'image/png',
|
|
474
|
+
'.jpg': 'image/jpeg',
|
|
475
|
+
'.jpeg': 'image/jpeg',
|
|
476
|
+
'.gif': 'image/gif',
|
|
477
|
+
'.webp': 'image/webp',
|
|
478
|
+
'.avif': 'image/avif',
|
|
479
|
+
'.ico': 'image/x-icon',
|
|
480
|
+
'.bmp': 'image/bmp',
|
|
481
|
+
'.woff': 'font/woff',
|
|
482
|
+
'.woff2': 'font/woff2',
|
|
483
|
+
'.ttf': 'font/ttf',
|
|
484
|
+
'.otf': 'font/otf',
|
|
485
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
486
|
+
'.wasm': 'application/wasm',
|
|
487
|
+
'.mp4': 'video/mp4',
|
|
488
|
+
'.webm': 'video/webm',
|
|
489
|
+
'.ogg': 'audio/ogg',
|
|
490
|
+
'.mp3': 'audio/mpeg',
|
|
491
|
+
'.wav': 'audio/wav',
|
|
492
|
+
};
|
|
493
|
+
function getHtmlPreviewMimeType(filePath) {
|
|
494
|
+
return HTML_PREVIEW_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
|
|
495
|
+
}
|
|
290
496
|
function toContentDispositionFilename(name) {
|
|
291
497
|
return name.replace(/["\\\r\n]/g, '_').replace(/[^\x20-\x7E]/g, '_');
|
|
292
498
|
}
|
|
@@ -2187,7 +2393,7 @@ router.get('/watch', async (req, res) => {
|
|
|
2187
2393
|
let closed = false;
|
|
2188
2394
|
let pending = [];
|
|
2189
2395
|
let flushTimer = null;
|
|
2190
|
-
const
|
|
2396
|
+
const attachedRoots = new Map();
|
|
2191
2397
|
const writeEvent = (type, payload) => {
|
|
2192
2398
|
if (closed || res.destroyed)
|
|
2193
2399
|
return;
|
|
@@ -2216,46 +2422,26 @@ router.get('/watch', async (req, res) => {
|
|
|
2216
2422
|
}
|
|
2217
2423
|
};
|
|
2218
2424
|
writeEvent('ready', { roots });
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
for (const event of events) {
|
|
2229
|
-
const changedPath = path.resolve(event.path);
|
|
2230
|
-
if (!isPathInside(rootPath, changedPath) || isIgnoredWatchPath(rootPath, changedPath))
|
|
2231
|
-
continue;
|
|
2232
|
-
if (event.type === 'delete') {
|
|
2233
|
-
enqueue({ type: 'deleted', path: changedPath });
|
|
2234
|
-
continue;
|
|
2235
|
-
}
|
|
2236
|
-
fs.promises.lstat(changedPath)
|
|
2237
|
-
.then((stat) => {
|
|
2238
|
-
enqueue({ type: event.type === 'create' ? 'created' : 'updated', path: changedPath, entry: toFileEntry(changedPath, stat) });
|
|
2239
|
-
})
|
|
2240
|
-
.catch(() => {
|
|
2241
|
-
enqueue({ type: 'deleted', path: changedPath });
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
|
-
}, {
|
|
2245
|
-
ignore: Array.from(WATCH_IGNORED_NAMES).map((name) => `**/${name}/**`),
|
|
2246
|
-
});
|
|
2247
|
-
subscriptions.push(subscription);
|
|
2248
|
-
}
|
|
2249
|
-
catch (error) {
|
|
2250
|
-
enqueue({ type: 'rescan-required', path: rootPath, reason: error instanceof Error ? error.message : 'watch-unavailable' });
|
|
2425
|
+
const attachRoot = async (rootPath) => {
|
|
2426
|
+
const client = { closed: false, enqueue };
|
|
2427
|
+
attachedRoots.set(rootPath, client);
|
|
2428
|
+
const ok = await acquireSharedWatch(rootPath, client);
|
|
2429
|
+
if (closed)
|
|
2430
|
+
return;
|
|
2431
|
+
if (!ok) {
|
|
2432
|
+
const reason = sharedWatchRegistry.get(rootPath)?.failure ?? 'watch-unavailable';
|
|
2433
|
+
enqueue({ type: 'rescan-required', path: rootPath, reason });
|
|
2251
2434
|
}
|
|
2435
|
+
};
|
|
2436
|
+
for (const rootPath of roots) {
|
|
2437
|
+
await attachRoot(rootPath);
|
|
2252
2438
|
}
|
|
2253
2439
|
req.on('close', () => {
|
|
2254
2440
|
closed = true;
|
|
2255
2441
|
if (flushTimer)
|
|
2256
2442
|
clearTimeout(flushTimer);
|
|
2257
|
-
for (const
|
|
2258
|
-
|
|
2443
|
+
for (const [rootPath, client] of attachedRoots) {
|
|
2444
|
+
releaseSharedWatch(rootPath, client);
|
|
2259
2445
|
}
|
|
2260
2446
|
});
|
|
2261
2447
|
});
|
|
@@ -2642,6 +2828,187 @@ router.get('/blob', async (req, res) => {
|
|
|
2642
2828
|
res.status(status).json({ ...payload, code });
|
|
2643
2829
|
}
|
|
2644
2830
|
});
|
|
2831
|
+
// Serve video files for the right-sidebar preview. The route streams with
|
|
2832
|
+
// HTTP Range support, so <video> playback starts quickly and seeking works
|
|
2833
|
+
// without buffering the whole file in the browser or server memory.
|
|
2834
|
+
router.get('/video', async (req, res) => {
|
|
2835
|
+
const requestId = ++fsIoRequestSeq;
|
|
2836
|
+
const startedAt = Date.now();
|
|
2837
|
+
const requestedPath = req.query.path;
|
|
2838
|
+
const action = getRequestAction(req, 'view_file');
|
|
2839
|
+
const requestSlotId = typeof req.query.requestSlotId === 'string' ? req.query.requestSlotId : undefined;
|
|
2840
|
+
const controller = new AbortController();
|
|
2841
|
+
const abortRequest = () => {
|
|
2842
|
+
if (!res.writableEnded)
|
|
2843
|
+
controller.abort(new SupersededRequestError('fs.video'));
|
|
2844
|
+
};
|
|
2845
|
+
req.on('aborted', abortRequest);
|
|
2846
|
+
res.on('close', abortRequest);
|
|
2847
|
+
registerIoSlot({ requestId, op: 'fs.video', action, slotId: requestSlotId, controller, path: requestedPath });
|
|
2848
|
+
let logged = false;
|
|
2849
|
+
const logOnce = (status, entry = {}) => {
|
|
2850
|
+
if (logged)
|
|
2851
|
+
return;
|
|
2852
|
+
logged = true;
|
|
2853
|
+
releaseIoSlot(requestSlotId, requestId);
|
|
2854
|
+
logFsIo({
|
|
2855
|
+
action,
|
|
2856
|
+
op: 'fs.video',
|
|
2857
|
+
id: requestId,
|
|
2858
|
+
startedAt,
|
|
2859
|
+
status,
|
|
2860
|
+
path: requestedPath,
|
|
2861
|
+
...entry,
|
|
2862
|
+
extra: { ...(entry.extra ?? {}), requestSlotId },
|
|
2863
|
+
});
|
|
2864
|
+
};
|
|
2865
|
+
try {
|
|
2866
|
+
if (!requestedPath) {
|
|
2867
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const { resolvedPath, stat, mimeType } = await withTimeout((async () => {
|
|
2871
|
+
const resolvedPath = await pathValidator.validatePathAsync(requestedPath);
|
|
2872
|
+
throwIfAborted(controller.signal, 'fs.video');
|
|
2873
|
+
const stat = await fs.promises.stat(resolvedPath);
|
|
2874
|
+
throwIfAborted(controller.signal, 'fs.video');
|
|
2875
|
+
if (!stat.isFile()) {
|
|
2876
|
+
throw new Error('Path is not a file');
|
|
2877
|
+
}
|
|
2878
|
+
const mimeType = getVideoMimeType(resolvedPath);
|
|
2879
|
+
if (!mimeType) {
|
|
2880
|
+
const error = new Error('Unsupported video type');
|
|
2881
|
+
error.status = 415;
|
|
2882
|
+
throw error;
|
|
2883
|
+
}
|
|
2884
|
+
if (stat.size > MAX_VIDEO_PREVIEW_SIZE) {
|
|
2885
|
+
const error = new Error('Video is too large to preview');
|
|
2886
|
+
error.code = 'VIDEO_TOO_LARGE';
|
|
2887
|
+
error.status = 413;
|
|
2888
|
+
throw error;
|
|
2889
|
+
}
|
|
2890
|
+
return { resolvedPath, stat, mimeType };
|
|
2891
|
+
})(), FS_ROUTE_TIMEOUT_MS, 'Video preview took too long. The file may be on slow storage or currently blocked by another process.', 'FS_VIDEO_TIMEOUT');
|
|
2892
|
+
const total = stat.size;
|
|
2893
|
+
const range = parseVideoRange(typeof req.headers.range === 'string' ? req.headers.range : undefined, total);
|
|
2894
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
2895
|
+
res.setHeader('Content-Type', mimeType);
|
|
2896
|
+
res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
2897
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
2898
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
2899
|
+
res.setHeader('Content-Disposition', buildContentDisposition('inline', path.basename(resolvedPath)));
|
|
2900
|
+
if (req.method === 'HEAD') {
|
|
2901
|
+
res.setHeader('Content-Length', total.toString());
|
|
2902
|
+
if (range.kind === 'range')
|
|
2903
|
+
res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${total}`);
|
|
2904
|
+
res.end();
|
|
2905
|
+
logOnce('ok', { path: resolvedPath, bytes: total, extra: { mimeType } });
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
if (range.kind === 'unsatisfiable') {
|
|
2909
|
+
res.status(416);
|
|
2910
|
+
res.setHeader('Content-Range', `bytes */${total}`);
|
|
2911
|
+
res.end();
|
|
2912
|
+
logOnce('ok', { path: resolvedPath, bytes: 0, extra: { mimeType, status: 416 } });
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (range.kind === 'range') {
|
|
2916
|
+
res.status(206);
|
|
2917
|
+
res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${total}`);
|
|
2918
|
+
res.setHeader('Content-Length', String(range.end - range.start + 1));
|
|
2919
|
+
}
|
|
2920
|
+
else {
|
|
2921
|
+
res.setHeader('Content-Length', total.toString());
|
|
2922
|
+
}
|
|
2923
|
+
const stream = fs.createReadStream(resolvedPath, range.kind === 'range' ? { start: range.start, end: range.end } : undefined);
|
|
2924
|
+
controller.signal.addEventListener('abort', () => stream.destroy(controller.signal.reason instanceof Error ? controller.signal.reason : undefined), { once: true });
|
|
2925
|
+
const bytesServed = range.kind === 'range' ? range.end - range.start + 1 : total;
|
|
2926
|
+
res.on('finish', () => logOnce('ok', { path: resolvedPath, bytes: bytesServed, extra: { mimeType } }));
|
|
2927
|
+
res.on('close', () => {
|
|
2928
|
+
if (!res.writableEnded)
|
|
2929
|
+
logOnce('error', { path: resolvedPath, bytes: bytesServed, code: 'CLIENT_CLOSED', error: 'Client closed video preview request' });
|
|
2930
|
+
});
|
|
2931
|
+
stream.on('error', (error) => {
|
|
2932
|
+
logOnce('error', { path: resolvedPath, code: 'FS_VIDEO_STREAM_ERROR', error: error instanceof Error ? error.message : 'Failed to read video' });
|
|
2933
|
+
if (!res.headersSent) {
|
|
2934
|
+
const message = error instanceof Error ? error.message : 'Failed to read video';
|
|
2935
|
+
res.status(500).json({ error: message });
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
res.destroy(error instanceof Error ? error : undefined);
|
|
2939
|
+
});
|
|
2940
|
+
stream.pipe(res);
|
|
2941
|
+
}
|
|
2942
|
+
catch (error) {
|
|
2943
|
+
const payload = getErrorPayload(error);
|
|
2944
|
+
const status = error instanceof OperationTimeoutError
|
|
2945
|
+
? 504
|
|
2946
|
+
: typeof error?.status === 'number'
|
|
2947
|
+
? error.status
|
|
2948
|
+
: 403;
|
|
2949
|
+
const code = payload.code ?? (typeof error?.code === 'string' ? error.code : undefined);
|
|
2950
|
+
logOnce('error', { code, error: payload.error });
|
|
2951
|
+
res.status(status).json({ ...payload, code });
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
// Serve workspace files for the right-sidebar HTML preview. The URL path
|
|
2955
|
+
// mirrors the absolute filesystem path (e.g. .../preview/home/user/proj/
|
|
2956
|
+
// index.html), so relative css/js/image references inside a document resolve
|
|
2957
|
+
// to the file's own directory without any content rewriting — the document
|
|
2958
|
+
// URL itself acts as the <base>. The frontend renders this in a sandboxed
|
|
2959
|
+
// iframe (no allow-same-origin), so previewed scripts never gain the termdock
|
|
2960
|
+
// origin.
|
|
2961
|
+
router.get('/preview/*path', async (req, res) => {
|
|
2962
|
+
try {
|
|
2963
|
+
const rawSegments = req.params.path;
|
|
2964
|
+
const segments = Array.isArray(rawSegments) ? rawSegments : rawSegments ? [rawSegments] : [];
|
|
2965
|
+
const requestedPath = `/${segments.join('/')}`;
|
|
2966
|
+
if (!requestedPath || requestedPath === '/') {
|
|
2967
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
const resolvedPath = await pathValidator.validatePathAsync(requestedPath);
|
|
2971
|
+
const stat = await fs.promises.stat(resolvedPath);
|
|
2972
|
+
if (!stat.isFile()) {
|
|
2973
|
+
res.status(400).json({ error: 'Path is not a file' });
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
const mimeType = getHtmlPreviewMimeType(resolvedPath);
|
|
2977
|
+
if (!mimeType) {
|
|
2978
|
+
res.status(415).json({ error: 'Unsupported file type for preview' });
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
if (stat.size > MAX_HTML_PREVIEW_SIZE) {
|
|
2982
|
+
res.status(413).json({
|
|
2983
|
+
error: 'File is too large to preview',
|
|
2984
|
+
code: 'FILE_TOO_LARGE',
|
|
2985
|
+
size: stat.size,
|
|
2986
|
+
maxSize: MAX_HTML_PREVIEW_SIZE,
|
|
2987
|
+
});
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
res.setHeader('Content-Type', mimeType);
|
|
2991
|
+
res.setHeader('Content-Length', stat.size.toString());
|
|
2992
|
+
res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
2993
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
2994
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
2995
|
+
res.setHeader('Content-Disposition', buildContentDisposition('inline', path.basename(resolvedPath)));
|
|
2996
|
+
const stream = fs.createReadStream(resolvedPath);
|
|
2997
|
+
stream.on('error', (error) => {
|
|
2998
|
+
if (!res.headersSent) {
|
|
2999
|
+
const message = error instanceof Error ? error.message : 'Failed to serve file';
|
|
3000
|
+
res.status(500).json({ error: message });
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
res.destroy(error instanceof Error ? error : undefined);
|
|
3004
|
+
});
|
|
3005
|
+
stream.pipe(res);
|
|
3006
|
+
}
|
|
3007
|
+
catch (error) {
|
|
3008
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
3009
|
+
res.status(403).json({ error: message });
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
2645
3012
|
// Download any file as an attachment. Works in both PWA and normal browser
|
|
2646
3013
|
// contexts — the frontend fetches this as a blob and either pipes it through
|
|
2647
3014
|
// the File System Access API (showSaveFilePicker, desktop PWA/Chromium) or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termdock",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.57",
|
|
4
4
|
"description": "A complete web-based terminal application with modern UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": ".desktop-dist/main.js",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"refractor": "^2.10.1",
|
|
84
84
|
"swiper": "^12.1.3",
|
|
85
85
|
"tailwind-merge": "^2.2.0",
|
|
86
|
+
"three": "^0.185.1",
|
|
86
87
|
"uuid": "^13.0.0",
|
|
87
88
|
"web-push": "^3.6.7",
|
|
88
89
|
"ws": "^8.21.0",
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
"@types/node": "^20.11.0",
|
|
100
101
|
"@types/react": "^18.2.0",
|
|
101
102
|
"@types/react-dom": "^18.2.0",
|
|
103
|
+
"@types/three": "^0.185.3",
|
|
102
104
|
"@types/uuid": "^10.0.0",
|
|
103
105
|
"@types/web-push": "^3.6.4",
|
|
104
106
|
"@types/ws": "^8.18.1",
|
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.57",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
|
-
"dependencyHash": "sha256-
|
|
9
|
-
"serverBundleHash": "sha256
|
|
8
|
+
"dependencyHash": "sha256-HxC6p3b4kFesefw+vmNYJtPtdIKJaZR1jFNb7DRJqjU=",
|
|
9
|
+
"serverBundleHash": "sha256-J9kh6qO+A3xC2pDnOVTjabHIwJicckHS3zzeXwAqheM=",
|
|
10
10
|
"entrypoint": "dist/server/cli.js",
|
|
11
11
|
"clientEntrypoint": "dist/client/index.html"
|
|
12
12
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as a,C as n}from"./mermaid.core-p5MzsEWg.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-Bg_lp0y3.js";import{b as i}from"./mermaid.core-p5MzsEWg.js";import"./chunk-5VM5RSS4-ZRKNZVGo.js";import"./chunk-XXDRQBXY-DHn8Vn3c.js";import"./chunk-VR4S4FIN-C93CSXty.js";import"./chunk-32BRIVSS-NdC1yaOj.js";import"./purify.es-CC4Brkr_.js";import"./index-7oItXRxL.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-Bg_lp0y3.js";import{b as i}from"./mermaid.core-p5MzsEWg.js";import"./chunk-5VM5RSS4-ZRKNZVGo.js";import"./chunk-XXDRQBXY-DHn8Vn3c.js";import"./chunk-VR4S4FIN-C93CSXty.js";import"./chunk-32BRIVSS-NdC1yaOj.js";import"./purify.es-CC4Brkr_.js";import"./index-7oItXRxL.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
|