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-explorer

v0.8.10

Published

远程开发启动器:把 dsh 装到远程主机并在本机浏览器中使用,凭据不离开本机

Downloads

4,163

Readme

dsh-remote-explorer

English | 中文

Remote development launcher: install dsh on a remote host and use it from your local browser. LLM credentials never leave your machine.

Inspired by VS Code Remote-SSH, Zed, and JetBrains Gateway — code and sessions live on the remote, the local machine only renders the UI.

Supported environments

  • Local (client): Windows / Linux / macOS. Node.js v20.19+ or v22+ and pnpm 11+ are only needed for the source-run mode (pnpm is the default dev package manager, pinned via packageManager); release packages bundle their own Node runtime.
  • Remote host: Linux or macOS (POSIX); aarch64 (arm64) and x86_64 both work. No Node preinstalled required — the tool installs and self-checks it.
  • WSL (Windows Subsystem for Linux): On Windows, WSL2 distributions are supported as remote targets. dsh is auto-installed inside WSL, tunneled via localhost forwarding — no SSH setup needed. Click the "WSL Sessions" card in the panel to use; the entry is hidden on non-Windows platforms. The reverse channel — shared by the remote-window handoff and the LLM credential proxy — supports both WSL2 networking modes (NAT binds the default-route gateway address alongside the loopback; mirrored rides the shared loopback), with automatic networking-mode detection and a reverse-link self-check after attach.
  • SSH authentication: private key (IdentityFile, recommended); with no key configured, an interactive terminal prompts for a password (no echo); --password also works (leaks via process list / shell history — the CLI warns).
  • Hosts come from Host entries in ~/.ssh/config, or ad-hoc user@host[:port] (IPv6 must go through the config).

Installation and running

The CLI has two run modes (identical commands and options); if you already run dsh locally, you can also install this tool as a dsh plugin (Option 3).

Option 1: run from source

The repository has no build step; .ts source is executed directly via tsx. The dev environment defaults to pnpm (version pinned via packageManager in package.json). After fetching the source:

pnpm install
pnpm exec tsx src/cli/bin.ts list

Option 2: run a release package

Download the archive for your platform from the release page (produced by the packaging script, named dsh-remote-explorer-<version>-<platform>.<zip|tar.gz>), unpack it, and run directly — no Node, npm, or network required on the target:

| Platform | Archive | How to run after unpacking | |---|---|---| | win32-x64 | .zip | dsh-remote-explorer.cmd <command> | | linux-x64 / linux-arm64 | .tar.gz | ./dsh-remote-explorer <command> | | darwin-x64 / darwin-arm64 | .tar.gz | ./dsh-remote-explorer <command> |

Each package bundles the official Node binary (SHASUMS256-verified at download time) and a single-file CLI dsh-remote-explorer.cjs (all dependencies bundled in). Verify the archive against the sha256 published with the release.

Option 3: install as a dsh plugin

If you already run dsh (Web/Desktop) on this machine, install this tool into dsh and manage remote sessions from a settings panel, a slash command, and agent tools:

# Requires pnpm on PATH (the dsh plugin command forwards to pnpm verbatim)
dsh plugin --profile web add dsh-remote-explorer
# When dsh is not on PATH:
npx --yes @deepseek-ai/dsh plugin --profile web add dsh-remote-explorer
# From a GitHub source (pre-built artifacts are committed; no extra config needed):
dsh plugin --profile web add github:btsd321/dsh-remote-explorer
# From a local checkout (build the plugin artifacts first):
pnpm run build:plugin && dsh plugin --profile web add /path/to/repo

Note for pnpm 11.7+ users (build-script approval gate): pnpm 11.7 treats undecided dependency build scripts as a hard failure, and this package's dependency tree carries three (cpu-features, esbuild via tsx, ssh2) — the first dsh plugin add fails with ERR_PNPM_IGNORED_BUILDS regardless of the install source. None of them are needed at plugin runtime: the artifacts are pre-built (lib/, committed) and ssh2 falls back to pure JS. Recommended: install through the dsh web GUI's plugin manager page — it offers a built-in approve-and-retry flow. CLI alternative: after the failed add, set the three pending allowBuilds entries to false in ~/.dsh/profiles/web/pnpm-workspace.yaml, remove the half-committed dsh-remote-explorer entry from dependencies in ~/.dsh/profiles/web/package.json (the failed add leaves it there, and a plain retry exits 0 without activating the plugin — a dsh CLI quirk present in 0.1.7-rc.1/rc.2), then re-run the add command.

Restart dsh web after installing. The plugin provides three surfaces:

  • "Remote SSH Sessions" global panel in the left navigation: pick a host, connect in two window modes (enter current tab / open new tab), disconnect, live progress log; the advanced-options area carries three config dialogs (jump hosts — read-only for ssh-config aliases, editable for user@host direct hosts; environment variables; proxy), all remembered as your last global input and echoed in the per-connection log; the remote window carries a status pill for returning to the manager or closing/stopping the connection
  • Slash command /remote-explorer: hosts | connect <alias> [remote-dir] | status | disconnect <alias|session-id> [--keep-remote]
  • Agent tools remote_hosts_list / remote_connect / remote_status / remote_kill (behind dsh's regular tool-approval gate)

The plugin shares the CLI's session orchestration and remote layout (~/.dsh-remote-explorer/btsd321/), and the session table is shared in both directions: dsh-remote-explorer status shows plugin-kept sessions, and the panel shows CLI-kept ones (read-only, marked "external"). Two differences: session lifetime rides the host dsh process — quitting dsh stops the remote dsh too by default (keepRemoteOnDispose: true in the profile patch keeps it); LLM keys are read from the environment of the process that launched dsh. See the usage guide.

Offline deployment

When neither the local nor the remote machine can reach the public internet, prepare resources on a connected machine first, then deploy over the intranet SSH:

  • Compatible local DSH already installed: install the plugin with the dedicated offline installer, import the remote runtime bundle, enable offline: true in the plugin config, and connect from the remote-sessions panel.
  • Standalone CLI: use a platform package with bundled Node, import the runtime bundle via offline-import, then connect with connect --offline; no local DSH required.
  • No runtime on the remote host: build a runtime bundle on an internet-connected machine of the same OS and architecture with offline-export; offline connections never download missing dependencies — versions follow the bundle manifest.

The offline model service and SSH authentication must be configured separately.

Quick start

Examples below use the source-run form. With a release package, replace pnpm exec tsx src/cli/bin.ts with ./dsh-remote-explorer (Windows: dsh-remote-explorer.cmd) — the options are identical.

# List hosts from ~/.ssh/config
pnpm exec tsx src/cli/bin.ts list

# Diagnose a host's provisioning conditions (replace myhost with your alias or user@host[:port])
pnpm exec tsx src/cli/bin.ts doctor myhost
pnpm exec tsx src/cli/bin.ts doctor myhost --refresh-mirrors   # force re-benchmark mirrors

# Main command: provision → start remote dsh → build tunnel → open browser (long-running)
# Export the API key for whichever provider you use (provider list comes from ~/.dsh/settings.yaml)
DEEPSEEK_API_KEY=sk-xxx pnpm exec tsx src/cli/bin.ts connect myhost --cwd //home/youruser

# Show all sessions maintained on this machine
pnpm exec tsx src/cli/bin.ts status

# Stop remote dsh
pnpm exec tsx src/cli/bin.ts kill myhost --all

# Clean up stale remote resources (old versions, dead session dirs; running sessions are protected)
pnpm exec tsx src/cli/bin.ts clean myhost
pnpm exec tsx src/cli/bin.ts clean myhost --keep 2   # keep 2 versions per category

# Provision only, don't start services (idempotent; reuses installed versions)
pnpm exec tsx src/cli/bin.ts provision myhost --cwd //home/youruser

# Use a different ssh config file
pnpm exec tsx src/cli/bin.ts list --ssh-config /path/to/config

After connect, the process must stay running — the local tunnel listener and LLM proxy live inside it. Ctrl-C stops the remote dsh as well (disconnect = clean). To disconnect but keep the remote process for reuse, add --keep-remote. If the session enters a terminal state due to failure, the remote process is also preserved.

For a detailed walkthrough of every command and option, see docs/usage-en.md.

How credentials work

Model calls do not go directly to the public internet. Instead, they traverse a reverse SSH tunnel. Multi-provider support: the proxy routes by path prefix — DeepSeek's native channel uses /anthropic, and other locally configured providers each use /r/<provider-name>. Routing is extracted automatically — no manual configuration needed.

Remote dsh ──(placeholder token)──▶ Remote 127.0.0.1:<reverse-port>/r/<provider> ──SSH reverse tunnel──▶ Local proxy
                                                                         ├─ /anthropic → api.deepseek.com
                                                                         └─ /r/<provider> → corresponding upstream
                                                                            (real key injected per route)
  • Each provider's real key (DEEPSEEK_API_KEY, etc.) exists only in the local process — never written to remote disk, never placed in the remote environment. The remote process environment contains proxy tokens (random values).
  • DeepSeek account sign-in works too: with a DeepSeek account signed in on your local dsh, the remote session can use the account models with no API key at all. A placeholder grant record (the proxy token) makes the remote dsh consider itself signed in; the real account token stays local and is swapped in by the tunnel proxy per request (x-dsh-auth-token). The remote web UI shows no avatar or sign-in button — dsh's account UI is Desktop-renderer-only — judge by the model picker instead; on expiry, sign in again locally and reconnect.
  • Real key resolution: reads process.env first, falls back to $DSH_HOME/.credentials.yaml refs — aligned with dsh's own credential resolution priority. Keys stored via the dsh Models page work automatically without exporting to environment variables.
  • Provider configuration source: read precisely per runtime form — Desktop dsh reads profiles/desktop/cordis.patch.yml, Web dsh reads profiles/web/cordis.patch.yml, CLI reads $DSH_HOME/settings.yaml. Configuration is mirrored into the remote session (keys like agent-default-model are mirrored so the remote default model matches local). Only provider baseURL is redirected into the tunnel. Only configuration is mirrored (credential references, no secrets); .credentials.yaml is never mirrored (it may contain real keys).
  • Missing a provider's key only affects that provider (502 with clear guidance); others continue normally.
  • The proxy token and reverse port are fixed per session, persisted to remote .runtime/ (token at permission 600), and read back on reconnect and reuse.
  • Multiple local CLIs sharing the same session share the credential path (reverse port is first-come-first-served; later views automatically yield).
  • Known residual risk: a same-privilege user on the remote could consume your quota via your tunnel (they cannot extract the key itself). Be aware on multi-user remote hosts: the proxy raises the bar with per-session tokens, rate limiting, and a path allowlist, but cannot fully block same-privilege users.

Remote proxy for GitHub access (DSH_REMOTE_PROXY)

The remote dsh is launched by this tool, so its environment carries no proxy variables by default — on a remote host without direct internet, installing a GitHub plugin (which uses HTTPS git ls-remote) times out even though SSH (port 22) works. Setting DSH_REMOTE_PROXY on the local machine fixes that: the launcher injects http_proxy / https_proxy / ALL_PROXY (both letter cases) into the remote dsh process, pointing at the proxy port your SSH reverse tunnel exposes on the remote (e.g. http://127.0.0.1:18890):

DSH_REMOTE_PROXY=http://127.0.0.1:18890 DEEPSEEK_API_KEY=sk-xxx pnpm exec tsx src/cli/bin.ts connect myhost

dsh itself passes proxy variables through to the git/pnpm child processes it spawns, so plugin installs and dependency fetches go through the same proxy. Leaving DSH_REMOTE_PROXY unset injects nothing — machines with direct internet are unaffected. In plugin form, the advanced-options Proxy dialog takes precedence over this fallback (last input remembered per transport form, persisted in ~/.dsh/remote-advanced.json). Proxy injection applies to SSH connections only — WSL connections never inject proxy variables (advanced options are stored per transport form: the SSH domain has env/proxy/jump hosts, the WSL domain has env only; see the panel's advanced options).

Remote disk isolation

Modeled after VS Code's ~/.vscode-server single-root self-contained model: everything this tool installs or manages on the remote is inside ~/.dsh-remote-explorer/btsd321/ (installations, per-session state, npm cache, temporary files). It never writes remote ~/.dsh (official dsh's home) or ~/.npm (shared npm cache).

The one deliberate exception is the remote account's own ~/.agents — the user-level agent root that native dsh uses too. Remote sessions read skills and the global AGENTS.md from it (on purpose: it connects the capabilities you already maintain on that machine). This tool writes there in only two places: an idempotent empty skills/ pre-create before launch (so dsh's watcher attaches immediately), and the one-time legacy migration.

  • Other users running official dsh on the same machine are unaffected (you share the same ~/.agents, which is exactly native dsh's semantics); doctor's isolation check reports usage.
  • Full uninstall = rm -rf ~/.dsh-remote-explorer/btsd321; your ~/.agents (skills and global instructions) is user data, so neither uninstall nor clean touches it.
  • Known low-risk sharing: remote pnpm store — only touched if someone actively runs dsh plugin on the remote; content-addressed and concurrency-safe.

Machine-level sharing and skills

Resources fall into three classes:

  • Machine-level (one per remote account, shared by every session): the Node and dsh installations and profiles/ (the single source of truth for plugins)
  • Account-level (~/.agents, shared with native dsh): skills and the global AGENTS.md; user-owned, outside this tool's root
  • Session-private (one per remote working directory): conversation history, storages, attachments, caches, and runtime material under sessions/<session id>/

DSH_AGENTS_HOME points at the remote account's real ~/.agents (dsh's default), so sessions see the skills and ~/.agents/AGENTS.md global instructions you already have on that machine — and skills installed from the remote window are visible to native dsh as well. dsh treats <agentsHome>/skills as a user-level skill root (rank 500, below project-level ranks 100/200) — it means "this machine's user", not "this session". So opening a session in a different working directory on the same remote keeps your installed skills; nothing needs reinstalling. Skills in a project's .dsh/skills or .agents/skills still travel with the repository and take precedence.

This tool writes into ~/.agents in only two places: an idempotent empty skills/ pre-create before launch (a failure does not block startup) and the legacy migration. Both are serialized by a flock at base/tmp/agents.lock. That lock only covers writes this tool initiates — third-party skill installers do not read it, so concurrently installing skills with an external tool can still clobber .skill-lock.json.

When upgrading from the old layout (DSH_AGENTS_HOME used to point at this tool's private btsd321/.agents), the first connect migrates automatically: skills and AGENTS.md from the old root are merged into ~/.agents; same-name entries are not migrated (the new copy wins and the old one stays in place), and external installer metadata (.skill-lock.json) is not carried over. The migration is idempotent; a failure only skips the stage and never blocks the session. A reused remote dsh process still runs with the old DSH_AGENTS_HOME, so restart the remote session to let it read the migrated skills.

Session-level skill copies from even older versions under sessions/<session id>/agents/ are still not migrated — they are unmaintained historical copies. mv what you want by hand, or let clean remove them together with the dead session directory.

When writing remote paths in Git Bash, use double slashes (--cwd //home/xxx) or set MSYS_NO_PATHCONV=1 first. MSYS rewrites /home/xxx into something like D:/SoftWare/Git/home/xxx before the argument reaches the program, which the CLI can only detect and reject.

doctor checks connectivity, platform, basic commands, disk space, installed runtimes, Node runtime stability, and live mirror latency. It is the first tool for troubleshooting remote environment issues — most remote development failures are environmental, not code.

Multi-user and remote plugin management

The multi-user model follows VS Code Remote-SSH:

  • Different remote OS accounts on the same host = fully isolated (separate remote roots, sessions, plugins)
  • Same remote account = shared session root: same (host, remote directory) means the same remote session (multiple views), sessions see each other and the credential proxy belongs to the first view — expected behavior (VS Code shares one server per account likewise). Use separate remote accounts per person for full isolation
  • kill --all and clean default to acting only on sessions started from this machine (owner fingerprint written to remote .runtime/owner at session start) plus process-less leftovers; other owners' sessions are skipped and listed, --include-others restores the old full-scope behavior
  • clean never touches agent directories: ~/.agents (skills and global AGENTS.md) is user data outside the tool's root, and the old <base>/.agents is kept so unmigrated same-name conflicts stay available; neither is owner-scoped or deleted

Remote plugins are managed entirely by the remote dsh itself. The plugin store is user-level (one per remote OS account, shared by all its sessions — the counterpart of ~/.vscode-server/extensions/; session profiles attach via symlink with zero copies).

There are two entry points, both on the remote side:

  • Inside the remote window: the remote dsh's own Settings plugin UI (provisioning installs pnpm on the remote) — list / install / enable / disable / uninstall, hot-applied through remote hmr
  • A remote terminal: the dsh plugin commands, with equivalent capabilities

The local panel deliberately offers no plugin management: once a connection succeeds the browser moves to the remote dsh interface, so a local manager page is no longer in view — keeping an invisible management section buys nothing. After the first connection, the remote window is your complete interface for that machine's plugins.

Architecture

flowchart TD
    n5["Real LLM API<br/>local direct egress only"]
    subgraph g_local["Local Machine (Windows / Linux / macOS)"]
        n0["Browser<br/>127.0.0.1:local-port"]
        n1["dsh-remote-explorer<br/>CLI long-running / local dsh plugin host"]
        n2["Remote provisioning<br/>Node · dsh · pnpm · offline suite"]
        n3["LLM credential proxy<br/>DEEPSEEK_API_KEY stays local"]
    end
    subgraph g_remote["Remote Host (Linux / macOS)"]
        n4["Remote dsh<br/>webserver 127.0.0.1 · session · terminal"]
    end
    n0 -->|"HTTP / WS + session token"| n1
    n1 -->|"open-pipeline trigger"| n2
    n1 -->|"SSH forward tunnel"| n4
    n2 -->|"SSH/SFTP runtime install"| n4
    n4 -->|"reverse tunnel · proxy token"| n3
    n3 -->|"real-key calls"| n5

Editable source: docs/architecture/architecture.drawio (open in draw.io; CN/EN pages share the same layout). The diagram above is converted from that source with drawio2mermaid — re-convert and update this block after editing the source.

Dependencies are strictly one-directional, top to bottom; lower layers must not import upper layers:

Entry        cli/   plugin/   plugin-client/
Orchestration session/
Capability   provision/   tunnel/   credential/   handoff/
Transport    transport/
Foundation   hosts/   util/

Local resource download (local_resource_download)

When the remote host has no public internet access, the provisioning downloads (Node tarball, npm packages) can be routed through the local machine's network via an SSH reverse-tunnel HTTP proxy. This is similar to VS Code's "Remote.SSH: Local Server Download" setting and is controlled by the local_resource_download option (SSH only; WSL has direct filesystem access and does not need it):

| Mode | Behavior | |------|----------| | auto (default) | Probe the remote network first; build a proxy tunnel only if the remote cannot reach the internet | | always | Always build a proxy tunnel (skip the probe) | | off | Remote downloads only — original behavior (fails if the remote has no internet) |

How it works: the local process starts a pure-JS HTTP CONNECT proxy and maps it to a remote port via SSH reverse forwarding (forwardIn). The remote curl / npm install commands receive http_proxy=http://127.0.0.1:<remote-port>, so their outbound traffic flows through the tunnel back to the local network — no change to the remote download commands, and npm dependency trees resolve normally. The whole provisioning network path goes through it (mirror latency probing, Node tarball download, npm version resolution, npm install), and the remote port is allocated from the same dynamic range as the other reverse tunnels, so concurrent sessions on one host do not collide. The tunnel lives only during provisioning and is torn down when provisioning completes; the dsh process's own proxy (DSH_REMOTE_PROXY) is a separate, independent path.

# CLI: always route through local network
pnpm exec tsx src/cli/bin.ts connect myhost --local-resource-download always

# Plugin panel: Advanced options → "Local resource download" dropdown (auto / always / off; saved per SSH domain)

Development

# Type check
pnpm run typecheck

# Unit tests (pure-function modules, no SSH host needed)
pnpm exec tsx --test tests/unit/*.test.ts

⚠️ Contributors: install the git hook once after cloning — do not commit without it.

pnpm run setup:hooks

pnpm install no longer builds anything: the prepare script was deliberately removed, because pnpm 11 refuses to install git-hosted packages that declare any lifecycle script (ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED). The plugin artifacts (lib/) are committed to the repository, and the pre-commit hook rebuilds them automatically whenever src/, scripts/, or tests/ change. If you skip setup:hooks and also forget to run pnpm run build:plugin manually, your commit ships a stale lib/ — everyone installing from GitHub then silently gets outdated plugin code. Without the hook, always run pnpm run build:plugin and include the updated lib/ in the same commit.

Code style guide is in docs/type_script_style.md — read it before writing any code.

Packaging

Produces release packages (see Installation and running): esbuild bundles the CLI with all runtime dependencies into a single dsh-remote-explorer.cjs, then the official Node binary for the target platform is added, along with launchers and docs, and everything is archived. Output lands in dist/ (gitignored) — this does not change how the source itself runs via tsx.

pnpm exec tsx scripts/package.ts                        # package for the current platform
pnpm exec tsx scripts/package.ts --all                  # full five-platform matrix
pnpm exec tsx scripts/package.ts --os linux --arch arm64

| Option | Description | |---|---| | --os <os> | Target OS: win32 / linux / darwin (default: current platform) | | --arch <arch> | Target architecture: x64 / arm64 (default: current architecture) | | --all | Build the full five-platform matrix; ignores --os / --arch | | --node-version <ver> | Node version to bundle (default: v24.21.0) | | --mirror <mirror> | Node download source: npmmirror (default) / official / custom URL prefix | | --out-dir <dir> | Output directory (default: dist) | | --minify | Minify the bundle (off by default, keeps readable stack traces) |

Node distributions are verified against SHASUMS256 on download and cached in dist/.node-cache, so repeated packaging skips the download. Each run prints the path, size, and sha256 of every artifact.

License

Apache License 2.0 — see LICENSE.