@zhangfengshun/dsh-remote-ssh 2.0.2 → 2.0.4

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.
Files changed (2) hide show
  1. package/lib/index.js +88 -85
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -12,8 +12,9 @@ import z from "schemastery";
12
12
  import { defineTool } from "@deepseek-ai/dsh-tools";
13
13
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
14
14
  import { mkdir, rm, writeFile, readFile, opendir, stat, open, rename, readdir } from "node:fs/promises";
15
+ import { existsSync, readFileSync } from "node:fs";
15
16
  import { homedir } from "node:os";
16
- import { join } from "node:path";
17
+ import { join, dirname } from "node:path";
17
18
 
18
19
  /** Plugin identity for cordis.yml rows. */
19
20
  const name = "@zhangfengshun/dsh-remote-ssh";
@@ -1402,10 +1403,9 @@ function apply(ctx, config) {
1402
1403
  function readRemoteInfoSync(mirrorCwd) {
1403
1404
  if (!mirrorCwd) return null;
1404
1405
  try {
1405
- const fs2 = require("fs");
1406
- const jsonPath = require("path").join(mirrorCwd, ".remote-ssh.json");
1407
- if (fs2.existsSync(jsonPath)) {
1408
- return JSON.parse(fs2.readFileSync(jsonPath, "utf8"));
1406
+ const jsonPath = join(mirrorCwd, ".remote-ssh.json");
1407
+ if (existsSync(jsonPath)) {
1408
+ return JSON.parse(readFileSync(jsonPath, "utf8"));
1409
1409
  }
1410
1410
  } catch (e) {}
1411
1411
  return null;
@@ -1480,7 +1480,7 @@ function apply(ctx, config) {
1480
1480
  async function localWriteFile(path, content) {
1481
1481
  const tmp = path + ".dsh-rssh-tmp-" + process.pid;
1482
1482
  try {
1483
- await mkdir(require("path").dirname(path), { recursive: true });
1483
+ await mkdir(dirname(path), { recursive: true });
1484
1484
  await writeFile(tmp, content, "utf8");
1485
1485
  await rename(tmp, path);
1486
1486
  } catch (e) {
@@ -1545,91 +1545,94 @@ function apply(ctx, config) {
1545
1545
  return { kind: "text", content: r.content || "", truncated: !!r.truncated };
1546
1546
  }
1547
1547
 
1548
- ctx.effect(() => ctx.webServer.register({
1549
- kind: "prefix",
1550
- path: "/sidebar/api/fs.",
1551
- handler: async (req, res) => {
1552
- if (!isTrusted(req)) { writeJson(res, 403, { ok: false, error: { code: "forbidden", message: "forbidden" } }); return; }
1553
- if (req.method !== "POST") { writeJson(res, 405, { ok: false, error: { code: "method-error", message: "method not allowed" } }); return; }
1554
- const pathname = new URL(req.url || "/", "http://dsh.internal").pathname;
1555
- const method = "fs." + pathname.slice("/sidebar/api/fs.".length);
1556
- let payload;
1557
- try { payload = await readJsonBody(req); }
1558
- catch (e) { writeJson(res, 400, { ok: false, error: { code: "bad-request", message: String(e && e.message ? e.message : e) } }); return; }
1559
- try {
1560
- // 获取会话 cwd
1561
- const sessionId = payload && payload.sessionId;
1562
- let sessionCwd = payload && payload.cwd;
1563
- if ((!sessionCwd || sessionCwd === "") && sessionId) {
1564
- const sessions = ctx.get("sessions");
1565
- const session = sessions ? sessions.get(sessionId) : null;
1566
- sessionCwd = session && session.header && session.header.cwd;
1567
- }
1568
- // 如果 sessionCwd 还是空,尝试用 payload.path 的父目录(文件操作时 path 是文件路径)
1569
- if ((!sessionCwd || sessionCwd === "") && payload && payload.path) {
1570
- sessionCwd = payload.path;
1571
- }
1572
- // 检查是否远程工作区
1573
- const remoteInfo = readRemoteInfoSync(sessionCwd);
1574
- // 如果 cwd 本身没有 .remote-ssh.json,尝试从 payload.path 向上查找
1575
- let remoteInfoFromPath = remoteInfo;
1576
- if (!remoteInfo && payload && payload.path) {
1577
- remoteInfoFromPath = readRemoteInfoSync(payload.path);
1578
- }
1579
- ctx.logger?.info("[dsh-remote-ssh] fs.* intercept: method=" + method + " sessionId=" + sessionId + " cwd=" + sessionCwd + " payload.path=" + (payload && payload.path) + " remoteInfo=" + (remoteInfo ? "YES" : "NO") + " remoteInfoFromPath=" + (remoteInfoFromPath ? "YES" : "NO"));
1580
- let result;
1581
- if (remoteInfoFromPath && remoteInfoFromPath.keyPath && remoteInfoFromPath.host && remoteInfoFromPath.user) {
1582
- const actualRemoteInfo = remoteInfo || remoteInfoFromPath;
1583
- const actualCwd = sessionCwd || (payload && payload.path) || "";
1584
- // ---- 远程工作区:走 SSH ----
1585
- const profile = getProfile(actualRemoteInfo.profileId);
1586
- if (!profile) { writeJson(res, 500, { ok: false, error: { code: "internal", message: "remote profile not found" } }); return; }
1587
- if (method === "fs.tree") {
1588
- const localPath = payload.path || actualCwd;
1589
- const rp = localToRemote(localPath, actualCwd, actualRemoteInfo.remotePath);
1590
- result = await remoteListForSidebar(profile, rp, localPath);
1591
- } else if (method === "fs.read") {
1592
- const localPath = payload.path || actualCwd;
1593
- const rp = localToRemote(localPath, actualCwd, actualRemoteInfo.remotePath);
1594
- result = await remoteReadForSidebar(profile, rp);
1595
- } else if (method === "fs.write") {
1596
- const localPath = payload.path || actualCwd;
1597
- const rp = localToRemote(localPath, actualCwd, actualRemoteInfo.remotePath);
1598
- result = await remoteWriteFile(runPooled, profile, rp, payload.content || "");
1599
- } else if (method === "fs.search") {
1600
- const rp = actualRemoteInfo.remotePath;
1601
- const r = await remoteGlob(runPooled, profile, "*" + (payload.query || "") + "*", rp);
1602
- result = { entries: (r.files || []).map(function (f) {
1603
- return { path: join(actualCwd, f), isDir: false };
1604
- }), truncated: !!r.truncated };
1605
- } else {
1606
- writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
1607
- }
1548
+ // exact 路由优先于 prefix 路由匹配,所以每个 fs.* 端点注册一个 exact 路由即可拦截。
1549
+ // webServer prefix 匹配要求 pathname 以 prefix+"/" 开头,但 fs.tree 用的是点分隔符,
1550
+ // 所以无法用 prefix 拦截,必须用 exact。
1551
+ async function interceptFsHandler(req, res, method) {
1552
+ if (!isTrusted(req)) { writeJson(res, 403, { ok: false, error: { code: "forbidden", message: "forbidden" } }); return; }
1553
+ if (req.method !== "POST") { writeJson(res, 405, { ok: false, error: { code: "method-error", message: "method not allowed" } }); return; }
1554
+ let payload;
1555
+ try { payload = await readJsonBody(req); }
1556
+ catch (e) { writeJson(res, 400, { ok: false, error: { code: "bad-request", message: String(e && e.message ? e.message : e) } }); return; }
1557
+ try {
1558
+ // 获取会话 cwd
1559
+ const sessionId = payload && payload.sessionId;
1560
+ let sessionCwd = payload && payload.cwd;
1561
+ if ((!sessionCwd || sessionCwd === "") && sessionId) {
1562
+ const sessions = ctx.get("sessions");
1563
+ const session = sessions ? sessions.get(sessionId) : null;
1564
+ sessionCwd = session && session.header && session.header.cwd;
1565
+ }
1566
+ // 如果 sessionCwd 还是空,尝试用 payload.path(文件操作时 path 包含工作区路径)
1567
+ if ((!sessionCwd || sessionCwd === "") && payload && payload.path) {
1568
+ sessionCwd = payload.path;
1569
+ }
1570
+ // 检查是否远程工作区:先查 sessionCwd,再查 payload.path
1571
+ let remoteInfo = readRemoteInfoSync(sessionCwd);
1572
+ let remoteBase = sessionCwd;
1573
+ if (!remoteInfo && payload && payload.path) {
1574
+ remoteInfo = readRemoteInfoSync(payload.path);
1575
+ remoteBase = payload.path;
1576
+ }
1577
+ console.log("[dsh-remote-ssh] intercept " + method + ": sessionId=" + sessionId + " cwd=" + sessionCwd + " path=" + (payload && payload.path) + " remote=" + (remoteInfo ? "YES" : "NO"));
1578
+ let result;
1579
+ if (remoteInfo && remoteInfo.keyPath && remoteInfo.host && remoteInfo.user) {
1580
+ // ---- 远程工作区:走 SSH ----
1581
+ const profile = getProfile(remoteInfo.profileId);
1582
+ if (!profile) { writeJson(res, 500, { ok: false, error: { code: "internal", message: "remote profile not found: " + remoteInfo.profileId } }); return; }
1583
+ if (method === "fs.tree") {
1584
+ const localPath = payload.path || sessionCwd;
1585
+ const rp = localToRemote(localPath, remoteBase, remoteInfo.remotePath);
1586
+ result = await remoteListForSidebar(profile, rp, localPath);
1587
+ } else if (method === "fs.read") {
1588
+ const localPath = payload.path || sessionCwd;
1589
+ const rp = localToRemote(localPath, remoteBase, remoteInfo.remotePath);
1590
+ result = await remoteReadForSidebar(profile, rp);
1591
+ } else if (method === "fs.write") {
1592
+ const localPath = payload.path || sessionCwd;
1593
+ const rp = localToRemote(localPath, remoteBase, remoteInfo.remotePath);
1594
+ result = await remoteWriteFile(runPooled, profile, rp, payload.content || "");
1595
+ } else if (method === "fs.search") {
1596
+ const rp = remoteInfo.remotePath;
1597
+ const r = await remoteGlob(runPooled, profile, "*" + (payload.query || "") + "*", rp);
1598
+ result = { entries: (r.files || []).map(function (f) {
1599
+ return { path: join(remoteBase, f), isDir: false };
1600
+ }), truncated: !!r.truncated };
1608
1601
  } else {
1609
- // ---- 本地工作区:走本地 fs ----
1610
- if (method === "fs.tree") {
1611
- const p = payload.path || sessionCwd;
1612
- result = await localListDir(p, LIST_LIMIT);
1613
- } else if (method === "fs.read") {
1614
- result = await localReadText(payload.path, READ_LIMIT);
1615
- } else if (method === "fs.write") {
1616
- result = await localWriteFile(payload.path, payload.content || "");
1617
- } else if (method === "fs.search") {
1618
- result = await localSearchFiles(sessionCwd, payload.query || "");
1619
- } else {
1620
- writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
1621
- }
1602
+ writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
1622
1603
  }
1623
- if (result && result.ok === false) {
1624
- writeJson(res, 400, { ok: false, error: { code: "fs-error", message: result.error || "operation failed" } });
1604
+ } else {
1605
+ // ---- 本地工作区:走本地 fs ----
1606
+ if (method === "fs.tree") {
1607
+ const p = payload.path || sessionCwd;
1608
+ result = await localListDir(p, LIST_LIMIT);
1609
+ } else if (method === "fs.read") {
1610
+ result = await localReadText(payload.path, READ_LIMIT);
1611
+ } else if (method === "fs.write") {
1612
+ result = await localWriteFile(payload.path, payload.content || "");
1613
+ } else if (method === "fs.search") {
1614
+ result = await localSearchFiles(sessionCwd, payload.query || "");
1625
1615
  } else {
1626
- writeOk(res, result);
1616
+ writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
1627
1617
  }
1628
- } catch (e) {
1629
- writeJson(res, 400, { ok: false, error: { code: "fs-error", message: String(e && e.message ? e.message : e) } });
1630
1618
  }
1619
+ if (result && result.ok === false) {
1620
+ writeJson(res, 400, { ok: false, error: { code: "fs-error", message: result.error || "operation failed" } });
1621
+ } else {
1622
+ writeOk(res, result);
1623
+ }
1624
+ } catch (e) {
1625
+ writeJson(res, 400, { ok: false, error: { code: "fs-error", message: String(e && e.message ? e.message : e) } });
1631
1626
  }
1632
- }), "dsh-remote-ssh: intercept /sidebar/api/fs.* for remote workspaces");
1627
+ }
1628
+
1629
+ ["fs.tree", "fs.read", "fs.write", "fs.search"].forEach(function (m) {
1630
+ ctx.effect(() => ctx.webServer.register({
1631
+ kind: "exact",
1632
+ path: "/sidebar/api/" + m,
1633
+ handler: function (req, res) { return interceptFsHandler(req, res, m); }
1634
+ }), "dsh-remote-ssh: intercept /sidebar/api/" + m);
1635
+ });
1633
1636
 
1634
1637
  // ---- 清理 ----
1635
1638
  ctx.effect(() => () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangfengshun/dsh-remote-ssh",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
5
5
  "keywords": [
6
6
  "dsh",