npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-remote-tunnel

v0.2.0

Published

Remote Host Tunnel Manager for dsh: allocate and register remote ports, run dsh web on a remote Linux server via systemd, and keep a resilient SSH tunnel from this machine to it.

Readme

dsh-remote-tunnel

中文 | English

Awesome DSH Plugin

Remote Host Tunnel Manager:把「本地浏览器 → 远程 Linux 服务器上的 dsh web」这条链路自动化——远程端口分配与登记、systemd 守护、SSH 隧道保活、本地 URL 输出、全生命周期管理,并面向多人共用同一台服务器的场景。

  • 会话与文件都在服务器上(远程 dsh web 的工作区 = 服务器目录),本地只开一条隧道
  • 每个使用者自动分到独立的远程端口,分配前在服务器上双重检查(真实占用 + 登记表),并发也安全
  • 每次分配都在服务器上的登记表留档(/etc/dsh-ports.tsv 或按权限自动降级),可随时 audit 对照审查
  • 隧道断线自动重连(进程退出后按退避重新拉起),心跳定期刷新登记表
  • 本地端口被占自动顺延,并报告占用者进程

如果你只是使用者(不开发)

# 1. 安装(npm 发布版)
dsh plugin --profile remote add dsh-remote-tunnel
#    想在 web UI 里也看到它(「设置 → 插件」)并用 /remote 斜杠命令?
#    再装进 web profile,并重启 dsh web:
dsh plugin --profile web add dsh-remote-tunnel

# ⚠️ 多人共用同一台服务器?先做一次性注册(需要 root,见下面「多用户共享服务器」一节):
#    不注册也能用,但会退化成每人一份私有登记表 —— audit 只看得到自己占的端口

# 2. 确认你的服务器能被识别(~/.ssh/config 里的 Host 别名自动发现)
dsh --profile remote hosts
#    没有?手动定义一台:
dsh --profile remote hosts add lab --host 192.0.2.10 --user alice --workspace /home/alice/project

# 3. 第一次先体检,缺什么它逐项告诉你(密钥/Node/dsh/登记表/systemd)
dsh --profile remote check lab

# 3.5 体检说 Node/dsh 缺失?按你登录的账号一键补齐(幂等;师兄师姐各自跑一次)
dsh --profile remote bootstrap lab
#     --upgrade 则强制把远程 dsh 更新到最新版

# 4. 起隧道,浏览器自动打开服务器上的 dsh web
dsh --profile remote up lab --open

# 日常:status 看状态 / down 收尾 / logs 看远端日志 / audit 审查端口登记
dsh --profile remote down lab

远程服务器需要:Node ≥ 22.19、dsh、systemd、免密钥 ssh 登录。每个账号(包括实验室里其他师兄师姐各自的账号)在自己电脑上跑一次初始化即可,幂等: dsh --profile remote bootstrap lab(同一脚本亦可手动:ssh <host> 'sh -s' < scripts/bootstrap-remote.sh)。其余参数在 $DSH_HOME/remote-tunnel/config.yaml,不改就能用。

remote CLI profile 是插件的主界面。装进 web profile 才会让插件出现在「设置 → 插件」里、并启用聊天中的 /remote 斜杠命令——装完记得重启一次 dsh web。

要求

  • 本地:Windows/macOS/Linux,自带 OpenSSH 客户端(Windows 10+ 已内置),Node ≥ 22.19
  • 远程:Linux,Node ≥ 22.19 + dsh(用 dsh --profile remote bootstrap <host> 或 scripts/bootstrap-remote.sh 安装,每个账号各自装一次),systemd(用户级即可,无需 root)
  • 推荐:远程已配置 SSH 免密钥登录(ssh <别名> 直接能进,不弹密码)

安装

# 1. 安装到专用 CLI profile(推荐;首次会自动初始化 remote profile)
cd <插件源码目录>          # 或 npm 包名 dsh-remote-tunnel
dsh plugin --profile remote add .

# 2. 装到 web profile,让插件在 web UI 的「设置 → 插件」里可见、
#    并启用聊天里的 /remote 斜杠命令;装完重启 dsh web
dsh plugin --profile web add .

快速上手

# 看看有哪些主机(~/.ssh/config 里的 Host 别名会被自动发现)
dsh --profile remote hosts

# 也可以手动定义一台主机(不含 ~/.ssh/config 时)
dsh --profile remote hosts add lab --host 192.0.2.10 --user alice --workspace /home/alice/project

# 就绪诊断:密钥/Node/dsh/登记表/systemd 逐项检查
dsh --profile remote check lab

# 一键:分配远程端口 → 登记 → 写 systemd 单元并启动远程 dsh web → 本地起隧道
dsh --profile remote up lab --open

# 输出示例:
#   allocated remote port 3081 (range 3080-3119, registered for alice)
#   ✓ tunnel up — http://127.0.0.1:3083 (remote lab:3081)
#   stop: dsh --profile remote down lab   (or Ctrl+C)

# 查询/停止/审查
dsh --profile remote status lab
dsh --profile remote logs lab            # 远程 dsh web 日志(journalctl)
dsh --profile remote audit lab           # 登记表 vs 真实占用
dsh --profile remote down lab            # 停隧道 + 登记表 released + 停服务 + 核实端口已释放

打开本地 URL 后,登录到的是服务器上的 dsh web:能对话、能读写服务器文件。API key 在远程 web 的「设置 → 模型」里配置(写入服务器 ~/.dsh/.credentials.yaml,本插件与隧道不触碰凭据)。

桌面端面板(DSH 桌面应用)

装进桌面 profile 后,远程的 dsh web 可以直接开在右侧栏「浏览器」面板里,不必跳系统浏览器。插件在桌面端提供三个入口:

| 入口 | 位置 | 用途 | |---|---|---| | 命令卡片 | 对话里发 /remote … | 命令结果 + 隧道状态 + 「在浏览器打开」「在侧栏打开」「启动隧道 / up」「断开连接 / down」 | | 状态条 | 输入框上方 | 常驻显示隧道状态与一键操作(可用 dock: false 整条隐藏) | | 插件页 | 左侧「插件」 | 开关插件;标题与描述跟随界面语言(中文界面显示「远程隧道 (dsh-remote-tunnel)」) |

安装到桌面端

推荐走 GUI(和其它插件一样):

「插件」页 →「添加插件」→ 输入 dsh-remote-tunnel → 安装 → 重启桌面应用

命令行等价写法:

dsh plugin --profile desktop add dsh-remote-tunnel

桌面应用只在自己启动时读取 profile,所以装完必须重启应用(窗口关掉后还要退出托盘进程)。

两种打开方式

  • 在侧栏打开(推荐):远程 dsh web 显示在右侧栏「浏览器」面板里,鉴权(?token= → cookie)由插件自动完成,不会再出现 dsh web authentication required;
  • 在浏览器打开:用系统默认浏览器打开同一条本地 URL,适合希望页面独占窗口、或需要浏览器扩展的场景。

openIn 可设置默认偏好(ask / browser / panel)。

桌面端配置(设置 → 插件)

| 字段 | 默认 | 说明 | |---|---|---| | home | $DSH_HOME/remote-tunnel | 隧道状态、日志与 config.yaml 所在目录 | | openIn | ask | 打开方式偏好 | | autoOpen | false | 启动桌面应用时自动把远程 dsh web 开进侧栏 | | dock | true | 是否显示输入框上方的常驻状态条 |

也可以直接改 profile 补丁层($DSH_HOME/profiles/desktop/cordis.patch.yml)里 remote-tunnel 那一行的 config,效果相同。

已知的外壳行为(不是故障)

  • 纯新会话里看不到命令卡片:会话视图在没有对话内容时显示欢迎页,而命令记录不属于模型历史;发出第一轮对话后,之前所有 /remote 卡片会一起出现(命令其实早已执行成功);
  • 状态条只在"会话已有内容"时出现:它挂在输入框的 dock 座位,而全新会话用的是居中(hero)布局,该座位不渲染 —— 0.2.1 会增加侧栏「远程主机」面板,任何会话状态都能点到;
  • 斜杠命令的描述不会被客户端本地化(只有第一方命令有本地化副本),因此插件里的命令描述写成中英双语。

命令一览

hosts / hosts add <别名> --host H [--port 22] [--user U] [--workspace DIR] / hosts rm <别名>
check <host>                     就绪诊断(可作 CI 探针,非零退出码 = 有问题)
bootstrap <host> [--upgrade]     按登录账号补齐远程环境:Node/dsh(~/.npm-global)/~/.dsh/linger(幂等)
provision <host> [--port N]      只做远程侧:分配端口 + systemd 单元 + 启动 + 登记(不起隧道)
up <host> [--port N] [--local-port N] [--open] [--heartbeat 秒]
down [host] [--keep-service]     停隧道 + released + 停单元 + 核实端口释放
status [host] [--json]
list
logs <host> [--lines N] [--follow] [--local]
audit <host> [--json] [--release <port>] [--clean-stale]
open [host]
config show / config path

工作原理

  1. 远程端口分配(原子):一条远程脚本在 flock 锁内完成——读登记表的 in-use 集合 + 对区间内每个端口做真实 bind 探测 → 取第一个「两者都空闲」的端口 → 追加 TSV 行 → 回显端口。多账号并发分配互不冲突。
  2. 远程守护:写入 systemd 单元并 enable --now。有密码 sudo 时用系统级单元(/etc/systemd/system/dsh-web-<user>.service,与任务书模板一致);没有 sudo 时自动改用用户级单元(~/.config/systemd/user/dsh-web.service)+ loginctl enable-linger,完全不需要 root。服务器重启自动拉起,崩溃自动重启。
  3. TOCTOU 兜底:若 dsh 启动时端口被抢(EADDRINUSE 出现在单元日志),自动把该端口加入排除集,顺延下一个空闲端口重试(默认最多 5 轮)。
  4. 本地隧道:ssh -N -L 127.0.0.1:<本地>:127.0.0.1:<远程> <别名>,本地端口先检查占用(被占自动顺延,并用 netstat+tasklist 报出占用者);ssh 进程退出后按退避序列(1s→2s→4s→8s→15s→30s 封顶)自动重连,永不断线(可配 maxAttempts)。隧道明确不传 ClearAllForwardings(Windows OpenSSH 会把它连同命令行 -L 一起清掉);exec 会话仍会清掉 config 里的转发。
  5. 心跳:隧道存活期间每 heartbeatSeconds(默认 120 秒)在锁内原位刷新登记表 last_heartbeat。
  6. 释放:down(或 up 的 Ctrl+C)按序:停隧道 → 删除本地状态 → 登记表 released → 停远端单元并 disable(禁用,避免服务器重启后自己回来占住已释放的端口)→ 核实端口真的释放。up/provision 会重新 enable;--keep-service 则完全不动远端单元。另一个进程里的 up 监督器检测到状态文件被删除后自动停止重连,不会「诈尸」。硬关终端(不按 Ctrl+C)则远端服务照跑、登记表仍是 in-use——这是真实占用,不是泄漏:下次 up 会自动清理残留的本地状态并复用同一个已登记端口,不会越攒越多。

配置

$DSH_HOME/remote-tunnel/config.yaml(dsh --profile remote config path 查看路径):

hosts:
  lab:                      # 手动定义的主机(与 ~/.ssh/config 的别名合并,二者同名时这里优先)
    host: 192.0.2.10
    port: 22
    user: alice
    workspace: /home/alice/project
    remotePortRange: [3080, 3119]   # 可选,按主机覆盖
defaults:
  remotePortRange: [3080, 3119]     # 远程 dsh 端口区间(先查占用再分配)
  localPortRange: [3081, 3140]      # 本地隧道端口区间
  registry:
    path: /etc/dsh-ports.tsv
    lockPath: /etc/dsh-ports.lock
    sudo: auto                      # auto | always | never
    fallbackPath: .dsh-ports.tsv    # 共享登记表不可写时,降级到远程家目录(相对路径)
  unit:
    prefix: dsh-web-
    restartSec: 5
    type: auto                      # auto | system | user
  heartbeatSeconds: 120             # 0 = 关闭心跳
  remoteWaitSeconds: 60             # 等远程端口就绪
  localWaitSeconds: 15              # 等本地 URL 可访问
  reconnect:
    delaysMs: [1000, 2000, 4000, 8000, 15000, 30000]
    maxAttempts: 0                  # 0 = 永不放弃
  allocateRetries: 5
  ssh:
    connectTimeout: 0               # 0 = 不传 -o ConnectTimeout(见排错表)
    extraArgs: []

多用户共享服务器(实验室场景:先做这一步)

多人用同一台服务器时,建议第一次先用 root 做一次性注册——否则插件会自动降级为"每人一份私有登记表", 端口分配只能靠真实占用探测避让(仍然不会连错、不会永久撞车,但 audit 看不到别人占了哪些端口)。

注册一次(需要 root,之后永不再做)——建共享登记表 + 共享组:

sudo groupadd -f dshports
sudo install -m 0664 -o root -g dshports /dev/null /etc/dsh-ports.tsv
sudo install -m 0664 -o root -g dshports /dev/null /etc/dsh-ports.tsv.lock
sudo usermod -aG dshports <用户名1> <用户名2> ...    # 需要用到插件的每个账号

两个文件都必须提前建好:它们位于仅 root 可写的目录里,成员自己无法创建锁文件,而所有登记操作都要先拿这把锁。 只有这两个文件带组写权限(0664),所在目录保持仅 root 即可——更新登记表时插件用用户级 mktemp 中转、原地改写, 既不触碰目录,也不改变文件的属主/组。

每个成员:① 重新 SSH 登录一次让组生效;② 首次准备环境 dsh --profile remote bootstrap <host>; ③ dsh --profile remote check <host> 应显示 registry: /etc/dsh-ports.tsv (shared-direct)——无需改任何配置, 插件会自动从私有登记表切到共享表。

之后每个人 up 的分配都在服务器上被 flock 串行化并写入同一张表:分到的远程端口必定互不相同, audit 能直接看出"谁占了哪个端口、有无 stale/冲突"。无主行定期清理:dsh --profile remote audit <host> --clean-stale。

| 服务器环境 | 登记表 | 服务守护 | |---|---|---| | 管理员按上面建了 dshports 组(推荐) | /etc/dsh-ports.tsv(组 0664,免 sudo) | 用户级单元 + linger | | 成员都有 passwordless sudo | /etc/dsh-ports.tsv(sudo 写入,0644) | 系统级单元,一人一个端口 | | 什么都没配 | 自动降级 ~/.dsh-ports.tsv(只含本人记录;check 会提示找管理员) | 用户级单元 + linger |

共享登记表全员可读(0644/0664),里面只有端口、账号、工作区、来源、时间戳和状态——不含任何密码、密钥或 token; 写入由 flock 串行化,非本组成员无法篡改。

每个账号各自准备好自己的环境(一台服务器 N 个用户 = 各自跑一次,幂等):

dsh --profile remote bootstrap <host>     # 在自己电脑上,以自己账号 ssh 登录后执行

这一步把 Node/dsh(装进该账号自己的 ~/.npm-global)/~/.dsh/linger 全部补齐——它不碰别的账号的任何东西,会话历史也按账号彼此独立。

两个用户各自 up → 自动分到不同远程端口;audit 能看出谁占哪个端口、有无 stale/冲突。

远程账号初始化(bootstrap,按登录账号)

dsh --profile remote bootstrap <host> 为 ssh 别名登录的那个账号补齐环境:Node ≥ 22.19(缺失时尽量装)、dsh(装进该账号 ~/.npm-global)、~/.dsh、systemd linger、npm-global 的 PATH 条目——幂等,新账号跑一次即可。--upgrade 则会强制把 dsh 更新到最新版。

dsh --profile remote bootstrap lab              # 准备好当前账号
dsh --profile remote bootstrap lab --upgrade    # 更新远程 dsh 到最新版

同一脚本也随包提供,可手动执行(和插件行为完全一致):

ssh <host> 'sh -s' < scripts/bootstrap-remote.sh

常见排错

| 症状 | 原因与处理 | |---|---| | dsh not found / node not found(check 或 up 报错) | 该 ssh 登录账号还没装 Node/dsh——插件为每个账号分别工作。dsh --profile remote bootstrap <host> 按当前账号补齐(幂等,--upgrade 可更新);已装但 ssh 通道 PATH 看不到时,插件也会自动探测 ~/.npm-global/bin/dsh。 | | Error: listen EADDRINUSE ... 127.0.0.1:3080 | 有人(或你上一个实例)占了该端口。本插件分配前双重检查,up 时若仍发生(TOCTOU)会自动顺延;手工起 dsh 才会看到这个报错。 | | Could not resolve hostname <别名> | 别名不在 ~/.ssh/config 里,且没在插件配置里定义。hosts add 或写入 ssh config 后重试。 | | Connection refused / remote port forwarding failed | 远端 dsh web 没起或端口不对。check <host> 看「web port listening」;logs <host> 看远端日志;ss -tln \| grep <port> 在服务器上核实。 | | channel_setup_fwd_listener_tcpip: cannot listen to port | 本地端口已被占(常见:两个 dsh web 实例)。本插件会自动顺延,并输出占用者进程名;也可 --local-port 手动指定。 | | Permission denied (publickey) / sudo: a password is required | 密钥没配好 / 没有 NOPASSWD sudo。前者 ssh-copy-id;后者见上表,无 sudo 也能用(用户级单元 + 兜底登记表)。 | | Could not create directory '/home/xxx/.ssh' + host key 提示 | 首次连接需接受主机指纹,插件默认 accept-new(TOFU),已在自动处理。 | | 断网后隧道没恢复 | 默认无限重连,status 看 ssh pid 是否 alive;logs <host> --local 看重连日志。若设了 reconnect.maxAttempts,达到上限会停止。 | | 隧道进程一直活着,但本地 URL 始终 not reachable(端口不通) | Windows OpenSSH 8.1 会把 -o ClearAllForwardings=yes 连同命令行自己的 -L 一起清掉,导致隧道只连接、不转发。已于 0.1.1 修复:隧道不再传该选项(exec 会话仍保留)。 | | 打开隧道 URL 只显示 dsh web authentication required; reopen the URL printed by dsh web. | dsh web ≥ 0.1.2-rc 用启动时打印的一次性 token URL 鉴权。up 现在会打印改写成本地端口的带 token 地址(auth: 行)。若已过期(服务重启过),把 logs <host> 里的 dsh web: http://…?token=… 整行复制到浏览器地址栏。 | | 登记表读不到(/etc/dsh-ports.tsv missing) | 首次分配时自动创建(需写入权限);无权限时自动降级到 ~/.dsh-ports.tsv,check 会给出管理员初始化命令。 | | 每条 ssh 命令都慢 ~N 秒 | 部分服务器上给 ssh 传 ConnectTimeout 会让每条连接都等满超时(即使秒连)。默认已不传该参数(ssh.connectTimeout: 0);需要时再显式打开。 | | Bad owner or permissions on .../.ssh/config(所有远程操作全挂) | 你的 ~/.ssh/config 里被别的工具(如 AtomGit DevEnv、conda 环境)注入了 Include,而那个被包含的文件权限过宽(带 Everyone:(F)),OpenSSH 直接拒绝加载整份配置。修复:icacls "<被包含的文件>" /inheritance:r /grant:r "$env:USERDOMAIN\$env:USERNAME:F" /grant:r "NT AUTHORITY\SYSTEM:F"(目录同样处理)。另一种成因是 HOME 被工具改指到别处,使 ssh 读了另一个目录下的 config——echo $env:HOME 确认。 | | audit 显示某些 in-use 行是 STALE,端口总被"占用" | 那是会话被杀/硬关终端后没来得及 down 留下的历史行(分配时会当作占用,避免撞车)。清理:dsh --profile remote audit <host> --clean-stale。 |

开发与测试

npm install             # 插件自身依赖(package-lock.json 已入库,构建可复现)
npm test                # 单元测试 + 假 ssh shim 集成测试(无需真实服务器)

集成测试用一个仿真的 ssh(把远程命令解释到临时「服务器」上,隧道真实转发 TCP),覆盖:分配/登记/释放、并发多人分配、TOCTOU 顺延、本地端口冲突顺延、断线自动重连、跨进程 down 取消、audit stale/orphan/clean。

安全说明

  • 隧道与远程 dsh 一律只绑 127.0.0.1(dsh 本身禁止 --host 0.0.0.0)
  • 插件不保存、不传输任何密码/密钥/API key;SSH 全走现有密钥(BatchMode,拒绝密码提示挂起)
  • 登记表不记录任何敏感信息(见 docs/registry-format.md · English)
  • 远程脚本仅在 flock 锁内追加/改写登记表与 systemd 单元,不执行其他写入
  • 桌面/网页端的 /remote-tunnel/* 路由走运行时自己的准入检查(ctx.connection.admit():与 /api 同一套 Host/Origin 栅栏 + 浏览器会话 cookie 校验),未通过返回 401/403 —— 因为这类路由会下发携带一次性 launch token 的 URL;确实无法携带 cookie 的载体可用 auth: false 显式关闭(不推荐)

非目标

  • 不实现 SSH/SFTP/远程挂载:方案本质是「在服务器上跑 dsh」,隧道只把 HTTP 引回本地
  • 不做新 TUI:CLI 子命令 + web 的 /remote 斜杠命令