dsh-over-ssh 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +119 -2
- package/agents/agent-manifest.json +20 -0
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/cordis.patch.yml +84 -0
- package/lib/client.js +6611 -0
- package/lib/file-references.js +366 -0
- package/lib/fs.js +1368 -0
- package/lib/index.js +19034 -0
- package/lib/local-bridge-refs.js +16 -0
- package/lib/local-bridge.js +16 -0
- package/lib/terminal-wrapper.js +294 -0
- package/lib/types/src/client/controls.d.ts +131 -0
- package/lib/types/src/client/core/card-focus-trap.d.ts +45 -0
- package/lib/types/src/client/core/controls.d.ts +164 -0
- package/lib/types/src/client/core/latest.d.ts +48 -0
- package/lib/types/src/client/core/locale.d.ts +38 -0
- package/lib/types/src/client/core/message.d.ts +37 -0
- package/lib/types/src/client/core/panel-model.d.ts +134 -0
- package/lib/types/src/client/core/portal.d.ts +266 -0
- package/lib/types/src/client/core/remote.d.ts +84 -0
- package/lib/types/src/client/core/styles.d.ts +458 -0
- package/lib/types/src/client/file-tree-io.d.ts +19 -0
- package/lib/types/src/client/file-tree.d.ts +271 -0
- package/lib/types/src/client/flows/directory-browser.d.ts +587 -0
- package/lib/types/src/client/flows/session-directory.d.ts +415 -0
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +480 -0
- package/lib/types/src/client/index.d.ts +49 -0
- package/lib/types/src/client/locale.d.ts +31 -0
- package/lib/types/src/client/panel-model.d.ts +37 -0
- package/lib/types/src/client/panel.d.ts +77 -0
- package/lib/types/src/client/roots/connection-test.d.ts +69 -0
- package/lib/types/src/client/roots/roots-panel-state.d.ts +307 -0
- package/lib/types/src/client/roots/roots-panel-view.d.ts +280 -0
- package/lib/types/src/client/roots/roots-panel.d.ts +364 -0
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +35 -0
- package/lib/types/src/client/roots/transfer-panel.d.ts +241 -0
- package/lib/types/src/client/roots/transfer-progress.d.ts +67 -0
- package/lib/types/src/client/roots-panel-view.d.ts +236 -0
- package/lib/types/src/client/roots-panel.d.ts +347 -0
- package/lib/types/src/client/session/session-panel-view.d.ts +336 -0
- package/lib/types/src/client/session/session-panel.d.ts +322 -0
- package/lib/types/src/client/session/session-remote-mark.d.ts +110 -0
- package/lib/types/src/client/session/session-row-actions.d.ts +398 -0
- package/lib/types/src/client/session-panel.d.ts +152 -0
- package/lib/types/src/client/styles.d.ts +252 -0
- package/lib/types/src/client/workspace-directory-flow.d.ts +342 -0
- package/lib/types/src/config.d.ts +312 -0
- package/lib/types/src/file-references.d.ts +25 -0
- package/lib/types/src/fs.d.ts +46 -0
- package/lib/types/src/host/app/backend.d.ts +170 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +247 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +38 -0
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +215 -0
- package/lib/types/src/host/app/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin-world.d.ts +35 -0
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +369 -0
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +130 -0
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +26 -0
- package/lib/types/src/host/app/roots/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/roots/user-roots.d.ts +552 -0
- package/lib/types/src/host/app/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots-api-contract.d.ts +339 -0
- package/lib/types/src/host/app/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots-api-support.d.ts +182 -0
- package/lib/types/src/host/app/roots-api-transfers.d.ts +13 -0
- package/lib/types/src/host/app/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/routed-fs.d.ts +239 -0
- package/lib/types/src/host/app/serialize.d.ts +37 -0
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +221 -0
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +40 -0
- package/lib/types/src/host/app/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin-contract.d.ts +214 -0
- package/lib/types/src/host/app/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin-service.d.ts +38 -0
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +227 -0
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +55 -0
- package/lib/types/src/host/app/transport/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +82 -0
- package/lib/types/src/host/app/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport-service.d.ts +75 -0
- package/lib/types/src/host/app/user-roots.d.ts +461 -0
- package/lib/types/src/host/domain/bin-location.d.ts +79 -0
- package/lib/types/src/host/domain/capability.d.ts +78 -0
- package/lib/types/src/host/domain/connection.d.ts +95 -0
- package/lib/types/src/host/domain/errors.d.ts +55 -0
- package/lib/types/src/host/domain/glob-pattern.d.ts +62 -0
- package/lib/types/src/host/domain/literal-text.d.ts +63 -0
- package/lib/types/src/host/domain/path-grammar.d.ts +84 -0
- package/lib/types/src/host/domain/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/root.d.ts +74 -0
- package/lib/types/src/host/domain/roots/bin-location.d.ts +91 -0
- package/lib/types/src/host/domain/roots/connection.d.ts +106 -0
- package/lib/types/src/host/domain/roots/root.d.ts +108 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +102 -0
- package/lib/types/src/host/domain/roots/session-roots.d.ts +151 -0
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/route-table.d.ts +80 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +345 -0
- package/lib/types/src/host/domain/session/terminal-target.d.ts +136 -0
- package/lib/types/src/host/domain/session-admin.d.ts +330 -0
- package/lib/types/src/host/domain/shell.d.ts +47 -0
- package/lib/types/src/host/domain/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/terminal-target.d.ts +132 -0
- package/lib/types/src/host/domain/tool-requests.d.ts +369 -0
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/transfer/transfer.d.ts +260 -0
- package/lib/types/src/host/domain/transfer.d.ts +260 -0
- package/lib/types/src/host/infra/backend/local-backend.d.ts +84 -0
- package/lib/types/src/host/infra/backend/local-version.d.ts +43 -0
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +89 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +239 -0
- package/lib/types/src/host/infra/cache/lru.d.ts +52 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +77 -0
- package/lib/types/src/host/infra/temp-name.d.ts +19 -0
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +132 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +140 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +564 -0
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +418 -0
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +123 -0
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +131 -0
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +509 -0
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +353 -0
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/command-pool.d.ts +61 -0
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +140 -0
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +63 -0
- package/lib/types/src/host/infra/transport/git-status.d.ts +101 -0
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +26 -0
- package/lib/types/src/host/infra/transport/session-window.d.ts +51 -0
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +59 -0
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +62 -0
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +328 -0
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +302 -0
- package/lib/types/src/host/infra/transport/transport-base.d.ts +34 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +527 -0
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +66 -0
- package/lib/types/src/host/integration/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/askpass-registry.d.ts +59 -0
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/file-reference-provider.d.ts +115 -0
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +142 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +368 -0
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +21 -0
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +94 -0
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +369 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +177 -0
- package/lib/types/src/host/integration/fs-provider.d.ts +226 -0
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +38 -0
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +67 -0
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +130 -0
- package/lib/types/src/host/integration/management-tools.d.ts +64 -0
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +247 -0
- package/lib/types/src/host/integration/remote/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/remote/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +108 -0
- package/lib/types/src/host/integration/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote-file-tools.d.ts +241 -0
- package/lib/types/src/host/integration/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/remote-index.d.ts +165 -0
- package/lib/types/src/host/integration/remote-job-tools.d.ts +295 -0
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +97 -0
- package/lib/types/src/host/integration/remote-tools.d.ts +206 -0
- package/lib/types/src/host/integration/root-registry.d.ts +115 -0
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +144 -0
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +219 -0
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +52 -0
- package/lib/types/src/host/integration/roots-api.d.ts +407 -0
- package/lib/types/src/host/integration/roots-wiring.d.ts +211 -0
- package/lib/types/src/host/integration/session/session-api.d.ts +88 -0
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session/session-header.d.ts +151 -0
- package/lib/types/src/host/integration/session/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-admin-service.d.ts +236 -0
- package/lib/types/src/host/integration/session-api.d.ts +81 -0
- package/lib/types/src/host/integration/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session-header.d.ts +118 -0
- package/lib/types/src/host/integration/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-scope.d.ts +113 -0
- package/lib/types/src/host/integration/session-tools.d.ts +44 -0
- package/lib/types/src/host/integration/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +62 -0
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +60 -0
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +167 -0
- package/lib/types/src/host/integration/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/tools/management-tools.d.ts +245 -0
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +316 -0
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +43 -0
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +236 -0
- package/lib/types/src/host/integration/tools/session-target.d.ts +52 -0
- package/lib/types/src/host/integration/tools/session-tools.d.ts +46 -0
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +66 -0
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +48 -0
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +212 -0
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/transfer-adapters.d.ts +44 -0
- package/lib/types/src/host/integration/transfer-service.d.ts +164 -0
- package/lib/types/src/host/integration/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/user-roots.d.ts +461 -0
- package/lib/types/src/index.d.ts +66 -0
- package/lib/types/src/local-bridge-refs.d.ts +26 -0
- package/lib/types/src/local-bridge.d.ts +36 -0
- package/lib/types/src/shared/format.d.ts +13 -0
- package/lib/types/src/wiring/connections.d.ts +189 -0
- package/lib/types/src/wiring/fs.d.ts +13 -0
- package/lib/types/src/wiring/helpers.d.ts +144 -0
- package/lib/types/src/wiring/install-memo.d.ts +32 -0
- package/lib/types/src/wiring/legacy-dirs.d.ts +62 -0
- package/lib/types/src/wiring/package-paths.d.ts +35 -0
- package/lib/types/src/wiring/roots.d.ts +21 -0
- package/lib/types/src/wiring/state-dir.d.ts +29 -0
- package/lib/types/src/wiring/tools.d.ts +35 -0
- package/package.json +120 -4
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-owned roots and connections, as the **panel** may edit them.
|
|
3
|
+
*
|
|
4
|
+
* Two files in the plugin state directory must never be confused:
|
|
5
|
+
*
|
|
6
|
+
* - `roots.json` — the terminal wrapper's manifest ({@link ./terminal-manifest.ts}). It is *derived*: the
|
|
7
|
+
* host rewrites it from the live registry after every mutation, and it contains already-resolved ssh
|
|
8
|
+
* argv. Nothing here touches it.
|
|
9
|
+
* - `user-roots.json` — **this module**. It is the *input*: what the user asked for in the UI, kept across
|
|
10
|
+
* restarts so a panel can list and edit roots without the host having to re-derive them from Config.
|
|
11
|
+
*
|
|
12
|
+
* Shape (version 1):
|
|
13
|
+
*
|
|
14
|
+
* ```json
|
|
15
|
+
* {
|
|
16
|
+
* "version": 1,
|
|
17
|
+
* "roots": [{ "location": "remote", "connectionId": "gpu01", "path": "/srv/work", "writable": true }],
|
|
18
|
+
* "connections": [{ "id": "gpu01", "host": "gpu01.example.net", "user": "root", "port": 22,
|
|
19
|
+
* "authKind": "key", "keyPath": "~/.ssh/id_ed25519" }]
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Non-negotiables this module owns:
|
|
24
|
+
*
|
|
25
|
+
* 1. **Atomic writes** — temp file + `rename`, so a reader never sees a half-written document. The temp name
|
|
26
|
+
* is unique per write: a shared `${file}.tmp` let two overlapping writers truncate each other's scratch
|
|
27
|
+
* file and race on the `rename`.
|
|
28
|
+
* 2. **Bad JSON never throws at the caller** — {@link parseUserRoots} returns the empty state plus a
|
|
29
|
+
* human-readable warning; a single corrupt entry is dropped, not fatal.
|
|
30
|
+
* 3. **No secret value, ever** — a `password` or `passphrase` **field** is refused with an error that says so
|
|
31
|
+
* (密码不入 UI 存储). Passwords belong in `ctx.credentials`. **`authKind: 'password'` itself is accepted**:
|
|
32
|
+
* what is stored for it is only `credentialKey`, the *name* of the stored value, and the reference is
|
|
33
|
+
* derived from the id when the caller supplies none. (This paragraph used to claim `authKind: 'password'`
|
|
34
|
+
* was refused while the same file's {@link normalizeConnectionEntry} accepted it — audit 0043.)
|
|
35
|
+
* 4. **Every mutation returns a new state** and persists it; nothing mutates a shared object in place.
|
|
36
|
+
* 5. **A connection rename takes its roots with it** ({@link updateConnectionInState}): a remote root names
|
|
37
|
+
* its connection in its key, so an id change that left the roots behind would break them silently.
|
|
38
|
+
* 6. **Mutations are serialised within this process** ({@link createUserRootsStore}): each one is a
|
|
39
|
+
* read-modify-write of one document, so overlapping requests would otherwise lose updates silently.
|
|
40
|
+
*
|
|
41
|
+
* This module deliberately imports **no** `@deepseek-ai/*` package: it is pure I/O plus validation, and the
|
|
42
|
+
* file system is injected so tests never touch the developer's home directory.
|
|
43
|
+
*
|
|
44
|
+
* @module dsh-remote-workspace/integration/user-roots
|
|
45
|
+
*/
|
|
46
|
+
/** The only format version this module reads or writes. */
|
|
47
|
+
export declare const USER_ROOTS_VERSION = 1;
|
|
48
|
+
/** File name inside the plugin state directory (`~/.dsh/ssh-dev` by default in the entry). */
|
|
49
|
+
export declare const USER_ROOTS_FILE_NAME = "user-roots.json";
|
|
50
|
+
/** Where a user root lives. */
|
|
51
|
+
export type UserRootLocation = 'local' | 'remote';
|
|
52
|
+
/**
|
|
53
|
+
* Which authentication a panel-added connection may use.
|
|
54
|
+
*
|
|
55
|
+
* The **secret** is what may never appear here: the validator refuses a `password` or `passphrase` field
|
|
56
|
+
* even when it arrives as raw JSON. `password` authentication is supported, but only as a
|
|
57
|
+
* {@link UserConnectionRecord.credentialKey} **reference** into `ctx.credentials`.
|
|
58
|
+
*/
|
|
59
|
+
export type UserConnectionAuthKind = 'agent' | 'key' | 'password';
|
|
60
|
+
/**
|
|
61
|
+
* Which transport a panel-added connection asks for.
|
|
62
|
+
*
|
|
63
|
+
* **Not the same thing as {@link UserConnectionAuthKind}'s `agent`**, and the two must never be conflated:
|
|
64
|
+
* `authKind: 'agent'` names the **local** ssh-agent as a credential source, while `transport: 'agent'` names
|
|
65
|
+
* the **remote** Tier 2 Go program this plugin uploads on first connect (ADR-0005/0007). The values mirror the
|
|
66
|
+
* host's own `TransportKind` (`domain/connection.ts`); this module keeps its own copy because it validates the
|
|
67
|
+
* file, and `transport` is what `toProfileInput` reads to build the profile.
|
|
68
|
+
*/
|
|
69
|
+
export type UserConnectionTransport = 'alias' | 'ssh2' | 'agent';
|
|
70
|
+
/** One root the user added. */
|
|
71
|
+
export interface UserRootEntry {
|
|
72
|
+
/** Local directory or remote directory. */
|
|
73
|
+
readonly location: UserRootLocation;
|
|
74
|
+
/** Non-empty local path, or a POSIX absolute path for a remote root. */
|
|
75
|
+
readonly path: string;
|
|
76
|
+
/** Connection id; required exactly for remote roots. */
|
|
77
|
+
readonly connectionId?: string;
|
|
78
|
+
/** Display label shown by the panel. */
|
|
79
|
+
readonly label?: string;
|
|
80
|
+
/** Whether writes under this root are allowed; the panel's default is `true`. */
|
|
81
|
+
readonly writable?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* The workspaces this root was added **to** (canonical local paths).
|
|
84
|
+
*
|
|
85
|
+
* **Absent means "not added to any session yet"** - it does **not** mean "applies to every session". A
|
|
86
|
+
* session lists only the roots whose `workspaces` names its own workspace path (`session-directory.ts`,
|
|
87
|
+
* `loadRoots`), so an unscoped root is registered with the host but is listed by no session until one opts
|
|
88
|
+
* in with 「添加目录」.
|
|
89
|
+
*
|
|
90
|
+
* Why it exists: the composer's 「添加目录」 reads as "add this directory to *this* conversation", and the
|
|
91
|
+
* first version wrote every such root into one flat list - so a directory added in one workspace showed up
|
|
92
|
+
* in the next one, which the user rightly called out. The requirement document already separates the two
|
|
93
|
+
* (FR-1.7: extra roots live in the plugin's own storage, grouped per project), so the grouping is recorded
|
|
94
|
+
* here.
|
|
95
|
+
*
|
|
96
|
+
* The settings panel adds **unscoped** roots because its form has no scope field at all
|
|
97
|
+
* (`RootsPanelDrafts` carries no `workspaces` member): it registers a root with the host, and a session opts
|
|
98
|
+
* in explicitly. An earlier revision of this comment justified listing unscoped roots in *every* session by
|
|
99
|
+
* claiming that the panel's "own copy says those apply to every session" - **no such copy existed**; every
|
|
100
|
+
* `roots*` string in both dictionaries is about paths, connections, testing and validation (audit 0056).
|
|
101
|
+
* The silent global visibility that followed - a remote directory appearing in a session that had never
|
|
102
|
+
* added it - is why the rule is now the other way round.
|
|
103
|
+
*
|
|
104
|
+
* Plural because identity is the **path** ({@link userRootKey}) and the same path can be added to more than
|
|
105
|
+
* one workspace. The live registry and the router are plugin-wide (a path resolves by scheme/anchor, not by
|
|
106
|
+
* workspace), so a remote root stays addressable from another workspace. Filtering is a view fact, not a
|
|
107
|
+
* sandbox.
|
|
108
|
+
*/
|
|
109
|
+
readonly workspaces?: readonly string[];
|
|
110
|
+
/**
|
|
111
|
+
* The local anchor directory this remote root stands on, when it has one.
|
|
112
|
+
*
|
|
113
|
+
* It is persisted because it is **identity at the platform level**: the platform creates a workspace on
|
|
114
|
+
* this host path, so a root whose anchor is re-derived differently (a connection rename changes both the
|
|
115
|
+
* `connectionId` segment and the digest) silently orphans the workspace that was created on the old one.
|
|
116
|
+
* The first version derived it on every read and never stored it; a rename then moved every anchor and left
|
|
117
|
+
* the user's remote workspaces pointing at paths no root covered (`ROOT_OUTSIDE`).
|
|
118
|
+
*
|
|
119
|
+
* Absent means "derive it from the connection id and path" (`index.ts`'s `toRoot`), which is what entries
|
|
120
|
+
* written before this field existed do. Only remote roots may carry it; a local root is its own anchor.
|
|
121
|
+
*/
|
|
122
|
+
readonly anchorPath?: string;
|
|
123
|
+
}
|
|
124
|
+
/** One connection the user added. Carries **no** credential. */
|
|
125
|
+
export interface UserConnectionRecord {
|
|
126
|
+
/** Slug, unique in the file. */
|
|
127
|
+
readonly id: string;
|
|
128
|
+
/** Host name or IP. */
|
|
129
|
+
readonly host: string;
|
|
130
|
+
/** Login user. */
|
|
131
|
+
readonly user: string;
|
|
132
|
+
/** TCP port; absent means the ssh default. */
|
|
133
|
+
readonly port?: number;
|
|
134
|
+
/** `agent` (ssh-agent / ssh config), `key` (identity file) or `password` (stored secret). */
|
|
135
|
+
readonly authKind: UserConnectionAuthKind;
|
|
136
|
+
/**
|
|
137
|
+
* Transport the user chose in the panel; absent means the default the reader applies (`agent`, see
|
|
138
|
+
* `toProfileInput` in the entry). Optional rather than defaulted here so an older file keeps loading: a
|
|
139
|
+
* record written before the selector existed has no opinion, and inventing one on read would be a silent
|
|
140
|
+
* migration. A **present** value is validated ({@link normalizeConnectionEntry}) and is always one of the
|
|
141
|
+
* three, so a reader never has to guess whether a spelling is meaningful.
|
|
142
|
+
*/
|
|
143
|
+
readonly transport?: UserConnectionTransport;
|
|
144
|
+
/** Identity file path; only meaningful for `authKind: 'key'`. */
|
|
145
|
+
readonly keyPath?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Name of the stored secret, never the secret; only meaningful for `authKind: 'password'`.
|
|
148
|
+
*
|
|
149
|
+
* Derived from the id when a caller does not supply one (see `credentialRefFor`), because the platform's
|
|
150
|
+
* reference grammar is a POSIX identifier and a user-chosen Chinese name is not one.
|
|
151
|
+
*/
|
|
152
|
+
readonly credentialKey?: string;
|
|
153
|
+
}
|
|
154
|
+
/** The whole persisted document. */
|
|
155
|
+
export interface UserRootsState {
|
|
156
|
+
readonly version: typeof USER_ROOTS_VERSION;
|
|
157
|
+
readonly roots: readonly UserRootEntry[];
|
|
158
|
+
readonly connections: readonly UserConnectionRecord[];
|
|
159
|
+
}
|
|
160
|
+
/** One read of the file: the state plus everything that was wrong with it. */
|
|
161
|
+
export interface UserRootsLoad {
|
|
162
|
+
readonly state: UserRootsState;
|
|
163
|
+
/** Non-fatal problems (missing file is **not** one: that is the pristine state). */
|
|
164
|
+
readonly warnings: readonly string[];
|
|
165
|
+
}
|
|
166
|
+
/** File-system surface, injected so tests never touch the developer's home directory. */
|
|
167
|
+
export interface UserRootsIo {
|
|
168
|
+
readFile(path: string): Promise<string>;
|
|
169
|
+
writeFile(path: string, data: string): Promise<void>;
|
|
170
|
+
mkdir(path: string): Promise<void>;
|
|
171
|
+
rename(from: string, to: string): Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
/** Real file-system binding: UTF-8, owner-only, atomic. */
|
|
174
|
+
export declare const nodeUserRootsIo: UserRootsIo;
|
|
175
|
+
/** The pristine state: no roots, no connections. */
|
|
176
|
+
export declare function emptyUserRootsState(): UserRootsState;
|
|
177
|
+
/**
|
|
178
|
+
* Stable identity of a user root, and the key `remove-root` accepts.
|
|
179
|
+
*
|
|
180
|
+
* Local paths are normalised for separators and trailing slashes but **keep the caller's case**; remote keys
|
|
181
|
+
* are POSIX and therefore case-sensitive. `RootRegistry.rootKey` folds case on Windows, so
|
|
182
|
+
* {@link removeRootFromState} retries once case-insensitively for `local:` keys — otherwise a root listed by
|
|
183
|
+
* the UI as `D:/Work` could not be removed with the key `local:d:/work`.
|
|
184
|
+
*
|
|
185
|
+
* @param entry - a validated (or at least well-formed) root entry.
|
|
186
|
+
* @returns `local:<path>` or `remote:<connectionId>:<path>`.
|
|
187
|
+
*/
|
|
188
|
+
export declare function userRootKey(entry: UserRootEntry): string;
|
|
189
|
+
/**
|
|
190
|
+
* The persisted state as JSON text: UTF-8 without BOM, two-space indent, trailing newline.
|
|
191
|
+
*
|
|
192
|
+
* The BOM-free part is deliberate — a `\uFEFF` prefix makes `JSON.parse` fail in some readers and is exactly
|
|
193
|
+
* what `Set-Content -Encoding UTF8` used to add to files in this repository.
|
|
194
|
+
*
|
|
195
|
+
* @param state - state to serialise.
|
|
196
|
+
* @returns the file contents.
|
|
197
|
+
*/
|
|
198
|
+
export declare function serializeUserRoots(state: UserRootsState): string;
|
|
199
|
+
/**
|
|
200
|
+
* Read the persisted state from text.
|
|
201
|
+
*
|
|
202
|
+
* **Never throws.** Every failure mode degrades to the empty state plus a warning that names the reason:
|
|
203
|
+
* unreadable text is not distinguishable here (the caller catches the I/O error), but malformed JSON, a
|
|
204
|
+
* foreign `version`, and individual corrupt entries all are.
|
|
205
|
+
*
|
|
206
|
+
* @param text - file contents, or `undefined` when the file does not exist.
|
|
207
|
+
* @returns the usable state and the warnings collected on the way.
|
|
208
|
+
*/
|
|
209
|
+
export declare function parseUserRoots(text: string | undefined): UserRootsLoad;
|
|
210
|
+
/**
|
|
211
|
+
* Normalise and validate one root entry.
|
|
212
|
+
*
|
|
213
|
+
* Rules: `location` must be `local` or `remote`; a local path must be non-empty; a remote path must be a
|
|
214
|
+
* POSIX **absolute** path and must not be `/` (the whole remote filesystem is not a workspace root); a
|
|
215
|
+
* remote root must name a valid connection id. Unknown keys are ignored by construction — only the fields
|
|
216
|
+
* below are read.
|
|
217
|
+
*
|
|
218
|
+
* @param input - untrusted value (a JSON body or one entry of the file).
|
|
219
|
+
* @returns a clean entry.
|
|
220
|
+
* @throws SshDevError `ROOT_INVALID` when the entry is not usable.
|
|
221
|
+
*/
|
|
222
|
+
export declare function normalizeRootEntry(input: unknown): UserRootEntry;
|
|
223
|
+
/**
|
|
224
|
+
* Normalise and validate one connection entry.
|
|
225
|
+
*
|
|
226
|
+
* **A secret is refused here**, with a message that says why: 密码不入 UI 存储 — a password in this file
|
|
227
|
+
* would be readable by anything that can read the state directory, and the plugin's credential path is
|
|
228
|
+
* `ctx.credentials`. A `password` or `passphrase` field is rejected outright. `authKind: 'password'` **is**
|
|
229
|
+
* accepted, but the record then carries only `credentialKey` — the *name* of the stored value — which is
|
|
230
|
+
* derived from the id when the caller does not supply one. `keyPath` is kept for `authKind: 'key'` and
|
|
231
|
+
* dropped for the other two.
|
|
232
|
+
*
|
|
233
|
+
* `transport` is optional (absent means "the reader's default", see {@link UserConnectionRecord.transport}),
|
|
234
|
+
* but a value that **is** present must be one of the three the union allows: an unknown spelling is refused as
|
|
235
|
+
* `CONNECTION_INVALID` rather than stored, because a record the transport selector cannot read would silently
|
|
236
|
+
* fall back to a transport the user did not choose.
|
|
237
|
+
*
|
|
238
|
+
* @param input - untrusted value.
|
|
239
|
+
* @returns a clean record, never containing a credential.
|
|
240
|
+
* @throws SshDevError `CONNECTION_INVALID` when the entry is not usable.
|
|
241
|
+
*/
|
|
242
|
+
export declare function normalizeConnectionEntry(input: unknown): UserConnectionRecord;
|
|
243
|
+
/** Find one persisted entry by key (case-folded like the registry), or `undefined`. */
|
|
244
|
+
export declare function findRootInState(state: UserRootsState, key: string): UserRootEntry | undefined;
|
|
245
|
+
/**
|
|
246
|
+
* Union `workspaces` into the entry at `key`, keeping the file's own order and de-duplicating.
|
|
247
|
+
*
|
|
248
|
+
* The **union** is the point, not a replace: two workspaces that both added the same directory must not
|
|
249
|
+
* un-list each other. Identity here is the path ({@link userRootKey}), and the live registry refuses the same
|
|
250
|
+
* path twice - so one directory added in two workspaces is one root that two workspaces list.
|
|
251
|
+
*
|
|
252
|
+
* @param state - the persisted state to update.
|
|
253
|
+
* @param key - key of the entry to update.
|
|
254
|
+
* @param workspaces - the workspaces to add.
|
|
255
|
+
* @returns a new state, or the same state when nothing changed.
|
|
256
|
+
*/
|
|
257
|
+
export declare function withWorkspaces(state: UserRootsState, key: string, workspaces: readonly string[]): UserRootsState;
|
|
258
|
+
/**
|
|
259
|
+
* Append a root, returning a new state.
|
|
260
|
+
*
|
|
261
|
+
* @param state - current state.
|
|
262
|
+
* @param input - untrusted entry.
|
|
263
|
+
* @returns the next state (the input state is untouched).
|
|
264
|
+
* @throws SshDevError `ROOT_INVALID` on an invalid entry or a duplicate key.
|
|
265
|
+
*/
|
|
266
|
+
export declare function addRootToState(state: UserRootsState, input: unknown): UserRootsState;
|
|
267
|
+
/**
|
|
268
|
+
* Remove a root by key, returning a new state.
|
|
269
|
+
*
|
|
270
|
+
* @param state - current state.
|
|
271
|
+
* @param key - key from {@link userRootKey} (or from a `GET` response).
|
|
272
|
+
* @returns the next state.
|
|
273
|
+
* @throws SshDevError `ROOT_INVALID` when no root matches — a silent no-op would make the panel report
|
|
274
|
+
* success for a removal that never happened.
|
|
275
|
+
*/
|
|
276
|
+
export declare function removeRootFromState(state: UserRootsState, key: string): UserRootsState;
|
|
277
|
+
/**
|
|
278
|
+
* Remove one root from **one** workspace, keeping it for the others.
|
|
279
|
+
*
|
|
280
|
+
* The add side is a union ({@link withWorkspaces}): one directory added to two workspaces is one root that
|
|
281
|
+
* both list. The first removal implementation only had the global delete, so removing it in the workspace the
|
|
282
|
+
* operator was looking at removed it from the other one too - the inverse of the union promise, and the reason
|
|
283
|
+
* the scoped action exists.
|
|
284
|
+
*
|
|
285
|
+
* The root itself disappears only when the last workspace drops it; a root that no workspace lists is a root
|
|
286
|
+
* no session can see, so keeping it would be a silently unusable entry.
|
|
287
|
+
*
|
|
288
|
+
* @param state - current state.
|
|
289
|
+
* @param key - key from {@link userRootKey} (or from a `GET` response).
|
|
290
|
+
* @param workspacePath - the workspace to remove it from (canonical local path, compared as stored).
|
|
291
|
+
* @returns the next state.
|
|
292
|
+
* @throws SshDevError `ROOT_INVALID` when no root matches, so a no-op cannot be reported as success.
|
|
293
|
+
*/
|
|
294
|
+
export declare function removeRootFromWorkspace(state: UserRootsState, key: string, workspacePath: string): UserRootsState;
|
|
295
|
+
/**
|
|
296
|
+
* Stamp the given anchor paths onto the entries they name, returning a new state.
|
|
297
|
+
*
|
|
298
|
+
* Used by the connection-rename path: the anchor is read from the live registry **before** the roots are
|
|
299
|
+
* re-keyed, so the new entries keep pointing at the directory the user's workspaces were created on.
|
|
300
|
+
*
|
|
301
|
+
* @param state - current state.
|
|
302
|
+
* @param anchors - root key → anchor path for the entries to stamp.
|
|
303
|
+
* @returns the next state, or the same object when nothing changed.
|
|
304
|
+
*/
|
|
305
|
+
export declare function withAnchorPaths(state: UserRootsState, anchors: ReadonlyMap<string, string>): UserRootsState;
|
|
306
|
+
/**
|
|
307
|
+
* Append a connection, returning a new state.
|
|
308
|
+
*
|
|
309
|
+
* @param state - current state.
|
|
310
|
+
* @param input - untrusted entry (must not carry a password).
|
|
311
|
+
* @returns the next state.
|
|
312
|
+
* @throws SshDevError `CONNECTION_INVALID` on an invalid entry, a duplicate id, or a password field.
|
|
313
|
+
*/
|
|
314
|
+
export declare function addConnectionToState(state: UserRootsState, input: unknown): UserRootsState;
|
|
315
|
+
/**
|
|
316
|
+
* Remove a connection **and every remote root that pointed at it**, returning a new state.
|
|
317
|
+
*
|
|
318
|
+
* Dropping the roots is not optional: a remote root whose connection is gone makes
|
|
319
|
+
* `RootRegistry.createRemoteBackend` throw `CONNECTION_NOT_FOUND` on the next use, so leaving it behind
|
|
320
|
+
* would turn a deletion into a later crash.
|
|
321
|
+
*
|
|
322
|
+
* @param state - current state.
|
|
323
|
+
* @param id - connection id.
|
|
324
|
+
* @returns the next state.
|
|
325
|
+
* @throws SshDevError `CONNECTION_INVALID` when no connection has that id.
|
|
326
|
+
*/
|
|
327
|
+
export declare function removeConnectionFromState(state: UserRootsState, id: string): UserRootsState;
|
|
328
|
+
/**
|
|
329
|
+
* The fields `update-connection` may change. Absent means **keep what is there**.
|
|
330
|
+
*
|
|
331
|
+
* `password` is deliberately not here (it is not part of a record, exactly as in
|
|
332
|
+
* {@link addConnectionToState}); `credentialKey` **is**, because a rename that keeps a password must be
|
|
333
|
+
* able to keep - or deliberately re-point - the stored reference name.
|
|
334
|
+
*/
|
|
335
|
+
export interface ConnectionPatch {
|
|
336
|
+
/** New id (a rename). The value must pass {@link normalizeConnectionEntry}'s id rule. */
|
|
337
|
+
readonly id?: string;
|
|
338
|
+
readonly host?: string;
|
|
339
|
+
readonly user?: string;
|
|
340
|
+
/** A number, or `null`/`''` to drop the field (back to the ssh default). */
|
|
341
|
+
readonly port?: number | null | '';
|
|
342
|
+
readonly authKind?: UserConnectionAuthKind;
|
|
343
|
+
readonly keyPath?: string;
|
|
344
|
+
readonly transport?: UserConnectionTransport;
|
|
345
|
+
readonly credentialKey?: string;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Update one connection **in place**, returning a new state.
|
|
349
|
+
*
|
|
350
|
+
* Semantics, all of them deliberate:
|
|
351
|
+
*
|
|
352
|
+
* 1. **Absent means unchanged.** The patch is merged over the existing record, so a caller that only wants to
|
|
353
|
+
* change the host does not have to restate the authentication - and cannot accidentally drop it.
|
|
354
|
+
* 2. **A rename migrates the roots.** A remote root names its connection in its own key
|
|
355
|
+
* (`remote:<connectionId>:<path>`), so leaving the old id behind would turn every root of that
|
|
356
|
+
* connection into a dangling reference that throws `CONNECTION_NOT_FOUND` on next use. The rewrite
|
|
357
|
+
* happens here, in the same pure transition, so the file can never hold the two halves out of step.
|
|
358
|
+
* 3. **The stored secret's reference name survives a rename** unless the patch says otherwise. The reference
|
|
359
|
+
* is a name in `ctx.credentials`, derived from the id only when nothing better is known; re-deriving it
|
|
360
|
+
* from the new id would **orphan** the stored password (still in the store, no longer reachable) and could
|
|
361
|
+
* silently hand it to a later connection whose derived name collides.
|
|
362
|
+
* 4. **The position in the list is kept**, so the panel's rows do not jump when one is edited.
|
|
363
|
+
*
|
|
364
|
+
* @param state - current state.
|
|
365
|
+
* @param id - id of the connection to update (the **current** id, before any rename).
|
|
366
|
+
* @param patch - the fields to change; anything absent keeps its value.
|
|
367
|
+
* @returns the next state.
|
|
368
|
+
* @throws SshDevError `CONNECTION_INVALID` when the id is unknown, the merged record is invalid, or the new
|
|
369
|
+
* id collides with another connection.
|
|
370
|
+
*/
|
|
371
|
+
export declare function updateConnectionInState(state: UserRootsState, id: string, patch: ConnectionPatch): UserRootsState;
|
|
372
|
+
/** What one store instance offers. Every mutation returns the new state **and** writes it. */
|
|
373
|
+
export interface UserRootsStore {
|
|
374
|
+
/** Absolute path of the state file. */
|
|
375
|
+
readonly filePath: string;
|
|
376
|
+
/** Read the file; never throws for content reasons (see {@link parseUserRoots}). */
|
|
377
|
+
load(): Promise<UserRootsLoad>;
|
|
378
|
+
/** Validate and persist a whole state. */
|
|
379
|
+
save(next: UserRootsState): Promise<UserRootsState>;
|
|
380
|
+
/** Resolves once every write queued so far has settled (both success and failure). */
|
|
381
|
+
settled(): Promise<unknown>;
|
|
382
|
+
/** Persist `state + entry` and return the new state. */
|
|
383
|
+
addRoot(entry: unknown): Promise<UserRootsState>;
|
|
384
|
+
/** Persist `state - key` and return the new state. */
|
|
385
|
+
removeRoot(key: string): Promise<UserRootsState>;
|
|
386
|
+
/** Persist `state + connection` and return the new state. */
|
|
387
|
+
addConnection(entry: unknown): Promise<UserRootsState>;
|
|
388
|
+
/** Persist `state - connection` (and its remote roots) and return the new state. */
|
|
389
|
+
removeConnection(id: string): Promise<UserRootsState>;
|
|
390
|
+
/**
|
|
391
|
+
* Persist the patched connection (renaming its roots too when the id changes) and return the new state.
|
|
392
|
+
*
|
|
393
|
+
* @param id - the connection's current id.
|
|
394
|
+
* @param patch - fields to change; anything absent keeps its value.
|
|
395
|
+
*/
|
|
396
|
+
updateConnection(id: string, patch: ConnectionPatch): Promise<UserRootsState>;
|
|
397
|
+
}
|
|
398
|
+
/** Options for {@link createUserRootsStore}. */
|
|
399
|
+
export interface UserRootsStoreOptions {
|
|
400
|
+
/** Plugin state directory; the file is {@link USER_ROOTS_FILE_NAME} inside it. */
|
|
401
|
+
readonly dir: string;
|
|
402
|
+
/** File-system binding; defaults to the real one. */
|
|
403
|
+
readonly io?: UserRootsIo;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Build the store over one state directory.
|
|
407
|
+
*
|
|
408
|
+
* Deliberately **not** a class: every method is "read the current file, compute the next state purely, write
|
|
409
|
+
* it", which is exactly the shape the HTTP handlers need and the shape a test can drive with an in-memory
|
|
410
|
+
* `io`. There is no in-process cache, so two writers cannot disagree about what is on disk — the cost is
|
|
411
|
+
* that concurrent writers can still lose an update (read-modify-write is not locked; see the audit's
|
|
412
|
+
* unverified list).
|
|
413
|
+
*
|
|
414
|
+
* @param options - state directory plus an optional injected file system.
|
|
415
|
+
* @returns the store.
|
|
416
|
+
*/
|
|
417
|
+
export declare function createUserRootsStore(options: UserRootsStoreOptions): UserRootsStore;
|
|
418
|
+
/**
|
|
419
|
+
* Whether the state already contains the root `key` names.
|
|
420
|
+
*
|
|
421
|
+
* The HTTP layer needs this to make its write **idempotent**: it asks the live seam to register a root and
|
|
422
|
+
* only then reconciles the file, so "already present" must not be treated as a duplicate error.
|
|
423
|
+
*
|
|
424
|
+
* @param state - current state.
|
|
425
|
+
* @param key - key from {@link userRootKey}.
|
|
426
|
+
* @returns whether a root with that key exists.
|
|
427
|
+
*/
|
|
428
|
+
export declare function stateHasRoot(state: UserRootsState, key: string): boolean;
|
|
429
|
+
/**
|
|
430
|
+
* Whether the state already contains a connection with that id.
|
|
431
|
+
*
|
|
432
|
+
* @param state - current state.
|
|
433
|
+
* @param id - connection id.
|
|
434
|
+
* @returns whether the connection exists.
|
|
435
|
+
*/
|
|
436
|
+
export declare function stateHasConnection(state: UserRootsState, id: string): boolean;
|
|
437
|
+
/**
|
|
438
|
+
* Validate a whole state that is about to be written.
|
|
439
|
+
*
|
|
440
|
+
* `save` runs this so a caller cannot persist an entry that {@link normalizeRootEntry} would have rejected —
|
|
441
|
+
* otherwise the file would become a source of invalid state for every later reader.
|
|
442
|
+
*
|
|
443
|
+
* @param next - candidate state.
|
|
444
|
+
* @returns the normalised state.
|
|
445
|
+
* @throws SshDevError when any entry is invalid or duplicated.
|
|
446
|
+
*/
|
|
447
|
+
export declare function validateState(next: UserRootsState): UserRootsState;
|
|
448
|
+
/**
|
|
449
|
+
* Public spelling of {@link sameRootKey} for the route layer.
|
|
450
|
+
*
|
|
451
|
+
* The route has to compare keys from three sources that fold differently: the live registry (Windows local
|
|
452
|
+
* paths already lower-cased), the persisted file (case preserved), and the client (built from the `path` the
|
|
453
|
+
* answer carried). Without one comparison rule, a Windows local root is listed with a key the host cannot
|
|
454
|
+
* remove.
|
|
455
|
+
*
|
|
456
|
+
* @param left - one key.
|
|
457
|
+
* @param right - the other key.
|
|
458
|
+
* @returns whether they name the same root.
|
|
459
|
+
*/
|
|
460
|
+
export declare function sameUserRootKey(left: string, right: string): boolean;
|
|
461
|
+
//# sourceMappingURL=user-roots.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a root's **file-level recycle bin** lives, and how to tell whether that place is usable.
|
|
3
|
+
*
|
|
4
|
+
* The bin used to sit at `<root>/.dsh-remote-workspace/trash` — inside the caller's own repository, so every
|
|
5
|
+
* `git status` and `git add .` saw the bytes someone had just "safely deleted". Moving it out is not a matter
|
|
6
|
+
* of changing a default (audit 0064 §3): the router refuses any path that is not inside a registered root, so
|
|
7
|
+
* the new location has to be *registered* as a root of its own — one that is deliberately **not** listed
|
|
8
|
+
* anywhere the user looks. The placement rules live here, as pure functions, because they are the part that
|
|
9
|
+
* must be identical for every root and every world.
|
|
10
|
+
*
|
|
11
|
+
* Two constraints decide the location, and both are hard:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Same device as the root.** The bin's whole guarantee is "move first, then confirm", and a cross-device
|
|
14
|
+
* `rename` is refused outright rather than degraded to a copy (`fs-commands.ts`), because a copy can fail
|
|
15
|
+
* half way and leave the bytes neither here nor there. The file-level bin has no copy fallback. So the bin
|
|
16
|
+
* must be on the root's own filesystem.
|
|
17
|
+
* 2. **Outside the repository, but not somewhere unguessable.** `$HOME` looks tidy and is the wrong answer:
|
|
18
|
+
* on the site this plugin was built against the login home is `/home1/<user>` while projects live under
|
|
19
|
+
* `/srv` or `/mnt`, which are usually separate mounts (NFS/automount) — so a home-based bin would make
|
|
20
|
+
* **every** delete fail with `cross-device`. The root's own **parent** is the closest directory that is
|
|
21
|
+
* almost always on the same filesystem.
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-remote-workspace/domain/bin-location
|
|
24
|
+
*/
|
|
25
|
+
import { type RootRef } from './root.ts';
|
|
26
|
+
/** The bin directory name used when the caller configures none (`binDir`). */
|
|
27
|
+
export declare const DEFAULT_BIN_DIR = ".dsh-remote-workspace/trash";
|
|
28
|
+
/**
|
|
29
|
+
* The root's own directory, in the spelling that world uses.
|
|
30
|
+
*
|
|
31
|
+
* Remote paths are always POSIX and are normalised as such; a local path is normalised by the host's rules
|
|
32
|
+
* (which is what makes `D:\work\a` and `D:/work/a` the same directory on Windows).
|
|
33
|
+
*
|
|
34
|
+
* @param root - the root whose directory is wanted.
|
|
35
|
+
* @returns a `/`-separated path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function rootDirectory(root: RootRef): string;
|
|
38
|
+
/**
|
|
39
|
+
* The bin directory **inside** the root — the historical location, kept as the documented fallback.
|
|
40
|
+
*
|
|
41
|
+
* @param root - the root being served.
|
|
42
|
+
* @param binDir - configured directory name, relative to the root.
|
|
43
|
+
* @returns the absolute bin directory.
|
|
44
|
+
*/
|
|
45
|
+
export declare function binRootInsideRoot(root: RootRef, binDir?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* A stable, filesystem-safe directory name for one root's bin.
|
|
48
|
+
*
|
|
49
|
+
* Two roots can share a parent (and even a basename, across connections), so the name carries a hash of the
|
|
50
|
+
* root's identity rather than only its last segment. Stable across runs: the bin directory must be findable
|
|
51
|
+
* again after a restart, and an unstable name would silently orphan every deleted byte.
|
|
52
|
+
*
|
|
53
|
+
* @param root - the root being served.
|
|
54
|
+
* @returns a name such as `work-1f3a9c02`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function binRootSlug(root: RootRef): string;
|
|
57
|
+
/**
|
|
58
|
+
* The bin directory **beside** the root, under its parent.
|
|
59
|
+
*
|
|
60
|
+
* The parent is used rather than `$HOME` because it is almost always the root's own filesystem, which is the
|
|
61
|
+
* one property the bin cannot do without. `binDir` keeps its configured name so the plugin's footprint reads
|
|
62
|
+
* the same as before; the per-root slug keeps two roots in one parent apart.
|
|
63
|
+
*
|
|
64
|
+
* @param root - the root being served.
|
|
65
|
+
* @param binDir - configured directory name, relative to the parent.
|
|
66
|
+
* @returns the absolute bin directory, or `undefined` when the root has no usable parent (it *is* `/`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function binRootBesideRoot(root: RootRef, binDir?: string): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The directory a same-device check must compare the bin directory against.
|
|
71
|
+
*
|
|
72
|
+
* The bin is created by the plugin, so on the first delete it may not exist yet; the parent is what has to be
|
|
73
|
+
* on the same filesystem, and it always exists (it contains the root).
|
|
74
|
+
*
|
|
75
|
+
* @param root - the root being served.
|
|
76
|
+
* @returns the absolute parent directory, or `undefined` for a root with no parent.
|
|
77
|
+
*/
|
|
78
|
+
export declare function binParentDirectory(root: RootRef): string | undefined;
|
|
79
|
+
//# sourceMappingURL=bin-location.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote capability probing.
|
|
3
|
+
*
|
|
4
|
+
* The plugin installs nothing on the remote host, so it must discover what the host already
|
|
5
|
+
* offers and degrade accordingly: `git ls-files` when git exists, a bounded `find` otherwise;
|
|
6
|
+
* `rg` when available, bounded `grep` otherwise.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-over-ssh/domain/capability
|
|
9
|
+
*/
|
|
10
|
+
/** What one probe learned about the remote host. */
|
|
11
|
+
export interface RemoteCapabilities {
|
|
12
|
+
/** `uname -s` output, or `unknown`. */
|
|
13
|
+
readonly uname: string;
|
|
14
|
+
/** Whether the host is a POSIX shell host this plugin supports. */
|
|
15
|
+
readonly posix: boolean;
|
|
16
|
+
readonly git: boolean;
|
|
17
|
+
readonly ripgrep: boolean;
|
|
18
|
+
readonly grep: boolean;
|
|
19
|
+
readonly base64: boolean;
|
|
20
|
+
readonly findMaxDepth: boolean;
|
|
21
|
+
readonly sha256: 'sha256sum' | 'shasum' | null;
|
|
22
|
+
readonly statFlavor: 'gnu' | 'bsd' | 'unknown';
|
|
23
|
+
/**
|
|
24
|
+
* Language servers found on the remote `PATH`.
|
|
25
|
+
*
|
|
26
|
+
* `undefined` means **nobody looked** (Tier 1 shell transport, or an agent older than the probe);
|
|
27
|
+
* an empty array means the probe ran and found none. The difference matters: the first case must not be
|
|
28
|
+
* reported to a caller as "not installed", because we have no evidence either way.
|
|
29
|
+
*/
|
|
30
|
+
readonly lspServers?: readonly LspServerInfo[];
|
|
31
|
+
}
|
|
32
|
+
/** One language server found on the remote `PATH`. */
|
|
33
|
+
export interface LspServerInfo {
|
|
34
|
+
readonly command: string;
|
|
35
|
+
readonly language: string;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The one-shot probe script.
|
|
40
|
+
*
|
|
41
|
+
* Every line is `key=value`; values never contain newlines. The script is written for `sh`
|
|
42
|
+
* and tolerates missing commands, because discovering a missing command is the point.
|
|
43
|
+
*
|
|
44
|
+
* @returns a POSIX shell script.
|
|
45
|
+
*/
|
|
46
|
+
export declare function probeScript(): string;
|
|
47
|
+
/** The capabilities assumed when probing failed: POSIX basics only. */
|
|
48
|
+
export declare function conservativeCapabilities(): RemoteCapabilities;
|
|
49
|
+
/**
|
|
50
|
+
* Parse probe output into capabilities.
|
|
51
|
+
*
|
|
52
|
+
* Unknown keys are ignored so the script can grow without breaking older parsers.
|
|
53
|
+
*
|
|
54
|
+
* @param stdout - the probe's standard output.
|
|
55
|
+
* @returns parsed capabilities.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseProbeOutput(stdout: string): RemoteCapabilities;
|
|
58
|
+
/** How the plugin will search file contents on a host. */
|
|
59
|
+
export type SearchStrategy = {
|
|
60
|
+
readonly kind: 'git-grep';
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: 'ripgrep';
|
|
63
|
+
} | {
|
|
64
|
+
readonly kind: 'grep-bounded';
|
|
65
|
+
};
|
|
66
|
+
/** How the plugin will enumerate files for `@` completion on a host. */
|
|
67
|
+
export type IndexStrategy = {
|
|
68
|
+
readonly kind: 'git-ls-files';
|
|
69
|
+
} | {
|
|
70
|
+
readonly kind: 'find-bounded';
|
|
71
|
+
};
|
|
72
|
+
/** Pick the content-search strategy for a host. */
|
|
73
|
+
export declare function searchStrategyFor(capabilities: RemoteCapabilities): SearchStrategy;
|
|
74
|
+
/** Pick the file-enumeration strategy for a host. */
|
|
75
|
+
export declare function indexStrategyFor(capabilities: RemoteCapabilities): IndexStrategy;
|
|
76
|
+
/** Whether the host can run the plugin's file-transfer path. */
|
|
77
|
+
export declare function canTransferBytes(capabilities: RemoteCapabilities): boolean;
|
|
78
|
+
//# sourceMappingURL=capability.d.ts.map
|