Bundle
dsh-web-lifecycle
DSH web plugin: Restart and Shutdown buttons in the sidebar footer. Restart relaunches the `dsh web` server on the same host/port so the tab reconnects itself; Shutdown stops the server to free the terminal and then closes the browser tab.
- Source
- DDA-DIGITAL
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-web-lifecycle
A DSH web plugin that puts two buttons in the sidebar footer:
```
sidebar footer → [ ⟳ Restart ] stop the server, bring an identical
instance back on the same host/port,
and reload this page
→ [ ⏻ Shutdown ] stop the server for good so the terminal
is free, then close this tab
```
Both ask for confirmation first. No more switching to a terminal several times a
day to restart the harness — and nothing to clean up when you are done for the
night.
- **Same port, same session.** A restart pins the resolved port, so the browser
cookie's authority is unchanged — you stay signed in and your open session
comes back after the reload.
- **Two restart transports.** Works standalone (detached relaunch), and gets
faster and terminal-friendly when launched through the bundled `dshweb`
wrapper.
- **No runtime dependencies.** Only Node built-ins.
- **Inert when unsupported.** No `webServer` / no `appExit` → nothing mounts, the
host is never affected.
## Install
```bash
# from GitHub
dsh plugin --profile web add github:DDA-DIGITAL/dsh-web-lifecycle
# from a local checkout
dsh plugin --profile web add /path/to/dsh-web-lifecycle
# from npm, once published
dsh plugin --profile web add dsh-web-lifecycle
```
Then restart `dsh web` once so the host half loads, and refresh the page for the
client half:
```bash
dsh --profile web --dump-config # the row `web-lifecycle` should be listed
```
## Restart
| Launch mode | What happens |
|---|---|
| `dshweb` (supervised) | Exits with code **75**; the wrapper relaunches in the same terminal in about a second. |
| plain `dsh web` (detached) | A coordinator in its own process session outlives the dying process, waits for the port to be released, spawns an identical `dsh web`, health-checks it, and logs the chain to `$DSH_HOME/plugins-data/dsh-web-lifecycle/restart.log`. |
Either way the page waits for the origin to drop, waits for it to answer, then
reloads itself.
## Shutdown
Shutdown exits with code **0** — deliberately not 75 — so the wrapper's relaunch
condition is false and your prompt comes back:
```
dshweb: dsh web stopped (exit 0); supervisor exiting
```
No coordinator is spawned, so nothing can bring the server back. Under plain
`dsh web` the process simply exits (it was detached, so your terminal was already
free).
### About closing the tab
The plugin does try to close the tab for you (`window.close()`), and that works
when the GUI runs as an installed PWA or app window. For an ordinary browser tab
it will **not** work: browsers only let a page close windows that a script
opened, and DSH hands the URL to your OS browser, so there is no opener.
So the tab is handled in two steps: try to close, then — if it is still there —
show a full-screen card that says **"dsh web has stopped"**, retitles the tab to
`dsh web stopped`, and reminds you to press **⌘W / Ctrl+W**. There is no
auto-reload on shutdown: there is nothing left to load.
## The supervisor wrapper (optional, recommended)
Mode A keeps the server attached to your terminal and makes restarts ~1 second:
```bash
install -m 0755 scripts/dshweb ~/bin/dshweb # or /usr/local/bin
dshweb # same flags as `dsh web`
dshweb --port 3080
```
The wrapper runs `dsh web` in a loop and treats **exit code 75** as "relaunch
me". Ctrl-C still works (SIGINT exits 130, which is not 75, so the loop stops),
and a Shutdown exits 0, which also stops the loop. Without the wrapper the plugin
still works — it just falls back to the detached relaunch.
## Configuration
Defaults live in the plugin row (`cordis.patch.yml`) and are re-applied by the
plugin itself:
```yaml
- id: web-lifecycle
config:
confirm: true # show the confirmation popover before either action
allowShutdown: true # false hides the Shutdown row entirely
healthTimeoutMs: 30000 # how long a detached restart waits for the new server
logToConsole: true # log the plugin's decisions into the dsh web log
```
Override the row by id in `$DSH_HOME/profiles/web/cordis.patch.yml` — that file is
watched live, so the change applies without a restart.
## What an action costs
Both actions interrupt everything the process was running: active turns,
subagents, background jobs and scheduled work. Sessions themselves are durable and
reopen after a restart, but anything in flight is gone — which is why the
confirmation popovers are on by default. Turn them off with `confirm: false` if
you prefer a single click.
After a **shutdown**, the only way back is a terminal: `dshweb` or `dsh web`.
## Troubleshooting
| Symptom | What to do |
|---|---|
| The buttons are missing | Check `dsh --profile web --dump-config` lists `web-lifecycle`, then restart `dsh web` and refresh the page. |
| Overlay says "did not come back" | Read `$DSH_HOME/plugins-data/dsh-web-lifecycle/restart.log`; it ends with the failing startup output. Start the server manually with `dsh web`. |
| Red status dot | The channel is not answering — the host half is not mounted, or the page is stale. Refresh; if it persists, restart once from the terminal. |
| Restart works but the tab asks you to authenticate | The port changed. Do not start the server with `--port 0`, and keep the wrapper's flags identical to the original invocation. |
| Shutdown card stays open | Expected on an ordinary tab — press ⌘W. It closes by itself in a PWA/app window. |
| A supervisor (launchd/systemd/pm2) keeps resurrecting the server | Unsupported: shutdown stops the process, the supervisor starts it again. |
## How it works
- **Host half** (`lib/index.js`) registers a prefix route on `webServer` at
`/dsh-web-lifecycle` with `POST /status`, `POST /restart` and `POST /shutdown`.
Requests pass through the host's own browser fence
(`connection.requestRejection`: Host/Origin trust plus the signed cookie),
falling back to a loopback check.
- **Browser half** (`lib/client.js`) is served by the client-modules host at
`/plugins/dsh-web-lifecycle/client.js` and registers **one** entry into the
`sidebar.footer.action` slot. That slot's container is a flex *row*, so a second
entry would render beside Restart rather than under it; both rows therefore live
inside one entry and are stacked by the plugin's own column container.
- **Exit codes are the protocol**: 75 = relaunch me, 0 = stop. Unit tests pin
both, because confusing them would turn Shutdown into a Restart.
Two host quirks are worked around deliberately, both found while building this:
1. `connection.rpc.handle` is unusable in `@deepseek-ai/dsh` 0.1.5-rc.1 — it
reaches `webServer` through a context that never injected it, so every call
throws `cannot get property "webServer" without inject`. Registering the route
directly is exactly what the connection plugin does for its own `/api` route.
2. `ctx.get("connection")` returns `undefined` on a context that never declared
it, which silently downgrades the browser fence to loopback-only. The service
is therefore named in the `inject` list and read as a property, which is the
only access shape cordis permits.
## Development
```bash
npm test # node --test: config, argv planning, waits, lifecycle policy, HTTP envelope, client bundle shape
npm run check
```
Layout:
```
lib/relaunch.js pure helpers shared by both halves (argv planning, waits, ports, exit codes)
lib/index.js host half: route, restart/shutdown policy, coordinator spawn
lib/restart-agent.mjs detached coordinator (detached restart only)
lib/client.js browser half: footer rows, popovers, status card, tab close
scripts/dshweb supervisor wrapper (exit 75 = relaunch, anything else = stop)
test/ unit tests
```
## License
MIT — see [LICENSE](./LICENSE).
Repository: <https://github.com/DDA-DIGITAL/dsh-web-lifecycle>
Install
dsh plugin --profile web add github:DDA-DIGITAL/dsh-web-lifecycle
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-web-lifecycle from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.