@zhangfengshun/dsh-remote-ssh 1.9.2 → 2.0.0
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/cordis.patch.yml +7 -6
- package/lib/index.js +257 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
|
|
4
4
|
|
|
5
|
+
## [2.0.0] — 内置文件页签直接 SSH 读写远程文件(不再需要同步)
|
|
6
|
+
### 重大变更
|
|
7
|
+
- **内置「文件」页签直接操作远程文件**:通过注册更长前缀 `/sidebar/api/fs.` 拦截 better-sidebar 的文件 API(`fs.tree`/`fs.read`/`fs.write`/`fs.search`),在远程工作区中直接通过 SSH 读写远程文件,不再需要 sync/push 同步。
|
|
8
|
+
- **透明路径映射**:客户端看到的是本地镜像路径,host 拦截器自动转换为远程路径,通过 SSH 执行操作后返回结果。
|
|
9
|
+
- **本地工作区照常**:非远程工作区的请求走本地 fs,行为与 better-sidebar 原始实现一致。
|
|
10
|
+
- 不再依赖 tar 同步——打开文件 = 直接读远程,保存文件 = 直接写远程。
|
|
11
|
+
|
|
12
|
+
## [1.9.3] — 修复 syncDown 删除 .remote-ssh.json 导致终端不工作
|
|
13
|
+
### 修复
|
|
14
|
+
- **`remoteSyncDown` 会清空镜像目录**:syncDown 先 `rm -rf` 镜像目录再 `tar xf` 展开,导致之前写入的 `.remote-ssh.json`(供 shell wrapper 读取连接信息)被删掉,终端 wrapper 检测不到远程工作区,降级到本地 shell。
|
|
15
|
+
- **修复**:在 `createRemoteWorkspace`、`syncDown` API、`remote_ssh_sync` 工具三处,都在 `remoteSyncDown` 完成后重新写入 `.remote-ssh.json`。
|
|
16
|
+
- 已为现有工作区补写 `.remote-ssh.json`(无需重新创建工作区)。
|
|
17
|
+
|
|
5
18
|
## [1.9.2] — 修复自动配置 patch 不生效(bundles 顺序)
|
|
6
19
|
### 修复
|
|
7
20
|
- **config 覆盖被跳过**:`cordis.patch.yml` 中的 `id: better-sidebar` config 覆盖需要在 `better-sidebar` 条目被 insert 之后才能生效。如果 `@zhangfengshun/dsh-remote-ssh` 在 bundles 列表中排在 `dsh-better-sidebar` 之前,patch 会因"entry not found"被跳过。
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ dsh plugin --profile <name> add /absolute/path/to/dsh-remote-ssh
|
|
|
29
29
|
### 发布后安装
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@
|
|
32
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.0.0
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
> ⚠️ 安装后需**重启 DSH** 才生效;后续仅修改 Client 半边时刷新浏览器即可。
|
package/cordis.patch.yml
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
- id: better-sidebar
|
|
14
14
|
config:
|
|
15
15
|
shell: !!js |
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
(() => {
|
|
17
|
+
var homedir = process.env.USERPROFILE || process.env.HOME || process.env.HOMEDRIVE + process.env.HOMEPATH;
|
|
18
|
+
var isWin = process.platform === 'win32';
|
|
19
|
+
var sep = isWin ? '\\' : '/';
|
|
20
|
+
var dir = homedir + sep + '.dsh' + sep + 'remote-ssh';
|
|
21
|
+
return isWin ? dir + sep + 'dsh-remote-shell.cmd' : dir + sep + 'dsh-remote-shell';
|
|
22
|
+
})()
|
package/lib/index.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import z from "schemastery";
|
|
12
12
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
13
13
|
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
14
|
-
import { mkdir, rm, writeFile, readFile } from "node:fs/promises";
|
|
14
|
+
import { mkdir, rm, writeFile, readFile, opendir, stat, open, rename, readdir } from "node:fs/promises";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
|
|
18
18
|
/** Plugin identity for cordis.yml rows. */
|
|
19
19
|
const name = "@zhangfengshun/dsh-remote-ssh";
|
|
20
20
|
/** Services required before mounting. */
|
|
21
|
-
const inject = ["webServer", "subprocess", "tools"];
|
|
21
|
+
const inject = ["webServer", "subprocess", "tools", "sessions", "webRuntime"];
|
|
22
22
|
/** Composition config schema(本插件暂无配置项)。 */
|
|
23
23
|
const Config = z.object({});
|
|
24
24
|
|
|
@@ -819,7 +819,13 @@ function apply(ctx, config) {
|
|
|
819
819
|
try {
|
|
820
820
|
await writeFile(join(mirrorDir, "README.md"), remoteWorkspaceReadme(p.name, a.remotePath), "utf8");
|
|
821
821
|
} catch (e) {}
|
|
822
|
+
// 自动同步:把远程文件拉到本地镜像,使内置「文件」页签可见真实远程文件。
|
|
823
|
+
// 注意:remoteSyncDown 会先清空镜像目录,所以 .remote-ssh.json 必须在同步之后写。
|
|
824
|
+
try {
|
|
825
|
+
await remoteSyncDown(subprocess, p, a.remotePath, mirrorDir);
|
|
826
|
+
} catch (e) {}
|
|
822
827
|
// 写 .remote-ssh.json:供 shell wrapper 读取连接信息(仅密钥认证,不含密码)。
|
|
828
|
+
// 必须在 remoteSyncDown 之后写,否则会被同步过程清空。
|
|
823
829
|
try {
|
|
824
830
|
const connInfo = {
|
|
825
831
|
profileId: p.id, host: p.host, port: p.port || 22, user: p.user,
|
|
@@ -827,10 +833,6 @@ function apply(ctx, config) {
|
|
|
827
833
|
};
|
|
828
834
|
await writeFile(join(mirrorDir, ".remote-ssh.json"), JSON.stringify(connInfo, null, 2), "utf8");
|
|
829
835
|
} catch (e) {}
|
|
830
|
-
// 自动同步:把远程文件拉到本地镜像,使内置「文件」页签可见真实远程文件。
|
|
831
|
-
try {
|
|
832
|
-
await remoteSyncDown(subprocess, p, a.remotePath, mirrorDir);
|
|
833
|
-
} catch (e) {}
|
|
834
836
|
let workspaceId = null;
|
|
835
837
|
if (workspaceRegistry) {
|
|
836
838
|
try {
|
|
@@ -936,7 +938,18 @@ function apply(ctx, config) {
|
|
|
936
938
|
if (!ws) return { ok: false, error: "未找到远程工作区" };
|
|
937
939
|
const p = getProfile(ws.profileId);
|
|
938
940
|
if (!p) return { ok: false, error: "未找到连接配置" };
|
|
939
|
-
|
|
941
|
+
const r = await remoteSyncDown(subprocess, p, ws.remotePath, ws.mirrorPath);
|
|
942
|
+
// remoteSyncDown 会清空镜像目录,需要重新写入 .remote-ssh.json
|
|
943
|
+
if (r.ok) {
|
|
944
|
+
try {
|
|
945
|
+
const connInfo = {
|
|
946
|
+
profileId: p.id, host: p.host, port: p.port || 22, user: p.user,
|
|
947
|
+
keyPath: p.keyPath || "", proxyJump: p.proxyJump || "", remotePath: ws.remotePath
|
|
948
|
+
};
|
|
949
|
+
await writeFile(join(ws.mirrorPath, ".remote-ssh.json"), JSON.stringify(connInfo, null, 2), "utf8");
|
|
950
|
+
} catch (e) {}
|
|
951
|
+
}
|
|
952
|
+
return r;
|
|
940
953
|
},
|
|
941
954
|
/** 本地镜像 → 远端(按工作区)。 */
|
|
942
955
|
syncUp: async (args) => {
|
|
@@ -1168,7 +1181,18 @@ function apply(ctx, config) {
|
|
|
1168
1181
|
if (!ws) return { ok: false, error: "未找到远程工作区(workspaceId 必填,或当前会话非远程工作区)" };
|
|
1169
1182
|
const p = getProfile(ws.profileId);
|
|
1170
1183
|
if (!p) return { ok: false, error: "未找到连接配置" };
|
|
1171
|
-
|
|
1184
|
+
const r = await remoteSyncDown(subprocess, p, ws.remotePath, ws.mirrorPath);
|
|
1185
|
+
// remoteSyncDown 会清空镜像目录,需要重新写入 .remote-ssh.json
|
|
1186
|
+
if (r.ok) {
|
|
1187
|
+
try {
|
|
1188
|
+
const connInfo = {
|
|
1189
|
+
profileId: p.id, host: p.host, port: p.port || 22, user: p.user,
|
|
1190
|
+
keyPath: p.keyPath || "", proxyJump: p.proxyJump || "", remotePath: ws.remotePath
|
|
1191
|
+
};
|
|
1192
|
+
await writeFile(join(ws.mirrorPath, ".remote-ssh.json"), JSON.stringify(connInfo, null, 2), "utf8");
|
|
1193
|
+
} catch (e) {}
|
|
1194
|
+
}
|
|
1195
|
+
return r;
|
|
1172
1196
|
}
|
|
1173
1197
|
});
|
|
1174
1198
|
register({
|
|
@@ -1369,6 +1393,231 @@ function apply(ctx, config) {
|
|
|
1369
1393
|
}
|
|
1370
1394
|
})();
|
|
1371
1395
|
|
|
1396
|
+
// ---- 拦截 better-sidebar 的 fs.* API:远程工作区走 SSH,本地走本地 fs ----
|
|
1397
|
+
// 注册更长前缀 /sidebar/api/fs. 优先于 better-sidebar 的 /sidebar/api 匹配。
|
|
1398
|
+
const READ_LIMIT = 524288;
|
|
1399
|
+
const LIST_LIMIT = 1000;
|
|
1400
|
+
|
|
1401
|
+
// 读取镜像目录下的 .remote-ssh.json,返回连接信息或 null
|
|
1402
|
+
function readRemoteInfoSync(mirrorCwd) {
|
|
1403
|
+
if (!mirrorCwd) return null;
|
|
1404
|
+
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"));
|
|
1409
|
+
}
|
|
1410
|
+
} catch (e) {}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// 把本地镜像路径转换为远程路径
|
|
1415
|
+
function localToRemote(localPath, mirrorCwd, remoteBase) {
|
|
1416
|
+
if (!localPath) return remoteBase;
|
|
1417
|
+
// 规范化:去掉前缀 mirrorCwd,剩余部分拼到 remoteBase
|
|
1418
|
+
let rel = String(localPath);
|
|
1419
|
+
const mc = String(mirrorCwd).replace(/\\/g, "/");
|
|
1420
|
+
rel = rel.replace(/\\/g, "/");
|
|
1421
|
+
if (rel.toLowerCase().startsWith(mc.toLowerCase())) {
|
|
1422
|
+
rel = rel.slice(mc.length);
|
|
1423
|
+
} else {
|
|
1424
|
+
// 可能是相对于 cwd 的路径
|
|
1425
|
+
}
|
|
1426
|
+
if (rel.startsWith("/")) rel = rel.slice(1);
|
|
1427
|
+
if (!rel) return remoteBase;
|
|
1428
|
+
// 拼接远程路径
|
|
1429
|
+
const rb = String(remoteBase).replace(/\/+$/, "");
|
|
1430
|
+
return rb + "/" + rel;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// 本地 listDirectory(复制 better-sidebar 的逻辑)
|
|
1434
|
+
async function localListDir(path, maxEntries) {
|
|
1435
|
+
let level;
|
|
1436
|
+
try { level = await opendir(path); }
|
|
1437
|
+
catch (e) { return { ok: false, error: "cannot list: " + String(e && e.message ? e.message : e) }; }
|
|
1438
|
+
const rows = [];
|
|
1439
|
+
let overflow = 0;
|
|
1440
|
+
try {
|
|
1441
|
+
for await (const dirent of level) {
|
|
1442
|
+
if (rows.length >= (maxEntries || 1000)) { overflow++; continue; }
|
|
1443
|
+
rows.push({
|
|
1444
|
+
name: dirent.name,
|
|
1445
|
+
path: join(path, dirent.name),
|
|
1446
|
+
isDir: dirent.isDirectory(),
|
|
1447
|
+
isSymlink: dirent.isSymbolicLink(),
|
|
1448
|
+
broken: false,
|
|
1449
|
+
hidden: dirent.name.startsWith(".")
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
} catch (e) { return { ok: false, error: "cannot list: " + String(e && e.message ? e.message : e) }; }
|
|
1453
|
+
rows.sort(function (a, b) {
|
|
1454
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1455
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
1456
|
+
});
|
|
1457
|
+
return { path: path, entries: rows, truncated: overflow > 0 };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// 本地 readText(复制 better-sidebar 的逻辑)
|
|
1461
|
+
async function localReadText(path, readLimit) {
|
|
1462
|
+
const info = await stat(path).catch(function (e) { throw new Error("cannot read: " + String(e && e.message ? e.message : e)); });
|
|
1463
|
+
if (info.isDirectory()) throw new Error("is a directory");
|
|
1464
|
+
const size = info.size;
|
|
1465
|
+
const truncated = size > readLimit;
|
|
1466
|
+
const handle = await open(path, "r");
|
|
1467
|
+
try {
|
|
1468
|
+
const buffer = Buffer.alloc(Math.min(size, readLimit));
|
|
1469
|
+
const r = await handle.read(buffer, 0, buffer.length, 0);
|
|
1470
|
+
const slice = buffer.subarray(0, r.bytesRead);
|
|
1471
|
+
const binary = slice.includes(0);
|
|
1472
|
+
if (binary) {
|
|
1473
|
+
return { kind: "binary", size: size, truncated: truncated, head: slice.subarray(0, Math.min(slice.length, 4096)).toString("base64") };
|
|
1474
|
+
}
|
|
1475
|
+
return { kind: "text", content: slice.toString("utf8"), truncated: truncated };
|
|
1476
|
+
} finally { await handle.close(); }
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// 本地 writeFile(原子写入)
|
|
1480
|
+
async function localWriteFile(path, content) {
|
|
1481
|
+
const tmp = path + ".dsh-rssh-tmp-" + process.pid;
|
|
1482
|
+
try {
|
|
1483
|
+
await mkdir(require("path").dirname(path), { recursive: true });
|
|
1484
|
+
await writeFile(tmp, content, "utf8");
|
|
1485
|
+
await rename(tmp, path);
|
|
1486
|
+
} catch (e) {
|
|
1487
|
+
try { await rm(tmp, { force: true }); } catch (e2) {}
|
|
1488
|
+
throw e;
|
|
1489
|
+
}
|
|
1490
|
+
return { ok: true };
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// 本地 searchFiles(简单递归文件名搜索)
|
|
1494
|
+
async function localSearchFiles(root, query) {
|
|
1495
|
+
if (!query) return { entries: [] };
|
|
1496
|
+
const q = query.toLowerCase();
|
|
1497
|
+
const results = [];
|
|
1498
|
+
async function walk(dir, depth) {
|
|
1499
|
+
if (depth > 10 || results.length > 500) return;
|
|
1500
|
+
let entries;
|
|
1501
|
+
try { entries = await readdir(dir, { withFileTypes: true }); }
|
|
1502
|
+
catch (e) { return; }
|
|
1503
|
+
for (const e of entries) {
|
|
1504
|
+
if (e.name.toLowerCase().includes(q)) {
|
|
1505
|
+
results.push({ path: join(dir, e.name), isDir: e.isDirectory() });
|
|
1506
|
+
if (results.length > 500) return;
|
|
1507
|
+
}
|
|
1508
|
+
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
|
|
1509
|
+
await walk(join(dir, e.name), depth + 1);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
await walk(root, 0);
|
|
1514
|
+
return { entries: results, truncated: false };
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// 远程 listDir → better-sidebar 格式
|
|
1518
|
+
async function remoteListForSidebar(profile, remotePath, localCwd) {
|
|
1519
|
+
const r = await remoteListDir(runPooled, profile, remotePath);
|
|
1520
|
+
if (!r.ok) return { ok: false, error: r.error || "list failed" };
|
|
1521
|
+
const entries = (r.entries || []).map(function (e) {
|
|
1522
|
+
return {
|
|
1523
|
+
name: e.name,
|
|
1524
|
+
path: join(localCwd, e.name), // 返回本地镜像路径格式,保持客户端兼容
|
|
1525
|
+
isDir: e.type === "directory",
|
|
1526
|
+
isSymlink: false,
|
|
1527
|
+
broken: false,
|
|
1528
|
+
hidden: e.name.startsWith(".")
|
|
1529
|
+
};
|
|
1530
|
+
});
|
|
1531
|
+
entries.sort(function (a, b) {
|
|
1532
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1533
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
1534
|
+
});
|
|
1535
|
+
return { path: localCwd, entries: entries, truncated: false };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// 远程 readText → better-sidebar 格式
|
|
1539
|
+
async function remoteReadForSidebar(profile, remotePath) {
|
|
1540
|
+
const r = await remoteReadFile(runPooled, profile, remotePath);
|
|
1541
|
+
if (!r.ok) return { ok: false, error: r.error || "read failed" };
|
|
1542
|
+
if (r.binary) {
|
|
1543
|
+
return { kind: "binary", size: 0, truncated: !!r.truncated, head: (r.content || "").slice(0, 4096) };
|
|
1544
|
+
}
|
|
1545
|
+
return { kind: "text", content: r.content || "", truncated: !!r.truncated };
|
|
1546
|
+
}
|
|
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 && ctx.sessions) {
|
|
1564
|
+
const session = ctx.sessions.get(sessionId);
|
|
1565
|
+
sessionCwd = session && session.header && session.header.cwd;
|
|
1566
|
+
}
|
|
1567
|
+
// 检查是否远程工作区
|
|
1568
|
+
const remoteInfo = readRemoteInfoSync(sessionCwd);
|
|
1569
|
+
let result;
|
|
1570
|
+
if (remoteInfo && remoteInfo.keyPath && remoteInfo.host && remoteInfo.user) {
|
|
1571
|
+
// ---- 远程工作区:走 SSH ----
|
|
1572
|
+
const profile = getProfile(remoteInfo.profileId);
|
|
1573
|
+
if (!profile) { writeJson(res, 500, { ok: false, error: { code: "internal", message: "remote profile not found" } }); return; }
|
|
1574
|
+
if (method === "fs.tree") {
|
|
1575
|
+
const localPath = payload.path || sessionCwd;
|
|
1576
|
+
const rp = localToRemote(localPath, sessionCwd, remoteInfo.remotePath);
|
|
1577
|
+
result = await remoteListForSidebar(profile, rp, localPath);
|
|
1578
|
+
} else if (method === "fs.read") {
|
|
1579
|
+
const localPath = payload.path || sessionCwd;
|
|
1580
|
+
const rp = localToRemote(localPath, sessionCwd, remoteInfo.remotePath);
|
|
1581
|
+
result = await remoteReadForSidebar(profile, rp);
|
|
1582
|
+
} else if (method === "fs.write") {
|
|
1583
|
+
const localPath = payload.path || sessionCwd;
|
|
1584
|
+
const rp = localToRemote(localPath, sessionCwd, remoteInfo.remotePath);
|
|
1585
|
+
result = await remoteWriteFile(runPooled, profile, rp, payload.content || "");
|
|
1586
|
+
} else if (method === "fs.search") {
|
|
1587
|
+
const rp = remoteInfo.remotePath;
|
|
1588
|
+
const r = await remoteGlob(runPooled, profile, "*" + (payload.query || "") + "*", rp);
|
|
1589
|
+
result = { entries: (r.files || []).map(function (f) {
|
|
1590
|
+
return { path: join(sessionCwd, f), isDir: false };
|
|
1591
|
+
}), truncated: !!r.truncated };
|
|
1592
|
+
} else {
|
|
1593
|
+
writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
|
|
1594
|
+
}
|
|
1595
|
+
} else {
|
|
1596
|
+
// ---- 本地工作区:走本地 fs ----
|
|
1597
|
+
if (method === "fs.tree") {
|
|
1598
|
+
const p = payload.path || sessionCwd;
|
|
1599
|
+
result = await localListDir(p, LIST_LIMIT);
|
|
1600
|
+
} else if (method === "fs.read") {
|
|
1601
|
+
result = await localReadText(payload.path, READ_LIMIT);
|
|
1602
|
+
} else if (method === "fs.write") {
|
|
1603
|
+
result = await localWriteFile(payload.path, payload.content || "");
|
|
1604
|
+
} else if (method === "fs.search") {
|
|
1605
|
+
result = await localSearchFiles(sessionCwd, payload.query || "");
|
|
1606
|
+
} else {
|
|
1607
|
+
writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (result && result.ok === false) {
|
|
1611
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: result.error || "operation failed" } });
|
|
1612
|
+
} else {
|
|
1613
|
+
writeOk(res, result);
|
|
1614
|
+
}
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: String(e && e.message ? e.message : e) } });
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}), "dsh-remote-ssh: intercept /sidebar/api/fs.* for remote workspaces");
|
|
1620
|
+
|
|
1372
1621
|
// ---- 清理 ----
|
|
1373
1622
|
ctx.effect(() => () => {
|
|
1374
1623
|
clearInterval(idleTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|