Skip to content
dsh.fish
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 14 hours ago

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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source