dsh-over-ssh 0.1.0 → 0.2.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.
Files changed (250) hide show
  1. package/README.md +19 -17
  2. package/agents/agent-manifest.json +6 -6
  3. package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
  4. package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
  5. package/lib/client.js +422 -337
  6. package/lib/file-references.js +92 -35
  7. package/lib/fs.js +295 -113
  8. package/lib/index.js +6912 -4330
  9. package/lib/terminal-wrapper.js +9 -4
  10. package/lib/types/src/config.d.ts +156 -9
  11. package/lib/types/src/file-references.d.ts +0 -1
  12. package/lib/types/src/host/app/backend.d.ts +44 -3
  13. package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +0 -1
  14. package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +0 -1
  15. package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +12 -1
  16. package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +0 -1
  17. package/lib/types/src/host/app/transport/transport-selector.d.ts +0 -1
  18. package/lib/types/src/host/app/transport/transport-service.d.ts +0 -1
  19. package/lib/types/src/host/domain/capability.d.ts +0 -1
  20. package/lib/types/src/host/domain/errors.d.ts +0 -1
  21. package/lib/types/src/host/domain/roots/connection.d.ts +0 -1
  22. package/lib/types/src/host/domain/roots/root.d.ts +24 -9
  23. package/lib/types/src/host/domain/roots/session-roots.d.ts +23 -1
  24. package/lib/types/src/host/domain/roots/ssh-config.d.ts +0 -1
  25. package/lib/types/src/host/domain/session/session-admin.d.ts +0 -1
  26. package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +0 -1
  27. package/lib/types/src/host/domain/transfer/transfer.d.ts +0 -1
  28. package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +0 -1
  29. package/lib/types/src/host/infra/transport/forward.d.ts +75 -0
  30. package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +5 -1
  31. package/lib/types/src/host/infra/transport/transport-base.d.ts +0 -1
  32. package/lib/types/src/host/infra/transport/transport.d.ts +42 -2
  33. package/lib/types/src/host/integration/agent/agent-preflight.d.ts +0 -1
  34. package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +0 -1
  35. package/lib/types/src/host/integration/fs/fs-provider.d.ts +18 -3
  36. package/lib/types/src/host/integration/transfer/transfer-service.d.ts +0 -1
  37. package/lib/types/src/index.d.ts +23 -1
  38. package/lib/types/src/local-bridge-refs.d.ts +0 -1
  39. package/lib/types/src/local-bridge.d.ts +0 -1
  40. package/lib/types/src/shared/root-keys.d.ts +120 -0
  41. package/lib/types/src/wiring/helpers.d.ts +0 -1
  42. package/package.json +5 -3
  43. package/lib/types/src/client/controls.d.ts +0 -131
  44. package/lib/types/src/client/core/card-focus-trap.d.ts +0 -45
  45. package/lib/types/src/client/core/controls.d.ts +0 -164
  46. package/lib/types/src/client/core/latest.d.ts +0 -48
  47. package/lib/types/src/client/core/locale.d.ts +0 -38
  48. package/lib/types/src/client/core/message.d.ts +0 -37
  49. package/lib/types/src/client/core/panel-model.d.ts +0 -134
  50. package/lib/types/src/client/core/portal.d.ts +0 -266
  51. package/lib/types/src/client/core/remote.d.ts +0 -84
  52. package/lib/types/src/client/core/styles.d.ts +0 -458
  53. package/lib/types/src/client/file-tree-io.d.ts +0 -19
  54. package/lib/types/src/client/file-tree.d.ts +0 -271
  55. package/lib/types/src/client/flows/directory-browser.d.ts +0 -587
  56. package/lib/types/src/client/flows/session-directory.d.ts +0 -415
  57. package/lib/types/src/client/flows/workspace-directory-flow.d.ts +0 -480
  58. package/lib/types/src/client/locale.d.ts +0 -31
  59. package/lib/types/src/client/panel-model.d.ts +0 -37
  60. package/lib/types/src/client/panel.d.ts +0 -77
  61. package/lib/types/src/client/roots/connection-test.d.ts +0 -69
  62. package/lib/types/src/client/roots/roots-panel-state.d.ts +0 -307
  63. package/lib/types/src/client/roots/roots-panel-view.d.ts +0 -280
  64. package/lib/types/src/client/roots/roots-panel.d.ts +0 -364
  65. package/lib/types/src/client/roots/transfer-panel-view.d.ts +0 -35
  66. package/lib/types/src/client/roots/transfer-panel.d.ts +0 -241
  67. package/lib/types/src/client/roots/transfer-progress.d.ts +0 -67
  68. package/lib/types/src/client/roots-panel-view.d.ts +0 -236
  69. package/lib/types/src/client/roots-panel.d.ts +0 -347
  70. package/lib/types/src/client/session/session-panel-view.d.ts +0 -336
  71. package/lib/types/src/client/session/session-panel.d.ts +0 -322
  72. package/lib/types/src/client/session/session-remote-mark.d.ts +0 -110
  73. package/lib/types/src/client/session/session-row-actions.d.ts +0 -398
  74. package/lib/types/src/client/session-panel.d.ts +0 -152
  75. package/lib/types/src/client/styles.d.ts +0 -252
  76. package/lib/types/src/client/workspace-directory-flow.d.ts +0 -342
  77. package/lib/types/src/fs.d.ts +0 -46
  78. package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +0 -18
  79. package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +0 -38
  80. package/lib/types/src/host/app/recycle-bin-contract.d.ts +0 -215
  81. package/lib/types/src/host/app/recycle-bin-move.d.ts +0 -18
  82. package/lib/types/src/host/app/recycle-bin-service.d.ts +0 -45
  83. package/lib/types/src/host/app/recycle-bin-world.d.ts +0 -35
  84. package/lib/types/src/host/app/roots/roots-api-connections.d.ts +0 -49
  85. package/lib/types/src/host/app/roots/roots-api-contract.d.ts +0 -369
  86. package/lib/types/src/host/app/roots/roots-api-directories.d.ts +0 -13
  87. package/lib/types/src/host/app/roots/roots-api-http.d.ts +0 -77
  88. package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +0 -130
  89. package/lib/types/src/host/app/roots/roots-api-projections.d.ts +0 -102
  90. package/lib/types/src/host/app/roots/roots-api-roots.d.ts +0 -13
  91. package/lib/types/src/host/app/roots/roots-api-state.d.ts +0 -22
  92. package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +0 -26
  93. package/lib/types/src/host/app/roots/roots-api.d.ts +0 -55
  94. package/lib/types/src/host/app/roots/user-roots.d.ts +0 -552
  95. package/lib/types/src/host/app/roots-api-connections.d.ts +0 -49
  96. package/lib/types/src/host/app/roots-api-contract.d.ts +0 -339
  97. package/lib/types/src/host/app/roots-api-directories.d.ts +0 -13
  98. package/lib/types/src/host/app/roots-api-http.d.ts +0 -77
  99. package/lib/types/src/host/app/roots-api-projections.d.ts +0 -102
  100. package/lib/types/src/host/app/roots-api-roots.d.ts +0 -13
  101. package/lib/types/src/host/app/roots-api-state.d.ts +0 -22
  102. package/lib/types/src/host/app/roots-api-support.d.ts +0 -182
  103. package/lib/types/src/host/app/roots-api-transfers.d.ts +0 -13
  104. package/lib/types/src/host/app/roots-api.d.ts +0 -55
  105. package/lib/types/src/host/app/routed-fs.d.ts +0 -239
  106. package/lib/types/src/host/app/serialize.d.ts +0 -37
  107. package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +0 -36
  108. package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +0 -34
  109. package/lib/types/src/host/app/session-admin-context.d.ts +0 -36
  110. package/lib/types/src/host/app/session-admin-contract.d.ts +0 -214
  111. package/lib/types/src/host/app/session-admin-delete.d.ts +0 -34
  112. package/lib/types/src/host/app/session-admin-service.d.ts +0 -38
  113. package/lib/types/src/host/app/takeover/takeover-plan.d.ts +0 -227
  114. package/lib/types/src/host/app/takeover/takeover-rows.d.ts +0 -55
  115. package/lib/types/src/host/app/transport-selector.d.ts +0 -98
  116. package/lib/types/src/host/app/transport-service.d.ts +0 -75
  117. package/lib/types/src/host/app/user-roots.d.ts +0 -461
  118. package/lib/types/src/host/domain/bin-location.d.ts +0 -79
  119. package/lib/types/src/host/domain/connection.d.ts +0 -95
  120. package/lib/types/src/host/domain/glob-pattern.d.ts +0 -62
  121. package/lib/types/src/host/domain/literal-text.d.ts +0 -63
  122. package/lib/types/src/host/domain/path-grammar.d.ts +0 -84
  123. package/lib/types/src/host/domain/recycle-bin.d.ts +0 -337
  124. package/lib/types/src/host/domain/root.d.ts +0 -74
  125. package/lib/types/src/host/domain/roots/bin-location.d.ts +0 -91
  126. package/lib/types/src/host/domain/roots/route-table.d.ts +0 -102
  127. package/lib/types/src/host/domain/route-table.d.ts +0 -80
  128. package/lib/types/src/host/domain/session/terminal-target.d.ts +0 -136
  129. package/lib/types/src/host/domain/session-admin.d.ts +0 -330
  130. package/lib/types/src/host/domain/shell.d.ts +0 -47
  131. package/lib/types/src/host/domain/ssh-config.d.ts +0 -73
  132. package/lib/types/src/host/domain/terminal-target.d.ts +0 -132
  133. package/lib/types/src/host/domain/tool-requests.d.ts +0 -369
  134. package/lib/types/src/host/domain/transfer.d.ts +0 -260
  135. package/lib/types/src/host/infra/backend/local-backend.d.ts +0 -84
  136. package/lib/types/src/host/infra/backend/local-version.d.ts +0 -43
  137. package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +0 -89
  138. package/lib/types/src/host/infra/backend/remote-backend.d.ts +0 -239
  139. package/lib/types/src/host/infra/cache/lru.d.ts +0 -52
  140. package/lib/types/src/host/infra/cache/versioned.d.ts +0 -77
  141. package/lib/types/src/host/infra/temp-name.d.ts +0 -19
  142. package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +0 -132
  143. package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +0 -140
  144. package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +0 -41
  145. package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +0 -197
  146. package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +0 -564
  147. package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +0 -418
  148. package/lib/types/src/host/infra/transport/agent-installer.d.ts +0 -121
  149. package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +0 -123
  150. package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +0 -131
  151. package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +0 -41
  152. package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +0 -197
  153. package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +0 -509
  154. package/lib/types/src/host/infra/transport/agent-transport.d.ts +0 -353
  155. package/lib/types/src/host/infra/transport/alias-transport.d.ts +0 -206
  156. package/lib/types/src/host/infra/transport/askpass-fs.d.ts +0 -20
  157. package/lib/types/src/host/infra/transport/askpass.d.ts +0 -133
  158. package/lib/types/src/host/infra/transport/command-pool.d.ts +0 -61
  159. package/lib/types/src/host/infra/transport/fs-commands.d.ts +0 -140
  160. package/lib/types/src/host/infra/transport/git-status-route.d.ts +0 -63
  161. package/lib/types/src/host/infra/transport/git-status.d.ts +0 -101
  162. package/lib/types/src/host/infra/transport/node-spawn.d.ts +0 -26
  163. package/lib/types/src/host/infra/transport/session-window.d.ts +0 -51
  164. package/lib/types/src/host/infra/transport/shell-quote.d.ts +0 -59
  165. package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +0 -206
  166. package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +0 -20
  167. package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +0 -133
  168. package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +0 -62
  169. package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +0 -43
  170. package/lib/types/src/host/infra/transport/ssh-argv.d.ts +0 -43
  171. package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +0 -302
  172. package/lib/types/src/host/integration/agent/agent-install-io.d.ts +0 -33
  173. package/lib/types/src/host/integration/agent/askpass-registry.d.ts +0 -66
  174. package/lib/types/src/host/integration/agent-install-io.d.ts +0 -33
  175. package/lib/types/src/host/integration/agent-preflight.d.ts +0 -56
  176. package/lib/types/src/host/integration/anchor-directory.d.ts +0 -56
  177. package/lib/types/src/host/integration/askpass-registry.d.ts +0 -59
  178. package/lib/types/src/host/integration/bin-root-resolver.d.ts +0 -81
  179. package/lib/types/src/host/integration/connection-test.d.ts +0 -73
  180. package/lib/types/src/host/integration/file-reference-provider.d.ts +0 -115
  181. package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +0 -21
  182. package/lib/types/src/host/integration/fs/platform-errors.d.ts +0 -94
  183. package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +0 -369
  184. package/lib/types/src/host/integration/fs/remote-index.d.ts +0 -177
  185. package/lib/types/src/host/integration/fs-provider.d.ts +0 -226
  186. package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +0 -38
  187. package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +0 -67
  188. package/lib/types/src/host/integration/lsp/lsp-session.d.ts +0 -130
  189. package/lib/types/src/host/integration/management-tools.d.ts +0 -64
  190. package/lib/types/src/host/integration/recycle-bin-service.d.ts +0 -247
  191. package/lib/types/src/host/integration/remote/connection-test.d.ts +0 -73
  192. package/lib/types/src/host/integration/remote/remote-face.d.ts +0 -127
  193. package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +0 -108
  194. package/lib/types/src/host/integration/remote-face.d.ts +0 -127
  195. package/lib/types/src/host/integration/remote-file-tools.d.ts +0 -241
  196. package/lib/types/src/host/integration/remote-home.d.ts +0 -69
  197. package/lib/types/src/host/integration/remote-index.d.ts +0 -165
  198. package/lib/types/src/host/integration/remote-job-tools.d.ts +0 -295
  199. package/lib/types/src/host/integration/remote-tool-backend.d.ts +0 -97
  200. package/lib/types/src/host/integration/remote-tools.d.ts +0 -206
  201. package/lib/types/src/host/integration/root-registry.d.ts +0 -115
  202. package/lib/types/src/host/integration/roots/anchor-directory.d.ts +0 -56
  203. package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +0 -81
  204. package/lib/types/src/host/integration/roots/remote-home.d.ts +0 -69
  205. package/lib/types/src/host/integration/roots/root-registry.d.ts +0 -144
  206. package/lib/types/src/host/integration/roots/roots-wiring.d.ts +0 -219
  207. package/lib/types/src/host/integration/roots/session-root-index.d.ts +0 -52
  208. package/lib/types/src/host/integration/roots-api.d.ts +0 -407
  209. package/lib/types/src/host/integration/roots-wiring.d.ts +0 -211
  210. package/lib/types/src/host/integration/session/session-api.d.ts +0 -88
  211. package/lib/types/src/host/integration/session/session-bin-lock.d.ts +0 -80
  212. package/lib/types/src/host/integration/session/session-header.d.ts +0 -151
  213. package/lib/types/src/host/integration/session/session-labels.d.ts +0 -56
  214. package/lib/types/src/host/integration/session/session-route-wiring.d.ts +0 -50
  215. package/lib/types/src/host/integration/session-admin-service.d.ts +0 -236
  216. package/lib/types/src/host/integration/session-api.d.ts +0 -81
  217. package/lib/types/src/host/integration/session-bin-lock.d.ts +0 -80
  218. package/lib/types/src/host/integration/session-header.d.ts +0 -118
  219. package/lib/types/src/host/integration/session-labels.d.ts +0 -56
  220. package/lib/types/src/host/integration/session-route-wiring.d.ts +0 -50
  221. package/lib/types/src/host/integration/session-scope.d.ts +0 -113
  222. package/lib/types/src/host/integration/session-tools.d.ts +0 -44
  223. package/lib/types/src/host/integration/strict-arguments.d.ts +0 -79
  224. package/lib/types/src/host/integration/takeover/takeover-api.d.ts +0 -62
  225. package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +0 -60
  226. package/lib/types/src/host/integration/takeover/takeover-service.d.ts +0 -167
  227. package/lib/types/src/host/integration/terminal-manifest.d.ts +0 -94
  228. package/lib/types/src/host/integration/tools/management-tools.d.ts +0 -245
  229. package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +0 -316
  230. package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +0 -43
  231. package/lib/types/src/host/integration/tools/remote-tools.d.ts +0 -236
  232. package/lib/types/src/host/integration/tools/session-target.d.ts +0 -52
  233. package/lib/types/src/host/integration/tools/session-tools.d.ts +0 -46
  234. package/lib/types/src/host/integration/tools/strict-arguments.d.ts +0 -79
  235. package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +0 -94
  236. package/lib/types/src/host/integration/transfer/remote-copy.d.ts +0 -66
  237. package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +0 -48
  238. package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +0 -106
  239. package/lib/types/src/host/integration/transfer-adapters.d.ts +0 -44
  240. package/lib/types/src/host/integration/transfer-service.d.ts +0 -164
  241. package/lib/types/src/host/integration/transfer-tracker.d.ts +0 -106
  242. package/lib/types/src/host/integration/user-roots.d.ts +0 -461
  243. package/lib/types/src/wiring/connections.d.ts +0 -189
  244. package/lib/types/src/wiring/fs.d.ts +0 -13
  245. package/lib/types/src/wiring/install-memo.d.ts +0 -32
  246. package/lib/types/src/wiring/legacy-dirs.d.ts +0 -62
  247. package/lib/types/src/wiring/package-paths.d.ts +0 -35
  248. package/lib/types/src/wiring/roots.d.ts +0 -21
  249. package/lib/types/src/wiring/state-dir.d.ts +0 -29
  250. package/lib/types/src/wiring/tools.d.ts +0 -35
package/lib/fs.js CHANGED
@@ -128,6 +128,33 @@ var LruCache = class {
128
128
  this.#bytes -= entry.bytes;
129
129
  return true;
130
130
  }
131
+ /**
132
+ * Remove every entry whose key starts with `prefix`, in one pass over the internal map.
133
+ *
134
+ * 存在的理由:调用方(`VersionedCache.invalidatePrefix`)在**每次写**之后都要按前缀失效,而在此之前
135
+ * 它只有 `keys()` 可用——`keys()` 会把整张表**物化**成一个数组,随后又用 `delete(key)` 逐个做哈希
136
+ * 查找。这里直接在 `#entries` 上过一遍:不建中间数组,字节记账就地扣减,边迭代边删除(Map 迭代器
137
+ * 规范保证删除当前条目后仍会访问后续条目)。
138
+ *
139
+ * **为什么是"前缀"而不是更通用的"谓词"**:谓词要为每条 key 付一次 JS 回调,而一次 `keys()` 物化只是
140
+ * 一次指针批量拷贝。实测(20 000 条 × 2 000 次,只命中 1 条)旧写法 1 289 ms、前缀版 962 ms、
141
+ * 谓词版 953 ms——两者同量级;但在**生产上限**(读缓存 256 条 / 目录缓存 512 条)下谓词版反而比旧
142
+ * 写法慢(20 000 次:160 ms vs 148 ms),前缀版打平(147 ms)。也就是说:谓词版只在大表上不亏,
143
+ * 在小表上亏;前缀版两边都不亏。热路径上的方法按真实规模选形状。
144
+ *
145
+ * @param prefix - key prefix.
146
+ * @returns the number of removed entries.
147
+ */
148
+ deleteByPrefix(prefix) {
149
+ let removed = 0;
150
+ for (const [key, entry] of this.#entries) {
151
+ if (!key.startsWith(prefix)) continue;
152
+ this.#entries.delete(key);
153
+ this.#bytes -= entry.bytes;
154
+ removed++;
155
+ }
156
+ return removed;
157
+ }
131
158
  /** Remove everything. */
132
159
  clear() {
133
160
  this.#entries.clear();
@@ -148,16 +175,38 @@ var LruCache = class {
148
175
  rejectedOversize: this.#rejectedOversize
149
176
  };
150
177
  }
178
+ /**
179
+ * Drop the least-recently-used entry.
180
+ *
181
+ * Exposed so a shared {@link CacheBudget} can make this cache give bytes back; `#evictToBounds` is the
182
+ * same operation driven by this cache's own limits.
183
+ *
184
+ * @returns `false` when the cache is empty (the caller must not retry in a loop).
185
+ */
186
+ evictOldest() {
187
+ const oldest = this.#entries.keys().next();
188
+ if (oldest.done === true) return false;
189
+ const entry = this.#entries.get(oldest.value);
190
+ if (entry === void 0) return false;
191
+ this.#entries.delete(oldest.value);
192
+ this.#bytes -= entry.bytes;
193
+ this.#evictions++;
194
+ return true;
195
+ }
196
+ /**
197
+ * Come down to `maxBytes` by dropping the oldest entries.
198
+ *
199
+ * @param maxBytes - the ceiling to reach.
200
+ * @returns how many entries were dropped.
201
+ */
202
+ trimTo(maxBytes) {
203
+ let dropped = 0;
204
+ while (this.#bytes > maxBytes && this.evictOldest()) dropped++;
205
+ return dropped;
206
+ }
151
207
  #evictToBounds() {
152
208
  while (this.#entries.size > this.#maxEntries || this.#bytes > this.#maxBytes) {
153
- const oldest = this.#entries.keys().next();
154
- if (oldest.done === true) return;
155
- const key = oldest.value;
156
- const entry = this.#entries.get(key);
157
- if (entry === void 0) return;
158
- this.#entries.delete(key);
159
- this.#bytes -= entry.bytes;
160
- this.#evictions++;
209
+ if (!this.evictOldest()) return;
161
210
  }
162
211
  }
163
212
  };
@@ -167,6 +216,7 @@ var VersionedCache = class {
167
216
  #ttlMs;
168
217
  #now;
169
218
  #lru;
219
+ #budget;
170
220
  #hits = 0;
171
221
  #misses = 0;
172
222
  #freshHits = 0;
@@ -177,6 +227,29 @@ var VersionedCache = class {
177
227
  this.#ttlMs = options.ttlMs;
178
228
  this.#now = options.now ?? Date.now;
179
229
  this.#lru = new LruCache({ maxEntries: options.maxEntries, maxBytes: options.maxBytes }, (entry) => sizeOf(entry.value));
230
+ this.#budget = options.budget;
231
+ this.#budget?.join(this);
232
+ }
233
+ /** Bytes this cache holds (the budget member contract). */
234
+ get bytes() {
235
+ return this.#lru.stats.bytes;
236
+ }
237
+ /** Drop oldest entries down to `maxBytes` (the budget member contract). */
238
+ trimTo(maxBytes) {
239
+ return this.#lru.trimTo(maxBytes);
240
+ }
241
+ /** Drop the oldest entry (the budget member contract). */
242
+ evictOldest() {
243
+ return this.#lru.evictOldest();
244
+ }
245
+ /**
246
+ * Leave the shared budget, so a retired cache stops counting against the total.
247
+ *
248
+ * Called by whoever owns the cache when its root goes away. Without it a removed root's cache stayed in the
249
+ * accounting forever, which is the leak the budget exists to prevent.
250
+ */
251
+ leaveBudget() {
252
+ this.#budget?.leave(this);
180
253
  }
181
254
  /**
182
255
  * Look up an entry.
@@ -199,7 +272,9 @@ var VersionedCache = class {
199
272
  }
200
273
  /** Insert or replace an entry, stamping it as just-read. */
201
274
  set(key, value, version) {
202
- return this.#lru.set(key, { value, version, storedAt: this.#now() });
275
+ const stored = this.#lru.set(key, { value, version, storedAt: this.#now() });
276
+ if (stored) this.#budget?.enforce(this);
277
+ return stored;
203
278
  }
204
279
  /**
205
280
  * Refresh an entry's timestamp after a cheap remote version check matched.
@@ -226,11 +301,7 @@ var VersionedCache = class {
226
301
  * @returns the number of removed entries.
227
302
  */
228
303
  invalidatePrefix(prefix) {
229
- let removed = 0;
230
- for (const key of this.#lru.keys()) {
231
- if (key.startsWith(prefix) && this.#lru.delete(key)) removed++;
232
- }
233
- return removed;
304
+ return this.#lru.deleteByPrefix(prefix);
234
305
  }
235
306
  /** Remove everything. */
236
307
  clear() {
@@ -267,12 +338,35 @@ function isSshDevError(value) {
267
338
  return value instanceof SshDevError;
268
339
  }
269
340
 
270
- // src/host/domain/path-grammar.ts
271
- function normalizeRemotePath(path) {
272
- if (path === "") throw new SshDevError("ROOT_INVALID", "\u8FDC\u7A0B\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A", {});
273
- if (!path.startsWith("/")) {
274
- throw new SshDevError("ROOT_INVALID", `\u8FDC\u7A0B\u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${path}`, { path });
341
+ // src/shared/root-keys.ts
342
+ function normalizeLocalPath(path, options = {}) {
343
+ const caseInsensitive = options.caseInsensitive ?? false;
344
+ const unified = path.replace(/\\/g, "/");
345
+ const unc = unified.startsWith("//");
346
+ const posixAbsolute = unified.startsWith("/");
347
+ const driveMatch = /^([A-Za-z]):/.exec(unified);
348
+ const driveLetter = driveMatch?.[1];
349
+ const drive = driveLetter === void 0 ? "" : `${driveLetter.toUpperCase()}:`;
350
+ const rest = unc ? unified.slice(2) : drive === "" ? unified : unified.slice(2);
351
+ const segments = [];
352
+ for (const segment of rest.split("/")) {
353
+ if (segment === "" || segment === ".") continue;
354
+ if (segment === "..") {
355
+ segments.pop();
356
+ continue;
357
+ }
358
+ segments.push(segment);
275
359
  }
360
+ const body = segments.join("/");
361
+ let normalized;
362
+ if (unc) normalized = `//${body}`;
363
+ else if (drive !== "") normalized = body === "" ? `${drive}/` : `${drive}/${body}`;
364
+ else if (posixAbsolute) normalized = body === "" ? "/" : `/${body}`;
365
+ else normalized = body === "" ? "." : body;
366
+ return caseInsensitive ? normalized.toLowerCase() : normalized;
367
+ }
368
+ function normalizePosixPath(path) {
369
+ if (!path.startsWith("/")) return void 0;
276
370
  const segments = [];
277
371
  for (const segment of path.split("/")) {
278
372
  if (segment === "" || segment === ".") continue;
@@ -284,12 +378,31 @@ function normalizeRemotePath(path) {
284
378
  }
285
379
  return segments.length === 0 ? "/" : `/${segments.join("/")}`;
286
380
  }
381
+
382
+ // src/host/domain/path-grammar.ts
383
+ function normalizeRemotePath(path) {
384
+ if (/[\u0000-\u001f]/.test(path)) {
385
+ throw new SshDevError("ROOT_INVALID", "\u8FDC\u7A0B\u8DEF\u5F84\u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26", { path: JSON.stringify(path) });
386
+ }
387
+ const normalized = normalizePosixPath(path);
388
+ if (normalized !== void 0) return normalized;
389
+ if (path === "") throw new SshDevError("ROOT_INVALID", "\u8FDC\u7A0B\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A", {});
390
+ throw new SshDevError("ROOT_INVALID", `\u8FDC\u7A0B\u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${path}`, { path });
391
+ }
287
392
  function remotePathContains(root, candidate) {
288
393
  const normalizedRoot = normalizeRemotePath(root);
289
394
  const normalizedCandidate = normalizeRemotePath(candidate);
290
395
  if (normalizedRoot === "/") return true;
291
396
  return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
292
397
  }
398
+ function longestContainingRoot(roots, candidate) {
399
+ let best;
400
+ for (const root of roots) {
401
+ if (!remotePathContains(root, candidate)) continue;
402
+ if (best === void 0 || normalizeRemotePath(root).length > normalizeRemotePath(best).length) best = root;
403
+ }
404
+ return best;
405
+ }
293
406
 
294
407
  // src/host/infra/transport/git-status.ts
295
408
  function isGitStatusTransport(value) {
@@ -429,31 +542,27 @@ function relativeDirectory(root, path) {
429
542
  }
430
543
 
431
544
  // src/host/domain/roots/root.ts
432
- function normalizeLocalPath(path, options = {}) {
433
- const caseInsensitive = options.caseInsensitive ?? process.platform === "win32";
434
- const unified = path.replace(/\\/g, "/");
435
- const unc = unified.startsWith("//");
436
- const posixAbsolute = unified.startsWith("/");
437
- const driveMatch = /^([A-Za-z]):/.exec(unified);
438
- const driveLetter = driveMatch?.[1];
439
- const drive = driveLetter === void 0 ? "" : `${driveLetter.toUpperCase()}:`;
440
- const rest = unc ? unified.slice(2) : drive === "" ? unified : unified.slice(2);
441
- const segments = [];
442
- for (const segment of rest.split("/")) {
443
- if (segment === "" || segment === ".") continue;
444
- if (segment === "..") {
445
- segments.pop();
446
- continue;
447
- }
448
- segments.push(segment);
545
+ function normalizeLocalPath2(path, options = {}) {
546
+ return normalizeLocalPath(path, { caseInsensitive: options.caseInsensitive ?? process.platform === "win32" });
547
+ }
548
+ function localPathContains(root, candidate, options = {}) {
549
+ const normalizedRoot = normalizeLocalPath2(root, options);
550
+ const normalizedCandidate = normalizeLocalPath2(candidate, options);
551
+ if (normalizedRoot === "/" || /^[a-z]:\/$/.test(normalizedRoot)) {
552
+ return normalizedCandidate.startsWith(normalizedRoot);
449
553
  }
450
- const body = segments.join("/");
451
- let normalized;
452
- if (unc) normalized = `//${body}`;
453
- else if (drive !== "") normalized = body === "" ? `${drive}/` : `${drive}/${body}`;
454
- else if (posixAbsolute) normalized = body === "" ? "/" : `/${body}`;
455
- else normalized = body === "" ? "." : body;
456
- return caseInsensitive ? normalized.toLowerCase() : normalized;
554
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
555
+ }
556
+
557
+ // src/host/app/roots/user-roots.ts
558
+ var USER_ROOTS_FILE_NAME = "user-roots.json";
559
+ var USER_ROOTS_LOCK_FILE_NAME = `${USER_ROOTS_FILE_NAME}.lock`;
560
+
561
+ // src/shared/routes.ts
562
+ function asRouteHost(value) {
563
+ if (typeof value !== "object" || value === null) return void 0;
564
+ const register = value.register;
565
+ return typeof register === "function" ? value : void 0;
457
566
  }
458
567
 
459
568
  // src/host/app/roots/roots-api-contract.ts
@@ -507,14 +616,14 @@ function parseAuthority(authority) {
507
616
  }
508
617
 
509
618
  // src/host/app/roots/roots-api-http.ts
510
- async function readBody(req) {
619
+ async function readBody(req, maxBytes) {
511
620
  const chunks = [];
512
621
  let total = 0;
513
622
  for await (const chunk of req) {
514
623
  const buffer = toBytes(chunk);
515
624
  total += buffer.length;
516
- if (total > ROOTS_API_MAX_BODY_BYTES) {
517
- throw new Error(`\u8BF7\u6C42\u4F53\u8D85\u8FC7\u4E0A\u9650 ${ROOTS_API_MAX_BODY_BYTES} \u5B57\u8282\uFF0C\u62D2\u7EDD`);
625
+ if (total > maxBytes) {
626
+ throw new SshDevError("ROOT_INVALID", `\u8BF7\u6C42\u4F53\u8D85\u8FC7\u4E0A\u9650 ${maxBytes} \u5B57\u8282\uFF0C\u62D2\u7EDD`);
518
627
  }
519
628
  chunks.push(buffer);
520
629
  }
@@ -543,11 +652,6 @@ function messageOf(error) {
543
652
 
544
653
  // src/host/infra/transport/git-status-route.ts
545
654
  var GIT_STATUS_API_PATH = "/api/over-ssh/git-status";
546
- function asRouteHost(value) {
547
- if (typeof value !== "object" || value === null) return void 0;
548
- const register = value.register;
549
- return typeof register === "function" ? value : void 0;
550
- }
551
655
  function toPublicAnswer(snapshot) {
552
656
  return {
553
657
  ok: true,
@@ -579,7 +683,7 @@ function createGitStatusApi(deps) {
579
683
  return;
580
684
  }
581
685
  try {
582
- const body = asRecord(JSON.parse(await readBody(req)));
686
+ const body = asRecord(JSON.parse(await readBody(req, ROOTS_API_MAX_BODY_BYTES)));
583
687
  const connectionId = body?.["connectionId"];
584
688
  const path = body?.["path"];
585
689
  if (typeof connectionId !== "string" || connectionId === "") {
@@ -641,15 +745,8 @@ function wireGitStatus(ctx) {
641
745
  resolveRoot: (connectionId, path) => {
642
746
  const registry = ctx.get("sshDevRoots");
643
747
  if (registry === void 0) return void 0;
644
- const normalized = normalizeRemotePath(path);
645
- let best;
646
- for (const record of registry.records) {
647
- const root = record.root;
648
- if (root.location !== "remote" || root.connectionId !== connectionId) continue;
649
- if (!remotePathContains(root.path, normalized)) continue;
650
- if (best === void 0 || root.path.length > best.length) best = root.path;
651
- }
652
- return best;
748
+ const roots = registry.records.map((record) => record.root).filter((root) => root.location === "remote" && root.connectionId === connectionId).map((root) => root.path);
749
+ return longestContainingRoot(roots, normalizeRemotePath(path));
653
750
  }
654
751
  });
655
752
  registerGitStatusRoute(ctx, { service });
@@ -699,9 +796,20 @@ var ancestorCache = /* @__PURE__ */ new Map();
699
796
  var ANCESTOR_CACHE_MAX = 256;
700
797
  function readSessionAncestors(ctx, sessionId) {
701
798
  const cached = ancestorCache.get(sessionId);
702
- if (cached !== void 0) return cached;
799
+ if (cached !== void 0) {
800
+ ancestorCache.delete(sessionId);
801
+ ancestorCache.set(sessionId, cached);
802
+ return cached;
803
+ }
703
804
  const walked = walkAncestors(ctx, sessionId);
704
- if (walked.settled && ancestorCache.size < ANCESTOR_CACHE_MAX) ancestorCache.set(sessionId, walked.ancestors);
805
+ if (walked.settled) {
806
+ ancestorCache.set(sessionId, walked.ancestors);
807
+ while (ancestorCache.size > ANCESTOR_CACHE_MAX) {
808
+ const oldest = ancestorCache.keys().next();
809
+ if (oldest.done === true) break;
810
+ ancestorCache.delete(oldest.value);
811
+ }
812
+ }
705
813
  return walked.ancestors;
706
814
  }
707
815
  function walkAncestors(ctx, sessionId) {
@@ -730,6 +838,92 @@ function walkAncestors(ctx, sessionId) {
730
838
  }
731
839
  return { ancestors, settled };
732
840
  }
841
+ function readSessionCwd(ctx, sessionId) {
842
+ const cached = sessionCwdCache.get(sessionId);
843
+ if (cached !== void 0) {
844
+ sessionCwdCache.delete(sessionId);
845
+ sessionCwdCache.set(sessionId, cached);
846
+ return cached.value === "" ? void 0 : cached.value;
847
+ }
848
+ const found = walkSessionCwd(ctx, sessionId);
849
+ if (found.settled) {
850
+ sessionCwdCache.set(sessionId, { value: found.cwd ?? "" });
851
+ while (sessionCwdCache.size > SESSION_CWD_CACHE_MAX) {
852
+ const oldest = sessionCwdCache.keys().next();
853
+ if (oldest.done === true) break;
854
+ sessionCwdCache.delete(oldest.value);
855
+ }
856
+ }
857
+ return found.cwd;
858
+ }
859
+ function readSessionCwds(ctx) {
860
+ let list;
861
+ try {
862
+ const sessions = ctx.get("sessions");
863
+ list = sessions?.list?.();
864
+ } catch {
865
+ return [];
866
+ }
867
+ if (!Array.isArray(list)) return [];
868
+ const cwds = [];
869
+ for (const session of list) {
870
+ const cwd = session?.header?.cwd;
871
+ if (typeof cwd === "string" && cwd !== "") cwds.push(cwd);
872
+ }
873
+ return cwds;
874
+ }
875
+ var sessionCwdCache = /* @__PURE__ */ new Map();
876
+ var SESSION_CWD_CACHE_MAX = 256;
877
+ function walkSessionCwd(ctx, sessionId) {
878
+ let list;
879
+ try {
880
+ const sessions = ctx.get("sessions");
881
+ list = sessions?.list?.();
882
+ } catch {
883
+ return { cwd: void 0, settled: false };
884
+ }
885
+ if (!Array.isArray(list)) return { cwd: void 0, settled: false };
886
+ for (const session of list) {
887
+ if (session?.header?.id !== sessionId) continue;
888
+ const cwd = session.header.cwd;
889
+ return { cwd: typeof cwd === "string" && cwd !== "" ? cwd : void 0, settled: true };
890
+ }
891
+ return { cwd: void 0, settled: false };
892
+ }
893
+
894
+ // src/host/domain/roots/session-roots.ts
895
+ function workspaceWriteArea(root, workspaceRoot, cwd, identity = {}) {
896
+ const sessions = root.sessions ?? [];
897
+ if (sessions.length > 0 && identity.sessionId !== void 0 && identity.sessionId !== "") {
898
+ if (addedBySession(sessions, identity)) return "whole-root";
899
+ } else if (sessions.length === 0 && cwd !== void 0 && cwd !== "") {
900
+ if ((root.workspaces ?? []).some((candidate) => sameWorkspacePath(candidate, cwd))) return "whole-root";
901
+ }
902
+ if (workspaceRoot === void 0 || workspaceRoot === "") return "deny";
903
+ if (root.location === "remote") {
904
+ const anchor = root.anchorPath;
905
+ if (anchor === void 0 || anchor === "") return "deny";
906
+ if (sameWorkspacePath(anchor, workspaceRoot) || localPathContains(anchor, workspaceRoot)) return "whole-root";
907
+ if (localPathContains(workspaceRoot, anchor)) return "whole-root";
908
+ return "deny";
909
+ }
910
+ if (localPathContains(root.path, workspaceRoot)) return "workspace-only";
911
+ if (cwd !== void 0 && cwd !== "" && sameWorkspacePath(root.path, cwd)) return "workspace-only";
912
+ return "deny";
913
+ }
914
+ function addedBySession(sessions, identity) {
915
+ const sessionId = identity.sessionId;
916
+ if (sessionId === void 0 || sessionId === "") return false;
917
+ if (sessions.includes(sessionId)) return true;
918
+ return (identity.ancestors ?? []).some((ancestor) => sessions.includes(ancestor));
919
+ }
920
+ function sameWorkspacePath(left, right) {
921
+ const fold = (value) => {
922
+ const normalized = normalizeLocalPath2(value, { caseInsensitive: false }).replace(/\/+/g, "/");
923
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
924
+ };
925
+ return fold(left) === fold(right);
926
+ }
733
927
 
734
928
  // src/host/integration/fs/fs-provider.ts
735
929
  var MAX_WHOLE_READ_BYTES = 16 * 1024 * 1024;
@@ -798,7 +992,7 @@ var RoutedFsProvider = class extends FileSystem {
798
992
  */
799
993
  processPathFromHostPath(hostPath) {
800
994
  if (!isAbsoluteHostPath(hostPath)) return void 0;
801
- return normalizeLocalPath(hostPath, { caseInsensitive: false });
995
+ return normalizeLocalPath2(hostPath, { caseInsensitive: false });
802
996
  }
803
997
  /** Canonical `file:` URI in the target's world. */
804
998
  fileUrl(target) {
@@ -932,16 +1126,6 @@ var RoutedFsProvider = class extends FileSystem {
932
1126
  const chunk = await this.call(() => routed.backend.readBytes(routed.absolute, end, signal));
933
1127
  return chunk.bytes.subarray(start, Math.min(end, chunk.bytes.byteLength));
934
1128
  }
935
- /**
936
- * Read a window through the injected offset reader.
937
- *
938
- * Kept as a thin method so the instance is the receiver here (a direct call is fine); the work happens in
939
- * the free {@link readChunkedWindow}, because the public read path invokes it from inside `this.call(...)`,
940
- * where `this` is the Cordis service Proxy and instance state would not be reachable.
941
- */
942
- #readChunkedWindow(routed, offset, length, signal) {
943
- return readChunkedWindow(this.rangeReader, this.rangeChunkBytes, routed, offset, length, signal);
944
- }
945
1129
  /** Direct children, in stable name order, with resolved child targets. */
946
1130
  async listDir(target, signal) {
947
1131
  const routed = this.routedTarget(target);
@@ -968,7 +1152,7 @@ var RoutedFsProvider = class extends FileSystem {
968
1152
  }
969
1153
  const guard = expected?.kind === "replaceIfVersion" ? expected.version : void 0;
970
1154
  const outcome = await this.call(
971
- () => routed.backend.writeText(routed.absolute, content, guard, signal)
1155
+ () => routed.backend.writeText(routed.absolute, content, guard, signal, sandboxPolicy)
972
1156
  ).catch((error) => {
973
1157
  throw mapWriteError(error, routed.displayPath);
974
1158
  });
@@ -1013,7 +1197,8 @@ var RoutedFsProvider = class extends FileSystem {
1013
1197
  edit.newString,
1014
1198
  edit.replaceAll === true,
1015
1199
  chunk.version,
1016
- signal
1200
+ signal,
1201
+ sandboxPolicy
1017
1202
  )
1018
1203
  );
1019
1204
  return { version: FsVersion(native.version), before, after };
@@ -1024,7 +1209,7 @@ var RoutedFsProvider = class extends FileSystem {
1024
1209
  }
1025
1210
  }
1026
1211
  const outcome = await this.call(
1027
- () => routed.backend.writeText(routed.absolute, restoreLineEndings(after, lineEndings), chunk.version, signal)
1212
+ () => routed.backend.writeText(routed.absolute, restoreLineEndings(after, lineEndings), chunk.version, signal, sandboxPolicy)
1028
1213
  ).catch((error) => {
1029
1214
  throw mapWriteError(error, routed.displayPath);
1030
1215
  });
@@ -1153,7 +1338,24 @@ var RoutedFsProvider = class extends FileSystem {
1153
1338
  throw isSshDevError(error) ? toFsError(error) : error;
1154
1339
  }
1155
1340
  }
1156
- /** Enforce the per-call policy and the root's own writability. */
1341
+ /**
1342
+ * Enforce the per-call policy and the root's own writability.
1343
+ *
1344
+ * Three gates, in order:
1345
+ * 1. `read-only` denies every mutation (the platform's own contract);
1346
+ * 2. a root registered `writable: false` denies;
1347
+ * 3. under **`workspace-write`**, the target must be inside the session's workspace — where "workspace"
1348
+ * includes the directories **this session added** (product decision 2026-11-04: 「添加到会话的目录」=
1349
+ * 该目录临时成为 workspace 的一部分). Before that decision the plugin admitted every writable root it
1350
+ * could route, which made the mode wider than the platform's own `fs-sandbox` fence
1351
+ * (`workspace-write` = "canonicalizes under `workspaceRoot` or the platform temp area").
1352
+ *
1353
+ * A call without a per-call policy keeps the historical behaviour (the mode comes from
1354
+ * {@link sandboxMode}); the area rule needs `workspaceRoot`, which only the policy carries.
1355
+ *
1356
+ * @param routed - the resolved target.
1357
+ * @param policy - the per-call sandbox policy the tool layer resolved.
1358
+ */
1157
1359
  assertWritable(routed, policy) {
1158
1360
  const mode = policy?.mode ?? this.sandboxMode;
1159
1361
  if (mode === "read-only") {
@@ -1162,6 +1364,19 @@ var RoutedFsProvider = class extends FileSystem {
1162
1364
  if (routed.root.writable === false) {
1163
1365
  throw platformFsError(`the root of "${routed.displayPath}" is registered read-only`, "FS_SANDBOX_DENIED");
1164
1366
  }
1367
+ if (mode !== "workspace-write" || policy === void 0) return;
1368
+ const sessionId = currentSessionId();
1369
+ const identity = sessionId === void 0 || sessionId === "" ? {} : { sessionId, ancestors: this.sessionAncestors(sessionId) };
1370
+ const cwd = sessionId === void 0 || sessionId === "" ? void 0 : this.sessionWorkspaceCwdOf(sessionId);
1371
+ const area = workspaceWriteArea(routed.root, policy.workspaceRoot, cwd, identity);
1372
+ if (area === "whole-root") return;
1373
+ if (area === "workspace-only" && routed.root.location === "local" && localPathContains(policy.workspaceRoot, routed.absolute, { caseInsensitive: process.platform === "win32" })) {
1374
+ return;
1375
+ }
1376
+ throw platformFsError(
1377
+ `sandbox denies writing "${routed.displayPath}": the path is outside this session's workspace (its workspace, or a directory this session added with \u300C\u6DFB\u52A0\u76EE\u5F55\u300D)`,
1378
+ "FS_SANDBOX_DENIED"
1379
+ );
1165
1380
  }
1166
1381
  /**
1167
1382
  * Previous text content, when it is readable text inside the cap; `undefined` otherwise.
@@ -1177,9 +1392,8 @@ var RoutedFsProvider = class extends FileSystem {
1177
1392
  return void 0;
1178
1393
  });
1179
1394
  if (chunk === void 0 || chunk.truncated) return void 0;
1180
- const bytes = Buffer.from(chunk.text, "utf8");
1181
- const sniff = bytes.subarray(0, BINARY_SNIFF_BYTES);
1182
- if (sniff.includes(0)) return void 0;
1395
+ const nul = chunk.text.indexOf("\0");
1396
+ if (nul !== -1 && nul < BINARY_SNIFF_BYTES) return void 0;
1183
1397
  return chunk.text;
1184
1398
  }
1185
1399
  /** Run a backend call, translating domain errors into core codes. */
@@ -1269,38 +1483,6 @@ function sessionSyncSignature(cwds, registry) {
1269
1483
  const roots = registry.sessionWorkspaceRecords.map((record) => record.key).join("\0");
1270
1484
  return `${cwds.join("\0")}${roots}`;
1271
1485
  }
1272
- function readSessionCwd(ctx, sessionId) {
1273
- let list;
1274
- try {
1275
- const sessions = ctx.get("sessions");
1276
- list = sessions?.list?.();
1277
- } catch {
1278
- return void 0;
1279
- }
1280
- if (!Array.isArray(list)) return void 0;
1281
- for (const session of list) {
1282
- if (session?.header?.id !== sessionId) continue;
1283
- const cwd = session.header.cwd;
1284
- return typeof cwd === "string" && cwd !== "" ? cwd : void 0;
1285
- }
1286
- return void 0;
1287
- }
1288
- function readSessionCwds(ctx) {
1289
- let list;
1290
- try {
1291
- const sessions = ctx.get("sessions");
1292
- list = sessions?.list?.();
1293
- } catch {
1294
- return [];
1295
- }
1296
- if (!Array.isArray(list)) return [];
1297
- const cwds = [];
1298
- for (const session of list) {
1299
- const cwd = session?.header?.cwd;
1300
- if (typeof cwd === "string" && cwd !== "") cwds.push(cwd);
1301
- }
1302
- return cwds;
1303
- }
1304
1486
  function assertText(displayPath, bytes) {
1305
1487
  if (bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) {
1306
1488
  throw platformFsError(`cannot read "${displayPath}": not a UTF-8 text file`, "FS_NOT_TEXT");