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/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, writeFile } from "node:fs/promises";
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 { absolute } = await workspacePath(cwd, path);
178
- if ((await readDocument(cwd, path)).revision !== expected) throw new WorkbenchError("revision-conflict", "file changed on disk; reload before saving", 409);
179
- const temp = join(dirname(absolute), `.${randomUUID()}.dsh-vscode.tmp`);
180
- await writeFile(temp, content, {
181
- encoding: "utf8",
182
- flag: "wx"
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
- } catch (error) {
187
- await rm(temp, { force: true });
188
- throw error;
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
- if (path === "") throw new WorkbenchError("root-delete", "cannot delete workspace root", 403);
226
- const { absolute } = await workspacePath(cwd, path);
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
- for (const record of result.stdout.split("\0")) if (record.startsWith("# branch.head ")) branch = record.slice(14);
447
- else if (record.startsWith("# branch.ab ")) {
448
- const match = /\+(\d+) -(\d+)/.exec(record);
449
- ahead = Number(match?.[1] ?? 0);
450
- behind = Number(match?.[2] ?? 0);
451
- } else if (record.startsWith("? ")) changes.push({
452
- path: record.slice(2),
453
- index: "?",
454
- worktree: "?",
455
- staged: false,
456
- untracked: true
457
- });
458
- else if (record.startsWith("1 ") || record.startsWith("2 ")) {
459
- const parts = record.split(" ");
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 sessionId = new URL(req.url ?? "/", "http://dsh.local").searchParams.get("sessionId");
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: /(^|[/\\])(\.git|node_modules|\.next|\.nuxt|\.turbo|\.vite|coverage)([/\\]|$)/,
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) => {