dsh-vscode-workbench 0.1.11 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +30 -6
- package/README.md +30 -6
- package/lib/client.js +421 -182
- package/lib/client.js.map +1 -1
- package/lib/index.js +185 -43
- package/lib/index.js.map +1 -1
- package/lib/types/client/Workbench.d.ts +11 -0
- package/lib/types/client/terminal.d.ts +1 -0
- package/lib/types/client/websocket.d.ts +15 -0
- package/lib/types/terminal.d.ts +5 -0
- package/lib/types/watch-ignore.d.ts +8 -0
- package/package.json +17 -16
- package/src/client/DshConversationPanel.tsx +2 -0
- package/src/client/TerminalPanel.tsx +12 -5
- package/src/client/Workbench.tsx +164 -18
- package/src/client/styles.ts +15 -6
- package/src/client/terminal.ts +15 -4
- package/src/client/websocket.ts +50 -0
- package/src/host.ts +47 -14
- package/src/index.ts +9 -2
- package/src/terminal.ts +27 -2
- package/src/watch-ignore.ts +63 -0
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat
|
|
2
|
+
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
4
4
|
import chokidar from "chokidar";
|
|
5
5
|
import { WebSocket, WebSocketServer } from "ws";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
-
import { constants, realpathSync } from "node:fs";
|
|
8
|
+
import { chmodSync, constants, realpathSync, statSync } from "node:fs";
|
|
9
9
|
import * as pty from "node-pty";
|
|
10
10
|
//#region src/wire.ts
|
|
11
11
|
var WorkbenchError = class extends Error {
|
|
@@ -84,7 +84,7 @@ function authorize(req, res, connection) {
|
|
|
84
84
|
}
|
|
85
85
|
//#endregion
|
|
86
86
|
//#region src/host.ts
|
|
87
|
-
const require = createRequire(import.meta.url);
|
|
87
|
+
const require$1 = createRequire(import.meta.url);
|
|
88
88
|
const TEXT_LIMIT = 5242880;
|
|
89
89
|
const PROCESS_TIMEOUT = 3e4;
|
|
90
90
|
function cwdFor(ctx, sessionId) {
|
|
@@ -173,21 +173,44 @@ async function readDocument(cwd, path) {
|
|
|
173
173
|
await handle.close();
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
+
const pendingSaves = /* @__PURE__ */ new Map();
|
|
176
177
|
async function saveDocument(cwd, path, content, expected) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
const resolved = await workspacePath(cwd, path);
|
|
179
|
+
const absolute = await realpath(resolved.absolute);
|
|
180
|
+
const previous = pendingSaves.get(absolute) ?? Promise.resolve();
|
|
181
|
+
let release;
|
|
182
|
+
const pending = new Promise((resolve) => {
|
|
183
|
+
release = resolve;
|
|
183
184
|
});
|
|
185
|
+
pendingSaves.set(absolute, pending);
|
|
186
|
+
await previous;
|
|
187
|
+
let temp;
|
|
184
188
|
try {
|
|
189
|
+
const checkRevision = async () => {
|
|
190
|
+
const checked = await workspacePath(cwd, path);
|
|
191
|
+
if (await realpath(checked.absolute) !== absolute || (await readDocument(cwd, path)).revision !== expected) throw new WorkbenchError("revision-conflict", "file changed on disk; reload before saving", 409);
|
|
192
|
+
};
|
|
193
|
+
await checkRevision();
|
|
194
|
+
const info = await stat(absolute);
|
|
195
|
+
temp = join(dirname(absolute), `.${randomUUID()}.dsh-vscode.tmp`);
|
|
196
|
+
const handle = await open(temp, "wx", info.mode & 511);
|
|
197
|
+
try {
|
|
198
|
+
await handle.writeFile(content, { encoding: "utf8" });
|
|
199
|
+
await handle.chmod(info.mode & 511);
|
|
200
|
+
} finally {
|
|
201
|
+
await handle.close();
|
|
202
|
+
}
|
|
203
|
+
await checkRevision();
|
|
185
204
|
await rename(temp, absolute);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
return await readDocument(cwd, path);
|
|
206
|
+
} finally {
|
|
207
|
+
try {
|
|
208
|
+
if (temp !== void 0) await rm(temp, { force: true });
|
|
209
|
+
} finally {
|
|
210
|
+
release();
|
|
211
|
+
if (pendingSaves.get(absolute) === pending) pendingSaves.delete(absolute);
|
|
212
|
+
}
|
|
189
213
|
}
|
|
190
|
-
return readDocument(cwd, path);
|
|
191
214
|
}
|
|
192
215
|
async function createEntry(cwd, path, kind) {
|
|
193
216
|
const { absolute } = await workspacePath(cwd, path, true);
|
|
@@ -222,8 +245,8 @@ async function renameEntry(cwd, path, nextPath) {
|
|
|
222
245
|
await rename(source.absolute, target.absolute);
|
|
223
246
|
}
|
|
224
247
|
async function deleteEntry(cwd, path) {
|
|
225
|
-
|
|
226
|
-
|
|
248
|
+
const { root, absolute } = await workspacePath(cwd, path);
|
|
249
|
+
if (absolute === root) throw new WorkbenchError("root-delete", "cannot delete workspace root", 403);
|
|
227
250
|
await rm(absolute, {
|
|
228
251
|
recursive: true,
|
|
229
252
|
force: false
|
|
@@ -257,7 +280,7 @@ async function workspacePreviewFile(cwd, path) {
|
|
|
257
280
|
/** Resolve the PDF.js worker shipped with this plugin's runtime dependency. */
|
|
258
281
|
function pdfWorkerAsset() {
|
|
259
282
|
return {
|
|
260
|
-
path: require.resolve("pdfjs-dist/legacy/build/pdf.worker.min.mjs"),
|
|
283
|
+
path: require$1.resolve("pdfjs-dist/legacy/build/pdf.worker.min.mjs"),
|
|
261
284
|
type: "text/javascript; charset=utf-8"
|
|
262
285
|
};
|
|
263
286
|
}
|
|
@@ -318,7 +341,7 @@ async function searchWorkspace(cwd, request, signal) {
|
|
|
318
341
|
limited: false
|
|
319
342
|
};
|
|
320
343
|
const root = await canonicalRoot(cwd);
|
|
321
|
-
const rg = require("@vscode/ripgrep").rgPath;
|
|
344
|
+
const rg = require$1("@vscode/ripgrep").rgPath;
|
|
322
345
|
const args = [
|
|
323
346
|
"--json",
|
|
324
347
|
"--line-number",
|
|
@@ -443,29 +466,36 @@ async function gitStatus(cwd) {
|
|
|
443
466
|
let ahead = 0;
|
|
444
467
|
let behind = 0;
|
|
445
468
|
const changes = [];
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const xy = parts[1] ?? "..";
|
|
461
|
-
const path = parts.slice(record.startsWith("2 ") ? 9 : 8).join(" ");
|
|
462
|
-
changes.push({
|
|
463
|
-
path,
|
|
464
|
-
index: xy[0] ?? ".",
|
|
465
|
-
worktree: xy[1] ?? ".",
|
|
466
|
-
staged: xy[0] !== ".",
|
|
467
|
-
untracked: false
|
|
469
|
+
const records = result.stdout.split("\0");
|
|
470
|
+
for (let index = 0; index < records.length; index++) {
|
|
471
|
+
const record = records[index];
|
|
472
|
+
if (record.startsWith("# branch.head ")) branch = record.slice(14);
|
|
473
|
+
else if (record.startsWith("# branch.ab ")) {
|
|
474
|
+
const match = /\+(\d+) -(\d+)/.exec(record);
|
|
475
|
+
ahead = Number(match?.[1] ?? 0);
|
|
476
|
+
behind = Number(match?.[2] ?? 0);
|
|
477
|
+
} else if (record.startsWith("? ")) changes.push({
|
|
478
|
+
path: record.slice(2),
|
|
479
|
+
index: "?",
|
|
480
|
+
worktree: "?",
|
|
481
|
+
staged: false,
|
|
482
|
+
untracked: true
|
|
468
483
|
});
|
|
484
|
+
else if (record.startsWith("1 ") || record.startsWith("2 ") || record.startsWith("u ")) {
|
|
485
|
+
const renamed = record.startsWith("2 ");
|
|
486
|
+
const parts = record.split(" ");
|
|
487
|
+
const xy = parts[1] ?? "..";
|
|
488
|
+
const path = parts.slice(renamed ? 9 : record.startsWith("u ") ? 10 : 8).join(" ");
|
|
489
|
+
const originalPath = renamed ? records[++index] : void 0;
|
|
490
|
+
changes.push({
|
|
491
|
+
path,
|
|
492
|
+
...originalPath === void 0 ? {} : { originalPath },
|
|
493
|
+
index: xy[0] ?? ".",
|
|
494
|
+
worktree: xy[1] ?? ".",
|
|
495
|
+
staged: xy[0] !== ".",
|
|
496
|
+
untracked: false
|
|
497
|
+
});
|
|
498
|
+
}
|
|
469
499
|
}
|
|
470
500
|
return {
|
|
471
501
|
repository,
|
|
@@ -685,14 +715,14 @@ async function gitAction(cwd, action, path, value) {
|
|
|
685
715
|
}
|
|
686
716
|
/** Resolve the official VS Code Codicon font shipped by @vscode/codicons. */
|
|
687
717
|
function codiconAsset() {
|
|
688
|
-
const packageRoot = dirname(require.resolve("@vscode/codicons/package.json"));
|
|
718
|
+
const packageRoot = dirname(require$1.resolve("@vscode/codicons/package.json"));
|
|
689
719
|
return {
|
|
690
720
|
path: resolve(packageRoot, "dist", "codicon.ttf"),
|
|
691
721
|
type: "font/ttf"
|
|
692
722
|
};
|
|
693
723
|
}
|
|
694
724
|
function monacoAsset(raw) {
|
|
695
|
-
const packageRoot = dirname(require.resolve("monaco-editor/package.json"));
|
|
725
|
+
const packageRoot = dirname(require$1.resolve("monaco-editor/package.json"));
|
|
696
726
|
const path = resolve(packageRoot, "min", raw);
|
|
697
727
|
if (!inside(resolve(packageRoot, "min"), path)) throw new WorkbenchError("asset-invalid", "invalid Monaco asset path", 403);
|
|
698
728
|
return {
|
|
@@ -702,6 +732,7 @@ function monacoAsset(raw) {
|
|
|
702
732
|
}
|
|
703
733
|
//#endregion
|
|
704
734
|
//#region src/terminal.ts
|
|
735
|
+
const require = createRequire(import.meta.url);
|
|
705
736
|
const SENSITIVE = /(api[_-]?key|token|credential|secret|password|passwd|private[_-]?key|^dsh_.*(?:key|token|secret))/i;
|
|
706
737
|
const MAX_TERMINALS = 8;
|
|
707
738
|
const REPLAY_BYTES = 2097152;
|
|
@@ -720,9 +751,30 @@ function defaultTerminalShell(platform = process.platform, env = process.env) {
|
|
|
720
751
|
args: ["-l"]
|
|
721
752
|
};
|
|
722
753
|
}
|
|
754
|
+
/** Add execute bits when a registry package extracted a native helper without them. */
|
|
755
|
+
function ensureExecutable(path) {
|
|
756
|
+
if (process.platform === "win32") return true;
|
|
757
|
+
try {
|
|
758
|
+
const mode = statSync(path).mode;
|
|
759
|
+
if ((mode & 73) === 0) chmodSync(path, mode | 73);
|
|
760
|
+
return true;
|
|
761
|
+
} catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Repair node-pty's macOS/Linux helper, whose npm package currently loses its execute bit. */
|
|
766
|
+
function repairNodePtyHelper() {
|
|
767
|
+
if (process.platform === "win32") return;
|
|
768
|
+
const packageDirectory = dirname(require.resolve("node-pty/package.json"));
|
|
769
|
+
ensureExecutable(join(packageDirectory, "build", "Release", "spawn-helper"));
|
|
770
|
+
ensureExecutable(join(packageDirectory, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"));
|
|
771
|
+
}
|
|
723
772
|
/** Own interactive terminals by their canonical workspace directory. */
|
|
724
773
|
var TerminalManager = class {
|
|
725
774
|
workspaces = /* @__PURE__ */ new Map();
|
|
775
|
+
constructor() {
|
|
776
|
+
repairNodePtyHelper();
|
|
777
|
+
}
|
|
726
778
|
workspace(cwd) {
|
|
727
779
|
return realpathSync(cwd);
|
|
728
780
|
}
|
|
@@ -916,6 +968,87 @@ function installTerminal(ctx) {
|
|
|
916
968
|
}, "dsh-vscode-workbench: terminal teardown");
|
|
917
969
|
}
|
|
918
970
|
//#endregion
|
|
971
|
+
//#region src/watch-ignore.ts
|
|
972
|
+
/** Exclusion rules for the recursive workspace file watcher. */
|
|
973
|
+
/** Directory names skipped by default; large dependency and build trees exhaust inotify watches. */
|
|
974
|
+
const DEFAULT_WATCH_IGNORES = [
|
|
975
|
+
".git",
|
|
976
|
+
".hg",
|
|
977
|
+
".svn",
|
|
978
|
+
"node_modules",
|
|
979
|
+
"bower_components",
|
|
980
|
+
".pnpm-store",
|
|
981
|
+
".yarn",
|
|
982
|
+
".next",
|
|
983
|
+
".nuxt",
|
|
984
|
+
".turbo",
|
|
985
|
+
".vite",
|
|
986
|
+
".cache",
|
|
987
|
+
"coverage",
|
|
988
|
+
"vendor",
|
|
989
|
+
"third_party",
|
|
990
|
+
"third-party",
|
|
991
|
+
"third_part",
|
|
992
|
+
"third-part",
|
|
993
|
+
"build",
|
|
994
|
+
"dist",
|
|
995
|
+
"out",
|
|
996
|
+
"target",
|
|
997
|
+
".venv",
|
|
998
|
+
"venv",
|
|
999
|
+
"__pycache__",
|
|
1000
|
+
".mypy_cache",
|
|
1001
|
+
".pytest_cache",
|
|
1002
|
+
".ruff_cache",
|
|
1003
|
+
".gradle",
|
|
1004
|
+
".idea",
|
|
1005
|
+
".vs",
|
|
1006
|
+
"Pods"
|
|
1007
|
+
];
|
|
1008
|
+
function normalize$1(entry) {
|
|
1009
|
+
return entry.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1010
|
+
}
|
|
1011
|
+
function segmentsOf(entry) {
|
|
1012
|
+
return normalize$1(entry).split("/").filter((part) => part !== "" && part !== ".");
|
|
1013
|
+
}
|
|
1014
|
+
/** Parse a newline- or comma-separated exclusion list, dropping blanks, comments and unsafe paths. */
|
|
1015
|
+
function parseWatchIgnores(value) {
|
|
1016
|
+
if (value === null || value === void 0) return [];
|
|
1017
|
+
const entries = /* @__PURE__ */ new Set();
|
|
1018
|
+
for (const raw of value.split(/[\n,]+/)) {
|
|
1019
|
+
const entry = normalize$1(raw);
|
|
1020
|
+
const parts = segmentsOf(entry);
|
|
1021
|
+
if (parts.length === 0 || parts.includes("..") || entry.startsWith("#")) continue;
|
|
1022
|
+
entries.add(parts.slice(0, 8).join("/").slice(0, 200));
|
|
1023
|
+
if (entries.size >= 200) break;
|
|
1024
|
+
}
|
|
1025
|
+
return [...entries];
|
|
1026
|
+
}
|
|
1027
|
+
/** Return true when a workspace-relative path contains one of the excluded segment sequences. */
|
|
1028
|
+
function isIgnoredPath(relativePath, patterns) {
|
|
1029
|
+
const segments = relativePath.split(/[\\/]+/).filter(Boolean).map((segment) => segment.toLowerCase());
|
|
1030
|
+
if (segments.length === 0) return false;
|
|
1031
|
+
for (const pattern of patterns) {
|
|
1032
|
+
const parts = segmentsOf(pattern).map((part) => part.toLowerCase());
|
|
1033
|
+
if (parts.length === 0 || parts.includes("..")) continue;
|
|
1034
|
+
if (parts.length === 1) {
|
|
1035
|
+
if (segments.includes(parts[0])) return true;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
for (let index = 0; index + parts.length <= segments.length; index++) if (parts.every((part, offset) => segments[index + offset] === part)) return true;
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
/** Build a chokidar `ignored` predicate for one workspace and the user's extra exclusions. */
|
|
1043
|
+
function watchIgnore(workspace, extra) {
|
|
1044
|
+
const patterns = [...DEFAULT_WATCH_IGNORES, ...extra];
|
|
1045
|
+
return (path) => {
|
|
1046
|
+
const path_ = relative(workspace, path);
|
|
1047
|
+
if (path_ === "" || path_.startsWith("..") || isAbsolute(path_)) return false;
|
|
1048
|
+
return isIgnoredPath(path_.split(sep).join("/"), patterns);
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
//#endregion
|
|
919
1052
|
//#region src/index.ts
|
|
920
1053
|
/** DSH host plugin for the VS Code workbench. */
|
|
921
1054
|
const name = "dsh-vscode-workbench";
|
|
@@ -1079,7 +1212,8 @@ function apply(ctx) {
|
|
|
1079
1212
|
return;
|
|
1080
1213
|
}
|
|
1081
1214
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1082
|
-
const
|
|
1215
|
+
const search = new URL(req.url ?? "/", "http://dsh.local").searchParams;
|
|
1216
|
+
const sessionId = search.get("sessionId");
|
|
1083
1217
|
if (sessionId === null) {
|
|
1084
1218
|
ws.close(1008, "sessionId required");
|
|
1085
1219
|
return;
|
|
@@ -1093,12 +1227,20 @@ function apply(ctx) {
|
|
|
1093
1227
|
}
|
|
1094
1228
|
const watcher = chokidar.watch(workspace, {
|
|
1095
1229
|
ignoreInitial: true,
|
|
1096
|
-
ignored:
|
|
1230
|
+
ignored: watchIgnore(workspace, parseWatchIgnores(search.get("ignore"))),
|
|
1097
1231
|
awaitWriteFinish: {
|
|
1098
1232
|
stabilityThreshold: 100,
|
|
1099
1233
|
pollInterval: 25
|
|
1100
1234
|
}
|
|
1101
1235
|
});
|
|
1236
|
+
watcher.on("error", (error) => {
|
|
1237
|
+
console.warn("[dsh-vscode-workbench] file watcher stopped:", error);
|
|
1238
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
|
|
1239
|
+
type: "files.watch-error",
|
|
1240
|
+
message: String(error)
|
|
1241
|
+
}));
|
|
1242
|
+
watcher.close();
|
|
1243
|
+
});
|
|
1102
1244
|
let paths = /* @__PURE__ */ new Set();
|
|
1103
1245
|
let timer;
|
|
1104
1246
|
watcher.on("all", (_event, path) => {
|