@zhangfengshun/dsh-remote-ssh 2.4.7 → 2.4.8

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.8] — 修复大仓里 `@` 补全不可用:索引排除必须发生在截断之前(issue #10 实测反馈)
6
+ ### 修复
7
+ - **根因**: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 分子序列分支)。
8
+ - **修法**:把排除**下推到远端**、置于 `head` 之前——git 分支新增 `grep -vE '<排除正则>'`(正则由 `REF_EXCLUDED_DIRS` 单一事实来源生成,只匹配完整路径段,`distribution/` 这类前缀相同的普通目录不会被误伤);find 分支本就用 `-prune` 在远端剪枝,无需改动。客户端的 `excludedSegment` 保留作双保险。
9
+ - **效果**(报告者仓库):过滤后 **3,470 条 < 20,000 上限**,`AGENTS.md` 回到第 1,537 行、`src/` 1,742 条全部保留、输出体积从 1.44 MB 降到 205 KB(**−85%**)——该仓库本就不该触发截断,纯属顺序问题造成的误伤。
10
+ - **新增截断告警**(采纳其建议 1):索引按上限 +1 行取样,若有效行数超限即打一条 warn(每个 workspace 只报一次),说明「文件过多、`@` 可能漏文件」并建议在远端 `.gitignore` 忽略构建产物/虚拟环境;避免用户只看到「搜不到」而不知被截断。
11
+
12
+ ### 测试
13
+ - 新增 `tests/file-references-truncation.test.mjs`(**38 条断言**),其中 **B 段是真实端到端**:临时 git 仓库(已跟踪 `AGENTS.md` + `src/**`,未跟踪 400 个 `node_modules` 包)→ 用**真实代码生成**的命令跑**真实 POSIX 管道**,断言「旧行为(先截断)AGENTS.md 被切掉、前 200 行全是 node_modules」而「新行为 AGENTS.md 存活、src 40 条全保留、node_modules 零残留、输出更小」;另含命令结构断言(grep 必须在 head 之前、正则单一事实来源、完整路径段匹配、不误伤 `distribution/`)、解析器双保险与告警接线。
14
+ - 该测试还**锁定了一个此前未被记录的事实**:`git ls-files --cached --others` 非全局字典序(未跟踪文件在前)——夹具中 `AGENTS.md` 落在第 401/441 行。
15
+
5
16
  ## [2.4.7] — `@` 文件引用补全支持远程工作区(issue #10)
6
17
  ### 新增
7
18
  - **远程工作区会话里 `@` 补全现在列远端文件**(感谢 @Linhaojing 的通道分析与挂载点核实):`@` 补全由宿主 `ctx.fileReferences` 服务提供(官方 provider `@deepseek-ai/dsh-file-reference-local`),它只遍历**本地磁盘**——远程工作区会话的 cwd 是本地镜像目录,于是候选只有镜像里那几个文件(通常只有本插件写入的 `README.md`)。该链路不经过任何 HTTP 路由(客户端经 remote gateway 调 `ctx.fileReferences.list`),因此采用**服务层包装**:
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.7
49
+ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.8
50
50
  ```
51
51
 
52
52
  安装后**重启 DSH**。`@zhangfengshun/dsh-remote-ssh` 必须在 bundles 列表中排在 `dsh-better-sidebar` **之后**。
@@ -161,6 +161,7 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
161
161
  远程工作区会话里输入 `@`,候选来自**远端**(与「文件」页签的树一致),而不是本地镜像目录:
162
162
 
163
163
  - **索引来源**:git 仓库用 `git ls-files --cached --others --exclude-standard`(尊重 `.gitignore`、含未跟踪文件;真实超算实测 0.117s / 137 条),非 git 目录回退有界 `find`(`maxdepth 5` + 剪枝,实测 1.57s / 3121 条);
164
+ - **排除在远端、截断之前**(2.4.8 修复):`git ls-files --cached --others` 的输出**不是全局字典序**(未跟踪文件按 readdir 顺序先输出),`node_modules/` 这类目录可能占满前两万行把配额吃光——因此排除目录由远端 `grep -vE` 在 `head` 之前完成(与 `-prune` 同源,正则由同一份排除表生成),客户端过滤仅作双保险;索引达到上限时会打一条 warn 提示可能漏文件;
164
165
  - **查询语义与官方 provider 逐条对齐**:`@` 与 `@src/` 走远端目录列举;`@read` 走索引模糊匹配(同名 > 前缀 > 名称子串 > 路径子串 > 子序列,目录加权 +25);
165
166
  - **不卡输入框**:索引按工作区缓存 60 秒(写文件/执行命令后自动失效),单次补全只等 900ms——超时先用旧索引作答、重建在后台进行;连接异常时自动回退到本地行为;
166
167
  - **本地工作区不受影响**:非远程会话直接委托宿主原实现,索引与排序完全没有改动。
@@ -199,6 +200,7 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
199
200
  | 终端落在远程 `$HOME` 而不是工作区目录 | 2.4.5 起已修复(wrapper 会 `cd` 到工作区 `remotePath`,目录不存在时回退 `$HOME`);若仍停在 `$HOME`,确认 2.4.5 已装入并重启 DSH |
200
201
  | 「文件」页签树根显示镜像目录 ID(如 `wmu3sxe24jpvg`) | 2.4.6 起已修复:树根改为显示**远程目录名**(如 `IB_Robot`),悬停可见完整远程路径;该标签不经过 `fs.*` 路由,由客户端渲染层替换 |
201
202
  | `@` 补全只搜到镜像里那几个文件 | 2.4.7 起已修复:远程工作区会话的 `@` 补全改列远端文件(索引缓存 60s + 900ms 查询预算);若仍只有镜像文件,确认 2.4.7 已装入并重启 DSH |
203
+ | 大仓里 `@` 搜不到真实文件(如根目录 `AGENTS.md`、`src/**`) | 2.4.8 起已修复:此前排除目录发生在截断之后,`node_modules/` 这类目录会吃光索引配额;现在排除由远端 `grep`/`-prune` 在截断前完成,并会在索引达上限时打 warn 提示 |
202
204
  | 安装时提示 `minimumReleaseAge` 或「No matching version」(刚发布) | npm 供应链新鲜度策略,等 1–5 分钟后重试即可 |
203
205
  | 命令卡住不返回 | 默认 120s 超时后自动丢弃会话;长时任务用 `timeoutMs: 0`,随时可用 `remote_ssh_kill` 强杀 |
204
206
  | 大文件读取被截断 | 单文件读取上限 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.7
49
+ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.8
50
50
  ```
51
51
 
52
52
  **Restart DSH** after installation. `@zhangfengshun/dsh-remote-ssh` must come **after** `dsh-better-sidebar` in the bundles list.
@@ -161,6 +161,7 @@ In a remote-workspace session, `profileId` and other connection params can be om
161
161
  In a remote-workspace session, typing `@` offers **remote** candidates (matching the Files tab tree) instead of the local mirror directory:
162
162
 
163
163
  - **Index source**: git repos use `git ls-files --cached --others --exclude-standard` (respects `.gitignore`, includes untracked files; measured 0.117s / 137 entries on a real HPC), non-git directories fall back to a bounded `find` (`maxdepth 5` + pruning, measured 1.57s / 3121 entries);
164
+ - **Exclusion happens remotely, before truncation** (fixed in 2.4.8): `git ls-files --cached --others` output is **not globally sorted** (untracked files come first in readdir order), so a `node_modules/` tree can fill the first 20,000 lines and exhaust the quota — the excluded directories are therefore filtered by a remote `grep -vE` *before* `head` (same source of truth as the `-prune` list), with the client-side filter kept as a second line of defence; hitting the index cap now logs a warning that files may be missing;
164
165
  - **Query semantics mirror the official provider**: `@` and `@src/` list a remote directory; `@read` runs the fuzzy index (exact name > prefix > name substring > path substring > subsequence, directories +25);
165
166
  - **The caret never stalls**: the index is cached per workspace for 60s (invalidated after writes/commands) and a single completion waits at most 900ms — on timeout the stale index answers and the rebuild continues in the background; connection failures fall back to local behaviour;
166
167
  - **Local workspaces are untouched**: non-remote sessions delegate straight to the host implementation.
@@ -199,6 +200,7 @@ The plugin never patches DSH sources or injects into the profile dependency tree
199
200
  | Terminal opens in the remote `$HOME` instead of the workspace directory | Fixed in 2.4.5 (the wrapper `cd`s into the workspace `remotePath`, falling back to `$HOME` when it no longer exists); if it still starts in `$HOME`, make sure 2.4.5 is installed and DSH restarted |
200
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 |
201
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
+ | 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 |
202
204
  | Install fails with `minimumReleaseAge` or "No matching version" right after a release | npm supply-chain freshness policy — retry after 1–5 minutes |
203
205
  | 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 |
204
206
  | 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/index.js CHANGED
@@ -1146,6 +1146,7 @@ const REF_EXCLUDED_DIRS = new Set([
1146
1146
  ]);
1147
1147
  const refIndexCache = new Map(); // key = profileKey|root -> { at, epoch, entries }
1148
1148
  const refIndexBuilds = new Map(); // key -> Promise<entries>(同一 workspace 只建一次)
1149
+ const refTruncationWarned = new Set(); // 已就索引截断告警过的 workspace(避免每次重建都刷日志)
1149
1150
 
1150
1151
  /** 拆分 @ 查询:返回 { directory, fragment, isDirectoryQuery }(与本地 list() 的分支判据一致)。 */
1151
1152
  function refSplitQuery(rawQuery) {
@@ -1243,14 +1244,31 @@ function refParseIndexOutput(text) {
1243
1244
  return out.length > REF_MAX_ENTRIES ? out.slice(0, REF_MAX_ENTRIES) : out;
1244
1245
  }
1245
1246
 
1246
- /** 远端建索引命令:git 仓库走 git ls-files(快且尊重 .gitignore),否则有界 find。 */
1247
+ /** REF_EXCLUDED_DIRS 生成远端 grep 的排除正则(单一事实来源)。
1248
+ * 只匹配「完整路径段」(`(^|/)node_modules(/|$)`),因此 `distribution/` 这类前缀相同、
1249
+ * 但并非排除目录的路径不会被误伤。 */
1250
+ function refExcludeRegex() {
1251
+ const alts = Array.from(REF_EXCLUDED_DIRS)
1252
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
1253
+ .join("|");
1254
+ return "(^|/)(" + alts + ")(/|$)";
1255
+ }
1256
+
1257
+ /** 远端建索引命令:git 仓库走 git ls-files(快且尊重 .gitignore),否则有界 find。
1258
+ *
1259
+ * 排除必须发生在 `head` **之前**(issue #10 实测反馈):`git ls-files --cached --others`
1260
+ * 的输出**不是全局字典序**——未跟踪文件按 readdir 顺序先输出,`node_modules/` 这类目录
1261
+ * 可能占满前两万行,把 `head` 的配额吃光,导致 `AGENTS.md`、`src/**` 等真实文件被整段切掉
1262
+ * (实测仓库:node_modules 18,880 条 = 84.5%,`AGENTS.md` 落在第 20417 行)。
1263
+ * 因此 git 分支在 head 前先 `grep -vE` 掉排除目录;find 分支本就用 `-prune` 在远端剪枝。
1264
+ * 客户端的 excludedSegment 仍保留作双保险(见 refParseIndexOutput)。 */
1247
1265
  function refIndexCommand(root) {
1248
1266
  const target = shellQuotePath(root || "~");
1249
1267
  const prune = Array.from(REF_EXCLUDED_DIRS).map((n) => "-name " + shellQuote(n)).join(" -o ");
1250
1268
  return [
1251
1269
  "cd " + target + " 2>/dev/null || exit 3",
1252
1270
  "if [ -d .git ] && command -v git >/dev/null 2>&1; then",
1253
- "git ls-files --cached --others --exclude-standard 2>/dev/null | head -n " + (REF_MAX_ENTRIES + 1) + " | sed 's/^/f\t/'",
1271
+ "git ls-files --cached --others --exclude-standard 2>/dev/null | grep -vE '" + refExcludeRegex() + "' | head -n " + (REF_MAX_ENTRIES + 1) + " | sed 's/^/f\t/'",
1254
1272
  "else",
1255
1273
  "find . -mindepth 1 -maxdepth " + REF_FIND_MAXDEPTH + " \\( " + prune + " \\) -prune -o -printf '%y\t%P\\n' 2>/dev/null | head -n " + (REF_MAX_ENTRIES + 1),
1256
1274
  "fi"
@@ -2999,9 +3017,21 @@ function apply(ctx, config) {
2999
3017
  build = (async () => {
3000
3018
  try {
3001
3019
  const r = await runPooled(profile, refIndexCommand(root), undefined, MAX_BYTES, false, REF_INDEX_BUILD_TIMEOUT_MS);
3002
- const entries = refParseIndexOutput(r && r.stdout ? r.stdout : "");
3020
+ const stdout = r && r.stdout ? String(r.stdout) : "";
3021
+ const entries = refParseIndexOutput(stdout);
3022
+ // 截断信号(issue #10 实测反馈建议):命令按上限 +1 行取样,若有效行数超过上限,
3023
+ // 说明索引被 head 截断、模糊搜索可能漏文件——打一条 warn,避免用户只看到「搜不到」。
3024
+ const rawLines = stdout.split("\n").filter((line) => line.indexOf("\t") > 0).length;
3025
+ if (rawLines > REF_MAX_ENTRIES && !refTruncationWarned.has(key)) {
3026
+ refTruncationWarned.add(key);
3027
+ try {
3028
+ ctx.logger?.warn("[dsh-remote-ssh] 文件引用索引已达上限(" + REF_MAX_ENTRIES + " 条,实际 " + rawLines + "+):"
3029
+ + "远端 " + String(root || "~") + " 的文件过多,@ 补全可能漏掉部分文件;"
3030
+ + "可在远端 .gitignore 中忽略构建产物/虚拟环境目录以缩小索引");
3031
+ } catch (e) {}
3032
+ }
3003
3033
  // 只有成功(或确实拿到了输出)才写缓存:cd 失败/断连时不能把空索引缓存 60 秒
3004
- if (r && (r.ok || (r.stdout && String(r.stdout).length > 0))) {
3034
+ if (r && (r.ok || stdout.length > 0)) {
3005
3035
  refIndexCache.set(key, { at: Date.now(), epoch, entries });
3006
3036
  }
3007
3037
  return entries;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangfengshun/dsh-remote-ssh",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
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",