DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wyzh0117 /

wyzh0117/dsh-port-manager

Verified

Native DSH sidebar app: every listening port on this machine, the app behind it, and one-click port actions.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a2826bbd

dsh-port-manager

A native DSH sidebar app — which ports this machine is listening on, which app owns each one, and one click to deal with it.

license node dsh platform dependencies tests

dsh · deepseek-harness · plugin · sidebar · port · lsof · port-manager

Suggested GitHub topics: dsh deepseek-harness plugin sidebar port lsof port-manager

中文文档见 README.zh.md。


A local-only, zero-runtime-dependency plugin for the DSH web client. It registers itself as a native right-sidebar page tab (through ctx.sidebarRightTabs + the keyed sidebar.right.pane.tab slot — the same mechanism the shipped Files page uses), and the data it renders is collected by its own host half with lsof / ps and delivered over a fenced JSON API.

Open the right sidebar  →  click the “Port Manager” capsule in the guide  →  the port list

Features

Language note. The panel UI ships in Simplified Chinese only — there is no i18n layer yet. This README uses English glosses for its labels, and gives the actual caption in parentheses the first time a control is named.

1. Sidebar entry

One guide capsule (order 15, plug glyph) in the right sidebar's guide page. Clicking it opens the app in the active pane; the tab chip carries its own icon, and splitting, floating and fullscreen are handled by the native docking kit.

2. Every listening port, and the app behind it

One card per listener:

Field What it shows
Port / protocol :3080, TCP (UDP behind a toggle)
Bind scope loopback only / LAN (a named interface) / all interfaces (exposed — orange warning)
App Friendly name: .app bundle name, node · vite, python · http.server, Docker container… plus a category badge (Node / Python / Docker / App / Service / Process)
Process PID, owning user, uptime, CPU, memory
Working directory The process cwd, on one line (open it from the 定位 / Reveal button)
Well-known port Hints such as :5173 Vite dev, :5432 PostgreSQL
Container Container name and image when a docker ps port mapping matches

The toolbar carries: the visible/total ports · N apps · M exposed · last scan time summary, a search box (port / app / command / directory / container name), filters (全部 all / 仅本机 loopback / 对外暴露 exposed / 可结束 killable), toggles (UDP, system entries), sorting (port / app / CPU / memory) and auto-refresh (manual / 3s / 10s / 30s, plus one refresh whenever the tab regains focus).

3. Per-port actions

Action Behaviour
打开 / Open Opens http://localhost:<port> in the system browser (TLS ports go to https)
复制 / Copy Menu: localhost:port / http://localhost:port / :port / lsof -i :port / kill <pid> / working directory / launch command
详情 / Details Expands the process view: full command line (click to copy), cwd, parent chain, every listening binding, container — plus an HTTP probe reporting status code, Server, X-Powered-By, Content-Type, page title and latency
定位 / Reveal Opens the process working directory in Finder (the file manager on Linux)
结束 / Kill After a second confirmation: SIGTERM first, escalating to SIGKILL automatically if the process is still alive 1.7s later. A direct force -9 is also available

The ⋯ menu refreshes the list immediately (立即刷新) and copies the whole port list as a Markdown table, or as :port app (pid) lines — handy for pasting straight into a conversation and asking a model to look at it.


Install

cd /path/to/dsh-PortManager        # this plugin directory
dsh plugin --profile web add "link:$PWD"

The command is a thin pnpm forwarder. Because this package declares dsh.bundle.patch, the package name is appended to the profile's dsh.profile.bundles. A new bundle layer needs a dsh web restart; afterwards, edits to lib/client.js alone are pushed to an open page by the client HMR driver, with no restart.

Verify the install:

node -e 'const h=process.env.DSH_HOME??process.env.HOME+"/.dsh";const p=require(h+"/profiles/web/package.json");console.log(p.dependencies["dsh-port-manager"], p.dsh.profile.bundles)'

(dsh --profile documents the profile directory as $DSH_HOME/profiles, which defaults to ~/.dsh/profiles.)

Uninstall:

dsh plugin --profile web remove dsh-port-manager

Compatibility

Platforms

Platform Scanning Open / Reveal Notes
macOS lsof (+ ps, optional docker ps) open The primary, fully exercised target. Docker enrichment is detected through /var/run/docker.sock and ~/.docker/run/docker.sock
Linux lsof, falling back to ss -ltnpH / ss -lunpH when lsof is absent xdg-open Full feature parity, including Docker
Windows lsof is not present and ss is Linux-only, so scanning reports a warning and an empty list cmd /c start, explorer Not a supported target: the scanning half has no Windows backend. open additionally rejects paths containing characters cmd would interpret

Requirements

Requirement Version Where it is declared
DSH ^0.1.5-rc.1 dsh.plugin.json → engines.dsh
Node.js >= 20 package.json → engines.node
@deepseek-ai/cordis ^4.0.1 (peer) package.json → peerDependencies
React ^18.2.0 (peer) package.json → peerDependencies
External commands lsof, ps (system-provided); optional ss, docker —

There are no dependencies: the host half imports only node:fs / node:http / node:https / node:child_process / node:os / node:util plus its sibling ./scan.js, so the plugin directory needs no node_modules and works immediately after a link: install.

Version adaptivity

The plugin is written so that a host or client it does not fully recognise degrades instead of breaking. Each row below is enforced by code, not by convention:

Surface What the plugin does What you get on a different version
Host half activation export const inject = ["webServer"] On profiles with no web server (headless / CLI) the plugin simply stays dormant instead of failing to load
Optional host service ctx.get("webRuntime")?.trustedHosts ?? [] Trusted hosts are honoured when the service exists; without it the request fence falls back to loopback-only, which is the stricter default
Client half activation export const inject = ["slots", "sidebarRightTabs"] A client build without the native sidebar-tab registry leaves the browser half inactive rather than throwing; the host half and its API keep working
Slot contract ctx.slots.inject(name, () => ctx.slots.register(...)) via ctx.effect Slot registration is reactive and disposable: if a slot appears later, or a version renames where it is contributed, the tab registers when the slot exists and is torn down when it goes away
Design tokens Every token is read as var(--dsw-alias-*, <literal fallback>) A renamed or missing token degrades to a readable hard-coded colour instead of an unstyled panel, and light/dark themes follow the host automatically
Module table The bundle requires only react Icons are inline SVG and styles ship through one <style data-plugin-css> tag. No bare imports means no node_modules resolution and no “missed the module table” bundle failure
Package manager A .gitignored, dependency-free tree link:, a tarball or npm all install the same bytes; nothing has to be built first
Command availability which on POSIX, where on Windows; lsof → ss fallback A missing scanner produces a warnings entry on the panel, never a crash

Architecture

lib/
├── index.js     host half — zero-dependency object plugin (export apply/inject), fenced /port-manager/api route
├── scan.js      scanning — lsof / ps / ss / docker ps → structured port records (pure, unit-tested)
└── client.js    browser half — a window.__ModuleLoader__ bundle registering the tab type + panel UI
test/            node --test: parsers, route + fence, bundle registration, real rendering assertions
scripts/         integration-scenario.mjs — real cordis Context + real dsh-host-webserver over real HTTP

The panel fetches /port-manager/api/<method>; the host half uses execFile, parses the output into port records and wraps them in a JSON envelope. Scans are cached for 1.2s with in-flight coalescing, so an auto-refresh interval never hammers lsof.

Endpoint Purpose
list Scan ports (includeUdp / force / docker optional)
detail One process in full (command line, cwd, parent chain, the ports it holds)
kill End a process (after re-verifying that it really still listens on that port)
open Open in the system browser
reveal Open a directory in the file manager
probe Local HTTP(S) probe

Security boundary

kill is this plugin's privileged surface, so:

  1. Browser fence. /port-manager/api is a raw node:http route registered by the plugin — it bypasses DSH's /api gateway and therefore does not inherit its Host/Origin check or auth cookie. The host half re-implements the same loopback / trustedHosts / same-origin / sec-fetch-site checks, and answers 403 to anything untrusted.
  2. Re-verified before terminating. The list may be seconds old and PIDs get reused. kill asks lsof -nP -iTCP:<port> -sTCP:LISTEN -Fpc again to confirm that this PID is listening on this port right now, and cross-checks the uid with ps.
  3. The protection policy is enforced server-side — a greyed-out button is not a security boundary. Refused (403 refused) are: other users' processes, system accounts (root / _windowserver / …), executables under system directories (/System, /usr/libexec, …), macOS ControlCenter (AirPlay holds 5000/7000), the DSH host's own process chain — the plugin lives inside the host process, so killing an ancestor would take the UI down with it; this is decided from process.pid's ancestry, never from the request's Host header — and the DSH port the request itself came from.
  4. Protected and unkillable entries carry a lock icon in the UI, and errors such as refused / not-listening are echoed verbatim as a toast in the panel. Nothing fails silently.

Development

node --test        # 32 cases: parsers / route / fence / bundle registration / component rendering / real cordis integration
node --check lib/index.js && node --check lib/scan.js && node --check lib/client.js

test/host.test.mjs drives the route handler with a fake ctx and fake req/res. test/integration.test.mjs (with scripts/integration-scenario.mjs) goes further: it boots a real cordis Context and a real dsh-host-webserver in a child process, mounts the plugin's apply into the live service tree, and verifies the fence (cross-origin 403, same-origin pass), method dispatch and real scan results over real HTTP.

test/client.test.mjs loads the bundle against a fake window.__ModuleLoader__ and a fake ctx, then renders the panel to HTML with react-dom/server (React is resolved by path from the local profile or a sibling plugin's node_modules; the rendering assertions are skipped when it cannot be found). The port cards really are rendered to HTML and then asserted on.

To change the UI, edit lib/client.js only — saving pushes the change into open pages through @deepseek-ai/dsh-client-hmr. Changing the host half (routes / scanning) needs a dsh web restart, or temporarily adding the insert row to the profile's cordis.patch.yml (that file is live-reloaded).


Known limitations

  • UDP has no LISTEN state — only TCP is scanned by default; UDP needs the toggle (lsof -nP -iUDP).
  • Other users' processes are visible but not killable — they are listed, the buttons are disabled, and kill answers 403.
  • No cwd means no “Reveal” — readable for same-user processes, not for system ones.
  • The list is a snapshot — there is no filesystem event feed, so it relies on auto-refresh or a manual refresh. One scan costs roughly 0.6s including the Docker query.
  • Narrow panels (under ~320px) wrap the action buttons; that is the native panel width, so drag the panel wider or go fullscreen.

Contributing

Issues and pull requests are welcome at https://github.com/wyzh0117/dsh-port-manager/issues.

Before opening a PR:

node --test && node --check lib/index.js && node --check lib/scan.js && node --check lib/client.js

Please keep the two constraints that make this plugin installable without a build step:

  • the host half (lib/index.js, lib/scan.js) imports nothing but node:* and ./scan.js;
  • the browser half (lib/client.js) requires nothing from the module table except react.

Adding a bare import of an @deepseek-ai/* package to either half breaks a link: install and the browser bundle respectively.

Changelog

0.1.0

  • Native right-sidebar page tab: guide capsule, keyed sidebar.right.pane.tab body, and a tab title with its own icon.
  • Port scanning through lsof (with an ss fallback) plus ps enrichment, Docker container mapping, well-known-port hints, search / filters / sorting / auto-refresh.
  • Per-port Open / Copy / Details (with HTTP probe) / Reveal / Kill, with SIGTERM → SIGKILL escalation and a server-side protection policy.
  • 32 tests, including a real-cordis integration scenario over real HTTP.

Acknowledgements

Built against the DSH plugin API of @deepseek-ai/dsh 0.1.5-rc.*, following the same conventions as the shipped sidebar pages. Thanks to the DSH plugin community for the reference implementations that made the native-tab wiring legible.

License

MIT © 2026 dsh-port-manager contributors


If this plugin saved you a lsof lookup, a star helps other people find it.

Star History Chart

—/ 5

No ratings yet

Verified DSH bundle

Commit a2826bbd07a8

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout