@zhangfengshun/dsh-remote-ssh 2.4.8 → 2.4.9

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
4
4
 
5
+ ## [2.4.9] — 「添加工作区」目录选择器支持新建目录(issue #11)
6
+ ### 新增
7
+ - **「添加工作区」弹窗的目录选择器新增「新建目录」**(感谢 @Linhaojing 的定位与接线建议):此前本地 / 远程两个 tab 都只有「打开 / 上级 / 选择此目录」,要把**尚不存在**的目录加为工作区(例如远端起新项目 `~/lhj/new-project`)必须先跳出 DSH 用别的终端 `mkdir`。现在底部操作区多一个「📁 新建目录」按钮:点开输入行 → 填名字(回车即提交)→ 在当前 `path` 下创建 → **自动进入新目录**,紧接着点「选择此目录」即可成为工作区,全程不离开 DSH(对齐 VSCode「新建文件夹」的交互预期)。
8
+ - **两个 tab 都支持**:远程走既有 `api("mkdir")`(`mkdir -p` + 缓存失效,与远程资源管理器的「新建目录」同一条已验证链路);本地新增宿主 API `mkdirLocal`(`fs.mkdir(recursive)`),补齐此前本地侧完全没有入口的缺口;
9
+ - **名字校验**:只接受单个路径段——空名、`.`、`..`、含 `/` 或 `\` 一律拦截并给出中英文提示(避免「在 path 下新建」变成任意位置写入);
10
+ - **路径拼接**:`joinChild()` 沿用基路径的分隔符(POSIX `/`、Windows `\`,含 `C:\` 根与 `~` 前缀),不改变远端路径语义;
11
+ - **失败与异常都不影响弹窗**:后端报错(如远端只读)展示原文且不跳转;Promise 异常被捕获;提交中按钮禁用并显示「处理中…」。
12
+
13
+ ### 测试
14
+ - 新增 `tests/dir-picker-newdir.test.mjs`(**48 条断言**):名字校验(空/空白/`.`/`..`/含分隔符/中文/含点号)、路径拼接(POSIX、尾部多分隔符、空基路径、根路径、`~`、Windows 反斜杠、盘符根、非 ASCII)、**组件行为**(提取 DirPicker 里真实的 `submitNewDir` 逻辑注入桩状态:非法名不调用 createFn、成功进入新目录、后端未回 path 时回退 joinChild、失败不跳转、异常被捕获、未进入新建状态时 no-op)、以及接线断言(两个 tab 都传 createFn、远程/本地分流、宿主 `mkdirLocal` 用 recursive mkdir、5 个 i18n 键中英齐全)。
15
+
5
16
  ## [2.4.8] — 修复大仓里 `@` 补全不可用:索引排除必须发生在截断之前(issue #10 实测反馈)
6
17
  ### 修复
7
18
  - **根因**:2.4.7 的索引命令在 git 分支里是 `git ls-files … | head -n 20001 | sed …`——**先截断、后排除**(排除原本只在客户端 `refParseIndexOutput` 里做)。而 `git ls-files --cached --others` 的输出**不是全局字典序**:未跟踪文件按 readdir 顺序先输出,`node_modules/` 这类目录可能占满前两万行。实测报告者的仓库 node_modules 有 **18,880 条(84.5%)**,把 `head` 配额吃掉 94%,`AGENTS.md` 落在第 **20417** 行被整段切掉,`src/**`(1,742 个文件)同样全部缺失——症状是 `@AGENTS` 搜不到、返回一堆 pip 内部目录(命中 300 分子序列分支)。
package/README.md CHANGED
@@ -46,7 +46,7 @@
46
46
  **一条命令安装**(无需 token、API Key 或额外配置):
47
47
 
48
48
  ```bash
49
- dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.8
49
+ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.9
50
50
  ```
51
51
 
52
52
  安装后**重启 DSH**。`@zhangfengshun/dsh-remote-ssh` 必须在 bundles 列表中排在 `dsh-better-sidebar` **之后**。
@@ -64,7 +64,7 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
64
64
  **三步上手**
65
65
 
66
66
  1. **设置 → 远程连接** → 添加连接(主机 / 端口 / 用户 / 密钥)→ 点「测试连接」验证;已有 `~/.ssh/config` 可直接一键导入
67
- 2. **添加工作区** → 选「选择远程目录…」→ 选连接 → 浏览并选择远程目录(该目录会成为原生 DSH 工作区)
67
+ 2. **添加工作区** → 选「选择远程目录…」→ 选连接 → 浏览并选择远程目录(该目录会成为原生 DSH 工作区);目录还不存在时点「📁 新建目录」就地创建(本地 / 远程 tab 均支持),创建后自动进入新目录
68
68
  3. 进入该工作区会话:内置「文件」页签直接显示远程文件(编辑保存直写远程),「终端」页签自动 SSH 到该工作区的**远程目录**(仅密钥认证)
69
69
 
70
70
  **会话内直接对模型说**(远程工作区会话中免填连接参数):
@@ -201,6 +201,7 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
201
201
  | 「文件」页签树根显示镜像目录 ID(如 `wmu3sxe24jpvg`) | 2.4.6 起已修复:树根改为显示**远程目录名**(如 `IB_Robot`),悬停可见完整远程路径;该标签不经过 `fs.*` 路由,由客户端渲染层替换 |
202
202
  | `@` 补全只搜到镜像里那几个文件 | 2.4.7 起已修复:远程工作区会话的 `@` 补全改列远端文件(索引缓存 60s + 900ms 查询预算);若仍只有镜像文件,确认 2.4.7 已装入并重启 DSH |
203
203
  | 大仓里 `@` 搜不到真实文件(如根目录 `AGENTS.md`、`src/**`) | 2.4.8 起已修复:此前排除目录发生在截断之后,`node_modules/` 这类目录会吃光索引配额;现在排除由远端 `grep`/`-prune` 在截断前完成,并会在索引达上限时打 warn 提示 |
204
+ | 想加的远程目录还不存在,「添加工作区」里没法创建 | 2.4.9 起「目录选择器」底部有「📁 新建目录」(本地 / 远程 tab 均有):输入名字即可就地创建并自动进入 |
204
205
  | 安装时提示 `minimumReleaseAge` 或「No matching version」(刚发布) | npm 供应链新鲜度策略,等 1–5 分钟后重试即可 |
205
206
  | 命令卡住不返回 | 默认 120s 超时后自动丢弃会话;长时任务用 `timeoutMs: 0`,随时可用 `remote_ssh_kill` 强杀 |
206
207
  | 大文件读取被截断 | 单文件读取上限 4MB、下载池化路径约 6.29MB(更大自动回落一次性连接);用 `remote_ssh_exec` + `head`/`tail` 分段处理 |
package/README_EN.md CHANGED
@@ -46,7 +46,7 @@ A **DSH** plugin like **VSCode Remote-SSH**: connect to remote HPC / servers via
46
46
  **One command** (no token, API key or extra configuration needed):
47
47
 
48
48
  ```bash
49
- dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.8
49
+ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.9
50
50
  ```
51
51
 
52
52
  **Restart DSH** after installation. `@zhangfengshun/dsh-remote-ssh` must come **after** `dsh-better-sidebar` in the bundles list.
@@ -64,7 +64,7 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
64
64
  **Three steps**
65
65
 
66
66
  1. **Settings → Remote SSH** → Add a connection (host / port / user / key) → Click "Test Connection"; an existing `~/.ssh/config` can be imported in one click
67
- 2. **Add Workspace** → Choose "Select Remote Directory…" → Pick a connection → Browse and select a remote directory (it becomes a native DSH workspace)
67
+ 2. **Add Workspace** → Choose "Select Remote Directory…" → Pick a connection → Browse and select a remote directory (it becomes a native DSH workspace); if the directory does not exist yet, click "📁 New folder" to create it in place (both the local and remote tabs) — the picker then enters it automatically
68
68
  3. Inside that workspace session: the built-in **Files** tab shows remote files (edits save straight back to remote), and the **Terminal** tab auto-SSHes **into the workspace's remote directory** (key auth only)
69
69
 
70
70
  **Just ask the model** (connection params are auto-filled inside a remote-workspace session):
@@ -201,6 +201,7 @@ The plugin never patches DSH sources or injects into the profile dependency tree
201
201
  | Files tab tree root shows the mirror directory id (e.g. `wmu3sxe24jpvg`) | Fixed in 2.4.6: the root row now shows the **remote directory name** (e.g. `IB_Robot`) with the full remote path on hover; that label never passes through the `fs.*` routes, so the client renders the replacement |
202
202
  | `@` completion only finds the few files in the mirror | Fixed in 2.4.7: in a remote-workspace session `@` now lists remote files (60s index cache + 900ms query budget); if only mirror files show up, make sure 2.4.7 is installed and DSH restarted |
203
203
  | In a large repo `@` cannot find real files (e.g. root `AGENTS.md`, `src/**`) | Fixed in 2.4.8: exclusion used to run *after* truncation, so a `node_modules/` tree could exhaust the index quota; exclusion now happens remotely (`grep`/`-prune`) before truncation, and hitting the cap logs a warning |
204
+ | The remote directory you want does not exist yet and "Add Workspace" cannot create it | Fixed in 2.4.9: the directory picker has a "📁 New folder" button (both tabs) — type a name to create it in place and enter it automatically |
204
205
  | Install fails with `minimumReleaseAge` or "No matching version" right after a release | npm supply-chain freshness policy — retry after 1–5 minutes |
205
206
  | A command hangs forever | The 120s timeout discards the pooled session automatically; use `timeoutMs: 0` for long jobs and `remote_ssh_kill` at any time |
206
207
  | Large files are truncated | 4MB per read, ≈6.29MB on the pooled download path (larger files fall back to a one-shot connection); use `remote_ssh_exec` with `head`/`tail` to page through |
package/lib/client.js CHANGED
@@ -119,6 +119,8 @@ window.__ModuleLoader__.load({
119
119
  "term.errSpawn": "启动终端失败",
120
120
  "picker.dir": "目录",
121
121
  "picker.choose": "选择此目录",
122
+ "picker.errNameEmpty": "目录名不能为空",
123
+ "picker.errNameBad": "目录名不能包含 / 或 \\,也不能是 . 或 ..",
122
124
  "flow.localTitle": "选择本地目录",
123
125
  "flow.remoteTitle": "选择远程目录",
124
126
  "flow.localDir": "本地目录",
@@ -130,6 +132,7 @@ window.__ModuleLoader__.load({
130
132
  "flow.chooseRemote": "选择远程目录…",
131
133
  "flow.backLocal": "← 选择本地目录",
132
134
  "flow.errCreate": "创建远程工作区失败",
135
+ "flow.errMkdir": "新建目录失败",
133
136
  "settings.importSshConfig": "从 ~/.ssh/config 导入…",
134
137
  "settings.importTitle": "导入 ~/.ssh/config 主机",
135
138
  "settings.noneFound": "未在 ~/.ssh/config 中发现可用主机",
@@ -219,6 +222,8 @@ window.__ModuleLoader__.load({
219
222
  "term.errSpawn": "Failed to start terminal",
220
223
  "picker.dir": "Directory",
221
224
  "picker.choose": "Choose this directory",
225
+ "picker.errNameEmpty": "Folder name cannot be empty",
226
+ "picker.errNameBad": "Folder name cannot contain / or \\, nor be . or ..",
222
227
  "flow.localTitle": "Select Local Directory",
223
228
  "flow.remoteTitle": "Select Remote Directory",
224
229
  "flow.localDir": "Local directory",
@@ -230,6 +235,7 @@ window.__ModuleLoader__.load({
230
235
  "flow.chooseRemote": "Choose remote directory…",
231
236
  "flow.backLocal": "← Choose local directory",
232
237
  "flow.errCreate": "Failed to create remote workspace",
238
+ "flow.errMkdir": "Failed to create the folder",
233
239
  "settings.importSshConfig": "Import from ~/.ssh/config…",
234
240
  "settings.importTitle": "Import ~/.ssh/config hosts",
235
241
  "settings.noneFound": "No usable hosts found in ~/.ssh/config",
@@ -777,14 +783,36 @@ window.__ModuleLoader__.load({
777
783
  return parent;
778
784
  }
779
785
 
786
+ // 新建目录名校验(issue #11):返回 i18n 键,合法则返回 null。
787
+ // 只接受单个路径段——分隔符会让「在 path 下新建」变成任意位置写入。
788
+ function newDirNameError(name) {
789
+ var s = String(name == null ? "" : name).trim();
790
+ if (!s) return "picker.errNameEmpty";
791
+ if (s === "." || s === "..") return "picker.errNameBad";
792
+ if (s.indexOf("/") >= 0 || s.indexOf("\\") >= 0) return "picker.errNameBad";
793
+ return null;
794
+ }
795
+
796
+ // 把子目录名拼到基路径之后:沿用基路径的分隔符(Windows 反斜杠 / POSIX 斜杠),
797
+ // 并保留根路径语义("/" + name、"C:\" + name)。
798
+ function joinChild(base, name) {
799
+ var raw = String(base == null ? "" : base);
800
+ var b = raw.replace(/[\/\\]+$/, "");
801
+ if (!b) return (raw.charAt(0) === "/" ? "/" : "") + String(name);
802
+ var sep = (b.indexOf("\\") >= 0 && b.indexOf("/") < 0) || /^[A-Za-z]:$/.test(b) ? "\\" : "/";
803
+ return b + sep + String(name);
804
+ }
805
+
780
806
  // 通用目录选择器:listFn(path) 返回 {ok, path, entries:[{name,type,path}]}。
781
807
  // resetKey 变化(如远程切换连接)时从根目录重新加载。
808
+ // createFn(path, name)(可选):在当前目录下新建子目录,远程/本地由调用方分流(issue #11)。
782
809
  function DirPicker(props) {
783
810
  var t = useT();
784
811
  var [path, setPath] = React.useState("");
785
812
  var [entries, setEntries] = React.useState([]);
786
813
  var [err, setErr] = React.useState(null);
787
814
  var [loading, setLoading] = React.useState(false);
815
+ var [newDir, setNewDir] = React.useState(null); // { name, busy, error } | null
788
816
 
789
817
  var listFnRef = React.useRef(props.listFn);
790
818
  listFnRef.current = props.listFn;
@@ -810,6 +838,26 @@ window.__ModuleLoader__.load({
810
838
  load(path || undefined);
811
839
  }
812
840
 
841
+ // 在当前 path 下新建子目录(远程/本地由 createFn 分流),成功后进入该目录,
842
+ // 便于紧接着点「选择此目录」——对齐 VSCode「新建文件夹」的交互预期(issue #11)。
843
+ function submitNewDir() {
844
+ if (!newDir || !props.createFn) return;
845
+ var name = String(newDir.name || "").trim();
846
+ var invalid = newDirNameError(name);
847
+ if (invalid) { setNewDir({ name: name, busy: false, error: t(invalid) }); return; }
848
+ setNewDir({ name: name, busy: true, error: null });
849
+ Promise.resolve(props.createFn(path, name)).then(function (r) {
850
+ if (r && r.ok) {
851
+ setNewDir(null);
852
+ load((r && r.path) || joinChild(path, name));
853
+ } else {
854
+ setNewDir({ name: name, busy: false, error: (r && r.error) || t("common.errList") });
855
+ }
856
+ }).catch(function (e) {
857
+ setNewDir({ name: name, busy: false, error: String(e && e.message ? e.message : e) });
858
+ });
859
+ }
860
+
813
861
  // 路径栏:输入框(回车导航)+ 打开按钮(紧挨在右侧)+ 上级
814
862
  return h("div", null,
815
863
  h("div", { className: "rssh-pathbar" },
@@ -820,6 +868,16 @@ window.__ModuleLoader__.load({
820
868
  h("button", { className: "rssh-btn", onClick: open, disabled: loading }, t("common.open")),
821
869
  h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
822
870
  ),
871
+ // 新建目录输入行(点底部「新建目录」按钮后出现)
872
+ newDir ? h("div", { className: "rssh-pathbar", style: { marginTop: 6 } },
873
+ h("input", { className: "rssh-input", value: newDir.name, autoFocus: true,
874
+ placeholder: t("ops.newDirPh"),
875
+ onChange: function (e) { setNewDir({ name: e.target.value, busy: false, error: null }); },
876
+ onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault(); submitNewDir(); } } }),
877
+ h("button", { className: "rssh-btn", onClick: submitNewDir, disabled: newDir.busy }, newDir.busy ? t("common.processing") : t("common.save")),
878
+ h("button", { className: "rssh-btn", onClick: function () { setNewDir(null); }, disabled: newDir.busy }, t("common.close"))
879
+ ) : null,
880
+ newDir && newDir.error ? h("div", { className: "rssh-err" }, newDir.error) : null,
823
881
  loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
824
882
  err ? h("div", { className: "rssh-err" }, err) : null,
825
883
  // 当前路径下的文件树(仅目录可进入)
@@ -832,7 +890,10 @@ window.__ModuleLoader__.load({
832
890
  })
833
891
  ),
834
892
  h("div", { className: "rssh-actions", style: { marginTop: 8 } },
835
- h("button", { className: "rssh-btn", disabled: !path || props.disabled, onClick: function () { props.onChoose(path); } }, t("picker.choose"))
893
+ h("button", { className: "rssh-btn", disabled: !path || props.disabled, onClick: function () { props.onChoose(path); } }, t("picker.choose")),
894
+ props.createFn ? h("button", { className: "rssh-btn",
895
+ disabled: !path || loading || props.disabled || !!(newDir && newDir.busy),
896
+ onClick: function () { setNewDir({ name: "", busy: false, error: null }); } }, t("ops.newDir")) : null
836
897
  )
837
898
  );
838
899
  }
@@ -882,11 +943,29 @@ window.__ModuleLoader__.load({
882
943
  profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
883
944
  )
884
945
  ) : null,
885
- // 目录选择器:远程需选好连接后才显示
946
+ // 目录选择器:远程需选好连接后才显示。
947
+ // createFn:在当前目录下新建子目录(远程走 SSH 的 mkdir -p,本地走宿主 mkdirLocal),
948
+ // 两个 tab 都提供——新建远程项目目录是本 issue 的痛点,本地顺手补齐(issue #11)。
886
949
  isRemote
887
- ? (profileId ? h(DirPicker, { resetKey: profileId, placeholder: t("flow.remoteDir"), listFn: function (p) { return remoteListFn(profileId, p); }, onChoose: chooseRemote, disabled: creating })
950
+ ? (profileId ? h(DirPicker, { resetKey: profileId, placeholder: t("flow.remoteDir"),
951
+ listFn: function (p) { return remoteListFn(profileId, p); },
952
+ createFn: function (p, name) {
953
+ var target = joinChild(p, name);
954
+ return api("mkdir", { profileId: profileId, path: target }).then(function (r) {
955
+ return (r && r.ok) ? { ok: true, path: target } : { ok: false, error: (r && r.error) || t("flow.errMkdir") };
956
+ });
957
+ },
958
+ onChoose: chooseRemote, disabled: creating })
888
959
  : h("div", { className: "rssh-pick-empty" }, t("common.selectProfile")))
889
- : h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"), listFn: function (p) { return localListFn(props.workspaces, p); }, onChoose: chooseLocal })
960
+ : h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"),
961
+ listFn: function (p) { return localListFn(props.workspaces, p); },
962
+ createFn: function (p, name) {
963
+ var target = joinChild(p, name);
964
+ return api("mkdirLocal", { path: target }).then(function (r) {
965
+ return (r && r.ok) ? { ok: true, path: target } : { ok: false, error: (r && r.error) || t("flow.errMkdir") };
966
+ });
967
+ },
968
+ onChoose: chooseLocal })
890
969
  )
891
970
  );
892
971
  }
package/lib/index.js CHANGED
@@ -1635,6 +1635,18 @@ function apply(ctx, config) {
1635
1635
  invalidateRemoteCaches(p, args.path, { subtree: false }); // 父目录列举变化
1636
1636
  return r;
1637
1637
  },
1638
+ /** 本地(宿主机器)新建目录:供「添加工作区」目录选择器的**本地** tab 创建目录用(issue #11)。
1639
+ * 远程侧走上面的 mkdir;本地侧此前没有任何入口,用户只能离开 DSH 去系统文件管理器建。 */
1640
+ mkdirLocal: async (args) => {
1641
+ const p = String((args && args.path) || "").trim();
1642
+ if (!p) return { ok: false, error: "path 为必填项" };
1643
+ try {
1644
+ await mkdir(p, { recursive: true });
1645
+ return { ok: true, path: p };
1646
+ } catch (e) {
1647
+ return { ok: false, error: "创建目录失败: " + String(e && e.message ? e.message : e) };
1648
+ }
1649
+ },
1638
1650
  deleteFile: async (args) => {
1639
1651
  const p = resolveProfile(args);
1640
1652
  if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangfengshun/dsh-remote-ssh",
3
- "version": "2.4.8",
3
+ "version": "2.4.9",
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",