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,16 @@
|
|
|
1
|
+
// src/local-bridge-refs.ts
|
|
2
|
+
import "@deepseek-ai/cordis";
|
|
3
|
+
var LocalFileReferencesBridge = class {
|
|
4
|
+
static inject = ["fileReferences"];
|
|
5
|
+
constructor(ctx) {
|
|
6
|
+
const local = ctx.get("fileReferences");
|
|
7
|
+
if (local === void 0) {
|
|
8
|
+
ctx.logger?.warn?.("local-bridge-refs: \u9694\u79BB\u57DF\u91CC\u6CA1\u6709 fileReferences\uFF1B\u8DEF\u7531\u5668\u5C06\u6CA1\u6709\u672C\u5730\u540E\u7AEF");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
ctx.provide("localFileReferences", local);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
LocalFileReferencesBridge as default
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/local-bridge.ts
|
|
2
|
+
import "@deepseek-ai/cordis";
|
|
3
|
+
var LocalFsBridge = class {
|
|
4
|
+
static inject = ["fs"];
|
|
5
|
+
constructor(ctx) {
|
|
6
|
+
const local = ctx.get("fs");
|
|
7
|
+
if (local === void 0) {
|
|
8
|
+
ctx.logger?.warn?.("local-bridge: \u9694\u79BB\u57DF\u91CC\u6CA1\u6709 fs\uFF1B\u8DEF\u7531\u5668\u5C06\u6CA1\u6709\u672C\u5730\u540E\u7AEF");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
ctx.provide("localFs", local);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
LocalFsBridge as default
|
|
16
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env -S node
|
|
2
|
+
|
|
3
|
+
// tools/over-ssh-terminal.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// src/host/domain/errors.ts
|
|
12
|
+
var SshDevError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
details;
|
|
15
|
+
constructor(code, message, details = {}) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "SshDevError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.details = details;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/host/domain/roots/root.ts
|
|
24
|
+
function normalizeLocalPath(path, options = {}) {
|
|
25
|
+
const caseInsensitive = options.caseInsensitive ?? process.platform === "win32";
|
|
26
|
+
const unified = path.replace(/\\/g, "/");
|
|
27
|
+
const unc = unified.startsWith("//");
|
|
28
|
+
const posixAbsolute = unified.startsWith("/");
|
|
29
|
+
const driveMatch = /^([A-Za-z]):/.exec(unified);
|
|
30
|
+
const driveLetter = driveMatch?.[1];
|
|
31
|
+
const drive = driveLetter === void 0 ? "" : `${driveLetter.toUpperCase()}:`;
|
|
32
|
+
const rest = unc ? unified.slice(2) : drive === "" ? unified : unified.slice(2);
|
|
33
|
+
const segments = [];
|
|
34
|
+
for (const segment of rest.split("/")) {
|
|
35
|
+
if (segment === "" || segment === ".") continue;
|
|
36
|
+
if (segment === "..") {
|
|
37
|
+
segments.pop();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
segments.push(segment);
|
|
41
|
+
}
|
|
42
|
+
const body = segments.join("/");
|
|
43
|
+
let normalized;
|
|
44
|
+
if (unc) normalized = `//${body}`;
|
|
45
|
+
else if (drive !== "") normalized = body === "" ? `${drive}/` : `${drive}/${body}`;
|
|
46
|
+
else if (posixAbsolute) normalized = body === "" ? "/" : `/${body}`;
|
|
47
|
+
else normalized = body === "" ? "." : body;
|
|
48
|
+
return caseInsensitive ? normalized.toLowerCase() : normalized;
|
|
49
|
+
}
|
|
50
|
+
function localPathContains(root, candidate, options = {}) {
|
|
51
|
+
const normalizedRoot = normalizeLocalPath(root, options);
|
|
52
|
+
const normalizedCandidate = normalizeLocalPath(candidate, options);
|
|
53
|
+
if (normalizedRoot === "/" || /^[a-z]:\/$/.test(normalizedRoot)) {
|
|
54
|
+
return normalizedCandidate.startsWith(normalizedRoot);
|
|
55
|
+
}
|
|
56
|
+
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
57
|
+
}
|
|
58
|
+
function fnv1aHex(input) {
|
|
59
|
+
let hash = 2166136261;
|
|
60
|
+
for (let index = 0; index < input.length; index++) {
|
|
61
|
+
hash ^= input.charCodeAt(index);
|
|
62
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
63
|
+
}
|
|
64
|
+
return hash.toString(16).padStart(8, "0");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/host/domain/shell.ts
|
|
68
|
+
function quote(value) {
|
|
69
|
+
if (value.includes("\0")) {
|
|
70
|
+
throw new SshDevError("REMOTE_COMMAND_FAILED", "\u8DEF\u5F84\u6216\u53C2\u6570\u4E2D\u4E0D\u80FD\u5305\u542B NUL \u5B57\u7B26", {});
|
|
71
|
+
}
|
|
72
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/host/domain/session/terminal-target.ts
|
|
76
|
+
var MANIFEST_VERSION = 1;
|
|
77
|
+
var SESSION_STATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
78
|
+
function parseRootManifest(raw) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
86
|
+
const candidate = parsed;
|
|
87
|
+
if (candidate.version !== MANIFEST_VERSION) return void 0;
|
|
88
|
+
if (!Array.isArray(candidate.roots)) return void 0;
|
|
89
|
+
const roots = [];
|
|
90
|
+
for (const entry of candidate.roots) {
|
|
91
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
92
|
+
const root = entry;
|
|
93
|
+
if (typeof root.connectionId !== "string" || typeof root.remoteRoot !== "string") continue;
|
|
94
|
+
if (typeof root.anchorPath !== "string" || root.anchorPath === "") continue;
|
|
95
|
+
if (root.ssh === void 0 || typeof root.ssh.destination !== "string" || !Array.isArray(root.ssh.args)) continue;
|
|
96
|
+
roots.push({
|
|
97
|
+
connectionId: root.connectionId,
|
|
98
|
+
remoteRoot: root.remoteRoot,
|
|
99
|
+
anchorPath: root.anchorPath,
|
|
100
|
+
ssh: { destination: root.ssh.destination, args: [...root.ssh.args] }
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { version: MANIFEST_VERSION, roots };
|
|
104
|
+
}
|
|
105
|
+
function parseSessionState(raw) {
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(raw);
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
113
|
+
const state = parsed;
|
|
114
|
+
if (typeof state.sessionId !== "string" || state.sessionId === "") return void 0;
|
|
115
|
+
if (typeof state.connectionId !== "string" || typeof state.remoteRoot !== "string") return void 0;
|
|
116
|
+
if (typeof state.recordedAt !== "number" || !Number.isFinite(state.recordedAt)) return void 0;
|
|
117
|
+
if (state.ssh === void 0 || typeof state.ssh.destination !== "string" || !Array.isArray(state.ssh.args)) return void 0;
|
|
118
|
+
return {
|
|
119
|
+
sessionId: state.sessionId,
|
|
120
|
+
connectionId: state.connectionId,
|
|
121
|
+
remoteRoot: state.remoteRoot,
|
|
122
|
+
ssh: { destination: state.ssh.destination, args: [...state.ssh.args] },
|
|
123
|
+
recordedAt: state.recordedAt
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function remoteCommandFor(remoteRoot, remoteShell = "$SHELL") {
|
|
127
|
+
return `cd ${quote(remoteRoot)} && exec ${remoteShell} -l`;
|
|
128
|
+
}
|
|
129
|
+
function sshArgvFor(ssh, remoteCommand) {
|
|
130
|
+
return ["ssh", "-tt", ...ssh.args, ssh.destination, "--", remoteCommand];
|
|
131
|
+
}
|
|
132
|
+
function decideTerminal(options) {
|
|
133
|
+
const now = options.now ?? Date.now();
|
|
134
|
+
const local = { kind: "local", argv: [options.localShell], reason: "no-target" };
|
|
135
|
+
const state = options.sessionState;
|
|
136
|
+
if (state !== void 0) {
|
|
137
|
+
if (now - state.recordedAt > SESSION_STATE_MAX_AGE_MS || state.recordedAt > now + 6e4) {
|
|
138
|
+
const manifestRoot = matchRoot(options.manifest, options.cwd, now);
|
|
139
|
+
return manifestRoot === void 0 ? { ...local, reason: "state-unusable" } : remoteDecision(manifestRoot, "anchor", options.remoteShell);
|
|
140
|
+
}
|
|
141
|
+
const command = remoteCommandFor(state.remoteRoot, options.remoteShell);
|
|
142
|
+
return {
|
|
143
|
+
kind: "remote",
|
|
144
|
+
connectionId: state.connectionId,
|
|
145
|
+
remoteRoot: state.remoteRoot,
|
|
146
|
+
argv: sshArgvFor(state.ssh, command),
|
|
147
|
+
remoteCommand: command,
|
|
148
|
+
reason: "session"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const matched = matchRoot(options.manifest, options.cwd, now);
|
|
152
|
+
if (matched !== void 0) return remoteDecision(matched, "anchor", options.remoteShell);
|
|
153
|
+
return options.manifest === void 0 ? { ...local, reason: "no-target" } : local;
|
|
154
|
+
}
|
|
155
|
+
function matchRoot(manifest, cwd, _now) {
|
|
156
|
+
if (manifest === void 0) return void 0;
|
|
157
|
+
return manifest.roots.find((root) => localPathContains(root.anchorPath, cwd, { caseInsensitive: false }));
|
|
158
|
+
}
|
|
159
|
+
function remoteDecision(root, reason, remoteShell) {
|
|
160
|
+
const command = remoteCommandFor(root.remoteRoot, remoteShell ?? "$SHELL");
|
|
161
|
+
return {
|
|
162
|
+
kind: "remote",
|
|
163
|
+
connectionId: root.connectionId,
|
|
164
|
+
remoteRoot: root.remoteRoot,
|
|
165
|
+
argv: sshArgvFor(root.ssh, command),
|
|
166
|
+
remoteCommand: command,
|
|
167
|
+
reason
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/host/integration/tools/terminal-manifest.ts
|
|
172
|
+
function sessionFileName(sessionId) {
|
|
173
|
+
const sanitized = sessionId.replaceAll(/[^A-Za-z0-9._-]/g, "_").slice(0, 48);
|
|
174
|
+
return `${sanitized}-${fnv1aHex(sessionId)}.json`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// tools/over-ssh-terminal.ts
|
|
178
|
+
var RECONNECT_DELAYS_MS = [500, 1e3, 2e3, 4e3];
|
|
179
|
+
var SSH_CONNECTION_FAILED = 255;
|
|
180
|
+
function parseWrapperArgs(argv) {
|
|
181
|
+
const flags = /* @__PURE__ */ new Map();
|
|
182
|
+
for (let index = 0; index < argv.length; index++) {
|
|
183
|
+
const token = argv[index];
|
|
184
|
+
if (token === void 0 || !token.startsWith("--")) continue;
|
|
185
|
+
const next = argv[index + 1];
|
|
186
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
187
|
+
flags.set(token.slice(2), next);
|
|
188
|
+
index++;
|
|
189
|
+
} else {
|
|
190
|
+
flags.set(token.slice(2), "true");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return flags;
|
|
194
|
+
}
|
|
195
|
+
function defaultLocalShell(platform, env) {
|
|
196
|
+
if (platform === "win32") return env["COMSPEC"] ?? "cmd.exe";
|
|
197
|
+
return env["SHELL"] ?? "/bin/sh";
|
|
198
|
+
}
|
|
199
|
+
async function readIfPresent(path) {
|
|
200
|
+
try {
|
|
201
|
+
return await readFile(path, "utf8");
|
|
202
|
+
} catch {
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function loadInputs(options) {
|
|
207
|
+
const manifestRaw = await readIfPresent(join(options.stateDir, "roots.json"));
|
|
208
|
+
const manifest = manifestRaw === void 0 ? void 0 : parseRootManifest(manifestRaw);
|
|
209
|
+
if (options.sessionId === void 0) return { manifest, sessionState: void 0 };
|
|
210
|
+
const stateRaw = await readIfPresent(join(options.stateDir, "state", sessionFileName(options.sessionId)));
|
|
211
|
+
return { manifest, sessionState: stateRaw === void 0 ? void 0 : parseSessionState(stateRaw) };
|
|
212
|
+
}
|
|
213
|
+
var STATE_DIR_NAMES = ["over-ssh", "remote-workspace", "ssh-dev"];
|
|
214
|
+
function defaultStateDir() {
|
|
215
|
+
const base = join(homedir(), ".dsh");
|
|
216
|
+
for (const name of STATE_DIR_NAMES) {
|
|
217
|
+
const dir = join(base, name);
|
|
218
|
+
if (existsSync(join(dir, "roots.json"))) return dir;
|
|
219
|
+
}
|
|
220
|
+
return join(base, STATE_DIR_NAMES[0]);
|
|
221
|
+
}
|
|
222
|
+
async function decide(input) {
|
|
223
|
+
const flags = parseWrapperArgs(input.argv);
|
|
224
|
+
const stateDir = flags.get("state-dir") ?? input.env["DSH_OVER_SSH_STATE_DIR"] ?? input.env["DSH_SSH_DEV_STATE_DIR"] ?? defaultStateDir();
|
|
225
|
+
const sessionId = flags.get("session") ?? input.env["DSH_SESSION_ID"];
|
|
226
|
+
const localShell = flags.get("local-shell") ?? defaultLocalShell(input.platform ?? process.platform, input.env);
|
|
227
|
+
const inputs = await loadInputs({ stateDir, sessionId });
|
|
228
|
+
const decideOptions = {
|
|
229
|
+
cwd: input.cwd,
|
|
230
|
+
localShell,
|
|
231
|
+
...inputs.manifest === void 0 ? {} : { manifest: inputs.manifest },
|
|
232
|
+
...inputs.sessionState === void 0 ? {} : { sessionState: inputs.sessionState }
|
|
233
|
+
};
|
|
234
|
+
const base = decideTerminal(decideOptions);
|
|
235
|
+
const override = flags.get("remote-command");
|
|
236
|
+
const decision = override === void 0 || base.kind !== "remote" ? base : { ...base, argv: [...base.argv.slice(0, -1), override], remoteCommand: override };
|
|
237
|
+
return { decision, label: flags.get("name") ?? "SSH / Local", stateDir, sessionId, localShell };
|
|
238
|
+
}
|
|
239
|
+
async function main(input) {
|
|
240
|
+
const { decision } = await decide(input);
|
|
241
|
+
if (parseWrapperArgs(input.argv).has("dry-run")) {
|
|
242
|
+
process.stdout.write(`${JSON.stringify(decision, void 0, 2)}
|
|
243
|
+
`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
const [executable, ...args] = decision.argv;
|
|
247
|
+
if (executable === void 0) throw new Error("terminal wrapper: empty argv");
|
|
248
|
+
const spawnFn = input.spawnFn ?? spawn;
|
|
249
|
+
const sleepFn = input.sleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
250
|
+
const runOnce = async () => {
|
|
251
|
+
const child = spawnFn(executable, args, {
|
|
252
|
+
stdio: "inherit",
|
|
253
|
+
// A local terminal keeps the caller's cwd; a remote one is entered by the ssh command itself.
|
|
254
|
+
...decision.kind === "local" ? { cwd: input.cwd } : {}
|
|
255
|
+
});
|
|
256
|
+
return await new Promise((resolve) => {
|
|
257
|
+
child.on("exit", (code, signal) => {
|
|
258
|
+
if (typeof code === "number") resolve({ code, signalled: false });
|
|
259
|
+
else resolve({ code: signal === null || signal === void 0 ? 0 : 1, signalled: true });
|
|
260
|
+
});
|
|
261
|
+
child.on("error", () => resolve({ code: 127, signalled: false }));
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
const first = await runOnce();
|
|
265
|
+
if (decision.kind !== "remote" || parseWrapperArgs(input.argv).has("no-reconnect")) return first.code;
|
|
266
|
+
let outcome = first;
|
|
267
|
+
for (const delay of RECONNECT_DELAYS_MS) {
|
|
268
|
+
if (outcome.signalled || outcome.code !== SSH_CONNECTION_FAILED) return outcome.code;
|
|
269
|
+
process.stderr.write(
|
|
270
|
+
`\r
|
|
271
|
+
[dsh] \u4E0E\u8FDC\u7AEF\u7684\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C${delay} ms \u540E\u91CD\u8FDE\uFF08\u65B0\u7684\u767B\u5F55 shell\uFF1B\u539F\u5148\u8FD0\u884C\u7684\u8FDB\u7A0B\u5DF2\u4E22\u5931\uFF09\u2026\r
|
|
272
|
+
`
|
|
273
|
+
);
|
|
274
|
+
await sleepFn(delay);
|
|
275
|
+
outcome = await runOnce();
|
|
276
|
+
}
|
|
277
|
+
return outcome.code;
|
|
278
|
+
}
|
|
279
|
+
var selfPath = fileURLToPath(import.meta.url);
|
|
280
|
+
if (process.argv[1] !== void 0 && process.argv[1] === selfPath) {
|
|
281
|
+
main({ argv: process.argv.slice(2), env: process.env, cwd: process.cwd(), platform: process.platform }).then((code) => process.exit(code)).catch((error) => {
|
|
282
|
+
process.stderr.write(`over-ssh terminal wrapper: ${error instanceof Error ? error.message : String(error)}
|
|
283
|
+
`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
RECONNECT_DELAYS_MS,
|
|
289
|
+
decide,
|
|
290
|
+
defaultLocalShell,
|
|
291
|
+
loadInputs,
|
|
292
|
+
main,
|
|
293
|
+
parseWrapperArgs
|
|
294
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controls the three surfaces share, as plain `createElement` templates.
|
|
3
|
+
*
|
|
4
|
+
* **No hooks, on purpose.** `:hover`, `:active`, `:focus-visible` and `:disabled` come from the stylesheet in
|
|
5
|
+
* [`styles.ts`](./styles.ts) via class names, because a component holding `useState` cannot be rendered by
|
|
6
|
+
* this repository's tests: they call the section functions **directly**, outside any React render, where a
|
|
7
|
+
* hook throws `Invalid hook call`. Classes keep the templates pure element trees — a test walks the returned
|
|
8
|
+
* tree and reads the same `type` / `name` / `disabled` / `role` it always did — while the browser still gets
|
|
9
|
+
* real interaction states.
|
|
10
|
+
*
|
|
11
|
+
* These are deliberately **not** the platform's `dsh-client-ui-primitives` atoms: those factories import
|
|
12
|
+
* per-component CSS modules, which a third-party bundle cannot supply (the loader materialises only
|
|
13
|
+
* registered factories), and the package is not a dependency of this repository. `styles.ts` carries the
|
|
14
|
+
* tokens those atoms are built from.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-remote-workspace/client/controls
|
|
17
|
+
*/
|
|
18
|
+
import { type ChangeEvent, type ReactNode } from 'react';
|
|
19
|
+
/** The sizes a control may have. */
|
|
20
|
+
export type ControlSize = 'normal' | 'compact';
|
|
21
|
+
/** The visual weight of a button. */
|
|
22
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'row' | 'chip';
|
|
23
|
+
/** Props every control shares: extra classes from the caller. */
|
|
24
|
+
export interface ControlClassProps {
|
|
25
|
+
/** Extra classes from the caller; appended after the vocabulary's own. */
|
|
26
|
+
readonly className?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The part of a keyboard event a card's Esc handler reads.
|
|
30
|
+
*
|
|
31
|
+
* Structural, so the pure templates need no DOM types and a test drives the handler with a plain object. It is
|
|
32
|
+
* assignable from React's `KeyboardEvent` (which carries both members), and it is the whole reason a dialog
|
|
33
|
+
* closes on Esc: the shell focuses into the card when it opens, so the event reaches the card's own handler.
|
|
34
|
+
*/
|
|
35
|
+
export interface KeyEventLike {
|
|
36
|
+
/** The key name, e.g. `'Escape'`. */
|
|
37
|
+
readonly key?: string;
|
|
38
|
+
/** Stop the platform's own Esc handling from also firing while the dialog is up. */
|
|
39
|
+
stopPropagation?(): void;
|
|
40
|
+
}
|
|
41
|
+
/** True when the event is the Esc key. */
|
|
42
|
+
export declare function isEscape(event: KeyEventLike): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* One button.
|
|
45
|
+
*
|
|
46
|
+
* @param props - the button's own props plus the variants this plugin uses.
|
|
47
|
+
* @returns the button element.
|
|
48
|
+
*/
|
|
49
|
+
export declare function Button(props: ControlClassProps & {
|
|
50
|
+
readonly variant: ButtonVariant;
|
|
51
|
+
readonly size?: ControlSize;
|
|
52
|
+
readonly type?: 'button' | 'submit';
|
|
53
|
+
readonly disabled?: boolean;
|
|
54
|
+
readonly name?: string;
|
|
55
|
+
readonly title?: string;
|
|
56
|
+
readonly onClick?: () => void;
|
|
57
|
+
readonly children?: ReactNode;
|
|
58
|
+
}): ReactNode;
|
|
59
|
+
/** Props of a text input, narrowed to what the templates pass. */
|
|
60
|
+
export interface TextInputProps extends ControlClassProps {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly value: string;
|
|
64
|
+
readonly disabled?: boolean;
|
|
65
|
+
readonly placeholder?: string;
|
|
66
|
+
readonly inputMode?: 'numeric';
|
|
67
|
+
/** `password` for a secret field: the browser masks it and no autofill offers to save it. */
|
|
68
|
+
readonly type?: 'text' | 'password';
|
|
69
|
+
readonly autoComplete?: 'off' | 'new-password';
|
|
70
|
+
readonly size?: ControlSize;
|
|
71
|
+
readonly onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One text input, with the platform's border, inset surface and focus ring.
|
|
75
|
+
*
|
|
76
|
+
* @param props - the input's own props.
|
|
77
|
+
* @returns the input element.
|
|
78
|
+
*/
|
|
79
|
+
export declare function TextInput(props: TextInputProps): ReactNode;
|
|
80
|
+
/** One option of a {@link Select}. */
|
|
81
|
+
export interface SelectOption {
|
|
82
|
+
readonly value: string;
|
|
83
|
+
readonly label: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A `<select>` over a fixed list of options.
|
|
87
|
+
*
|
|
88
|
+
* @param props - id, name, the current value, the options and the change handler.
|
|
89
|
+
* @returns the select element.
|
|
90
|
+
*/
|
|
91
|
+
export declare function Select(props: ControlClassProps & {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly name: string;
|
|
94
|
+
readonly value: string;
|
|
95
|
+
readonly options: readonly SelectOption[];
|
|
96
|
+
readonly disabled?: boolean;
|
|
97
|
+
readonly size?: ControlSize;
|
|
98
|
+
readonly onChange: (value: string) => void;
|
|
99
|
+
}): ReactNode;
|
|
100
|
+
/**
|
|
101
|
+
* A labelled field: the caption above its control, so the column and its label move together.
|
|
102
|
+
*
|
|
103
|
+
* @param props - the label text, the control's id and the control itself.
|
|
104
|
+
* @returns the labelled column.
|
|
105
|
+
*/
|
|
106
|
+
export declare function Field(props: {
|
|
107
|
+
readonly label: string;
|
|
108
|
+
readonly htmlFor: string;
|
|
109
|
+
readonly children: ReactNode;
|
|
110
|
+
}): ReactNode;
|
|
111
|
+
/**
|
|
112
|
+
* An inline notice block: the surface for a refusal, a host warning or "no host data".
|
|
113
|
+
*
|
|
114
|
+
* @param props - `warn` for something the operator may need to act on, `info` for a neutral status.
|
|
115
|
+
* @returns the notice element carrying the role its caller needs (`alert` / `note` / `status`).
|
|
116
|
+
*/
|
|
117
|
+
export declare function Notice(props: {
|
|
118
|
+
readonly role: 'alert' | 'note' | 'status';
|
|
119
|
+
readonly tone?: 'warn' | 'info';
|
|
120
|
+
readonly children: ReactNode;
|
|
121
|
+
}): ReactNode;
|
|
122
|
+
/**
|
|
123
|
+
* The status dot of a connection row, so the row's state is not carried by colour alone.
|
|
124
|
+
*
|
|
125
|
+
* @param props - the dot's accessible description.
|
|
126
|
+
* @returns the dot element.
|
|
127
|
+
*/
|
|
128
|
+
export declare function StatusDot(props: {
|
|
129
|
+
readonly label: string;
|
|
130
|
+
}): ReactNode;
|
|
131
|
+
//# sourceMappingURL=controls.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **one** Tab trap every card in this plugin installs.
|
|
3
|
+
*
|
|
4
|
+
* Five surfaces are dialogs and every one of them has to keep Tab inside itself, for two different reasons:
|
|
5
|
+
*
|
|
6
|
+
* - the settings-hosted cards sit **outside** the platform settings modal, whose own focus trap listens on
|
|
7
|
+
* `document` in the bubble phase and pulls focus back into its panel — a bubble-phase listener of ours would
|
|
8
|
+
* run after it (registration order) and could no longer tell which field Tab came from, so the trap has to
|
|
9
|
+
* be in the **capture** phase (audit 0090);
|
|
10
|
+
* - the overlay and composer cards are simply modals: Tab walking into the page behind them is a dialog the
|
|
11
|
+
* keyboard user leaves without dismissing.
|
|
12
|
+
*
|
|
13
|
+
* Both cases want the same mechanics, so they live here instead of being spelled out per surface (the two that
|
|
14
|
+
* were first — the connection editor and the session-panel confirmation — each carried their own copy of the
|
|
15
|
+
* same eight lines, and the two newer ones had none at all; audit 2026-10-31 `client.md` §2.3).
|
|
16
|
+
*
|
|
17
|
+
* The split is the same one the rest of `core/` uses: {@link installCardTabTrap} is pure and takes the document
|
|
18
|
+
* as an argument (so an offline test drives it with a fake), and {@link useCardTabTrap} is the two-line React
|
|
19
|
+
* binding.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-over-ssh/client/card-focus-trap
|
|
22
|
+
*/
|
|
23
|
+
import { type PortalDocument } from './portal.ts';
|
|
24
|
+
/**
|
|
25
|
+
* Install the card's Tab trap on one document.
|
|
26
|
+
*
|
|
27
|
+
* Capture phase on purpose: see the module note. The listener is removed with the same `options` value, so a
|
|
28
|
+
* runtime that keys listeners by phase cannot leak one.
|
|
29
|
+
*
|
|
30
|
+
* @param doc - the page's document, or `undefined` off-browser (where this is a no-op).
|
|
31
|
+
* @param container - the card's body-level container; `undefined` while it is not attached.
|
|
32
|
+
* @returns the remover; safe to call more than once.
|
|
33
|
+
*/
|
|
34
|
+
export declare function installCardTabTrap(doc: PortalDocument | undefined, container: unknown): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Trap Tab while a card is open.
|
|
37
|
+
*
|
|
38
|
+
* `open` is the caller's own condition (`editingId !== undefined`, `pending !== undefined`, `request !==
|
|
39
|
+
* undefined`); the container comes from that surface's portal. Nothing is installed while the card is closed, so
|
|
40
|
+
* the settings panel keeps its own Tab behaviour the rest of the time.
|
|
41
|
+
*
|
|
42
|
+
* @param open - whether the card is up.
|
|
43
|
+
* @param container - the card's container from its portal.
|
|
44
|
+
*/
|
|
45
|
+
export declare function useCardTabTrap(open: boolean, container: unknown): void;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controls the three surfaces share, as plain `createElement` templates.
|
|
3
|
+
*
|
|
4
|
+
* **No hooks, on purpose.** `:hover`, `:active`, `:focus-visible` and `:disabled` come from the stylesheet in
|
|
5
|
+
* [`styles.ts`](./styles.ts) via class names, because a component holding `useState` cannot be rendered by
|
|
6
|
+
* this repository's tests: they call the section functions **directly**, outside any React render, where a
|
|
7
|
+
* hook throws `Invalid hook call`. Classes keep the templates pure element trees — a test walks the returned
|
|
8
|
+
* tree and reads the same `type` / `name` / `disabled` / `role` it always did — while the browser still gets
|
|
9
|
+
* real interaction states.
|
|
10
|
+
*
|
|
11
|
+
* These are deliberately **not** the platform's `dsh-client-ui-primitives` atoms: those factories import
|
|
12
|
+
* per-component CSS modules, which a third-party bundle cannot supply (the loader materialises only
|
|
13
|
+
* registered factories), and the package is not a dependency of this repository. `styles.ts` carries the
|
|
14
|
+
* tokens those atoms are built from.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-over-ssh/client/controls
|
|
17
|
+
*/
|
|
18
|
+
import { type ChangeEvent, type ReactNode } from 'react';
|
|
19
|
+
/** The sizes a control may have. */
|
|
20
|
+
export type ControlSize = 'normal' | 'compact';
|
|
21
|
+
/** The visual weight of a button. */
|
|
22
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'row' | 'chip';
|
|
23
|
+
/** Props every control shares: extra classes from the caller. */
|
|
24
|
+
export interface ControlClassProps {
|
|
25
|
+
/** Extra classes from the caller; appended after the vocabulary's own. */
|
|
26
|
+
readonly className?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The part of a keyboard event a card's Esc handler reads.
|
|
30
|
+
*
|
|
31
|
+
* Structural, so the pure templates need no DOM types and a test drives the handler with a plain object. It is
|
|
32
|
+
* assignable from React's `KeyboardEvent` (which carries both members), and it is the whole reason a dialog
|
|
33
|
+
* closes on Esc: the shell focuses into the card when it opens, so the event reaches the card's own handler.
|
|
34
|
+
*/
|
|
35
|
+
export interface KeyEventLike {
|
|
36
|
+
/** The key name, e.g. `'Escape'`. */
|
|
37
|
+
readonly key?: string;
|
|
38
|
+
/** Stop the platform's own Esc handling from also firing while the dialog is up. */
|
|
39
|
+
stopPropagation?(): void;
|
|
40
|
+
}
|
|
41
|
+
/** True when the event is the Esc key. */
|
|
42
|
+
export declare function isEscape(event: KeyEventLike): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* One button.
|
|
45
|
+
*
|
|
46
|
+
* @param props - the button's own props plus the variants this plugin uses.
|
|
47
|
+
* @returns the button element.
|
|
48
|
+
*/
|
|
49
|
+
export declare function Button(props: ControlClassProps & {
|
|
50
|
+
readonly variant: ButtonVariant;
|
|
51
|
+
readonly size?: ControlSize;
|
|
52
|
+
readonly type?: 'button' | 'submit';
|
|
53
|
+
readonly disabled?: boolean;
|
|
54
|
+
readonly name?: string;
|
|
55
|
+
readonly title?: string;
|
|
56
|
+
/** The name a screen reader reads instead of the visible text, when the text alone is not a name. */
|
|
57
|
+
readonly ariaLabel?: string;
|
|
58
|
+
/** For a control that opens something: whether it is open right now (`aria-expanded`). */
|
|
59
|
+
readonly ariaExpanded?: boolean;
|
|
60
|
+
/** What the control opens (`aria-haspopup`). */
|
|
61
|
+
readonly ariaHaspopup?: 'dialog' | 'menu';
|
|
62
|
+
readonly onClick?: () => void;
|
|
63
|
+
readonly children?: ReactNode;
|
|
64
|
+
}): ReactNode;
|
|
65
|
+
/** Props of a text input, narrowed to what the templates pass. */
|
|
66
|
+
export interface TextInputProps extends ControlClassProps {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly value: string;
|
|
70
|
+
readonly disabled?: boolean;
|
|
71
|
+
readonly placeholder?: string;
|
|
72
|
+
readonly inputMode?: 'numeric';
|
|
73
|
+
/** `password` for a secret field: the browser masks it and no autofill offers to save it. */
|
|
74
|
+
readonly type?: 'text' | 'password';
|
|
75
|
+
readonly autoComplete?: 'off' | 'new-password';
|
|
76
|
+
readonly size?: ControlSize;
|
|
77
|
+
readonly onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One text input, with the platform's border, inset surface and focus ring.
|
|
81
|
+
*
|
|
82
|
+
* @param props - the input's own props.
|
|
83
|
+
* @returns the input element.
|
|
84
|
+
*/
|
|
85
|
+
export declare function TextInput(props: TextInputProps): ReactNode;
|
|
86
|
+
/** One option of a {@link Select}. */
|
|
87
|
+
export interface SelectOption {
|
|
88
|
+
readonly value: string;
|
|
89
|
+
readonly label: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A `<select>` over a fixed list of options.
|
|
93
|
+
*
|
|
94
|
+
* @param props - id, name, the current value, the options and the change handler.
|
|
95
|
+
* @returns the select element.
|
|
96
|
+
*/
|
|
97
|
+
export declare function Select(props: ControlClassProps & {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly value: string;
|
|
101
|
+
readonly options: readonly SelectOption[];
|
|
102
|
+
readonly disabled?: boolean;
|
|
103
|
+
readonly size?: ControlSize;
|
|
104
|
+
readonly onChange: (value: string) => void;
|
|
105
|
+
}): ReactNode;
|
|
106
|
+
/**
|
|
107
|
+
* A labelled field: the caption above its control, so the column and its label move together.
|
|
108
|
+
*
|
|
109
|
+
* @param props - the label text, the control's id and the control itself.
|
|
110
|
+
* @returns the labelled column.
|
|
111
|
+
*/
|
|
112
|
+
export declare function Field(props: {
|
|
113
|
+
readonly label: string;
|
|
114
|
+
readonly htmlFor: string;
|
|
115
|
+
readonly children: ReactNode;
|
|
116
|
+
}): ReactNode;
|
|
117
|
+
/**
|
|
118
|
+
* An inline notice block: the surface for a refusal, a host warning or "no host data".
|
|
119
|
+
*
|
|
120
|
+
* Three tones, and the split is deliberate: **`warn` is the default** (something the operator may need to act
|
|
121
|
+
* on), `info` is a neutral status, and `error` is a hard failure. Before this, a failed read and a soft
|
|
122
|
+
* warning rendered identically — both `role: 'alert'` on the amber surface — so "the host refused" and
|
|
123
|
+
* "the host is telling you something" looked the same. A failure is not a warning.
|
|
124
|
+
*
|
|
125
|
+
* @param props - the tone, the role, and the text.
|
|
126
|
+
* @returns the notice element carrying the role its caller needs (`alert` / `note` / `status`).
|
|
127
|
+
*/
|
|
128
|
+
export declare function Notice(props: {
|
|
129
|
+
readonly role: 'alert' | 'note' | 'status';
|
|
130
|
+
readonly tone?: 'warn' | 'info' | 'error';
|
|
131
|
+
readonly children: ReactNode;
|
|
132
|
+
}): ReactNode;
|
|
133
|
+
/**
|
|
134
|
+
* A checkbox whose whole row is the hit target.
|
|
135
|
+
*
|
|
136
|
+
* The templates used to render a bare `<input type="checkbox">`: the stylesheet had **no rule for it at all**,
|
|
137
|
+
* so it kept the browser's default size and accent colour, and — in the session inventory — it sat next to a
|
|
138
|
+
* body that was not a `<label>`, so clicking the row's own text did nothing and the target was a 16px square.
|
|
139
|
+
*
|
|
140
|
+
* `id`/`name` are optional because the two call sites differ: the ssh-config list already pairs the box with a
|
|
141
|
+
* separate `<label htmlFor>` naming the alias, while the session row wants the box and its body wrapped
|
|
142
|
+
* together. Passing neither is allowed; passing `id` makes the input addressable by a sibling label.
|
|
143
|
+
*
|
|
144
|
+
* @param props - the box's identity, state and handler.
|
|
145
|
+
* @returns the input element.
|
|
146
|
+
*/
|
|
147
|
+
export declare function Checkbox(props: {
|
|
148
|
+
readonly checked: boolean;
|
|
149
|
+
readonly disabled?: boolean;
|
|
150
|
+
readonly id?: string;
|
|
151
|
+
readonly name?: string;
|
|
152
|
+
readonly onChange: () => void;
|
|
153
|
+
/** The accessible name, when no `<label>` wraps or names this box. */
|
|
154
|
+
readonly ariaLabel?: string;
|
|
155
|
+
}): ReactNode;
|
|
156
|
+
/**
|
|
157
|
+
* The status dot of a connection row, so the row's state is not carried by colour alone.
|
|
158
|
+
*
|
|
159
|
+
* @param props - the dot's accessible description.
|
|
160
|
+
* @returns the dot element.
|
|
161
|
+
*/
|
|
162
|
+
export declare function StatusDot(props: {
|
|
163
|
+
readonly label: string;
|
|
164
|
+
}): ReactNode;
|