Skip to content
dsh.fish
Bundle

dsh-remote-explorer

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

Source
btsd321
stars
2 stars
License
Apache-2.0
Updated
Updated 17 days ago

Readme

# dsh-remote-explorer

**[English](README.md)** | [中文](README.cn.md)

Remote development launcher: install [dsh](https://www.npmjs.com/package/@deepseek-ai/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:

```bash
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](#packaging), 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:

```bash
# 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](docs/usage-en.md).

## 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.

```bash
# 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](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`):

```bash
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 writes on the remote is inside `~/.dsh-remote-explorer/btsd321/` (installations, per-session state, the skill directory, npm cache, temporary files). It **never writes to** remote `~/.dsh` (official dsh's home) or `~/.npm` (shared npm cache), and never reads the machine-global `~/.agents`.

- Other users running official dsh on the same machine are not affected; `doctor`'s isolation check section reports usage.
- Full uninstall = `rm -rf ~/.dsh-remote-explorer/btsd321`, one command, clean.
- 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 under this root fall into two classes:

- **Machine-level** (one per remote account, shared by every session): the Node and dsh version installations, `profiles/` (the single source of truth for plugins), and `.agents/` (agent capabilities, including skills)
- **Session-private** (one per remote working directory): conversation history, storages, attachments, caches, and runtime material under `sessions/<session id>/`

`DSH_AGENTS_HOME` points at `btsd321/.agents`, mirroring `~/.agents` on a local machine. 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.

Concurrent writes to the skill directory are serialized by a flock at `.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 from several sessions can still clobber `.skill-lock.json`.

When upgrading from an earlier version, skills under `sessions/<session id>/agents/` are **not carried over automatically** — they are no longer read, but they are not deleted either. `mv` them into `.agents/skills/` by hand, or simply reinstall (less work); leftovers disappear when `clean` removes 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
- **Exception: `clean` does not owner-scope the machine-level `.agents`** — it is a single account-wide directory, so `clean` removes it entirely (installed skills included) regardless of fingerprint. Skills are reinstallable; the next connection sets them up again

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

```
Local (Windows/Linux/macOS)                      Remote (Linux/macOS)
┌────────────────────────────────┐              ┌──────────────────────────────┐
│ Browser                        │              │ dsh (full npm install)        │
│ 127.0.0.1:<local-port>         │              │ webserver 127.0.0.1:<port>    │
└───────────────┬────────────────┘              │                              │
                │ HTTP / WS + session token     │  ├ session / agent           │
┌───────────────▼────────────────┐  forward     │  ├ fs / subprocess           │
│ dsh-remote-explorer CLI (long-running)  │══════════════▶│  ├ terminal / lsp            │
│ ├ transport  ssh2/WSL transport │              │  └ sandbox                    │
│ ├ provision  Node/dsh/pnpm setu │              │                              │
│ ├ tunnel     fwd+rev listener   │  reverse     │                              │
│ ├ session    heartbeat & recon │◀═════════════│  baseURL → 127.0.0.1:<rev>   │
│ └ credential LLM proxy         │              │                              │
│   ▲ DEEPSEEK_API_KEY only here │              └──────────────────────────────┘
└───┼────────────────────────────┘
    │
Real LLM API (local direct egress)
```

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/
```

| Module | Responsibility |
|---|---|
| [src/util/](src/util/) | Shell escaping, error types, interactive password prompt (no echo) |
| [src/hosts/ssh-config-parser.ts](src/hosts/ssh-config-parser.ts) | **Sole** source of host config: parses ssh config (plus ad-hoc `user@host[:port]`), recursively resolves ProxyJump, applies auth overrides; also resolves panel-configured jump chains (`resolveJumpChain`) and tells config hosts from direct hosts (`isConfigHost`) |
| [src/transport/types.ts](src/transport/types.ts) | Transport abstraction (SSH and WSL implementations) |
| [src/transport/ssh-transport.ts](src/transport/ssh-transport.ts) | ssh2 implementation: jump host chains, command execution, SFTP, forward/reverse forwarding, password auth (retries on rejection, up to 3) |
| [src/transport/wsl-transport.ts](src/transport/wsl-transport.ts) | WSL distro transport: command execution and detached launch via wsl.exe |
| [src/transport/channel-pool.ts](src/transport/channel-pool.ts) | SSH channel quota, avoids exceeding MaxSessions |
| [src/transport/platform.ts](src/transport/platform.ts) | Shared platform detection and command building: arch mapping, uname parsing, PATH assembly |
| [src/transport/wsl-network.ts](src/transport/wsl-network.ts) | WSL networking-mode probe pure functions: NAT/mirrored detection, default-route gateway resolution, loopback/reverse probe command builders |
| [src/provision/probe.ts](src/provision/probe.ts) | Remote probe + **Node stability self-check** |
| [src/provision/mirror-selector.ts](src/provision/mirror-selector.ts) | Live mirror latency measurement and adaptive selection |
| [src/provision/remote-paths.ts](src/provision/remote-paths.ts) | Single source of truth for remote path rules |
| [src/provision/node-installer.ts](src/provision/node-installer.ts) | Install Node, version-isolated, self-checks after install |
| [src/provision/dsh-installer.ts](src/provision/dsh-installer.ts) | Install dsh: latest published version by default (dist-tag `latest` lags in practice), fallback floor on resolution failure |
| [src/provision/pnpm-installer.ts](src/provision/pnpm-installer.ts) | Install pnpm (pinned 11.7.0, reuses majors 10/11/12); version probe reads the on-disk package.json |
| [src/provision/pnpm-profile.ts](src/provision/pnpm-profile.ts) | Idempotent pnpm 11 prerequisites in the host profile's pnpm-workspace.yaml (allowBuilds / minimumReleaseAge) |
| [src/provision/profile-writer.ts](src/provision/profile-writer.ts) | Per-session independent `DSH_HOME` and profile/patch generation |
| [src/provision/provisioner.ts](src/provision/provisioner.ts) | Provisioning orchestration, each step idempotent |
| [src/provision/remote-context.ts](src/provision/remote-context.ts) | Remote execution context: binds transport instance and path info |
| [src/util/session-id.ts](src/util/session-id.ts) | Deterministic session id from host alias + remote directory |
| [src/tunnel/port-allocator.ts](src/tunnel/port-allocator.ts) | Remote port allocation and listen confirmation |
| [src/tunnel/forward-local.ts](src/tunnel/forward-local.ts) | Forward tunneling, **listener survives reconnection** |
| [src/tunnel/reverse-listener.ts](src/tunnel/reverse-listener.ts) | Windows-side reverse listener: **bind-first allocation**, ghost-port candidate retry, OS-assigned port when the whole range is silently held |
| [src/session/remote-process.ts](src/session/remote-process.ts) | Remote dsh detach launch, token capture, safe shutdown |
| [src/session/transport/factory.ts](src/session/transport/factory.ts) | Transport instantiation + **jump-host plan** (`planJumpHosts`: config alias → config ProxyJump, direct host → panel entries) — the single routing rule shared by open/reconnect and the connect log |
| [src/session/options.ts](src/session/options.ts) | Open/close option contract (auth overrides, extra env, proxy, jump entries — passwords memory-only) |
| [src/session/lifecycle-state.ts](src/session/lifecycle-state.ts) | Session state machine, pure functions |
| [src/session/heartbeat.ts](src/session/heartbeat.ts) | Heartbeat: process + port + HTTP application-level, single command |
| [src/session/reconnect.ts](src/session/reconnect.ts) | Bounded exponential backoff |
| [src/session/cancellable-wait.ts](src/session/cancellable-wait.ts) | Shared cancellable wait primitive (unref'd timer + abort rejection) — single implementation for polling and backoff waits |
| [src/session/session-registry.ts](src/session/session-registry.ts) | Local session table, lock file + atomic replacement; reads cached by mtime+size (re-stat on every read, cross-process consistent) |
| [src/session/session-manager.ts](src/session/session-manager.ts) | Session orchestration: lifecycle, heartbeat, reconnect, close; open flow split into open-pipeline |
| [src/session/open-pipeline/](src/session/open-pipeline/) | Open flow in four stages: prepare / probe / provision / tunnels (with the transport factory) |
| [src/session/wsl-reverse.ts](src/session/wsl-reverse.ts) | WSL reverse-channel orchestration: networking-mode detection, gateway refresh on reconnect, reverse-link self-check |
| [src/credential/tunnel-proxy.ts](src/credential/tunnel-proxy.ts) | Reverse tunnel LLM proxy (multi-provider routing), injects real keys; account channel swaps the `x-dsh-auth-token` header for the real DeepSeek account token |
| [src/credential/provider-routes.ts](src/credential/provider-routes.ts) | Extract provider routes from local config (settings.yaml / profile patch), produce remote mirror |
| [src/credential/local-credentials.ts](src/credential/local-credentials.ts) | Read local `.credentials.yaml` refs as env-var credential fallback, plus the DeepSeek account grant token (account channel) |
| [src/credential/token.ts](src/credential/token.ts) | Proxy token: generation and constant-time comparison |
| [src/credential/proxy-secret.ts](src/credential/proxy-secret.ts) | Credential material I/O: session-scoped token, reverse port, machine-level placeholder account credentials |
| [src/handoff/](src/handoff/) | Remote-window handoff: host half (bundle inside remote dsh) + browser half (status pill and management menu); protocol.ts is the cross-layer shared contract (constants referenced by the entry/orchestration/capability layers, zero dependencies) |
| [src/plugin/](src/plugin/) | Plugin host half: session supervisor (booking + bounded log buffer), `/api` routes, advanced-options store (per transport form: SSH domain has env/proxy/jump hosts, WSL domain env only, 0600), connect-option summary log |
| [src/plugin-client/](src/plugin-client/) | Browser half: SSH/WSL panels, connect form with the three advanced-option dialogs (jump hosts / env vars / proxy), session polling, desktop floating window |
| [src/cli/](src/cli/) | Command dispatch, argument parsing, terminal output, per-command auth wiring |

## Development

```bash
# 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.**
>
> ```bash
> 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](docs/type_script_style.md) — read it before writing any code.

## Packaging

Produces release packages (see [Installation and running](#option-2-run-a-release-package)): 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**.

```bash
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](LICENSE).

Install

dsh plugin --profile web add github:btsd321/dsh-remote-explorer#f4ff89bd85bd002cfa5a79e73196527e5a2c7462

Profile: web

Source