@hyzyn/dsh-tty 0.18.3 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +61 -30
- package/README.md +49 -27
- package/client.js +39 -32
- package/lib/index.d.ts +367 -1
- package/lib/index.js +417 -135
- package/lib/index.js.map +1 -1
- package/lib/known-hosts.d.ts +7 -1
- package/lib/known-hosts.js +26 -12
- package/lib/known-hosts.js.map +1 -1
- package/lib/probe.d.ts +2 -2
- package/lib/probe.js +7 -35
- package/lib/probe.js.map +1 -1
- package/lib/sftp.d.ts +63 -10
- package/lib/sftp.js +244 -44
- package/lib/sftp.js.map +1 -1
- package/lib/shell-integration.js +35 -15
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh.d.ts +21 -7
- package/lib/ssh.js +113 -18
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.js +67 -13
- package/lib/tunnels.js.map +1 -1
- package/package.json +8 -3
- package/scripts/build-client.mjs +30 -0
- package/scripts/integration.mjs +1691 -0
- package/scripts/lib/test-sshd.mjs +314 -0
- package/scripts/live.mjs +68 -0
- package/scripts/preview/harness.html +75 -0
- package/scripts/preview/harness.js +875 -0
- package/scripts/preview/mock-host.js +365 -0
- package/scripts/preview.mjs +502 -0
- package/scripts/probe-route-smoke.mjs +111 -0
- package/scripts/probe-smoke.mjs +143 -0
- package/scripts/probe.mjs +240 -0
- package/scripts/sftplimits-smoke.mjs +92 -0
- package/scripts/ssh-smoke.mjs +593 -0
- package/scripts/tui-smoke.mjs +112 -0
- package/scripts/windows-smoke.mjs +179 -0
package/README.en.md
CHANGED
|
@@ -87,9 +87,15 @@ Both were reproduced on **Windows 11 ARM (24H2) + Node 22 ARM64**. The fix:
|
|
|
87
87
|
throughput / temperature show “无” (remote **Windows** hosts go through the PowerShell hop — see the
|
|
88
88
|
status-bar section);
|
|
89
89
|
- **How far this was verified**: on Windows 11 ARM a full install (`dsh plugin add @hyzyn/dsh-all`), all nine
|
|
90
|
-
plugins mounting, `dsh web` serving, and the browser half being delivered (the same artifact macOS serves
|
|
91
|
-
|
|
90
|
+
plugins mounting, `dsh web` serving, and the browser half being delivered (the same artifact macOS serves;
|
|
91
|
+
size and new-code markers compared after a build rather than pinning a byte count, which changes on every
|
|
92
|
+
rebuild), plus `[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`.
|
|
92
93
|
x64 Windows is covered by the CI matrix (build / typecheck / test, see Development).
|
|
94
|
+
- **End-to-end coverage (0.19.0)**: `scripts/windows-smoke.mjs` drives a real `cmd.exe` through
|
|
95
|
+
spawn → input echo → kill → respawn on the CI windows-latest runner. It caught and now pins a
|
|
96
|
+
Windows-only failure class: force-killing a local PTY crashed the host — node-pty rejects signals
|
|
97
|
+
on Windows, and its `_deferNoArgs` re-throws that error from a socket callback, where the caller’s
|
|
98
|
+
try/catch cannot see it.
|
|
93
99
|
|
|
94
100
|
## Agent tools (P1)
|
|
95
101
|
|
|
@@ -98,16 +104,16 @@ The plugin injects thirteen tools into the agent (with the same power as the bas
|
|
|
98
104
|
| Tool | Purpose |
|
|
99
105
|
| --- | --- |
|
|
100
106
|
| `tty_list` | List active terminal sessions (sid / kind (`local\|ssh`) / target / pid / **cwd tracked live as you `cd`** / activity time; tmux persistent sessions carry a `persist` marker) |
|
|
101
|
-
| `tty_capture` | Read recent output (last N lines, ANSI stripped by default, `raw:true` for the raw stream); **`last:true` returns only the output + exit code of the previous completed command** (shell integration markers, see the next section) |
|
|
107
|
+
| `tty_capture` | Read recent output (last N lines, ANSI stripped by default, `raw:true` for the raw stream); **`last:true` returns only the output + exit code of the previous completed command** (shell integration markers, see the next section); when a command is **in flight** (just sent, completion marker not in yet) it returns `inProgress:true` without the stale result, so the previous command is never mistaken for this one (0.19.0) |
|
|
102
108
|
| `tty_screen` | Read the **currently visible screen** as rendered (xterm-headless virtual screen, plain text) — it can genuinely read TUI interfaces such as vim / htop / menus |
|
|
103
|
-
| `tty_expect` | Wait with a regex for a readiness signal in **subsequent output** (dev server URL, build finished, …); a timeout does not throw (`matched:false` + tail output), and a command that ends early also returns early with its exit code |
|
|
109
|
+
| `tty_expect` | Wait with a regex for a readiness signal in **subsequent output** (dev server URL, build finished, …); a timeout does not throw (`matched:false` + tail output), and a command that ends early also returns early with its exit code; at most 5 in-flight calls per session, and the accumulated window keeps only the last 64KB (0.19.0) |
|
|
104
110
|
| `tty_send` | Send keys/text to a given session (such as `q` to a dev server, or a menu selection) |
|
|
105
|
-
| `sftp_list` | List a remote SSH directory (name/type/size/mtime, directories first); `book` is the connection-book entry name and `path` defaults to the login home |
|
|
106
|
-
| `sftp_read` | Read a remote **text** file (≤256KB by default, adjustable to 1MB, truncated beyond that;
|
|
111
|
+
| `sftp_list` | List a remote SSH directory (name/type/size/mtime, directories first); `book` is the connection-book entry name and `path` defaults to the login home; at most 500 entries by default (`truncated:true` beyond that), and `isSymlink` distinguishes a symlink from a real directory (0.19.0) |
|
|
112
|
+
| `sftp_read` | Read a remote **text** file (≤256KB by default, adjustable to 1MB, truncated beyond that); `offset` pages from a given byte (handy for log tails), an invalid `maxBytes` errors out instead of silently falling back, and binary detection is a double test (NUL + illegal-UTF-8 ratio) (0.19.0) |
|
|
107
113
|
| `sftp_write` | Write a remote text file (overwrite by default, `append:true` appends; ≤1MB per call) |
|
|
108
114
|
| `sftp_mkdir` | Create a remote directory; `parents:true` fills in missing parents level by level (equivalent to `mkdir -p`, created bottom-up, existing directories skipped idempotently) |
|
|
109
115
|
| `sftp_rename` | Rename/move a remote file or directory (a `to` in a different directory means a move; never overwrites an existing target) |
|
|
110
|
-
| `sftp_remove` | Delete a remote file/directory; a directory uses rmdir by default (a non-empty one errors explicitly), and `recursive:true` deletes the whole tree (irrecoverable) |
|
|
116
|
+
| `sftp_remove` | Delete a remote file/directory; a directory uses rmdir by default (a non-empty one errors explicitly), and `recursive:true` deletes the whole tree (irrecoverable); paths pointing at `/`, `~`, or containing `.`/`..` segments are refused (guard for an irrecoverable operation, 0.19.0) |
|
|
111
117
|
| `sftp_tree` | Recursively list a remote directory structure (depth-first, directories first; `maxDepth` 1~8 / `maxEntries` 1~2000 cap it, `truncated:true` when exceeded; symlinks are not followed, to avoid cycles) |
|
|
112
118
|
| `tunnel_list` | List port-forwarding tunnels and their live state (active/connecting/error/stopped, rules, connection counts) |
|
|
113
119
|
|
|
@@ -252,7 +258,7 @@ slot (`ssh2`’s sftp subsystem, host half in `src/sftp.ts`):
|
|
|
252
258
|
panel nor interrupts browsing); when another panel (such as the containers panel) already holds the slot on
|
|
253
259
|
the same tab, or the panel is not open, it falls back to the original centered dialog, **without pushing
|
|
254
260
|
anyone else’s panel out**. The title / collapse / ✕ come from tty’s mount slot;
|
|
255
|
-
- **Follows the tab (0.
|
|
261
|
+
- **Follows the tab (0.19.0)**: the File Browser talks to the host of the tab that opened it, so it belongs to
|
|
256
262
|
that tab: switching away hides it (in-flight transfers keep running and the scene is restored when you come
|
|
257
263
|
back), and closing the tab tears it down. **Every entry follows the same rule** — the connection bar’s
|
|
258
264
|
“SFTP”, the 📂 on a connection-book entry and “File Browser” in the SSH dialog / settings card all become
|
|
@@ -395,7 +401,13 @@ and the agent tools all reuse the same scheduling.
|
|
|
395
401
|
“Terminal N”); while connecting it first echoes a grey `Connecting user@host …`, and once ready the status
|
|
396
402
|
bar shows `SSH user@host connected`; a failed connection (connection timeout / authentication rejected /
|
|
397
403
|
host unreachable) comes back in an `error` frame with the reason, and since the tab spec was saved with the
|
|
398
|
-
tab, clicking the terminal area reopens it from the original spec;
|
|
404
|
+
tab, clicking the terminal area reopens it from the original spec; the status bar describes the **currently
|
|
405
|
+
active tab** — switching or closing a tab immediately swaps in that tab’s own state (the failure reason and
|
|
406
|
+
the exit code are kept on the tab itself), so the red text no longer stays on screen after you close the tab
|
|
407
|
+
that could not connect (host-level messages such as “connection lost — reconnecting” belong to no tab and
|
|
408
|
+
are not wiped by a tab switch); conversely, **a background tab’s own failure is only recorded on that tab**
|
|
409
|
+
(its tab-bar status dot turns red and the terminal overlay carries the full text) and never takes over the
|
|
410
|
+
active tab’s status;
|
|
399
411
|
- **Agent forwarding (0.4.0)**: with “agent forwarding” ticked in the SSH dialog, the remote side can use the
|
|
400
412
|
local ssh-agent’s keys (such as `git clone` of a private repository remotely). It can be enabled with any
|
|
401
413
|
authentication method (credentials still never touch disk); if no ssh-agent is running locally the
|
|
@@ -430,14 +442,17 @@ and the agent tools all reuse the same scheduling.
|
|
|
430
442
|
box takes focus); the only difference is presentation — there it is an **inline** list rather than an overlay,
|
|
431
443
|
because that card is a long scrollable form where an overlay would be clipped;
|
|
432
444
|
- **Host-key TOFU pinning (0.3.0)**: after the first successful connection the host’s (host:port) sha256
|
|
433
|
-
fingerprint is recorded in `hostKeys` (persisted with settings); every later connection is verified,
|
|
434
|
-
|
|
435
|
-
against impersonation), with a reset pointer in the error message.
|
|
445
|
+
fingerprint is recorded in `hostKeys` (persisted with settings); every later connection is verified, any
|
|
446
|
+
fingerprint in the set matches, and **a changed fingerprint rejects the connection outright** (defense
|
|
447
|
+
against impersonation), with a reset pointer in the error message. A host’s multiple keys (0.19.0, e.g.
|
|
448
|
+
rsa + ed25519) are each recorded and merged into one record — algorithm negotiation changes no longer
|
|
449
|
+
cause false alarms. After a host reinstall or key change,
|
|
436
450
|
delete the record under Settings → Plugins → Terminal Panel → “SSH host key
|
|
437
451
|
records” and reconnect (the record list supports deletion). **“Import from known_hosts”
|
|
438
|
-
(0.4.1)**: parses `~/.ssh/known_hosts` in one click to pre-fill existing host fingerprints in bulk
|
|
452
|
+
(0.4.1)**: parses `~/.ssh/known_hosts` in one click to pre-fill existing host fingerprints in bulk, keeping
|
|
453
|
+
all of a host’s rsa/ed25519 entries (no longer first-entry-only); host
|
|
439
454
|
names from the connection book are also used to restore `|1|` hashed entries, and non-default ports are
|
|
440
|
-
parsed as `[host]:port
|
|
455
|
+
parsed as `[host]:port`;
|
|
441
456
|
- **Connection test (0.11.0)**: a “Test” button on each connection-book row of the settings card, plus a
|
|
442
457
|
“Test connection” button in the SSH connection dialog — both perform **link diagnostics only** (no session,
|
|
443
458
|
no `maxSessions` slot, no shell): first a TCP pre-check (DNS + connect, failures classified as
|
|
@@ -496,7 +511,7 @@ session belongs to (visually aligned with FinalShell’s session monitor bar):
|
|
|
496
511
|
| `cwd` | host startup directory | Fallback working directory (the client’s current session cwd wins) |
|
|
497
512
|
| `reconnectGraceSec` | 120 | Seconds a session is kept alive after an abnormal disconnect (0~3600): the session survives a page refresh/network blip waiting for a reconnect, and the reaper ends it on timeout; `0` = the old behavior, end immediately on disconnect |
|
|
498
513
|
| `sshHosts` | `[]` | SSH connection book (selectable in the panel “+” menu): entries `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, persist=false}`; saved as a whole-set replacement, the same name overwrites; `password` / `passphrase` support `env:VAR` references so no plaintext is stored; with persistence on, clicking an entry opens a tmux persistent session by default, and `persist=false` is an **opt-out** |
|
|
499
|
-
| `hostKeys` | `[]` | SSH host key records (TOFU, maintained automatically): entries `{host, port,
|
|
514
|
+
| `hostKeys` | `[]` | SSH host key records (TOFU, maintained automatically): entries `{host, port, fingerprints[]}` (the legacy single `fingerprint` field is migrated and merged on read); unique by host:port, one record holds all of a host’s keys, appended automatically on the first connection, any matching fingerprint is allowed, and a full mismatch rejects the connection; the settings card can delete them to reset |
|
|
500
515
|
| `shellIntegration` | true | Injects the OSC 133/7 shell integration (command boundary markers + cwd reporting; `tty_capture{last}` depends on it); zsh/bash supported, other shells skipped automatically; can be turned off when compatibility problems appear |
|
|
501
516
|
| `tunnels` | `[]` | Port-forwarding tunnels: entries `{name, bookName, direction=local\|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled}`; `bookName` references a connection-book entry for host and authentication; maintained graphically in the “Port forwarding” block of the card |
|
|
502
517
|
| `sftpStyle` | `dialog` | SFTP File Browser UI style: `dialog` single pane (remote directory + upload/download/drag & drop) / `dual` two panes (local left / remote right, inline `⇨/⇦` server-side direct transfer); reopen SFTP for it to take effect |
|
|
@@ -610,7 +625,7 @@ which stays visible, clickable and typable instead of being covered by a full-sc
|
|
|
610
625
|
the pain before 0.15).
|
|
611
626
|
|
|
612
627
|
A mount slot is **connection-scoped**: its credentials / target come from the terminal tab that opened it.
|
|
613
|
-
So since 0.
|
|
628
|
+
So since 0.19.0 every pane records its **owner tab** (`options.ownerSid`, defaulting to the active tab at
|
|
614
629
|
mount time): switching to another tab **hides** the pane (`data-dock-hidden`; its DOM and your rendered tree
|
|
615
630
|
survive, in-flight transfers keep running) and switching back restores the scene; closing the owner tab tears
|
|
616
631
|
the pane down. Without this, the pane stayed put across a tab switch — its title read `SFTP · cdc-test-161`
|
|
@@ -663,10 +678,11 @@ ctx.inject(['ttyPanel'], (c) => {
|
|
|
663
678
|
- The title bar (title / collapse / ✕) is provided by tty, and consumers only own their own body; a throwing
|
|
664
679
|
`onClose` is only logged with `console.warn`, without affecting closing the panel.
|
|
665
680
|
|
|
666
|
-
> Contract versions: `ttyConnbar.version === 1`, `ttyTerminal.version ===
|
|
667
|
-
> 2 = adds `mount
|
|
668
|
-
>
|
|
669
|
-
>
|
|
681
|
+
> Contract versions: `ttyConnbar.version === 1`, `ttyTerminal.version === 3` (1 = `open` only,
|
|
682
|
+
> 2 = adds `mount`, 3 = `open` reuses an existing live tab for the same connection + command by default),
|
|
683
|
+
> `ttyPanel.version === 2` (1 = `mountPane` + `isOpen`, 2 = adds `minimize`). Consumers **decide capabilities
|
|
684
|
+
> by version number**; do not rely on assumptions beyond `typeof fn === 'function'`; on an older tty the
|
|
685
|
+
> `inject` still fires, but the corresponding fields are absent.
|
|
670
686
|
|
|
671
687
|
## Frame protocol (/api/dsh-tty/ws, JSON text frames; v3 = one connection with many sessions + reconnect)
|
|
672
688
|
|
|
@@ -705,9 +721,17 @@ pnpm --filter @hyzyn/dsh-tty build # tsc host + esbuild browser half (cli
|
|
|
705
721
|
pnpm --filter @hyzyn/dsh-tty typecheck
|
|
706
722
|
pnpm --filter @hyzyn/dsh-tty probe # M0 probe: PTY primitive verification (needs a real PTY)
|
|
707
723
|
pnpm --filter @hyzyn/dsh-tty integration # integration tests: real plugin × real DSH service composition
|
|
708
|
-
pnpm --filter @hyzyn/dsh-tty live # liveness smoke
|
|
709
|
-
pnpm --filter @hyzyn/dsh-tty tui # TUI smoke: vim/
|
|
724
|
+
pnpm --filter @hyzyn/dsh-tty live # liveness smoke: start dsh web first (default ws://127.0.0.1:3080; DSH_TTY_WS_URL overrides)
|
|
725
|
+
pnpm --filter @hyzyn/dsh-tty tui # TUI smoke: vim/htop full-screen rendering (start dsh web first, default :3090; DSH_TTY_WS_URL overrides)
|
|
710
726
|
pnpm --filter @hyzyn/dsh-tty ssh-smoke # SSH smoke: in-memory SSH server (ssh2.Server) × real spawnSsh end to end (build first)
|
|
727
|
+
pnpm --filter @hyzyn/dsh-tty probe-smoke # probe classification & TOFU (7 cases, self-contained)
|
|
728
|
+
pnpm --filter @hyzyn/dsh-tty probe-route-smoke # probe HTTP route + connection book (9 cases)
|
|
729
|
+
pnpm --filter @hyzyn/dsh-tty sftplimits-smoke # sftpLimits normalisation (6 cases)
|
|
730
|
+
pnpm --filter @hyzyn/dsh-tty windows-smoke # Windows end to end (only meaningful on Windows; skips elsewhere)
|
|
731
|
+
|
|
732
|
+
CI (`.github/workflows/ci.yml`) runs `integration` + `ssh-smoke` + the three smokes on ubuntu and
|
|
733
|
+
`windows-smoke` on windows-latest, plus a “build artifacts match sources” gate (`client.js` + `lib/`).
|
|
734
|
+
The 48 fixes shipped in 0.19.0 and their audit index live in [`DEFECTS.md`](./DEFECTS.md).
|
|
711
735
|
pnpm --filter @hyzyn/dsh-tty preview # visual preview: headless Chrome screenshots per scene (see below)
|
|
712
736
|
```
|
|
713
737
|
|
|
@@ -732,14 +756,14 @@ node scripts/preview.mjs --theme=light # light theme
|
|
|
732
756
|
|
|
733
757
|
Coverage: local terminal / multi-tab + SSH connection bar / the “+” menu / SSH dialog (new, edit, probe)/
|
|
734
758
|
settings card (also side by side with docker)/ SFTP (single pane, dual pane, placement fallback)/
|
|
735
|
-
**mount slot follows the tab** (`dock-pane-tab`, the 0.
|
|
759
|
+
**mount slot follows the tab** (`dock-pane-tab`, the 0.19.0 regression)/ minimized badge / exit and error
|
|
736
760
|
overlays / tunnel popover / search box / toast / embedded terminals (alone and alongside the panel)/
|
|
737
761
|
docker panel and “containers → terminal drawer”.
|
|
738
762
|
|
|
739
763
|
A scene may attach a **function-shaped** assertion to `window.__previewAssert` (returning `null` means pass,
|
|
740
764
|
a string / array means fail); the script runs it and folds the result into `✓/✗`. An assertion that only
|
|
741
765
|
lives in the fixture, seen by nobody unless someone pulls `diag` by hand, is a regression that is not really
|
|
742
|
-
pinned — which is exactly what bit the 0.
|
|
766
|
+
pinned — which is exactly what bit the 0.19.0 “panel does not follow the tab” fix: the assertion was written
|
|
743
767
|
already, but because it was mixed into a `diag` object containing a function, the whole evaluation failed
|
|
744
768
|
silently and everything reported ✓.
|
|
745
769
|
|
|
@@ -818,15 +842,22 @@ verify things like “is there still a white panel after switching light/dark th
|
|
|
818
842
|
sandbox/chroot); downloads go through browser memory (for very large files prefer `scp`/`rsync` in the
|
|
819
843
|
terminal); the agent tool `sftp_read` is ≤1MB and rejects binaries, and `sftp_write` is ≤1MB per call (use
|
|
820
844
|
panel upload or the terminal for larger content); the `sftp_*` tools accept only connection-book entry
|
|
821
|
-
names, and inline credentials are for the panel dialog only
|
|
845
|
+
names, and inline credentials are for the panel dialog only; overwrite writes go through a same-directory
|
|
846
|
+
temp part `.dsh-part-<uuid>` + rename (atomic) — if the host process crashes / loses power, a part orphan
|
|
847
|
+
may remain, and the next overwrite upload to the same directory automatically cleans parts older than 24h.
|
|
822
848
|
- **SSH host keys are TOFU-pinned**: the first connection records the sha256 fingerprint automatically
|
|
823
|
-
(trust on first use), after which
|
|
849
|
+
(trust on first use), after which any recorded fingerprint matching is allowed and a full mismatch is
|
|
850
|
+
rejected — no longer an
|
|
824
851
|
unconditional accept-and-log. Note TOFU’s inherent boundary: if the first connection already met a MITM,
|
|
825
852
|
what was recorded is a fake fingerprint; `hostKeys` is persisted with settings, and a fingerprint change
|
|
826
|
-
requires a human to confirm in the settings card and delete the record;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
853
|
+
requires a human to confirm in the settings card and delete the record; a host’s multiple key types
|
|
854
|
+
(rsa/ed25519/ecdsa) are merged into one record’s fingerprint set (0.19.0), so algorithm negotiation
|
|
855
|
+
changes no longer report a false “fingerprint changed”; known_hosts import likewise keeps every
|
|
856
|
+
fingerprint of a host.
|
|
857
|
+
- **Browser tab persistence contains no plaintext credentials** (0.19.0): the spec copies that SSH tabs
|
|
858
|
+
write into sessionStorage / localStorage have plaintext `password` / `passphrase` stripped (`env:`
|
|
859
|
+
references are kept) and are flagged with `credsStripped` — on restore/respawn the terminal asks for
|
|
860
|
+
re-entry. The in-memory spec of the live session is unaffected.
|
|
830
861
|
- **SSH passwords / passphrases should use `env:VAR` references**: the connection book is persisted in the
|
|
831
862
|
settings file, so plaintext `password` / `passphrase` is an exposure surface; prefer `env:VAR` +
|
|
832
863
|
dsh-env-manager, or `agent` authentication outright (credentials never touch disk).
|
package/README.md
CHANGED
|
@@ -83,9 +83,13 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
83
83
|
- **服务器状态条**:本地只拿得到 CPU / 内存 / 在线时长(`node:os`),磁盘 / TCP / 网速 / 温度
|
|
84
84
|
显示「无」(**远端** Windows 主机走 PowerShell 那一跳,见「服务器状态条」一节);
|
|
85
85
|
- **验证到什么程度**:Windows 11 ARM 上跑过完整安装(`dsh plugin add @hyzyn/dsh-all`)、九个插件
|
|
86
|
-
装载、`dsh web` 起服务与浏览器半体交付(与 macOS
|
|
87
|
-
|
|
86
|
+
装载、`dsh web` 起服务与浏览器半体交付(与 macOS 上服务的是同一份产物,构建后比对大小与
|
|
87
|
+
内容标记一致;不写死字节数——每次重建都会变)、`[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`;x64 Windows 由 CI
|
|
88
88
|
的三平台矩阵覆盖(build / typecheck / test,见「开发」)。
|
|
89
|
+
- **端到端覆盖(0.19.0)**:`scripts/windows-smoke.mjs` 在 CI 的 windows-latest 上跑真实 `cmd.exe`
|
|
90
|
+
的 spawn → 输入回显 → kill → 重开。它抓出并钉住了「强杀本地 PTY 会崩宿主」这类只在 Windows
|
|
91
|
+
出现的问题——node-pty 在 Windows 上不接受 signal,而它的 `_deferNoArgs` 会把这个异常推迟到
|
|
92
|
+
socket 回调里抛出,调用方的 try/catch 拦不住。
|
|
89
93
|
|
|
90
94
|
## agent 工具(P1)
|
|
91
95
|
|
|
@@ -94,16 +98,16 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
94
98
|
| 工具 | 作用 |
|
|
95
99
|
| --- | --- |
|
|
96
100
|
| `tty_list` | 列出活跃终端会话(sid / kind(local\|ssh)/ target / pid / **cwd 实时跟随 cd** / 活动时间;tmux 持久会话带 `persist` 标记) |
|
|
97
|
-
| `tty_capture` | 读取近期输出(尾部 N 行,默认清洗 ANSI,`raw:true` 取原始流);**`last:true` 只返回上一条已完成命令的输出 + 退出码**(shell
|
|
101
|
+
| `tty_capture` | 读取近期输出(尾部 N 行,默认清洗 ANSI,`raw:true` 取原始流);**`last:true` 只返回上一条已完成命令的输出 + 退出码**(shell 集成标记,见下节);命令**在途**时(刚发送、完成标记未到)返回 `inProgress:true` 且不带旧结果——避免把上一条的输出当成这一条(0.19.0) |
|
|
98
102
|
| `tty_screen` | 读取**当前可见屏幕**的渲染结果(xterm-headless 虚拟屏,纯文本)——能真正读懂 vim / htop / 菜单等 TUI 界面 |
|
|
99
|
-
| `tty_expect` | 用正则**等待后续输出**中的就绪信号(dev server URL、构建完成等);超时不抛错(`matched:false` +
|
|
103
|
+
| `tty_expect` | 用正则**等待后续输出**中的就绪信号(dev server URL、构建完成等);超时不抛错(`matched:false` + 尾部输出),命令提前结束也会带退出码早停;同一会话在途调用最多 5 个,累积窗口只保留尾部 64KB(0.19.0) |
|
|
100
104
|
| `tty_send` | 向指定会话发送按键/文本(如 dev server 的 q 键、菜单选择) |
|
|
101
|
-
| `sftp_list` | 列出 SSH 远程目录内容(名称/类型/大小/修改时间,目录在前);`book` 为连接簿条目名,`path` 缺省为登录 home |
|
|
102
|
-
| `sftp_read` | 读取远程**文本**文件(默认 ≤256KB 可调至 1MB
|
|
105
|
+
| `sftp_list` | 列出 SSH 远程目录内容(名称/类型/大小/修改时间,目录在前);`book` 为连接簿条目名,`path` 缺省为登录 home;默认最多 500 项(超限 `truncated:true`),`isSymlink` 区分软链与真目录(0.19.0) |
|
|
106
|
+
| `sftp_read` | 读取远程**文本**文件(默认 ≤256KB 可调至 1MB,超出截断);`offset` 可从指定字节分页(适合读日志尾部),非法 `maxBytes` 直接报错,二进制判定 = NUL + 非法 UTF-8 占比双判据(0.19.0) |
|
|
103
107
|
| `sftp_write` | 写远程文本文件(默认覆盖,`append:true` 追加;单次 ≤1MB) |
|
|
104
108
|
| `sftp_mkdir` | 创建远程目录;`parents:true` 逐级补齐缺失父目录(等效 `mkdir -p`,自底向上创建,已存在目录幂等跳过) |
|
|
105
109
|
| `sftp_rename` | 重命名/移动远程文件或目录(`to` 与 `from` 不同目录即移动;不覆盖已存在的目标) |
|
|
106
|
-
| `sftp_remove` | 删除远程文件/目录;目录默认 rmdir(非空明确报错),`recursive:true`
|
|
110
|
+
| `sftp_remove` | 删除远程文件/目录;目录默认 rmdir(非空明确报错),`recursive:true` 整树删除(不可恢复);会拒绝 `/`、`~`、含 `.`/`..` 段的路径(不可恢复操作的前置护栏,0.19.0) |
|
|
107
111
|
| `sftp_tree` | 递归列举远程目录结构(深度优先、目录优先;`maxDepth` 1~8 / `maxEntries` 1~2000 限流,超限 `truncated:true`;symlink 不跟随防环) |
|
|
108
112
|
| `tunnel_list` | 列出端口转发隧道及其实时状态(活跃/连接中/错误/停止、规则、连接数) |
|
|
109
113
|
|
|
@@ -228,7 +232,7 @@ subsystem,宿主半体 `src/sftp.ts`):
|
|
|
228
232
|
一条标题栏(折叠不关面板、不中断浏览);同一标签的挂载位已被别的面板(如容器面板)
|
|
229
233
|
占用、或面板没开时,退回原来的居中对话框,**不会把别人的面板挤掉**。标题 / 折叠 / ✕ 由
|
|
230
234
|
tty 的挂载位提供;
|
|
231
|
-
- **跟着标签切(0.
|
|
235
|
+
- **跟着标签切(0.19.0)**:文件浏览连的是**打开它的那个标签**那台主机,所以它归属该标签:
|
|
232
236
|
切到别的标签时整块收起(在途传输照跑,切回来还在原地),标签关掉时一并收掉。**所有入口
|
|
233
237
|
用同一条规则**——连接栏「SFTP」、连接簿条目的 📂、SSH 对话框 / 设置卡片的「文件浏览」都
|
|
234
238
|
归属打开那一刻的活动标签;只有「面板开着但一个标签都没有」才算不隶属任何标签(永远可见)。
|
|
@@ -354,7 +358,11 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
354
358
|
「终端 N」);连接中先回显灰字 `Connecting user@host …`,就绪后状态栏
|
|
355
359
|
显示 `SSH user@host 已连接`;连接失败(连接超时 / 认证被拒 / 主机
|
|
356
360
|
不可达)以 `error` 帧带回原因,标签规格已随标签保存,点终端区域可按
|
|
357
|
-
|
|
361
|
+
原规格重开;状态栏描述的是**当前活动标签**——切标签 / 关标签立刻换成
|
|
362
|
+
该标签自己的状态(失败原因、退出码都记在标签身上),所以关掉那个连不上
|
|
363
|
+
的标签后,红字不会继续挂在头上(宿主级消息如「连接断开 — 自动重连中」
|
|
364
|
+
不属于任何标签,不会被切标签抹掉);反过来说,**后台标签自己的失败只记在
|
|
365
|
+
它身上**(标签栏状态点转红、终端区浮层带原文),不会顶掉活动标签的显示;
|
|
358
366
|
- **agent forwarding(0.4.0)**:SSH 对话框勾选「agent forwarding」后远程
|
|
359
367
|
可用本地 ssh-agent 的钥匙(远程 `git clone` 私有仓库等)。任意认证方式下
|
|
360
368
|
都可开(凭证仍不落盘);本机未运行 ssh-agent 时连接会明确报错而非静默
|
|
@@ -384,12 +392,14 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
384
392
|
可滚动的长表单,浮层在那边会被裁掉;
|
|
385
393
|
- **主机指纹 TOFU 钉扎(0.3.0)**:首次连接成功后把该主机(host:port)的
|
|
386
394
|
sha256 指纹记录进 `hostKeys`(随 settings 持久化);之后每次连接校验,
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
`
|
|
395
|
+
集合内任一指纹匹配即放行,**指纹变更直接拒绝连接**(防中间人冒充),错误
|
|
396
|
+
信息带重置指引。同一主机的多把钥匙(0.19.0,如 rsa + ed25519)各记一条
|
|
397
|
+
指纹、合并存于同一记录——算法协商变化不再误报变更。主机重装/换钥匙后,
|
|
398
|
+
到 设置 → 插件 → 终端面板 → 「SSH 主机密钥记录」删除对应记录再重连即可
|
|
399
|
+
(记录列表支持删除)。**「从 known_hosts 导入」(0.4.1)**:一键解析
|
|
400
|
+
`~/.ssh/known_hosts` 把已有主机指纹批量预填充,同主机的 rsa/ed25519 等多条
|
|
401
|
+
记录全部保留(不再只留首条);连接簿里的主机名还会用于还原 `|1|` hashed
|
|
402
|
+
条目,非默认端口按 `[host]:port` 解析;
|
|
393
403
|
- **连接测试(0.11.0)**:设置卡片连接簿条目行内「测试」按钮,SSH 连接
|
|
394
404
|
对话框另有「试连」按钮——两者都只做**链路诊断**(不建会话、不占
|
|
395
405
|
`maxSessions` 名额、不开 shell):先 TCP 预检(DNS + 建连,失败分类为
|
|
@@ -444,7 +454,7 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
444
454
|
| `cwd` | 宿主启动目录 | 兜底工作目录(客户端当前会话 cwd 优先) |
|
|
445
455
|
| `reconnectGraceSec` | 120 | 异常断开后会话保活秒数(0~3600):刷新页面/网络抖动后会话存活等待重连,超时由回收器结束;`0` = 旧行为,断开立即结束 |
|
|
446
456
|
| `sshHosts` | `[]` | SSH 连接簿(面板「+」菜单可选):条目 `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, persist=false}`;保存时整体替换、同名覆盖;`password` / `passphrase` 支持 `env:VAR` 引用,避免明文入库;持久化开启时条目点击默认以 tmux 持久会话打开,`persist=false` 是**取消项** |
|
|
447
|
-
| `hostKeys` | `[]` | SSH 主机指纹记录(TOFU,自动维护):条目 `{host, port,
|
|
457
|
+
| `hostKeys` | `[]` | SSH 主机指纹记录(TOFU,自动维护):条目 `{host, port, fingerprints[]}`(旧版单 `fingerprint` 字段读取时自动迁移合并);按 host:port 唯一,一机多把钥匙共用一条记录,首次连接自动追加、任一指纹匹配放行、全部不匹配拒绝连接;设置卡片可删除重置 |
|
|
448
458
|
| `shellIntegration` | true | 注入 OSC 133/7 shell 集成(命令边界标记 + cwd 上报;`tty_capture{last}` 依赖它);zsh/bash 支持,其他 shell 自动跳过;出兼容问题时可关闭 |
|
|
449
459
|
| `tunnels` | `[]` | 端口转发隧道:条目 `{name, bookName, direction=local\|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled}`;`bookName` 引用连接簿条目提供主机与认证;卡片「端口转发」区块可视化维护 |
|
|
450
460
|
| `sftpStyle` | `dialog` | SFTP 文件浏览界面风格:`dialog` 单窗体(远程目录 + 上传/下载/拖拽)/ `dual` 双栏(左本机 / 右远程,行内 `⇨/⇦` 宿主服务端直传);重新打开 SFTP 生效 |
|
|
@@ -564,7 +574,7 @@ ctx.inject(['ttyTerminal'], (c) => {
|
|
|
564
574
|
连接栏点「容器」,容器面板挂在终端**右侧**,终端继续可见、可点、可输入,而不是被整屏
|
|
565
575
|
弹窗盖住(0.15 之前那正是用户的痛点)。
|
|
566
576
|
|
|
567
|
-
挂载位是**连接级**的:它的凭证 / 目标来自**打开它的那个终端标签**。所以 0.
|
|
577
|
+
挂载位是**连接级**的:它的凭证 / 目标来自**打开它的那个终端标签**。所以 0.19.0 起每块
|
|
568
578
|
pane 记一个「归属标签」(`options.ownerSid`,默认 = 挂载那一刻的活动标签):切到别的
|
|
569
579
|
标签时整块**收起**(`data-dock-hidden`,DOM 与你 render 的树都保活,在途传输继续跑),
|
|
570
580
|
切回来恢复现场;归属标签被关掉时 pane 一并收掉。不这么做的话,切完标签面板还停在原处
|
|
@@ -627,7 +637,7 @@ ctx.inject(['ttyPanel'], (c) => {
|
|
|
627
637
|
})
|
|
628
638
|
```
|
|
629
639
|
|
|
630
|
-
> 契约版本:`ttyConnbar.version === 1`、`ttyTerminal.version ===
|
|
640
|
+
> 契约版本:`ttyConnbar.version === 1`、`ttyTerminal.version === 3`(1 = 只有 `open`,
|
|
631
641
|
> 2 = 增加 `mount`,3 = `open` 默认复用同「连接 + 命令」的活标签)、`ttyPanel.version === 2`(1 = `mountPane` + `isOpen`,2 = 增加
|
|
632
642
|
> `minimize`)。消费方**按版本号判断能力**,不要用
|
|
633
643
|
> `typeof fn === 'function'` 之外的假设;老版本 tty 上 `inject` 依然会触发,但没有
|
|
@@ -669,9 +679,17 @@ pnpm --filter @hyzyn/dsh-tty build # tsc 宿主 + esbuild 浏览器半体
|
|
|
669
679
|
pnpm --filter @hyzyn/dsh-tty typecheck
|
|
670
680
|
pnpm --filter @hyzyn/dsh-tty probe # M0 探针:PTY 原语验证(需真实 PTY)
|
|
671
681
|
pnpm --filter @hyzyn/dsh-tty integration # 集成测试:真实插件 × 真实 DSH 服务组合
|
|
672
|
-
pnpm --filter @hyzyn/dsh-tty live #
|
|
673
|
-
pnpm --filter @hyzyn/dsh-tty tui # TUI 冒烟:vim/
|
|
682
|
+
pnpm --filter @hyzyn/dsh-tty live # 存活冒烟:需先起 dsh web(默认连 ws://127.0.0.1:3080,DSH_TTY_WS_URL 可覆盖)
|
|
683
|
+
pnpm --filter @hyzyn/dsh-tty tui # TUI 冒烟:vim/htop 全屏渲染(需先起 dsh web,默认连 :3090,DSH_TTY_WS_URL 可覆盖)
|
|
674
684
|
pnpm --filter @hyzyn/dsh-tty ssh-smoke # SSH 冒烟:内存 SSH server(ssh2.Server)× 真实 spawnSsh 端到端(需先 build)
|
|
685
|
+
pnpm --filter @hyzyn/dsh-tty probe-smoke # 试连分类与 TOFU(7 例,自包含)
|
|
686
|
+
pnpm --filter @hyzyn/dsh-tty probe-route-smoke # 试连 HTTP 路由 + 连接簿(9 例)
|
|
687
|
+
pnpm --filter @hyzyn/dsh-tty sftplimits-smoke # sftpLimits 配置归一化(6 例)
|
|
688
|
+
pnpm --filter @hyzyn/dsh-tty windows-smoke # Windows 端到端(只在 Windows 上有意义,其他平台打印原因后跳过)
|
|
689
|
+
|
|
690
|
+
CI(`.github/workflows/ci.yml`)在 ubuntu 跑 `integration` + `ssh-smoke` + 三个 smoke,在
|
|
691
|
+
windows-latest 跑 `windows-smoke`,另有一道「构建产物与源码一致」闸门(`client.js` + `lib/`)。
|
|
692
|
+
0.19.0 的 48 项修复与审计索引见 [`DEFECTS.md`](./DEFECTS.md)。
|
|
675
693
|
pnpm --filter @hyzyn/dsh-tty preview # 视觉预览:headless Chrome 逐场景截图(见下)
|
|
676
694
|
```
|
|
677
695
|
|
|
@@ -695,12 +713,12 @@ node scripts/preview.mjs --theme=light # 浅色主题
|
|
|
695
713
|
|
|
696
714
|
覆盖:本地终端 / 多标签 + SSH 连接栏 / 「+」菜单 / SSH 对话框(新建、编辑、试连)/
|
|
697
715
|
设置卡片(含与 docker 并排对照)/ SFTP(单窗体、双栏、落点回退)/
|
|
698
|
-
**挂载位跟着标签切**(`dock-pane-tab`,0.
|
|
716
|
+
**挂载位跟着标签切**(`dock-pane-tab`,0.19.0 的回归)/ 最小化徽标 / 退出与错误遮罩 /
|
|
699
717
|
隧道弹层 / 搜索框 / toast / 嵌入式终端(单独与面板共存)/ docker 面板与「容器 → 终端抽屉」。
|
|
700
718
|
|
|
701
719
|
场景可以把**函数形态**的断言挂到 `window.__previewAssert`(返回 `null` = 通过,返回
|
|
702
720
|
字符串 / 数组 = 失败),脚本会跑掉它并把结果计入 `✓/✗`。断言光挂在夹具里、只有手工
|
|
703
|
-
取 `diag` 时才有人看,回归等于没钉——0.
|
|
721
|
+
取 `diag` 时才有人看,回归等于没钉——0.19.0 修「面板不跟标签切」时就吃到这个亏:断言
|
|
704
722
|
早写好了,但因为混在带函数的 `diag` 对象里,整条求值静默失败,一路都是 ✓。
|
|
705
723
|
|
|
706
724
|
夹具还会把 `--dsw-*` 皮肤变量与真实界面一并渲染,因此能验
|
|
@@ -748,14 +766,18 @@ node scripts/preview.mjs --theme=light # 浅色主题
|
|
|
748
766
|
反馈补丁兼容。
|
|
749
767
|
- **端口转发边界**:本地监听固定 127.0.0.1(不暴露局域网);remote 方向服务端监听还受服务端 sshd `GatewayPorts` 限制;隧道的 SSH 连接与终端会话独立,均走 TOFU 钉扎与连接簿认证;隧道规格变更(端口/目标/启停)经「保存」热生效,热改连接簿凭证则在下次重连时生效。
|
|
750
768
|
- **会话持久化(tmux)边界**:持久标签由 tmux server(专用 socket `dsh-tty`)托管——宿主被硬杀 / 保活回收 / 浏览器丢失标签规格时,tmux 会话会**留存**(这正是恢复能力的前提),直到机器重启或手动 `tmux -L dsh-tty kill-server`;agent 命令粒度工具(capture{last}/expect)依赖 tmux ≥3.3 的 DCS `allow-passthrough`,更低版本持久化可用但该能力降级(SSH 远程会话不注入 shell 集成钩子,capture{last} 本就不可用,与持久化无关);恢复接回重画的是当前可见屏,断线前的滚动历史在 tmux 自己的 history buffer(copy-mode)里,不在外层 xterm 滚动区;持久标签的 `exit` 帧退出码是 tmux 客户端的(0),shell 退出码经 OSC 133;D 标记照常可用;`tmux.conf` 只在 tmux server 首启时读取(改配置后需 `tmux -L dsh-tty kill-server` 让下次 spawn 重建 server);`grace=0` 的「断开立即结束」对持久标签同样会 kill-session(tmux 会话不存活);持久 SSH 会话要求远程装有 tmux(未装自动降级普通会话,连接栏常驻「未持久化」标记),且远程 `~/.tmux.conf` 不影响专用 socket 的独立 conf(`-f /dev/null`);SSH 持久会话名随 settings 留存;**两个窗口同时接回同一持久会话**时共享同一个宿主 PTY(0.10.1,单客户端扇出,名额不翻倍),两侧行数以最近调整尺寸的窗口为准(尺寸不一致时较大一侧由 onResize 自愈重画)。
|
|
751
|
-
- **SFTP 边界**:文件权限 = 对应 SSH 账号的终端权限(无额外沙箱/chroot);下载经浏览器内存(超大文件建议终端 `scp`/`rsync`);agent 工具 `sftp_read` ≤1MB 且拒绝二进制、`sftp_write` 单次 ≤1MB(大内容走面板上传或终端);`sftp_*`
|
|
769
|
+
- **SFTP 边界**:文件权限 = 对应 SSH 账号的终端权限(无额外沙箱/chroot);下载经浏览器内存(超大文件建议终端 `scp`/`rsync`);agent 工具 `sftp_read` ≤1MB 且拒绝二进制、`sftp_write` 单次 ≤1MB(大内容走面板上传或终端);`sftp_*` 工具只收连接簿条目名,内联凭证仅供面板对话框使用;覆盖写走同目录临时分片 `.dsh-part-<uuid>` + rename 原子落盘——宿主进程崩溃 / 断电时可能留下分片孤儿(下次向同目录覆盖上传时会自动清理超过 24h 的残留)。
|
|
752
770
|
- **SSH host key 为 TOFU 钉扎**:首次连接自动记录 sha256 指纹(trust on
|
|
753
|
-
first use
|
|
771
|
+
first use),之后任一记录指纹匹配放行、全部不匹配拒绝——不再是无条件放行的
|
|
754
772
|
accept-and-log。注意 TOFU 的固有边界:首次连接若已遭遇 MITM 则记录的
|
|
755
773
|
就是伪指纹;`hostKeys` 随 settings 落盘,指纹变更需人工在设置卡片确认
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
774
|
+
并删除记录;同一主机的多把钥匙(rsa/ed25519/ecdsa)合并为一条记录的多
|
|
775
|
+
指纹集合(0.19.0),算法协商变化不再误报「指纹变更」;known_hosts 导入
|
|
776
|
+
同样保留同主机的全部指纹。
|
|
777
|
+
- **浏览器标签持久化不含明文凭证**(0.19.0):SSH 标签写入
|
|
778
|
+
sessionStorage / localStorage 的规格副本会剥掉明文 `password` / `passphrase`
|
|
779
|
+
(`env:` 引用保留),并打 `credsStripped` 标记——恢复/重开时终端里提示
|
|
780
|
+
重新输入。本会话内存里的规格不受影响。
|
|
759
781
|
- **SSH 密码 / 口令建议 `env:VAR` 引用**:连接簿随 settings 文件落盘,
|
|
760
782
|
`password` / `passphrase` 明文入库有泄露面;建议 `env:VAR` +
|
|
761
783
|
dsh-env-manager 托管,或直接用 `agent` 认证(凭证不落盘)。
|