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,280 @@
|
|
|
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-over-ssh/client/roots-panel-view
|
|
29
|
+
*/
|
|
30
|
+
import { type ReactNode } from 'react';
|
|
31
|
+
import { type CardPortal } from '../core/portal.ts';
|
|
32
|
+
import { type RootLocation, type RootsApi } from './roots-panel.ts';
|
|
33
|
+
import type { RootsPanelCopy, RootsPanelDraftPatch, RootsPanelEditDraftPatch, RootsPanelState } from './roots-panel-state.ts';
|
|
34
|
+
export * from './roots-panel-state.ts';
|
|
35
|
+
export * from './transfer-panel-view.ts';
|
|
36
|
+
/**
|
|
37
|
+
* Placement of the connection **edit dialog**'s portal.
|
|
38
|
+
*
|
|
39
|
+
* `CLASS.portal` is the fixed body-level container; `CLASS.portalTop` is the wider placement the other two
|
|
40
|
+
* dialogs share, which this form needs because it carries five fields plus the rename note.
|
|
41
|
+
*/
|
|
42
|
+
export declare const ROOTS_EDIT_PORTAL_CLASS: string;
|
|
43
|
+
/**
|
|
44
|
+
* The click sink for that card: the settings modal's own layers are 900/1001, so the sink sits at 1001 and the
|
|
45
|
+
* card above it (audit 2026-11-01b ui P2-1). The edit dialog is the case where a click *through* the gap used to
|
|
46
|
+
* be worst: the 「新增连接」 form behind it shares the screen, and its 保存 was reachable.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ROOTS_EDIT_BACKDROP_CLASS: string;
|
|
49
|
+
/** The calls the section makes. `roots-panel.ts` owns how they are implemented. */
|
|
50
|
+
export interface RootsPanelController {
|
|
51
|
+
/** The current state; a stable reference until something changes (safe for `useSyncExternalStore`). */
|
|
52
|
+
readonly getState: () => RootsPanelState;
|
|
53
|
+
/** Watch for changes; returns the disposer. */
|
|
54
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
55
|
+
/** Ask for the first answer once; a remount does not re-fetch. */
|
|
56
|
+
readonly start: () => void;
|
|
57
|
+
/** Reload unconditionally (used after a successful change). */
|
|
58
|
+
readonly reload: () => Promise<void>;
|
|
59
|
+
/** Change one field of the **add connection** form. */
|
|
60
|
+
readonly changeDraft: (patch: RootsPanelDraftPatch) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Change one field of the **edit dialog**.
|
|
63
|
+
*
|
|
64
|
+
* A separate writer because the two forms have separate drafts (`RootsPanelEditDrafts`): one set made opening
|
|
65
|
+
* the dialog overwrite the add form and cancelling it wipe it (audit 2026-11-01b ui P1-4).
|
|
66
|
+
*/
|
|
67
|
+
readonly changeEditDraft: (patch: RootsPanelEditDraftPatch) => void;
|
|
68
|
+
/** Validate and add a root of the given location. */
|
|
69
|
+
readonly submitRoot: (location: RootLocation) => Promise<void>;
|
|
70
|
+
/** Validate and add a connection. */
|
|
71
|
+
readonly submitConnection: () => Promise<void>;
|
|
72
|
+
/** Open the edit form for one connection, prefilled from the last host answer. */
|
|
73
|
+
readonly startEdit: (id: string) => void;
|
|
74
|
+
/** Close the edit form without saving. */
|
|
75
|
+
readonly cancelEdit: () => void;
|
|
76
|
+
/** Validate and save the edit form (a rename included). */
|
|
77
|
+
readonly submitEdit: () => Promise<void>;
|
|
78
|
+
/** Ask for confirmation before removing a connection (it takes its remote roots with it). */
|
|
79
|
+
readonly askRemoveConnection: (id: string) => void;
|
|
80
|
+
/** Dismiss the confirmation. */
|
|
81
|
+
readonly cancelRemoveConnection: () => void;
|
|
82
|
+
/** Remove a connection, once the confirmation was accepted. */
|
|
83
|
+
readonly confirmRemoveConnection: (id: string) => Promise<void>;
|
|
84
|
+
/** Remove one row, if the view allows it. */
|
|
85
|
+
readonly remove: (key: string) => Promise<void>;
|
|
86
|
+
/** Open the ssh-config import dialog and read the file (FR-2.1.4). */
|
|
87
|
+
readonly openImport: () => Promise<void>;
|
|
88
|
+
/** Re-read `~/.ssh/config`, replacing the candidate list. */
|
|
89
|
+
readonly reloadImport: () => Promise<void>;
|
|
90
|
+
/** Close the dialog. */
|
|
91
|
+
readonly closeImport: () => void;
|
|
92
|
+
/** Tick or untick one alias. */
|
|
93
|
+
readonly toggleImport: (alias: string) => void;
|
|
94
|
+
/** Tick every candidate. */
|
|
95
|
+
readonly selectAllImports: () => void;
|
|
96
|
+
/** Untick everything. */
|
|
97
|
+
readonly clearImports: () => void;
|
|
98
|
+
/** Import the ticked aliases. */
|
|
99
|
+
readonly submitImport: () => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Tell the controller the settings surface is on screen again.
|
|
102
|
+
*
|
|
103
|
+
* Cancels a pending {@link RootsPanelController.surfaceDetached} — see there for why unmounting is not by
|
|
104
|
+
* itself "the page was closed".
|
|
105
|
+
*/
|
|
106
|
+
readonly surfaceAttached: () => void;
|
|
107
|
+
/**
|
|
108
|
+
* Tell the controller the settings surface went away.
|
|
109
|
+
*
|
|
110
|
+
* **Not** "close the editor": the platform re-registers this section whenever its inject face changes, and
|
|
111
|
+
* opening the editor is such a change, so the component unmounts and remounts as a normal part of opening
|
|
112
|
+
* it. Closing the settings page looks the same except that **nothing remounts**. The two are told apart in
|
|
113
|
+
* time: the clear is deferred by one task, and a remount cancels it (audit 0091).
|
|
114
|
+
*/
|
|
115
|
+
readonly surfaceDetached: () => void;
|
|
116
|
+
/** Stop accepting results and drop listeners. */
|
|
117
|
+
readonly dispose: () => void;
|
|
118
|
+
}
|
|
119
|
+
/** What a controller needs: the one call, plus the one scheduling choice. */
|
|
120
|
+
export interface RootsPanelControllerOptions {
|
|
121
|
+
readonly api: RootsApi;
|
|
122
|
+
/**
|
|
123
|
+
* Defer one task and return its canceller.
|
|
124
|
+
*
|
|
125
|
+
* Injectable so {@link RootsPanelController.surfaceDetached} is testable without timers: the policy it
|
|
126
|
+
* encodes ("did anything remount before this ran?") is the whole point, and a test that has to wait on a
|
|
127
|
+
* real `setTimeout` would be both slow and flaky.
|
|
128
|
+
*/
|
|
129
|
+
readonly defer?: (run: () => void) => () => void;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build the stateful half of the panel.
|
|
133
|
+
*
|
|
134
|
+
* The refresh-after-success is the reason this exists at all: `POST` answers with the *persisted* state,
|
|
135
|
+
* whose `roots` are the user's roots - the same field name `GET` uses for the *config* roots. Rendering that
|
|
136
|
+
* answer directly would show a config root as a user root (or the reverse), so a success always ends with a
|
|
137
|
+
* fresh `GET` and the POST body is used only as a signal.
|
|
138
|
+
*
|
|
139
|
+
* @param options - the injected API.
|
|
140
|
+
* @returns the controller the container subscribes to.
|
|
141
|
+
*/
|
|
142
|
+
export declare function createRootsPanelController(options: RootsPanelControllerOptions): RootsPanelController;
|
|
143
|
+
/**
|
|
144
|
+
* The section's callbacks, derived from the controller.
|
|
145
|
+
*
|
|
146
|
+
* A factory rather than an inline object literal so the shell can `useMemo(() => rootsPanelSectionActions(controller),
|
|
147
|
+
* [controller])` — stable props are the precondition for ever putting `memo` on the two child components — **and**
|
|
148
|
+
* so "each callback really drives the controller" is a decision a test can hold on to (audit 2026-11-01b ui P3-7).
|
|
149
|
+
*
|
|
150
|
+
* @param controller - the panel's controller.
|
|
151
|
+
* @returns the callbacks the pure template calls.
|
|
152
|
+
*/
|
|
153
|
+
export declare function rootsPanelSectionActions(controller: RootsPanelController): RootsPanelSectionActions;
|
|
154
|
+
/** What the section's controls call. All of them are no-ops when the gate is closed. */
|
|
155
|
+
export interface RootsPanelSectionActions {
|
|
156
|
+
readonly onDraftChange: (patch: RootsPanelDraftPatch) => void;
|
|
157
|
+
/** One field of the edit dialog changed (its own draft set). */
|
|
158
|
+
readonly onEditDraftChange: (patch: RootsPanelEditDraftPatch) => void;
|
|
159
|
+
readonly onSubmitRoot: (location: RootLocation) => void;
|
|
160
|
+
readonly onSubmitConnection: () => void;
|
|
161
|
+
readonly onRemove: (key: string) => void;
|
|
162
|
+
readonly onSubmitEdit: () => void;
|
|
163
|
+
readonly onCancelEdit: () => void;
|
|
164
|
+
/** FR-2.1.4: the ssh-config import dialog's controls. */
|
|
165
|
+
readonly onImportOpen: () => void;
|
|
166
|
+
readonly onImportClose: () => void;
|
|
167
|
+
readonly onImportReload: () => void;
|
|
168
|
+
readonly onImportToggle: (alias: string) => void;
|
|
169
|
+
readonly onImportAll: () => void;
|
|
170
|
+
readonly onImportNone: () => void;
|
|
171
|
+
readonly onImportSubmit: () => void;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The `~/.ssh/config` import dialog (FR-2.1.4).
|
|
175
|
+
*
|
|
176
|
+
* Three things it deliberately does:
|
|
177
|
+
*
|
|
178
|
+
* - **It shows the refusals.** Every alias the host would not offer is listed with the reason, because the
|
|
179
|
+
* user is looking at their own config file and can see the entry that is missing from the list. A silent
|
|
180
|
+
* subset reads as a broken feature.
|
|
181
|
+
* - **It shows the id each alias will become.** `deploy@bastion` is a valid alias and not a valid id, so the
|
|
182
|
+
* dialog says what the id will be instead of letting `add-connection` refuse it later.
|
|
183
|
+
* - **It never shows an identity path.** The host answers `hasIdentityFile` as a boolean; the path itself is
|
|
184
|
+
* not on the wire (the route's invariant 1), so nothing here can leak one.
|
|
185
|
+
*
|
|
186
|
+
* @param props - copy, state and callbacks.
|
|
187
|
+
* @returns the dialog, or nothing when it is closed.
|
|
188
|
+
*/
|
|
189
|
+
export declare function SshConfigImportDialog({ copy, state, actions, idPrefix }: RootsPanelSectionProps): ReactNode;
|
|
190
|
+
/**
|
|
191
|
+
* The **edit** form for one saved connection.
|
|
192
|
+
*
|
|
193
|
+
* It exists as its own component because it is the one form whose fields are prefilled from a host answer: the
|
|
194
|
+
* panel's top list renders the row buttons that open it, and this renders what those buttons open. Splitting
|
|
195
|
+
* them keeps `RootsPanelSection` about the roots it owns and this about one connection.
|
|
196
|
+
*
|
|
197
|
+
* Two rules it inherits and one it adds:
|
|
198
|
+
*
|
|
199
|
+
* - **Nothing is submitted before validation** ({@link validateConnectionEdit} is the only way in);
|
|
200
|
+
* - **no credential is ever rendered** - the password box is always empty, because no answer carries a
|
|
201
|
+
* secret back, and an empty box means "keep the stored password" (see the key's own note);
|
|
202
|
+
* - **the id is editable, and the rename is explained**: a rename re-keys the connection's remote roots and
|
|
203
|
+
* is therefore not a cosmetic field, so the form says so rather than leaving it to be discovered.
|
|
204
|
+
*
|
|
205
|
+
* @param props - copy, state and callbacks.
|
|
206
|
+
* @returns the form, or nothing when no connection is open for editing.
|
|
207
|
+
*/
|
|
208
|
+
export declare function ConnectionEditForm({ copy, state, actions, idPrefix }: RootsPanelSectionProps): ReactNode;
|
|
209
|
+
/** Props of the pure template: state in, callbacks out, no hooks and no store access. */
|
|
210
|
+
/**
|
|
211
|
+
* The `id` prefix the pure templates use when the shell does not pass one (direct template calls, tests).
|
|
212
|
+
*
|
|
213
|
+
* The shell always passes `useId()`'s value instead, so the ids are **per mount** — see
|
|
214
|
+
* {@link RootsPanelSectionProps.idPrefix}.
|
|
215
|
+
*/
|
|
216
|
+
export declare const DEFAULT_ROOTS_ID_PREFIX = "dsh-rw-roots";
|
|
217
|
+
export interface RootsPanelSectionProps {
|
|
218
|
+
readonly copy: RootsPanelCopy;
|
|
219
|
+
readonly state: RootsPanelState;
|
|
220
|
+
readonly actions: RootsPanelSectionActions;
|
|
221
|
+
/**
|
|
222
|
+
* `id` prefix for this mount's form controls (`{prefix}-edit-host`, `{prefix}-import-<alias>`, …).
|
|
223
|
+
*
|
|
224
|
+
* A module constant was enough while there was exactly one section on one page, and it stopped being enough
|
|
225
|
+
* the moment the platform started re-registering the section on every inject-face change: for one commit the
|
|
226
|
+
* old mount and the new one are both in the DOM, so two inputs share an `id` and every `<label for>` in the
|
|
227
|
+
* overlap points at whichever one the browser found first. `useId()` in the container makes that impossible.
|
|
228
|
+
*
|
|
229
|
+
* Optional only so the offline view tests can call these templates directly (audit 2026-10-31 client.md §2.4).
|
|
230
|
+
*/
|
|
231
|
+
readonly idPrefix?: string;
|
|
232
|
+
/**
|
|
233
|
+
* The body-level portal the connection **edit dialog** is rendered into.
|
|
234
|
+
*
|
|
235
|
+
* Optional on purpose: the pure template is called directly by the offline view tests (and by any caller
|
|
236
|
+
* without a DOM), where the card is rendered in place. The shell always passes one, so the real UI gets a
|
|
237
|
+
* card that floats over the settings page instead of being pushed below the "新增连接" form (audit 0088).
|
|
238
|
+
*/
|
|
239
|
+
readonly portal?: CardPortal;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Props of the whole settings section.
|
|
243
|
+
*
|
|
244
|
+
* It is {@link RootsPanelSectionProps} plus the one element that needs the container: the transfer progress
|
|
245
|
+
* panel. The remote file **tree** is not here at all: it was deleted (audit 0085) because remote paths are
|
|
246
|
+
* reached through the conversation, and because this function is called **directly** by the view tests,
|
|
247
|
+
* outside any React render, where a hook throws `Invalid hook call` (the constraint documented in
|
|
248
|
+
* `controls.ts`).
|
|
249
|
+
*/
|
|
250
|
+
export interface RootsPanelSectionViewProps extends RootsPanelSectionProps {
|
|
251
|
+
/** The transfer progress panel element, or `undefined` in a template-only render. */
|
|
252
|
+
readonly transfers?: ReactNode;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Render the editable section. Pure: calling it twice with the same props yields the same tree.
|
|
256
|
+
*
|
|
257
|
+
* @param props - copy, state and callbacks.
|
|
258
|
+
* @returns the section element.
|
|
259
|
+
*/
|
|
260
|
+
export declare function RootsPanelSection({ copy, state, actions, transfers, portal, idPrefix, }: RootsPanelSectionViewProps): ReactNode;
|
|
261
|
+
/** Props of the React shell: the controller owns the state, the shell only subscribes. */
|
|
262
|
+
export interface RootsPanelContainerProps {
|
|
263
|
+
readonly copy: RootsPanelCopy;
|
|
264
|
+
readonly controller: RootsPanelController;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Subscribe to a controller and render {@link RootsPanelSection}.
|
|
268
|
+
*
|
|
269
|
+
* `useSyncExternalStore` is the whole reason this is a component and not a function call: the slot's inject
|
|
270
|
+
* face is a synchronous snapshot, so the section has to be re-rendered by the store it reads.
|
|
271
|
+
*
|
|
272
|
+
* The right-pane entry point is the one piece of state this shell owns itself: whether the composition
|
|
273
|
+
* publishes the right sidebar is not a fact the controller's store carries (it is answered live by the entry
|
|
274
|
+
* point), and an open that failed after the button was enabled must say so somewhere. Both are re-read on
|
|
275
|
+
* every render, which is exactly when a `ctx.get` seam can have changed.
|
|
276
|
+
*
|
|
277
|
+
* @param props - the dictionary and the controller.
|
|
278
|
+
* @returns the section element.
|
|
279
|
+
*/
|
|
280
|
+
export declare function RootsPanelContainer({ copy, controller }: RootsPanelContainerProps): ReactNode;
|
|
@@ -0,0 +1,364 @@
|
|
|
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-over-ssh/client/roots-panel
|
|
16
|
+
*/
|
|
17
|
+
import type { MessageKey } from '../core/message.ts';
|
|
18
|
+
/**
|
|
19
|
+
* Where the host route lives, as the browser must request it.
|
|
20
|
+
*
|
|
21
|
+
* **Relative on purpose, and this is the platform's own convention.** The GUI is served from two different
|
|
22
|
+
* origins depending on the carrier, and only a relative path is correct in both:
|
|
23
|
+
*
|
|
24
|
+
* - `dsh web` (browser): the page origin *is* the host, so `/api/…` reaches
|
|
25
|
+
* `ctx.webServer` directly;
|
|
26
|
+
* - the Desktop shell: the page is `dsh-app://app/` and `/api/…` resolves to
|
|
27
|
+
* `dsh-app://app/api/…`, which Electron's `protocol.handle('dsh-app')` forwards to the owned host with
|
|
28
|
+
* the browser-session cookie the shell keeps out of the page
|
|
29
|
+
* (`resources/app.asar/lib/main.js`, `forwardWebRequest`: non-asset paths are proxied with
|
|
30
|
+
* `headers.set('cookie', cookie)`; `serveWebDocument` never sees `/api/…`).
|
|
31
|
+
*
|
|
32
|
+
* The absolute host origin (`globalThis.__DSH_TRANSPORT__.streamBaseUrl`) is deliberately **not** used here:
|
|
33
|
+
* the shell strips `set-cookie` from proxied responses and the session cookie is host-only + `HttpOnly`, so
|
|
34
|
+
* a direct fetch to the host origin cannot authenticate — it would turn a working request into a 401. The
|
|
35
|
+
* platform uses that origin for the WebSocket only (`dsh-api-gateway`, `REMOTE_STREAM_MUX_PATH`), and its
|
|
36
|
+
* own HTTP callers resolve against `document.baseURI` (`dsh-client-file-upload`, `new URL(request.path,
|
|
37
|
+
* document.baseURI)`).
|
|
38
|
+
*/
|
|
39
|
+
export declare const ROOTS_API_PATH = "/api/over-ssh/roots";
|
|
40
|
+
/** A root either lives on this machine or behind a connection. */
|
|
41
|
+
export type RootLocation = 'local' | 'remote';
|
|
42
|
+
/** One root as the panel shows it. */
|
|
43
|
+
export interface RootRow {
|
|
44
|
+
/** Stable identity: `<location>:<connectionId or '-'>:<path>`. */
|
|
45
|
+
readonly key: string;
|
|
46
|
+
readonly location: RootLocation;
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly connectionId?: string;
|
|
49
|
+
readonly label?: string;
|
|
50
|
+
readonly writable: boolean;
|
|
51
|
+
/** `config` rows come from the profile patch; `user` rows were added in this panel. */
|
|
52
|
+
readonly source: 'config' | 'user';
|
|
53
|
+
/** Whether this root also appears in the panel's own file — only those may be deleted. */
|
|
54
|
+
readonly inUserFile: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The workspaces that list this root; absent means **no session lists it yet**.
|
|
57
|
+
*
|
|
58
|
+
* The composer's card shows exactly the roots whose list names its own workspace (one session must not
|
|
59
|
+
* inherit another's directories). The settings panel ignores it, because that panel manages the whole list
|
|
60
|
+
* and registers roots with **no** scope of its own — its form has no `workspaces` member (`RootsPanelDrafts`),
|
|
61
|
+
* so a root added there is not listed by any session until one adds it (audit 0056).
|
|
62
|
+
*/
|
|
63
|
+
readonly workspaces?: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* The **sessions** that added this root (ADR-0013); absent on entries written before it.
|
|
66
|
+
*
|
|
67
|
+
* The composer's card prefers this over `workspaces`: two conversations opened on the same directory share
|
|
68
|
+
* the workspace path and differ only here.
|
|
69
|
+
*/
|
|
70
|
+
readonly sessions?: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
/** A connection, narrowed to what the panel may display. **No credential fields exist on this type.** */
|
|
73
|
+
export interface ConnectionSummary {
|
|
74
|
+
readonly id: string;
|
|
75
|
+
readonly host: string;
|
|
76
|
+
readonly user?: string;
|
|
77
|
+
readonly port?: number;
|
|
78
|
+
readonly authKind: 'agent' | 'key' | 'password';
|
|
79
|
+
/**
|
|
80
|
+
* The transport in force, **when the host reports it**.
|
|
81
|
+
*
|
|
82
|
+
* `publicRoot`/`publicUserState` do not carry it today (the transport is a live fact of the platform's remote
|
|
83
|
+
* namespace, not of the profile), so production answers leave this `undefined` and the panel says 「未知」
|
|
84
|
+
* rather than claiming `unknown` — but a host that starts publishing it is honoured without another round
|
|
85
|
+
* (audit 2026-11-01b ui P2-6).
|
|
86
|
+
*/
|
|
87
|
+
readonly transport?: ConnectionTransport;
|
|
88
|
+
/** True when that transport is a degraded substitute, when the host reports it. */
|
|
89
|
+
readonly degraded?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** The transports a connection row may name; `unknown` means "the host did not say". */
|
|
92
|
+
/**
|
|
93
|
+
* Re-exported from `core/panel-model.ts`, which **owns** it: `core` must not depend on `roots`, so the
|
|
94
|
+
* definition lives in the lower layer and this file points up at it. `export type {…} from` alone does not put
|
|
95
|
+
* the name in this module's scope, hence the separate `import type` as well.
|
|
96
|
+
*/
|
|
97
|
+
export type { ConnectionTransport } from '../core/panel-model.ts';
|
|
98
|
+
import type { ConnectionTransport } from '../core/panel-model.ts';
|
|
99
|
+
/** Everything one GET answers. */
|
|
100
|
+
export interface RootsSnapshot {
|
|
101
|
+
readonly roots: readonly RootRow[];
|
|
102
|
+
readonly connections: readonly ConnectionSummary[];
|
|
103
|
+
readonly warnings: readonly string[];
|
|
104
|
+
/**
|
|
105
|
+
* The plugin's anchor root (`<home>/.dsh/over-ssh/roots`), when the host publishes one.
|
|
106
|
+
*
|
|
107
|
+
* Not a row: it is what lets a client recognise that a session's cwd *is* a remote workspace (see
|
|
108
|
+
* `client/session-remote-mark.ts`). Absent means "the host did not say", and every caller must treat that
|
|
109
|
+
* as unknown rather than as "no remote sessions".
|
|
110
|
+
*/
|
|
111
|
+
readonly anchorRoot?: string | undefined;
|
|
112
|
+
}
|
|
113
|
+
/** The outcome of one call: the panel never throws on a host failure, it shows it. */
|
|
114
|
+
export type ApiResult<T> = {
|
|
115
|
+
readonly ok: true;
|
|
116
|
+
readonly value: T;
|
|
117
|
+
} | {
|
|
118
|
+
readonly ok: false;
|
|
119
|
+
readonly error: string;
|
|
120
|
+
};
|
|
121
|
+
/** What this module needs from `fetch`; injected so tests run without a server (红线 3). */
|
|
122
|
+
export type FetchLike = (input: string, init?: {
|
|
123
|
+
readonly method?: string;
|
|
124
|
+
readonly headers?: Record<string, string>;
|
|
125
|
+
readonly body?: string;
|
|
126
|
+
readonly signal?: AbortSignal;
|
|
127
|
+
}) => Promise<{
|
|
128
|
+
readonly ok: boolean;
|
|
129
|
+
readonly status: number;
|
|
130
|
+
json: () => Promise<unknown>;
|
|
131
|
+
}>;
|
|
132
|
+
/**
|
|
133
|
+
* A message is a key plus its parameters; the component resolves it against the locale dictionaries.
|
|
134
|
+
*
|
|
135
|
+
* The shape (and the resolver, `resolveMessage`) live in `../core/message.ts` because the session panel and the
|
|
136
|
+
* transfer panel produce messages too, and both are pure models that must not import React. Re-exported here so
|
|
137
|
+
* every existing `import { MessageKey } from './roots-panel.ts'` keeps working.
|
|
138
|
+
*/
|
|
139
|
+
export type { MessageKey };
|
|
140
|
+
/** A validated draft, or the reason it was refused. */
|
|
141
|
+
export type Validation<T> = {
|
|
142
|
+
readonly ok: true;
|
|
143
|
+
readonly value: T;
|
|
144
|
+
} | {
|
|
145
|
+
readonly ok: false;
|
|
146
|
+
readonly message: MessageKey;
|
|
147
|
+
};
|
|
148
|
+
/** The payload the panel sends to add a root. */
|
|
149
|
+
export interface RootDraft {
|
|
150
|
+
readonly location: RootLocation;
|
|
151
|
+
readonly path: string;
|
|
152
|
+
readonly connectionId?: string;
|
|
153
|
+
readonly label?: string;
|
|
154
|
+
readonly writable?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The transport a connection record asks for.
|
|
158
|
+
*
|
|
159
|
+
* It stays in the wire payload type because the host still accepts and persists it, but the **panel no
|
|
160
|
+
* longer offers a choice**: every save asks for `agent`, and the host's selector/preflight degrades to the
|
|
161
|
+
* zero-install transports automatically when the Tier 2 ladder cannot serve the connection. Keeping the type
|
|
162
|
+
* here documents what the payload can contain without re-opening the UI decision.
|
|
163
|
+
*/
|
|
164
|
+
export type ConnectionTransportChoice = 'alias' | 'ssh2' | 'agent';
|
|
165
|
+
/**
|
|
166
|
+
* The payload the panel sends to add a connection.
|
|
167
|
+
*
|
|
168
|
+
* `password` is the **only** secret this client ever holds, and only long enough to post it: the host writes
|
|
169
|
+
* it into `ctx.credentials` and answers with a projection that never contains it (FR-2.1.2).
|
|
170
|
+
*/
|
|
171
|
+
export interface ConnectionDraft {
|
|
172
|
+
readonly id: string;
|
|
173
|
+
readonly host: string;
|
|
174
|
+
readonly user: string;
|
|
175
|
+
readonly authKind: 'agent' | 'key' | 'password';
|
|
176
|
+
/**
|
|
177
|
+
* The transport the payload asks for.
|
|
178
|
+
*
|
|
179
|
+
* Always `agent` now: the panel offers no choice, and the host degrades automatically. Kept in the payload
|
|
180
|
+
* (rather than omitted) so an older record's `alias`/`ssh2` is rewritten to the preferred path on the first
|
|
181
|
+
* save, and so the host keeps the "always explicit" contract it already validates.
|
|
182
|
+
*/
|
|
183
|
+
readonly transport?: ConnectionTransportChoice;
|
|
184
|
+
readonly keyPath?: string;
|
|
185
|
+
readonly password?: string;
|
|
186
|
+
readonly port?: number;
|
|
187
|
+
}
|
|
188
|
+
/** The identity the host also uses, so removals address the same row the list shows. */
|
|
189
|
+
export declare function rootKey(location: RootLocation, path: string, connectionId?: string): string;
|
|
190
|
+
/**
|
|
191
|
+
* Narrow one GET answer to the panel's model, field by field.
|
|
192
|
+
*
|
|
193
|
+
* Unknown fields are dropped rather than copied: an answer carrying `keyPath` reaches the DOM only as a
|
|
194
|
+
* missing field, never as a value. A malformed answer becomes an empty snapshot with a warning instead of a
|
|
195
|
+
* throw — a broken channel must degrade the panel, not break the whole settings page.
|
|
196
|
+
*
|
|
197
|
+
* @param value - the parsed JSON the route answered with.
|
|
198
|
+
* @returns a snapshot that is safe to render.
|
|
199
|
+
*/
|
|
200
|
+
export declare function parseRootsSnapshot(value: unknown): RootsSnapshot;
|
|
201
|
+
/** True when a string is an absolute path this machine can open (drive letter, POSIX root, or UNC). */
|
|
202
|
+
export declare function isAbsoluteLocalPath(path: string): boolean;
|
|
203
|
+
/**
|
|
204
|
+
* Refuse a draft before it becomes a request.
|
|
205
|
+
*
|
|
206
|
+
* @param draft - what the form holds.
|
|
207
|
+
* @param existing - roots already shown, to reject a duplicate.
|
|
208
|
+
* @returns the wire payload, or the message key to show next to the field.
|
|
209
|
+
*/
|
|
210
|
+
export declare function validateRootDraft(draft: RootDraft, existing?: readonly RootRow[]): Validation<RootDraft>;
|
|
211
|
+
/**
|
|
212
|
+
* Refuse a connection draft before it becomes a request.
|
|
213
|
+
*
|
|
214
|
+
* The id rule is the **host's** rule (a user-chosen name, Chinese included), so the panel refuses exactly
|
|
215
|
+
* what `path-grammar.ts` would refuse instead of sending a request that comes back 400.
|
|
216
|
+
*
|
|
217
|
+
* Password auth is supported: the draft carries the password once, the host stores it in `ctx.credentials`,
|
|
218
|
+
* and nothing in any answer ever carries it back (FR-2.1.2).
|
|
219
|
+
*
|
|
220
|
+
* The payload always carries `transport: 'agent'`: the panel no longer offers a choice, and the host downgrades
|
|
221
|
+
* automatically when the Tier 2 ladder cannot serve this connection.
|
|
222
|
+
*
|
|
223
|
+
* @param draft - what the form holds.
|
|
224
|
+
* @param existing - connections already shown, to reject a duplicate id.
|
|
225
|
+
* @returns the wire payload, or the message key to show next to the field.
|
|
226
|
+
*/
|
|
227
|
+
export declare function validateConnectionDraft(draft: ConnectionDraft, existing?: readonly ConnectionSummary[]): Validation<ConnectionDraft>;
|
|
228
|
+
/**
|
|
229
|
+
* Refuse an **edit** draft before it becomes a request.
|
|
230
|
+
*
|
|
231
|
+
* The same rules as {@link validateConnectionDraft} for every field, with the three differences an edit
|
|
232
|
+
* needs, and each one exists for a reason:
|
|
233
|
+
*
|
|
234
|
+
* 1. **A rename is allowed but must not collide with a *different* connection.** Uniqueness is checked
|
|
235
|
+
* against the rows except the one being edited - `validateConnectionDraft` cannot express that, and using
|
|
236
|
+
* it here would refuse every save that kept the id.
|
|
237
|
+
* 2. **An empty password means "do not touch the stored secret", not "refuse".** No answer ever carries a
|
|
238
|
+
* secret back, so the form's password box is always empty on render; requiring it would make it
|
|
239
|
+
* impossible to change a host without retyping the password, and treating empty as "clear" would delete
|
|
240
|
+
* the password on every unrelated edit. The field is therefore **omitted** from the payload unless the
|
|
241
|
+
* user typed one, which is exactly how the host reads it.
|
|
242
|
+
* 3. **`keyPath` follows the same rule as the password: blank means "keep the stored one".** The panel
|
|
243
|
+
* cannot prefill it, because the host's answer deliberately never carries a `keyPath` (it names a real
|
|
244
|
+
* file). Refusing a blank one would therefore make it impossible to edit a key connection's host without
|
|
245
|
+
* retyping its private-key path, and *filling* it with a guess would silently repoint the connection.
|
|
246
|
+
* So it is omitted unless the user typed one - the host's merge keeps what is stored.
|
|
247
|
+
*
|
|
248
|
+
* @param draft - what the form holds.
|
|
249
|
+
* @param originalId - the id of the connection being edited (the lookup key the host needs).
|
|
250
|
+
* @param existing - connections already shown, to reject a colliding rename.
|
|
251
|
+
* @returns the wire payload, or the message key to show next to the field.
|
|
252
|
+
*/
|
|
253
|
+
export declare function validateConnectionEdit(draft: ConnectionDraft, originalId: string, existing?: readonly ConnectionSummary[]): Validation<Record<string, unknown>>;
|
|
254
|
+
/** One row as the component renders it. */
|
|
255
|
+
export interface RootRowView {
|
|
256
|
+
readonly key: string;
|
|
257
|
+
readonly location: RootLocation;
|
|
258
|
+
readonly title: string;
|
|
259
|
+
readonly subtitle: string;
|
|
260
|
+
readonly badgeKey: string;
|
|
261
|
+
readonly removable: boolean;
|
|
262
|
+
}
|
|
263
|
+
/** The panel's complete render input. */
|
|
264
|
+
export interface RootsView {
|
|
265
|
+
readonly rows: readonly RootRowView[];
|
|
266
|
+
readonly connections: readonly ConnectionSummary[];
|
|
267
|
+
/** Set when the host did not answer, so the panel can say why instead of looking empty. */
|
|
268
|
+
readonly notice?: MessageKey;
|
|
269
|
+
/** False while a request is in flight, so the form cannot double-submit. */
|
|
270
|
+
readonly canEdit: boolean;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Turn a snapshot (or the failure to get one) into render input.
|
|
274
|
+
*
|
|
275
|
+
* @param input - the last answer, or the last error, plus whether a request is running.
|
|
276
|
+
* @returns rows, connections, notice and the edit gate.
|
|
277
|
+
*/
|
|
278
|
+
export declare function buildRootsView(input: {
|
|
279
|
+
readonly snapshot?: RootsSnapshot;
|
|
280
|
+
readonly error?: string;
|
|
281
|
+
readonly pending?: boolean;
|
|
282
|
+
}): RootsView;
|
|
283
|
+
/** The calls the panel makes. */
|
|
284
|
+
export interface RootsApi {
|
|
285
|
+
load(signal?: AbortSignal): Promise<ApiResult<RootsSnapshot>>;
|
|
286
|
+
send(body: Record<string, unknown>): Promise<ApiResult<RootsSnapshot>>;
|
|
287
|
+
/**
|
|
288
|
+
* A POST whose answer is **not** a snapshot.
|
|
289
|
+
*
|
|
290
|
+
* The import actions answer a candidate list and an import report, neither of which is a roots snapshot. It
|
|
291
|
+
* is a separate method rather than a looser return type on {@link RootsApi.send} so that every existing call
|
|
292
|
+
* keeps the snapshot it already depends on: widening `send` would make `state` optional for all of them.
|
|
293
|
+
*/
|
|
294
|
+
sendRaw(body: Record<string, unknown>): Promise<ApiResult<Record<string, unknown>>>;
|
|
295
|
+
}
|
|
296
|
+
/** One importable `~/.ssh/config` alias, as the import dialog renders it. */
|
|
297
|
+
export interface SshConfigCandidate {
|
|
298
|
+
/** The literal alias; also the `ssh` target. Shown verbatim so the user recognises their own config. */
|
|
299
|
+
readonly alias: string;
|
|
300
|
+
/** Resolved host name. */
|
|
301
|
+
readonly host: string;
|
|
302
|
+
readonly user?: string;
|
|
303
|
+
readonly port?: number;
|
|
304
|
+
/** Whether the block named an identity file. The path itself never reaches the client. */
|
|
305
|
+
readonly hasIdentityFile: boolean;
|
|
306
|
+
readonly hasProxyJump: boolean;
|
|
307
|
+
/** The id the import would create. Editable in spirit: the user sees it before committing. */
|
|
308
|
+
readonly suggestedId: string;
|
|
309
|
+
}
|
|
310
|
+
/** One alias the host refused, with the machine-readable reason. */
|
|
311
|
+
export interface SshConfigSkipped {
|
|
312
|
+
readonly pattern: string;
|
|
313
|
+
readonly reason: string;
|
|
314
|
+
readonly line: number;
|
|
315
|
+
}
|
|
316
|
+
/** What `list-ssh-config` answers. */
|
|
317
|
+
export interface SshConfigListing {
|
|
318
|
+
readonly candidates: readonly SshConfigCandidate[];
|
|
319
|
+
readonly skipped: readonly SshConfigSkipped[];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Narrow the host's `list-ssh-config` answer.
|
|
323
|
+
*
|
|
324
|
+
* Defensive in the same way {@link parseRootsSnapshot} is: the client never trusts the wire's shape, and a row
|
|
325
|
+
* without a usable alias or id is **dropped** rather than rendered as a checkbox that would import nothing.
|
|
326
|
+
*
|
|
327
|
+
* @param value - the parsed JSON body.
|
|
328
|
+
* @returns the listing, with unusable rows removed.
|
|
329
|
+
*/
|
|
330
|
+
export declare function parseSshConfigListing(value: unknown): SshConfigListing;
|
|
331
|
+
/**
|
|
332
|
+
* The human-readable reason one alias was refused.
|
|
333
|
+
*
|
|
334
|
+
* The mapping lives here rather than in the view so the reasons are testable without rendering, and an unknown
|
|
335
|
+
* reason from a newer host still produces a sentence instead of an empty cell.
|
|
336
|
+
*
|
|
337
|
+
* @param reason - the machine-readable reason.
|
|
338
|
+
* @returns a message key.
|
|
339
|
+
*/
|
|
340
|
+
export declare function sshConfigSkipKey(reason: string): string;
|
|
341
|
+
/** A candidate's one-line summary: `user@host:port`, with the flags that matter for the import. */
|
|
342
|
+
export declare function sshConfigCandidateSummary(candidate: SshConfigCandidate): string;
|
|
343
|
+
/**
|
|
344
|
+
* The reason a failed response reports, when the host did not name one.
|
|
345
|
+
*
|
|
346
|
+
* The path is part of the text on purpose: `http-404` alone cannot distinguish "this route is not
|
|
347
|
+
* registered" (the host half never claimed it — that was a real defect) from "the request never reached the
|
|
348
|
+
* host" (the shell's proxy or the composition is at fault). Both are shown verbatim in the panel's notice.
|
|
349
|
+
*
|
|
350
|
+
* @param status - the HTTP status.
|
|
351
|
+
* @param path - the exact URL that was requested.
|
|
352
|
+
* @returns the machine-readable reason.
|
|
353
|
+
*/
|
|
354
|
+
export declare function transportFailureReason(status: number, path: string): string;
|
|
355
|
+
/**
|
|
356
|
+
* Bind the route to the panel's two operations.
|
|
357
|
+
*
|
|
358
|
+
* @param fetchImpl - `window.fetch` in the app, a stub in tests.
|
|
359
|
+
* @param path - override for tests; defaults to {@link ROOTS_API_PATH}.
|
|
360
|
+
* @returns the calls the component uses.
|
|
361
|
+
*/
|
|
362
|
+
export declare function createRootsApi(fetchImpl: FetchLike, path?: string): RootsApi;
|
|
363
|
+
/** The field names the panel must never send, asserted by tests rather than trusted to reviewers. */
|
|
364
|
+
export declare function isForbiddenField(name: string): boolean;
|