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.
- package/README.md +19 -17
- package/agents/agent-manifest.json +6 -6
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +422 -337
- package/lib/file-references.js +92 -35
- package/lib/fs.js +295 -113
- package/lib/index.js +6912 -4330
- package/lib/terminal-wrapper.js +9 -4
- package/lib/types/src/config.d.ts +156 -9
- package/lib/types/src/file-references.d.ts +0 -1
- package/lib/types/src/host/app/backend.d.ts +44 -3
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +0 -1
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +0 -1
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +12 -1
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-selector.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-service.d.ts +0 -1
- package/lib/types/src/host/domain/capability.d.ts +0 -1
- package/lib/types/src/host/domain/errors.d.ts +0 -1
- package/lib/types/src/host/domain/roots/connection.d.ts +0 -1
- package/lib/types/src/host/domain/roots/root.d.ts +24 -9
- package/lib/types/src/host/domain/roots/session-roots.d.ts +23 -1
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +0 -1
- package/lib/types/src/host/domain/session/session-admin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/transfer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/forward.d.ts +75 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +5 -1
- package/lib/types/src/host/infra/transport/transport-base.d.ts +0 -1
- package/lib/types/src/host/infra/transport/transport.d.ts +42 -2
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +0 -1
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +0 -1
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +18 -3
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +0 -1
- package/lib/types/src/index.d.ts +23 -1
- package/lib/types/src/local-bridge-refs.d.ts +0 -1
- package/lib/types/src/local-bridge.d.ts +0 -1
- package/lib/types/src/shared/root-keys.d.ts +120 -0
- package/lib/types/src/wiring/helpers.d.ts +0 -1
- package/package.json +5 -3
- package/lib/types/src/client/controls.d.ts +0 -131
- package/lib/types/src/client/core/card-focus-trap.d.ts +0 -45
- package/lib/types/src/client/core/controls.d.ts +0 -164
- package/lib/types/src/client/core/latest.d.ts +0 -48
- package/lib/types/src/client/core/locale.d.ts +0 -38
- package/lib/types/src/client/core/message.d.ts +0 -37
- package/lib/types/src/client/core/panel-model.d.ts +0 -134
- package/lib/types/src/client/core/portal.d.ts +0 -266
- package/lib/types/src/client/core/remote.d.ts +0 -84
- package/lib/types/src/client/core/styles.d.ts +0 -458
- package/lib/types/src/client/file-tree-io.d.ts +0 -19
- package/lib/types/src/client/file-tree.d.ts +0 -271
- package/lib/types/src/client/flows/directory-browser.d.ts +0 -587
- package/lib/types/src/client/flows/session-directory.d.ts +0 -415
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +0 -480
- package/lib/types/src/client/locale.d.ts +0 -31
- package/lib/types/src/client/panel-model.d.ts +0 -37
- package/lib/types/src/client/panel.d.ts +0 -77
- package/lib/types/src/client/roots/connection-test.d.ts +0 -69
- package/lib/types/src/client/roots/roots-panel-state.d.ts +0 -307
- package/lib/types/src/client/roots/roots-panel-view.d.ts +0 -280
- package/lib/types/src/client/roots/roots-panel.d.ts +0 -364
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +0 -35
- package/lib/types/src/client/roots/transfer-panel.d.ts +0 -241
- package/lib/types/src/client/roots/transfer-progress.d.ts +0 -67
- package/lib/types/src/client/roots-panel-view.d.ts +0 -236
- package/lib/types/src/client/roots-panel.d.ts +0 -347
- package/lib/types/src/client/session/session-panel-view.d.ts +0 -336
- package/lib/types/src/client/session/session-panel.d.ts +0 -322
- package/lib/types/src/client/session/session-remote-mark.d.ts +0 -110
- package/lib/types/src/client/session/session-row-actions.d.ts +0 -398
- package/lib/types/src/client/session-panel.d.ts +0 -152
- package/lib/types/src/client/styles.d.ts +0 -252
- package/lib/types/src/client/workspace-directory-flow.d.ts +0 -342
- package/lib/types/src/fs.d.ts +0 -46
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +0 -38
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +0 -215
- package/lib/types/src/host/app/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin-service.d.ts +0 -45
- package/lib/types/src/host/app/recycle-bin-world.d.ts +0 -35
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +0 -369
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +0 -130
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +0 -26
- package/lib/types/src/host/app/roots/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/roots/user-roots.d.ts +0 -552
- package/lib/types/src/host/app/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots-api-contract.d.ts +0 -339
- package/lib/types/src/host/app/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots-api-support.d.ts +0 -182
- package/lib/types/src/host/app/roots-api-transfers.d.ts +0 -13
- package/lib/types/src/host/app/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/routed-fs.d.ts +0 -239
- package/lib/types/src/host/app/serialize.d.ts +0 -37
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin-contract.d.ts +0 -214
- package/lib/types/src/host/app/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-service.d.ts +0 -38
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +0 -227
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +0 -55
- package/lib/types/src/host/app/transport-selector.d.ts +0 -98
- package/lib/types/src/host/app/transport-service.d.ts +0 -75
- package/lib/types/src/host/app/user-roots.d.ts +0 -461
- package/lib/types/src/host/domain/bin-location.d.ts +0 -79
- package/lib/types/src/host/domain/connection.d.ts +0 -95
- package/lib/types/src/host/domain/glob-pattern.d.ts +0 -62
- package/lib/types/src/host/domain/literal-text.d.ts +0 -63
- package/lib/types/src/host/domain/path-grammar.d.ts +0 -84
- package/lib/types/src/host/domain/recycle-bin.d.ts +0 -337
- package/lib/types/src/host/domain/root.d.ts +0 -74
- package/lib/types/src/host/domain/roots/bin-location.d.ts +0 -91
- package/lib/types/src/host/domain/roots/route-table.d.ts +0 -102
- package/lib/types/src/host/domain/route-table.d.ts +0 -80
- package/lib/types/src/host/domain/session/terminal-target.d.ts +0 -136
- package/lib/types/src/host/domain/session-admin.d.ts +0 -330
- package/lib/types/src/host/domain/shell.d.ts +0 -47
- package/lib/types/src/host/domain/ssh-config.d.ts +0 -73
- package/lib/types/src/host/domain/terminal-target.d.ts +0 -132
- package/lib/types/src/host/domain/tool-requests.d.ts +0 -369
- package/lib/types/src/host/domain/transfer.d.ts +0 -260
- package/lib/types/src/host/infra/backend/local-backend.d.ts +0 -84
- package/lib/types/src/host/infra/backend/local-version.d.ts +0 -43
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +0 -89
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +0 -239
- package/lib/types/src/host/infra/cache/lru.d.ts +0 -52
- package/lib/types/src/host/infra/cache/versioned.d.ts +0 -77
- package/lib/types/src/host/infra/temp-name.d.ts +0 -19
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +0 -132
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +0 -140
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +0 -564
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +0 -418
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +0 -121
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +0 -123
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +0 -131
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +0 -509
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +0 -353
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/command-pool.d.ts +0 -61
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +0 -140
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +0 -63
- package/lib/types/src/host/infra/transport/git-status.d.ts +0 -101
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +0 -26
- package/lib/types/src/host/infra/transport/session-window.d.ts +0 -51
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +0 -59
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +0 -62
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +0 -302
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +0 -66
- package/lib/types/src/host/integration/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent-preflight.d.ts +0 -56
- package/lib/types/src/host/integration/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/askpass-registry.d.ts +0 -59
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/file-reference-provider.d.ts +0 -115
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +0 -21
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +0 -94
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +0 -369
- package/lib/types/src/host/integration/fs/remote-index.d.ts +0 -177
- package/lib/types/src/host/integration/fs-provider.d.ts +0 -226
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +0 -38
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +0 -67
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +0 -130
- package/lib/types/src/host/integration/management-tools.d.ts +0 -64
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +0 -247
- package/lib/types/src/host/integration/remote/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/remote/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +0 -108
- package/lib/types/src/host/integration/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote-file-tools.d.ts +0 -241
- package/lib/types/src/host/integration/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/remote-index.d.ts +0 -165
- package/lib/types/src/host/integration/remote-job-tools.d.ts +0 -295
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +0 -97
- package/lib/types/src/host/integration/remote-tools.d.ts +0 -206
- package/lib/types/src/host/integration/root-registry.d.ts +0 -115
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/roots/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/roots/root-registry.d.ts +0 -144
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +0 -219
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +0 -52
- package/lib/types/src/host/integration/roots-api.d.ts +0 -407
- package/lib/types/src/host/integration/roots-wiring.d.ts +0 -211
- package/lib/types/src/host/integration/session/session-api.d.ts +0 -88
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session/session-header.d.ts +0 -151
- package/lib/types/src/host/integration/session/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-admin-service.d.ts +0 -236
- package/lib/types/src/host/integration/session-api.d.ts +0 -81
- package/lib/types/src/host/integration/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session-header.d.ts +0 -118
- package/lib/types/src/host/integration/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-scope.d.ts +0 -113
- package/lib/types/src/host/integration/session-tools.d.ts +0 -44
- package/lib/types/src/host/integration/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +0 -62
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +0 -60
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +0 -167
- package/lib/types/src/host/integration/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/tools/management-tools.d.ts +0 -245
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +0 -316
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +0 -43
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +0 -236
- package/lib/types/src/host/integration/tools/session-target.d.ts +0 -52
- package/lib/types/src/host/integration/tools/session-tools.d.ts +0 -46
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +0 -66
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +0 -48
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/transfer-adapters.d.ts +0 -44
- package/lib/types/src/host/integration/transfer-service.d.ts +0 -164
- package/lib/types/src/host/integration/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/user-roots.d.ts +0 -461
- package/lib/types/src/wiring/connections.d.ts +0 -189
- package/lib/types/src/wiring/fs.d.ts +0 -13
- package/lib/types/src/wiring/install-memo.d.ts +0 -32
- package/lib/types/src/wiring/legacy-dirs.d.ts +0 -62
- package/lib/types/src/wiring/package-paths.d.ts +0 -35
- package/lib/types/src/wiring/roots.d.ts +0 -21
- package/lib/types/src/wiring/state-dir.d.ts +0 -29
- package/lib/types/src/wiring/tools.d.ts +0 -35
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The transfer panel's **long-poll driver**: the thing that was a `setInterval` before task-19.
|
|
3
|
-
*
|
|
4
|
-
* ## Why a controller and not four `useEffect`s
|
|
5
|
-
*
|
|
6
|
-
* The behaviour that matters is a small state machine — one request in flight, the next one sent the moment the
|
|
7
|
-
* answer lands, a user action that supersedes a parked request, and a dispose that aborts it. This repository cannot
|
|
8
|
-
* render hooks in tests, so a controller with an injectable clock/timer is the only way those four rules are
|
|
9
|
-
* verifiable (the same reason `roots-panel-state.ts` and `session-panel` exist).
|
|
10
|
-
*
|
|
11
|
-
* ## What is preserved from the polling version, deliberately
|
|
12
|
-
*
|
|
13
|
-
* - **`clearing` / `clearError`**: `clear()` is a separate action from the list loop; a `cleared` list arriving
|
|
14
|
-
* while the clear request is in flight must not silently drop the "clearing…" state.
|
|
15
|
-
* - **A background read never raises `loading`.** `loading` is the *visible* "reading…" line, and only the user's
|
|
16
|
-
* own action (opening the panel, pressing refresh) may show it — a long-poll that times out every 25 s and
|
|
17
|
-
* flashed "reading…" would be the same flicker the 1 s timer used to cause (audit 0057).
|
|
18
|
-
* - **Newest answer wins.** Requests are strictly sequential, but a user refresh *aborts* the parked one; if the
|
|
19
|
-
* aborted request still resolves (a fetch can), its generation is stale and its rows are dropped instead of
|
|
20
|
-
* overwriting the answer that came after it.
|
|
21
|
-
*
|
|
22
|
-
* ## What changed
|
|
23
|
-
*
|
|
24
|
-
* The loop sends `waitMs` and re-arms **immediately** on every answer. A timeout is a normal answer, not an error;
|
|
25
|
-
* only a real failure backs off ({@link TRANSFER_RETRY_MS}) so a dead host is not hammered.
|
|
26
|
-
*
|
|
27
|
-
* @module dsh-over-ssh/client/transfer-progress
|
|
28
|
-
*/
|
|
29
|
-
import { type TransferPanelIo, type TransferPanelState } from './transfer-panel.ts';
|
|
30
|
-
/** Long-poll budget the panel asks the host for (the route clamps it to 25 s as well). */
|
|
31
|
-
export declare const TRANSFER_LONG_POLL_MS = 25000;
|
|
32
|
-
/** Delay before re-asking after a **failed** read. A timeout is not a failure and never waits. */
|
|
33
|
-
export declare const TRANSFER_RETRY_MS = 1000;
|
|
34
|
-
/** One driver over the transfer list. */
|
|
35
|
-
export interface TransferProgress {
|
|
36
|
-
/** The current state; a stable reference until something changed. */
|
|
37
|
-
getState(): TransferPanelState;
|
|
38
|
-
/** Subscribe to state changes; returns the unsubscriber. */
|
|
39
|
-
subscribe(listener: () => void): () => void;
|
|
40
|
-
/** Begin the loop. Idempotent. */
|
|
41
|
-
start(): void;
|
|
42
|
-
/** The user's refresh: abort the parked request and ask again **now**, visibly. */
|
|
43
|
-
refresh(): void;
|
|
44
|
-
/** The user's clear: only finished rows; keeps `clearing`/`clearError`. */
|
|
45
|
-
clear(): void;
|
|
46
|
-
/** Stop the loop and abort the in-flight request. Idempotent. */
|
|
47
|
-
dispose(): void;
|
|
48
|
-
/** How many snapshot requests were issued (tests and diagnostics). */
|
|
49
|
-
requests(): number;
|
|
50
|
-
}
|
|
51
|
-
/** What the driver needs from its caller. */
|
|
52
|
-
export interface TransferProgressOptions {
|
|
53
|
-
readonly io: TransferPanelIo;
|
|
54
|
-
/** Override the long-poll budget (tests use a few milliseconds). */
|
|
55
|
-
readonly waitMs?: number;
|
|
56
|
-
/** Override the failure backoff (tests pass a no-op sleep). */
|
|
57
|
-
readonly retryDelayMs?: number;
|
|
58
|
-
/** Injected timer, so "backs off after a failure" costs nothing in a test. */
|
|
59
|
-
readonly sleep?: (ms: number) => Promise<void>;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Build the driver.
|
|
63
|
-
*
|
|
64
|
-
* @param options - the io, the budget and the injected timer.
|
|
65
|
-
* @returns the controller the React shell subscribes to.
|
|
66
|
-
*/
|
|
67
|
-
export declare function createTransferProgress(options: TransferProgressOptions): TransferProgress;
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The **editable** remote-workspaces section of the settings panel.
|
|
3
|
-
*
|
|
4
|
-
* Three layers, deliberately separated so the interesting parts can be tested without a browser:
|
|
5
|
-
*
|
|
6
|
-
* 1. {@link rootsPanelReducer} — a pure state machine over the panel's drafts, the last host answer and
|
|
7
|
-
* whether a request is in flight. No DOM, no I/O, no React: `tests/client-roots-view.test.ts` drives it
|
|
8
|
-
* directly.
|
|
9
|
-
* 2. {@link createRootsPanelController} — the same state plus the two calls from
|
|
10
|
-
* `./roots-panel.ts` (`load` / `send`). Everything that is *not* pure lives here: validation gate,
|
|
11
|
-
* double-submit gate, the refresh after a success, and dropping answers that a newer request superseded.
|
|
12
|
-
* 3. {@link RootsPanelSection} — a pure template (`createElement`, never JSX: Node's native type stripping
|
|
13
|
-
* cannot transform JSX) and {@link RootsPanelContainer}, the thin React shell that subscribes to the
|
|
14
|
-
* controller.
|
|
15
|
-
*
|
|
16
|
-
* Rules this file is responsible for, and where they are enforced:
|
|
17
|
-
*
|
|
18
|
-
* - **A `config` root never gets a delete button.** The button is rendered from
|
|
19
|
-
* {@link RootRowView.removable} and from nothing else; the controller refuses such a key before a POST.
|
|
20
|
-
* - **Nothing is submitted before validation.** `validateRootDraft` / `validateConnectionDraft` are the only
|
|
21
|
-
* way a draft becomes a request body, and their `MessageKey` refusal is rendered as text through the
|
|
22
|
-
* locale dictionaries.
|
|
23
|
-
* - **No double submit.** `canEdit === false` (no answer yet, or a request in flight) disables every control
|
|
24
|
-
* and stops every handler.
|
|
25
|
-
* - **No credentials.** This file can only send the fields its drafts hold; the password field does not
|
|
26
|
-
* exist, and `validateConnectionDraft` refuses `authKind: 'password'` outright.
|
|
27
|
-
*
|
|
28
|
-
* @module dsh-remote-workspace/client/roots-panel-view
|
|
29
|
-
*/
|
|
30
|
-
import { type ReactNode } from 'react';
|
|
31
|
-
import type { SshDevKey } from './locale.ts';
|
|
32
|
-
import { type MessageKey, type RootLocation, type RootsApi, type RootsSnapshot, type RootsView } from './roots-panel.ts';
|
|
33
|
-
/** One shipped dictionary, resolved to plain strings. Passed to the component through the inject face. */
|
|
34
|
-
export type RootsPanelCopy = Record<SshDevKey, string>;
|
|
35
|
-
/** The three forms' contents. Kept flat so a patch is one spread and a test can name a single field. */
|
|
36
|
-
export interface RootsPanelDrafts {
|
|
37
|
-
/** Local root path (form 1). */
|
|
38
|
-
readonly localPath: string;
|
|
39
|
-
/** Remote root path (form 2). */
|
|
40
|
-
readonly remotePath: string;
|
|
41
|
-
/** Connection chosen for the remote root (form 2). */
|
|
42
|
-
readonly remoteConnectionId: string;
|
|
43
|
-
/** Optional label for the remote root (form 2). */
|
|
44
|
-
readonly remoteLabel: string;
|
|
45
|
-
/** New connection: id. */
|
|
46
|
-
readonly newId: string;
|
|
47
|
-
/** New connection: host. */
|
|
48
|
-
readonly newHost: string;
|
|
49
|
-
/** New connection: user. */
|
|
50
|
-
readonly newUser: string;
|
|
51
|
-
/** New connection: authentication mode. `password` is not offered at all. */
|
|
52
|
-
readonly newAuthKind: 'agent' | 'key';
|
|
53
|
-
/** New connection: key path, only meaningful for `newAuthKind === 'key'`. */
|
|
54
|
-
readonly newKeyPath: string;
|
|
55
|
-
/** New connection: port as typed; parsed by the controller so a bad value can be refused, not coerced. */
|
|
56
|
-
readonly newPort: string;
|
|
57
|
-
}
|
|
58
|
-
/** A change to the drafts; every field is optional so one handler can serve every input. */
|
|
59
|
-
export type RootsPanelDraftPatch = Partial<RootsPanelDrafts>;
|
|
60
|
-
/** Where an inline form message belongs. */
|
|
61
|
-
export type RootsPanelFormScope = 'roots' | 'connection';
|
|
62
|
-
/** A refusal or a failed submit, shown next to the form it belongs to. */
|
|
63
|
-
export interface RootsPanelFormNotice {
|
|
64
|
-
readonly scope: RootsPanelFormScope;
|
|
65
|
-
readonly message: MessageKey;
|
|
66
|
-
}
|
|
67
|
-
/** Everything the section renders from. */
|
|
68
|
-
export interface RootsPanelState {
|
|
69
|
-
/** True once an initial `load()` has been asked for, so a remount does not re-fetch. */
|
|
70
|
-
readonly started: boolean;
|
|
71
|
-
/** True while a request is in flight; the edit gate is `buildRootsView`'s `canEdit`. */
|
|
72
|
-
readonly pending: boolean;
|
|
73
|
-
/** True once a `load()` answered (ok or not). */
|
|
74
|
-
readonly loaded: boolean;
|
|
75
|
-
/** The last host answer, if any. */
|
|
76
|
-
readonly snapshot?: RootsSnapshot;
|
|
77
|
-
/** The last transport-level failure, if any. */
|
|
78
|
-
readonly error?: string;
|
|
79
|
-
/** The forms' contents. */
|
|
80
|
-
readonly drafts: RootsPanelDrafts;
|
|
81
|
-
/** The last validation refusal or submit failure, with the form it belongs to. */
|
|
82
|
-
readonly formNotice?: RootsPanelFormNotice;
|
|
83
|
-
/** Bumped by every success; a container can watch it to know "the host changed, reload". */
|
|
84
|
-
readonly reloadToken: number;
|
|
85
|
-
}
|
|
86
|
-
/** What the reducer accepts. */
|
|
87
|
-
export type RootsPanelAction =
|
|
88
|
-
/** A request is being sent; also marks the panel as started. */
|
|
89
|
-
{
|
|
90
|
-
readonly type: 'load';
|
|
91
|
-
} | {
|
|
92
|
-
readonly type: 'loaded';
|
|
93
|
-
readonly snapshot: RootsSnapshot;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* A request failed. With a `scope` it is a submit failure and is shown next to that form; without one it
|
|
97
|
-
* is the initial load failing, which the view's own `notice` already reports — showing both would say the
|
|
98
|
-
* same thing twice.
|
|
99
|
-
*/
|
|
100
|
-
| {
|
|
101
|
-
readonly type: 'failed';
|
|
102
|
-
readonly error: string;
|
|
103
|
-
readonly scope?: RootsPanelFormScope;
|
|
104
|
-
}
|
|
105
|
-
/** One form field changed: clears the inline message, because the user is fixing it. */
|
|
106
|
-
| {
|
|
107
|
-
readonly type: 'draft-changed';
|
|
108
|
-
readonly patch: RootsPanelDraftPatch;
|
|
109
|
-
}
|
|
110
|
-
/** A submit is in flight. */
|
|
111
|
-
| {
|
|
112
|
-
readonly type: 'submitted';
|
|
113
|
-
}
|
|
114
|
-
/** A draft was refused before any request; nothing was sent. */
|
|
115
|
-
| {
|
|
116
|
-
readonly type: 'refused';
|
|
117
|
-
readonly scope: RootsPanelFormScope;
|
|
118
|
-
readonly message: MessageKey;
|
|
119
|
-
}
|
|
120
|
-
/** A request succeeded. `resetDrafts` clears the forms (an add did); a removal keeps them. */
|
|
121
|
-
| {
|
|
122
|
-
readonly type: 'succeeded';
|
|
123
|
-
readonly resetDrafts?: boolean;
|
|
124
|
-
}
|
|
125
|
-
/** A removal is in flight; the reducer itself refuses a row that is not removable. */
|
|
126
|
-
| {
|
|
127
|
-
readonly type: 'remove';
|
|
128
|
-
readonly key: string;
|
|
129
|
-
};
|
|
130
|
-
/** The starting state: nothing loaded, no drafts, no queue. */
|
|
131
|
-
export declare function emptyRootsPanelDrafts(): RootsPanelDrafts;
|
|
132
|
-
/** @returns the state a freshly created controller holds. */
|
|
133
|
-
export declare function initialRootsPanelState(): RootsPanelState;
|
|
134
|
-
/**
|
|
135
|
-
* The panel's state machine. Pure: same state + same action ⇒ same result, and an action that changes
|
|
136
|
-
* nothing returns the **same object**, so a subscriber is never woken for a no-op.
|
|
137
|
-
*
|
|
138
|
-
* @param state - the current state.
|
|
139
|
-
* @param action - what happened.
|
|
140
|
-
* @returns the next state.
|
|
141
|
-
*/
|
|
142
|
-
export declare function rootsPanelReducer(state: RootsPanelState, action: RootsPanelAction): RootsPanelState;
|
|
143
|
-
/**
|
|
144
|
-
* The view model for a state.
|
|
145
|
-
*
|
|
146
|
-
* @param state - the panel state.
|
|
147
|
-
* @returns exactly what `buildRootsView` says: rows with `removable`, connections, notice and the edit gate.
|
|
148
|
-
*/
|
|
149
|
-
export declare function selectRootsView(state: RootsPanelState): RootsView;
|
|
150
|
-
/** True when the panel may accept input right now: an answer arrived and no request is in flight. */
|
|
151
|
-
export declare function canEdit(state: RootsPanelState): boolean;
|
|
152
|
-
/**
|
|
153
|
-
* Turn one `MessageKey` into text, with its parameters substituted.
|
|
154
|
-
*
|
|
155
|
-
* The key union is closed at compile time, but a `MessageKey` is a plain string by contract (the pure module
|
|
156
|
-
* cannot import the dictionary), so an unknown key is **shown as the key** rather than as an empty string: a
|
|
157
|
-
* missing translation must be visible, never silent.
|
|
158
|
-
*
|
|
159
|
-
* @param copy - the dictionary of the active locale.
|
|
160
|
-
* @param message - the key plus optional `{name}` parameters.
|
|
161
|
-
* @returns the rendered text, or `''` when there is no message.
|
|
162
|
-
*/
|
|
163
|
-
export declare function resolveMessage(copy: RootsPanelCopy, message: MessageKey | undefined): string;
|
|
164
|
-
/** The calls the section makes. `roots-panel.ts` owns how they are implemented. */
|
|
165
|
-
export interface RootsPanelController {
|
|
166
|
-
/** The current state; a stable reference until something changes (safe for `useSyncExternalStore`). */
|
|
167
|
-
readonly getState: () => RootsPanelState;
|
|
168
|
-
/** Watch for changes; returns the disposer. */
|
|
169
|
-
readonly subscribe: (listener: () => void) => () => void;
|
|
170
|
-
/** Ask for the first answer once; a remount does not re-fetch. */
|
|
171
|
-
readonly start: () => void;
|
|
172
|
-
/** Reload unconditionally (used after a successful change). */
|
|
173
|
-
readonly reload: () => Promise<void>;
|
|
174
|
-
/** Change one form field. */
|
|
175
|
-
readonly changeDraft: (patch: RootsPanelDraftPatch) => void;
|
|
176
|
-
/** Validate and add a root of the given location. */
|
|
177
|
-
readonly submitRoot: (location: RootLocation) => Promise<void>;
|
|
178
|
-
/** Validate and add a connection. */
|
|
179
|
-
readonly submitConnection: () => Promise<void>;
|
|
180
|
-
/** Remove one row, if the view allows it. */
|
|
181
|
-
readonly remove: (key: string) => Promise<void>;
|
|
182
|
-
/** Stop accepting results and drop listeners. */
|
|
183
|
-
readonly dispose: () => void;
|
|
184
|
-
}
|
|
185
|
-
/** What a controller needs: the two calls. */
|
|
186
|
-
export interface RootsPanelControllerOptions {
|
|
187
|
-
readonly api: RootsApi;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Build the stateful half of the panel.
|
|
191
|
-
*
|
|
192
|
-
* The refresh-after-success is the reason this exists at all: `POST` answers with the *persisted* state,
|
|
193
|
-
* whose `roots` are the user's roots — the same field name `GET` uses for the *config* roots. Rendering that
|
|
194
|
-
* answer directly would show a config root as a user root (or the reverse), so a success always ends with a
|
|
195
|
-
* fresh `GET` and the POST body is used only as a signal.
|
|
196
|
-
*
|
|
197
|
-
* @param options - the injected API.
|
|
198
|
-
* @returns the controller the container subscribes to.
|
|
199
|
-
*/
|
|
200
|
-
export declare function createRootsPanelController(options: RootsPanelControllerOptions): RootsPanelController;
|
|
201
|
-
/** What the section's controls call. All of them are no-ops when the gate is closed. */
|
|
202
|
-
export interface RootsPanelSectionActions {
|
|
203
|
-
readonly onDraftChange: (patch: RootsPanelDraftPatch) => void;
|
|
204
|
-
readonly onSubmitRoot: (location: RootLocation) => void;
|
|
205
|
-
readonly onSubmitConnection: () => void;
|
|
206
|
-
readonly onRemove: (key: string) => void;
|
|
207
|
-
}
|
|
208
|
-
/** Props of the pure template: state in, callbacks out, no hooks and no store access. */
|
|
209
|
-
export interface RootsPanelSectionProps {
|
|
210
|
-
readonly copy: RootsPanelCopy;
|
|
211
|
-
readonly state: RootsPanelState;
|
|
212
|
-
readonly actions: RootsPanelSectionActions;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Render the editable section. Pure: calling it twice with the same props yields the same tree.
|
|
216
|
-
*
|
|
217
|
-
* @param props - copy, state and callbacks.
|
|
218
|
-
* @returns the section element.
|
|
219
|
-
*/
|
|
220
|
-
export declare function RootsPanelSection({ copy, state, actions }: RootsPanelSectionProps): ReactNode;
|
|
221
|
-
/** Props of the React shell: the controller owns the state, the shell only subscribes. */
|
|
222
|
-
export interface RootsPanelContainerProps {
|
|
223
|
-
readonly copy: RootsPanelCopy;
|
|
224
|
-
readonly controller: RootsPanelController;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Subscribe to a controller and render {@link RootsPanelSection}.
|
|
228
|
-
*
|
|
229
|
-
* `useSyncExternalStore` is the whole reason this is a component and not a function call: the slot's inject
|
|
230
|
-
* face is a synchronous snapshot, so the section has to be re-rendered by the store it reads.
|
|
231
|
-
*
|
|
232
|
-
* @param props - the dictionary and the controller.
|
|
233
|
-
* @returns the section element.
|
|
234
|
-
*/
|
|
235
|
-
export declare function RootsPanelContainer({ copy, controller }: RootsPanelContainerProps): ReactNode;
|
|
236
|
-
//# sourceMappingURL=roots-panel-view.d.ts.map
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The roots panel's whole decision surface: wire parsing, the HTTP calls to the host route, input validation
|
|
3
|
-
* and the view model. **No React, no DOM, no `@deepseek-ai/*`** — the component renders what this module
|
|
4
|
-
* returns and nothing else, which is what makes the panel testable without a browser (红线 2).
|
|
5
|
-
*
|
|
6
|
-
* Two rules this module owns:
|
|
7
|
-
*
|
|
8
|
-
* 1. **No credentials, ever.** {@link parseRootsSnapshot} copies field by field and drops anything else, so a
|
|
9
|
-
* host answer that leaked `keyPath` / `password` still cannot reach the DOM. The host route filters its
|
|
10
|
-
* side; this is the client's half of the same rule.
|
|
11
|
-
* 2. **Password authentication is not editable here.** A password must never round-trip through a browser
|
|
12
|
-
* form and a same-origin POST, so {@link validateConnectionDraft} refuses it outright rather than
|
|
13
|
-
* growing a field that leaks secrets into web storage.
|
|
14
|
-
*
|
|
15
|
-
* @module dsh-remote-workspace/client/roots-panel
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Where the host route lives, as the browser must request it.
|
|
19
|
-
*
|
|
20
|
-
* **Relative on purpose, and this is the platform's own convention.** The GUI is served from two different
|
|
21
|
-
* origins depending on the carrier, and only a relative path is correct in both:
|
|
22
|
-
*
|
|
23
|
-
* - `dsh web` (browser): the page origin *is* the host, so `/api/…` reaches
|
|
24
|
-
* `ctx.webServer` directly;
|
|
25
|
-
* - the Desktop shell: the page is `dsh-app://app/` and `/api/…` resolves to
|
|
26
|
-
* `dsh-app://app/api/…`, which Electron's `protocol.handle('dsh-app')` forwards to the owned host with
|
|
27
|
-
* the browser-session cookie the shell keeps out of the page
|
|
28
|
-
* (`resources/app.asar/lib/main.js`, `forwardWebRequest`: non-asset paths are proxied with
|
|
29
|
-
* `headers.set('cookie', cookie)`; `serveWebDocument` never sees `/api/…`).
|
|
30
|
-
*
|
|
31
|
-
* The absolute host origin (`globalThis.__DSH_TRANSPORT__.streamBaseUrl`) is deliberately **not** used here:
|
|
32
|
-
* the shell strips `set-cookie` from proxied responses and the session cookie is host-only + `HttpOnly`, so
|
|
33
|
-
* a direct fetch to the host origin cannot authenticate — it would turn a working request into a 401. The
|
|
34
|
-
* platform uses that origin for the WebSocket only (`dsh-api-gateway`, `REMOTE_STREAM_MUX_PATH`), and its
|
|
35
|
-
* own HTTP callers resolve against `document.baseURI` (`dsh-client-file-upload`, `new URL(request.path,
|
|
36
|
-
* document.baseURI)`).
|
|
37
|
-
*/
|
|
38
|
-
export declare const ROOTS_API_PATH = "/api/remote-workspace/roots";
|
|
39
|
-
/** A root either lives on this machine or behind a connection. */
|
|
40
|
-
export type RootLocation = 'local' | 'remote';
|
|
41
|
-
/** One root as the panel shows it. */
|
|
42
|
-
export interface RootRow {
|
|
43
|
-
/** Stable identity: `<location>:<connectionId or '-'>:<path>`. */
|
|
44
|
-
readonly key: string;
|
|
45
|
-
readonly location: RootLocation;
|
|
46
|
-
readonly path: string;
|
|
47
|
-
readonly connectionId?: string;
|
|
48
|
-
readonly label?: string;
|
|
49
|
-
readonly writable: boolean;
|
|
50
|
-
/** `config` rows come from the profile patch; `user` rows were added in this panel. */
|
|
51
|
-
readonly source: 'config' | 'user';
|
|
52
|
-
/** Whether this root also appears in the panel's own file — only those may be deleted. */
|
|
53
|
-
readonly inUserFile: boolean;
|
|
54
|
-
/**
|
|
55
|
-
* The workspaces that list this root; absent means plugin-wide.
|
|
56
|
-
*
|
|
57
|
-
* The composer's card filters on it (one workspace must not inherit another's directories); the settings
|
|
58
|
-
* panel ignores it, because that panel manages the plugin-wide list.
|
|
59
|
-
*/
|
|
60
|
-
readonly workspaces?: readonly string[];
|
|
61
|
-
}
|
|
62
|
-
/** A connection, narrowed to what the panel may display. **No credential fields exist on this type.** */
|
|
63
|
-
export interface ConnectionSummary {
|
|
64
|
-
readonly id: string;
|
|
65
|
-
readonly host: string;
|
|
66
|
-
readonly user?: string;
|
|
67
|
-
readonly port?: number;
|
|
68
|
-
readonly authKind: 'agent' | 'key' | 'password';
|
|
69
|
-
/**
|
|
70
|
-
* The transport the record asks for, when it names one.
|
|
71
|
-
*
|
|
72
|
-
* The edit form prefills from this: a form that always showed the default would silently rewrite a
|
|
73
|
-
* connection the user had deliberately set to zero-install. Absent means "the reader's default" (the
|
|
74
|
-
* record carries no choice), which is a different fact from "the user chose the default".
|
|
75
|
-
*/
|
|
76
|
-
readonly transport?: ConnectionTransportChoice;
|
|
77
|
-
}
|
|
78
|
-
/** Everything one GET answers. */
|
|
79
|
-
export interface RootsSnapshot {
|
|
80
|
-
readonly roots: readonly RootRow[];
|
|
81
|
-
readonly connections: readonly ConnectionSummary[];
|
|
82
|
-
readonly warnings: readonly string[];
|
|
83
|
-
}
|
|
84
|
-
/** The outcome of one call: the panel never throws on a host failure, it shows it. */
|
|
85
|
-
export type ApiResult<T> = {
|
|
86
|
-
readonly ok: true;
|
|
87
|
-
readonly value: T;
|
|
88
|
-
} | {
|
|
89
|
-
readonly ok: false;
|
|
90
|
-
readonly error: string;
|
|
91
|
-
};
|
|
92
|
-
/** What this module needs from `fetch`; injected so tests run without a server (红线 3). */
|
|
93
|
-
export type FetchLike = (input: string, init?: {
|
|
94
|
-
readonly method?: string;
|
|
95
|
-
readonly headers?: Record<string, string>;
|
|
96
|
-
readonly body?: string;
|
|
97
|
-
readonly signal?: AbortSignal;
|
|
98
|
-
}) => Promise<{
|
|
99
|
-
readonly ok: boolean;
|
|
100
|
-
readonly status: number;
|
|
101
|
-
json: () => Promise<unknown>;
|
|
102
|
-
}>;
|
|
103
|
-
/** A message is a key plus its parameters; the component resolves it against the locale dictionaries. */
|
|
104
|
-
export interface MessageKey {
|
|
105
|
-
readonly key: string;
|
|
106
|
-
readonly params?: Readonly<Record<string, string | number>>;
|
|
107
|
-
}
|
|
108
|
-
/** A validated draft, or the reason it was refused. */
|
|
109
|
-
export type Validation<T> = {
|
|
110
|
-
readonly ok: true;
|
|
111
|
-
readonly value: T;
|
|
112
|
-
} | {
|
|
113
|
-
readonly ok: false;
|
|
114
|
-
readonly message: MessageKey;
|
|
115
|
-
};
|
|
116
|
-
/** The payload the panel sends to add a root. */
|
|
117
|
-
export interface RootDraft {
|
|
118
|
-
readonly location: RootLocation;
|
|
119
|
-
readonly path: string;
|
|
120
|
-
readonly connectionId?: string;
|
|
121
|
-
readonly label?: string;
|
|
122
|
-
readonly writable?: boolean;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Which transport a connection should use, as the form offers it.
|
|
126
|
-
*
|
|
127
|
-
* Spelled out here rather than imported from the host's `TransportKind` (`host/domain/connection.ts`): this
|
|
128
|
-
* module is the pure client half and imports nothing from the host (红线 2). The two unions must stay equal,
|
|
129
|
-
* and the host re-validates whatever arrives (`normalizeConnectionEntry`), so a drift cannot reach the file.
|
|
130
|
-
*
|
|
131
|
-
* **Not to be confused with `authKind: 'agent'`**, which names the *local* ssh-agent as a credential source.
|
|
132
|
-
* `transport: 'agent'` names the *remote* Tier 2 Go program this plugin uploads (ADR-0005/0007).
|
|
133
|
-
*/
|
|
134
|
-
export type ConnectionTransportChoice = 'alias' | 'ssh2' | 'agent';
|
|
135
|
-
/** Every transport the form may offer, in the order the selector lists them (the default first). */
|
|
136
|
-
export declare const CONNECTION_TRANSPORTS: readonly ConnectionTransportChoice[];
|
|
137
|
-
/** The transport a draft uses when it names none: the Tier 2 agent, matching the host's own default. */
|
|
138
|
-
export declare const DEFAULT_CONNECTION_TRANSPORT: ConnectionTransportChoice;
|
|
139
|
-
/**
|
|
140
|
-
* Narrow an untrusted value to a transport this build knows.
|
|
141
|
-
*
|
|
142
|
-
* Anything unrecognised degrades to the default rather than reaching the wire: the field is a closed union, and
|
|
143
|
-
* a value the host would reject as `CONNECTION_INVALID` is worse than the documented default.
|
|
144
|
-
*
|
|
145
|
-
* @param value - the raw field.
|
|
146
|
-
* @returns the transport, or {@link DEFAULT_CONNECTION_TRANSPORT}.
|
|
147
|
-
*/
|
|
148
|
-
export declare function toConnectionTransport(value: unknown): ConnectionTransportChoice;
|
|
149
|
-
/**
|
|
150
|
-
* The payload the panel sends to add a connection.
|
|
151
|
-
*
|
|
152
|
-
* `password` is the **only** secret this client ever holds, and only long enough to post it: the host writes
|
|
153
|
-
* it into `ctx.credentials` and answers with a projection that never contains it (FR-2.1.2).
|
|
154
|
-
*/
|
|
155
|
-
export interface ConnectionDraft {
|
|
156
|
-
readonly id: string;
|
|
157
|
-
readonly host: string;
|
|
158
|
-
readonly user: string;
|
|
159
|
-
readonly authKind: 'agent' | 'key' | 'password';
|
|
160
|
-
/**
|
|
161
|
-
* The transport to use. Optional on the way in (an omitted value means {@link DEFAULT_CONNECTION_TRANSPORT})
|
|
162
|
-
* and **always present on the way out**: the host persists the user's explicit choice, so "the default" is a
|
|
163
|
-
* decision the record carries rather than one every reader has to re-derive.
|
|
164
|
-
*/
|
|
165
|
-
readonly transport?: ConnectionTransportChoice;
|
|
166
|
-
readonly keyPath?: string;
|
|
167
|
-
readonly password?: string;
|
|
168
|
-
readonly port?: number;
|
|
169
|
-
}
|
|
170
|
-
/** The identity the host also uses, so removals address the same row the list shows. */
|
|
171
|
-
export declare function rootKey(location: RootLocation, path: string, connectionId?: string): string;
|
|
172
|
-
/**
|
|
173
|
-
* Narrow one GET answer to the panel's model, field by field.
|
|
174
|
-
*
|
|
175
|
-
* Unknown fields are dropped rather than copied: an answer carrying `keyPath` reaches the DOM only as a
|
|
176
|
-
* missing field, never as a value. A malformed answer becomes an empty snapshot with a warning instead of a
|
|
177
|
-
* throw — a broken channel must degrade the panel, not break the whole settings page.
|
|
178
|
-
*
|
|
179
|
-
* @param value - the parsed JSON the route answered with.
|
|
180
|
-
* @returns a snapshot that is safe to render.
|
|
181
|
-
*/
|
|
182
|
-
export declare function parseRootsSnapshot(value: unknown): RootsSnapshot;
|
|
183
|
-
/** True when a string is an absolute path this machine can open (drive letter, POSIX root, or UNC). */
|
|
184
|
-
export declare function isAbsoluteLocalPath(path: string): boolean;
|
|
185
|
-
/**
|
|
186
|
-
* Refuse a draft before it becomes a request.
|
|
187
|
-
*
|
|
188
|
-
* @param draft - what the form holds.
|
|
189
|
-
* @param existing - roots already shown, to reject a duplicate.
|
|
190
|
-
* @returns the wire payload, or the message key to show next to the field.
|
|
191
|
-
*/
|
|
192
|
-
export declare function validateRootDraft(draft: RootDraft, existing?: readonly RootRow[]): Validation<RootDraft>;
|
|
193
|
-
/**
|
|
194
|
-
* Refuse a connection draft before it becomes a request.
|
|
195
|
-
*
|
|
196
|
-
* The id rule is the **host's** rule (a user-chosen name, Chinese included), so the panel refuses exactly
|
|
197
|
-
* what `path-grammar.ts` would refuse instead of sending a request that comes back 400.
|
|
198
|
-
*
|
|
199
|
-
* Password auth is supported: the draft carries the password once, the host stores it in `ctx.credentials`,
|
|
200
|
-
* and nothing in any answer ever carries it back (FR-2.1.2).
|
|
201
|
-
*
|
|
202
|
-
* The transport is **always** in the returned value, even when the draft omits it: the form has a selector,
|
|
203
|
-
* and the host must receive the user's choice explicitly rather than re-deriving the default per reader.
|
|
204
|
-
*
|
|
205
|
-
* @param draft - what the form holds.
|
|
206
|
-
* @param existing - connections already shown, to reject a duplicate id.
|
|
207
|
-
* @returns the wire payload, or the message key to show next to the field.
|
|
208
|
-
*/
|
|
209
|
-
export declare function validateConnectionDraft(draft: ConnectionDraft, existing?: readonly ConnectionSummary[]): Validation<ConnectionDraft>;
|
|
210
|
-
/**
|
|
211
|
-
* Refuse an **edit** draft before it becomes a request.
|
|
212
|
-
*
|
|
213
|
-
* The same rules as {@link validateConnectionDraft} for every field, with the three differences an edit
|
|
214
|
-
* needs, and each one exists for a reason:
|
|
215
|
-
*
|
|
216
|
-
* 1. **A rename is allowed but must not collide with a *different* connection.** Uniqueness is checked
|
|
217
|
-
* against the rows except the one being edited - `validateConnectionDraft` cannot express that, and using
|
|
218
|
-
* it here would refuse every save that kept the id.
|
|
219
|
-
* 2. **An empty password means "do not touch the stored secret", not "refuse".** No answer ever carries a
|
|
220
|
-
* secret back, so the form's password box is always empty on render; requiring it would make it
|
|
221
|
-
* impossible to change a host without retyping the password, and treating empty as "clear" would delete
|
|
222
|
-
* the password on every unrelated edit. The field is therefore **omitted** from the payload unless the
|
|
223
|
-
* user typed one, which is exactly how the host reads it.
|
|
224
|
-
* 3. **`keyPath` follows the same rule as the password: blank means "keep the stored one".** The panel
|
|
225
|
-
* cannot prefill it, because the host's answer deliberately never carries a `keyPath` (it names a real
|
|
226
|
-
* file). Refusing a blank one would therefore make it impossible to edit a key connection's host without
|
|
227
|
-
* retyping its private-key path, and *filling* it with a guess would silently repoint the connection.
|
|
228
|
-
* So it is omitted unless the user typed one - the host's merge keeps what is stored.
|
|
229
|
-
*
|
|
230
|
-
* @param draft - what the form holds.
|
|
231
|
-
* @param originalId - the id of the connection being edited (the lookup key the host needs).
|
|
232
|
-
* @param existing - connections already shown, to reject a colliding rename.
|
|
233
|
-
* @returns the wire payload, or the message key to show next to the field.
|
|
234
|
-
*/
|
|
235
|
-
export declare function validateConnectionEdit(draft: ConnectionDraft, originalId: string, existing?: readonly ConnectionSummary[]): Validation<Record<string, unknown>>;
|
|
236
|
-
/** One row as the component renders it. */
|
|
237
|
-
export interface RootRowView {
|
|
238
|
-
readonly key: string;
|
|
239
|
-
readonly location: RootLocation;
|
|
240
|
-
readonly title: string;
|
|
241
|
-
readonly subtitle: string;
|
|
242
|
-
readonly badgeKey: string;
|
|
243
|
-
readonly removable: boolean;
|
|
244
|
-
}
|
|
245
|
-
/** The panel's complete render input. */
|
|
246
|
-
export interface RootsView {
|
|
247
|
-
readonly rows: readonly RootRowView[];
|
|
248
|
-
readonly connections: readonly ConnectionSummary[];
|
|
249
|
-
/** Set when the host did not answer, so the panel can say why instead of looking empty. */
|
|
250
|
-
readonly notice?: MessageKey;
|
|
251
|
-
/** False while a request is in flight, so the form cannot double-submit. */
|
|
252
|
-
readonly canEdit: boolean;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Turn a snapshot (or the failure to get one) into render input.
|
|
256
|
-
*
|
|
257
|
-
* @param input - the last answer, or the last error, plus whether a request is running.
|
|
258
|
-
* @returns rows, connections, notice and the edit gate.
|
|
259
|
-
*/
|
|
260
|
-
export declare function buildRootsView(input: {
|
|
261
|
-
readonly snapshot?: RootsSnapshot;
|
|
262
|
-
readonly error?: string;
|
|
263
|
-
readonly pending?: boolean;
|
|
264
|
-
}): RootsView;
|
|
265
|
-
/** The calls the panel makes. */
|
|
266
|
-
export interface RootsApi {
|
|
267
|
-
load(signal?: AbortSignal): Promise<ApiResult<RootsSnapshot>>;
|
|
268
|
-
send(body: Record<string, unknown>): Promise<ApiResult<RootsSnapshot>>;
|
|
269
|
-
/**
|
|
270
|
-
* A POST whose answer is **not** a snapshot.
|
|
271
|
-
*
|
|
272
|
-
* The import actions answer a candidate list and an import report, neither of which is a roots snapshot. It
|
|
273
|
-
* is a separate method rather than a looser return type on {@link RootsApi.send} so that every existing call
|
|
274
|
-
* keeps the snapshot it already depends on: widening `send` would make `state` optional for all of them.
|
|
275
|
-
*/
|
|
276
|
-
sendRaw(body: Record<string, unknown>): Promise<ApiResult<Record<string, unknown>>>;
|
|
277
|
-
}
|
|
278
|
-
/** One importable `~/.ssh/config` alias, as the import dialog renders it. */
|
|
279
|
-
export interface SshConfigCandidate {
|
|
280
|
-
/** The literal alias; also the `ssh` target. Shown verbatim so the user recognises their own config. */
|
|
281
|
-
readonly alias: string;
|
|
282
|
-
/** Resolved host name. */
|
|
283
|
-
readonly host: string;
|
|
284
|
-
readonly user?: string;
|
|
285
|
-
readonly port?: number;
|
|
286
|
-
/** Whether the block named an identity file. The path itself never reaches the client. */
|
|
287
|
-
readonly hasIdentityFile: boolean;
|
|
288
|
-
readonly hasProxyJump: boolean;
|
|
289
|
-
/** The id the import would create. Editable in spirit: the user sees it before committing. */
|
|
290
|
-
readonly suggestedId: string;
|
|
291
|
-
}
|
|
292
|
-
/** One alias the host refused, with the machine-readable reason. */
|
|
293
|
-
export interface SshConfigSkipped {
|
|
294
|
-
readonly pattern: string;
|
|
295
|
-
readonly reason: string;
|
|
296
|
-
readonly line: number;
|
|
297
|
-
}
|
|
298
|
-
/** What `list-ssh-config` answers. */
|
|
299
|
-
export interface SshConfigListing {
|
|
300
|
-
readonly candidates: readonly SshConfigCandidate[];
|
|
301
|
-
readonly skipped: readonly SshConfigSkipped[];
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Narrow the host's `list-ssh-config` answer.
|
|
305
|
-
*
|
|
306
|
-
* Defensive in the same way {@link parseRootsSnapshot} is: the client never trusts the wire's shape, and a row
|
|
307
|
-
* without a usable alias or id is **dropped** rather than rendered as a checkbox that would import nothing.
|
|
308
|
-
*
|
|
309
|
-
* @param value - the parsed JSON body.
|
|
310
|
-
* @returns the listing, with unusable rows removed.
|
|
311
|
-
*/
|
|
312
|
-
export declare function parseSshConfigListing(value: unknown): SshConfigListing;
|
|
313
|
-
/**
|
|
314
|
-
* The human-readable reason one alias was refused.
|
|
315
|
-
*
|
|
316
|
-
* The mapping lives here rather than in the view so the reasons are testable without rendering, and an unknown
|
|
317
|
-
* reason from a newer host still produces a sentence instead of an empty cell.
|
|
318
|
-
*
|
|
319
|
-
* @param reason - the machine-readable reason.
|
|
320
|
-
* @returns a message key.
|
|
321
|
-
*/
|
|
322
|
-
export declare function sshConfigSkipKey(reason: string): string;
|
|
323
|
-
/** A candidate's one-line summary: `user@host:port`, with the flags that matter for the import. */
|
|
324
|
-
export declare function sshConfigCandidateSummary(candidate: SshConfigCandidate): string;
|
|
325
|
-
/**
|
|
326
|
-
* The reason a failed response reports, when the host did not name one.
|
|
327
|
-
*
|
|
328
|
-
* The path is part of the text on purpose: `http-404` alone cannot distinguish "this route is not
|
|
329
|
-
* registered" (the host half never claimed it — that was a real defect) from "the request never reached the
|
|
330
|
-
* host" (the shell's proxy or the composition is at fault). Both are shown verbatim in the panel's notice.
|
|
331
|
-
*
|
|
332
|
-
* @param status - the HTTP status.
|
|
333
|
-
* @param path - the exact URL that was requested.
|
|
334
|
-
* @returns the machine-readable reason.
|
|
335
|
-
*/
|
|
336
|
-
export declare function transportFailureReason(status: number, path: string): string;
|
|
337
|
-
/**
|
|
338
|
-
* Bind the route to the panel's two operations.
|
|
339
|
-
*
|
|
340
|
-
* @param fetchImpl - `window.fetch` in the app, a stub in tests.
|
|
341
|
-
* @param path - override for tests; defaults to {@link ROOTS_API_PATH}.
|
|
342
|
-
* @returns the calls the component uses.
|
|
343
|
-
*/
|
|
344
|
-
export declare function createRootsApi(fetchImpl: FetchLike, path?: string): RootsApi;
|
|
345
|
-
/** The field names the panel must never send, asserted by tests rather than trusted to reviewers. */
|
|
346
|
-
export declare function isForbiddenField(name: string): boolean;
|
|
347
|
-
//# sourceMappingURL=roots-panel.d.ts.map
|