procdeck 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,156 +1,148 @@
1
1
  # procdeck
2
2
 
3
- An interactive dev-process multiplexer with a **web UI** instead of a TUI — the thing
4
- `mprocs`/`overmind` do, but the panes live in a browser tab, and the tool also solves
5
- the two problems terminal multiplexers can't touch: **port conflicts** and **remembering
6
- which port is which**.
3
+ [![npm](https://img.shields.io/npm/v/procdeck)](https://www.npmjs.com/package/procdeck)
4
+ [![Build](https://github.com/kondaurovDev/procdeck/actions/workflows/build.yml/badge.svg)](https://github.com/kondaurovDev/procdeck/actions/workflows/build.yml)
7
5
 
6
+ Run your whole dev stack with one command — and actually see what it's doing.
7
+
8
+ ![The example stack in procdeck: six processes tiled, every pane a live terminal, every service on its own *.localhost address, the flaky one flagged](https://raw.githubusercontent.com/kondaurovDev/procdeck/main/docs/screenshot.webp)
9
+
10
+ procdeck is four tools in one ~2 MB package — the process runner, the port
11
+ allocator, the log viewer and the network tab of your dev environment:
12
+
13
+ - **Runs everything.** Every proc is a real terminal in a browser tab — and
14
+ your own terminal is free again.
15
+ - **Hands out ports.** `${port}` is a free port, wired to whoever needs it —
16
+ no hardcoded numbers, no collisions — and `api.localhost:4820` always
17
+ reaches the right one.
18
+ - **Sees the traffic.** The HTTP and WebSocket requests _between_ your
19
+ services — the half a browser's DevTools never shows.
20
+ - **Answers from the CLI.** `procdeck logs --since-mark fix`, `http --digest`,
21
+ `status --json` — for you, or for a coding agent over MCP.
22
+
23
+ ## Quick start
24
+
25
+ ```sh
26
+ npx procdeck init # writes procdeck.config.json from your workspace's dev scripts
27
+ npx procdeck # starts the deck in the background and opens the UI
8
28
  ```
9
- ┌──────────┬───────────────────────────────────────────────┐
10
- │ ● api │ Restart Stop Start api.localhost:4820 │
11
- │ ● web ├───────────────────────────────────────────────┤
12
- │ ● clock │ $ node servers/api.mjs │
13
- │ ● flaky ⚠│ api listening on :52341 │
14
- │ │ GET / │
15
- └──────────┴───────────────────────────────────────────────┘
29
+
30
+ The UI opens at <http://localhost:4820> and your terminal is free again.
31
+ `procdeck down` stops everything. Requires **Node ≥ 22**, macOS or Linux.
32
+
33
+ Prefer it in the project (and you do, if you want the config in TypeScript):
34
+
35
+ ```sh
36
+ pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
16
37
  ```
17
38
 
18
- ## What it does
19
-
20
- - **Terminal panes in the browser.** Every process runs in a real PTY and renders in
21
- xterm.js — colours, spinners, cursor redraws and interactive hotkeys work exactly like
22
- in a terminal. Keystrokes are posted back into the PTY.
23
- - **Assigned ports.** Write `${port}` in a proc's command, env or url and procdeck hands
24
- it a free port before spawning (also exported as `PORT`). `${port:api}` references
25
- another proc's assigned port — so dependents get wired
26
- (`API_URL=http://localhost:${port:api}`) without a single hardcoded number. Nothing
27
- can conflict: ports are assigned once per run and never collide.
28
- - **A reverse proxy on `*.localhost`.** Every pane is reachable at a stable, memorable
29
- address on the UI's own port — `api.localhost:4820`, `web.localhost:4820` — whatever
30
- port the process actually got. Browsers hardcode `*.localhost` → 127.0.0.1, so there
31
- is zero system setup. WebSocket upgrades pass through (vite HMR works); the `Host`
32
- header is rewritten upstream so host-allowlisting dev servers need no config.
33
- - **Port auto-detection.** A 1s poll asks the OS (`pgrep` + `lsof`) which TCP ports each
34
- process _group_ listens on; they render as links in the UI. No cooperation from the
35
- process needed.
36
- - **Declarative dependencies.** `needs: ["api"]` parks a proc in `waiting` until every
37
- dependency is ready. Ready = "listening on its port" by default, or
38
- `readyWhen: "started"` for procs that never listen. Unknown names, duplicates and
39
- cycles are rejected by the config schema.
40
- - **Preflight gates.** A shell command that must pass before the proc spawns (plus an
41
- optional `expect` regex for checks that exit 0 either way, like `wrangler whoami`).
42
- Failure parks the pane in `blocked` with the output and a hint for the human — keeps
43
- interactive auth flows out of supervised panes, where a restart would kill an OAuth
44
- callback server mid-handshake.
45
- - **Alerts.** Regexes matched against a rolling tail of pane output; a match raises a
46
- badge in the UI ("needs login").
47
- - **Knows when you're not looking.** Crashed, blocked or alerting procs and unread error
48
- lines count into the tab title (`(2) garage`) and a red dot on the favicon; turn on the
49
- bell and a crash or alert while the tab is hidden becomes a system notification.
50
- - **Whole-tree restarts.** `pnpm` → `wrangler` → `workerd`: killing the pid would leave
51
- the grandchild holding the port. procdeck signals the process group, escalating
52
- SIGTERM → SIGKILL, and Ctrl-C tears down every tree it spawned.
53
- - **Single or grid layout.** One pane at a time with a sidebar, or every pane tiled
54
- (⌥G, ⌥Z zooms one tile in and out); each pane header carries its address, status and
55
- restart/stop/clear. Too many tiles? **Pin** the ones you watch (📌, ⌥P) — the grid
56
- shows only those and the rest collapse into a tray strip with their status dots and
57
- badges, one click from a peek. Layout, pins, selected pane and theme (system / light /
58
- dark — terminals included) survive a reload.
59
- - **Runs in the background.** `procdeck up` starts the deck, opens the UI and returns
60
- — no terminal tab kept hostage. `down`, `restart`, `status`, `logs` and `open` work
61
- from anywhere inside the project; `procdeck ls` lists every deck on the machine;
62
- ⏻ in the UI shuts one down. Prefer a terminal? `up --fg`.
63
- - **Installable as an app.** The UI ships a web-app manifest named after the deck
64
- (`name` in the config, else the directory), so "⤓ install" in the bar — or File →
65
- Add to Dock in Safari — gives each project its own window and Dock icon. Works over
66
- plain `http://localhost`, no HTTPS needed. Several projects? The deck name in the
67
- bar lists the other running decks, one click away.
68
-
69
- ## Install
70
-
71
- Nothing to install, if you don't want to:
39
+ > **Install footprint.** ~2 MB, one dependency. The server ships as a bundle,
40
+ > so nothing but the PTY bindings is installed — and those are
41
+ > [`@lydell/node-pty`](https://github.com/lydell/node-pty): the same sources as
42
+ > [node-pty](https://github.com/microsoft/node-pty), as per-platform packages.
43
+ > Nothing compiles and no install script runs, so there is no
44
+ > `pnpm approve-builds` detour and no compiler needed on Linux.
45
+
46
+ ## What you get
47
+
48
+ - **Terminal panes in the browser.** Every process runs in a real PTY and
49
+ renders in xterm.js — colours, spinners, cursor redraws and interactive
50
+ hotkeys all work. Single pane with a sidebar, or every pane tiled (⌥G); pin
51
+ the ones you watch and the rest collapse into a tray.
52
+ - **Ports that never collide.** Write `${port}` in a command, env value or url
53
+ and procdeck assigns a free port before spawning. `${port:api}` is another
54
+ proc's port, so dependents get wired — `API_URL=http://localhost:${port:api}`
55
+ — with no hardcoded numbers anywhere.
56
+ - **A memorable address for each service.** `api.localhost:4820`,
57
+ `web.localhost:4820` — whatever port the process actually got. Browsers
58
+ resolve `*.localhost` themselves, so there is zero system setup, and
59
+ WebSocket upgrades (vite HMR) pass through.
60
+ - **Dependencies that mean something.** `needs: ["api"]` parks a proc until the
61
+ API is actually listening — detected from the OS, with no cooperation from
62
+ the process. `preflight` gates a proc behind a check that must pass first
63
+ (`wrangler whoami`), so interactive logins stay out of supervised panes.
64
+ - **It tells you when something breaks.** Crashes, blocked procs and alert
65
+ patterns raise a badge, count into the tab title and can fire a system
66
+ notification while the tab is hidden.
67
+ - **Clean restarts.** `pnpm` → `wrangler` → `workerd`: procdeck signals the
68
+ whole process group, escalating SIGTERM → SIGKILL, so nothing is left holding
69
+ a port.
70
+ - **Out of your terminal's way.** `procdeck up` runs detached; `down`,
71
+ `status`, `logs`, `restart` and `open` work from any subdirectory of the
72
+ project. `procdeck ls` lists every deck on the machine.
73
+ - **Installable as an app.** The UI ships a web-app manifest named after the
74
+ deck, so "⤓ install" gives each project its own window and Dock icon.
75
+
76
+ ## Traffic, not just logs
77
+
78
+ Procs that use `${port}` get an HTTP observer on their assigned port, so
79
+ procdeck sees the requests _between_ your services — the half a browser's
80
+ DevTools never shows:
72
81
 
73
82
  ```sh
74
- npx procdeck init # writes procdeck.config.json from your workspace's dev scripts
75
- npx procdeck # picks up ./procdeck.config.json
76
- npx procdeck decks/backend.json # or point it at any config file
83
+ procdeck http --digest # 4xx/5xx grouped by route, with counts
84
+ procdeck http api --body # bodies (text only, auth headers redacted)
77
85
  ```
78
86
 
79
- The deck comes up in the background, the UI opens at <http://localhost:4820>, and the
80
- terminal is yours again. Requires **Node ≥ 22**.
87
+ WebSocket messages land in the same view, and ⇄ in the UI is the network tab
88
+ for your processes:
81
89
 
82
- Or add it to the project, which is what you want if you keep the config in TypeScript:
90
+ ![The traffic view: every request between the example's services with method, status, timing and size, filterable by proc](https://raw.githubusercontent.com/kondaurovDev/procdeck/main/docs/screenshot-http.webp)
91
+
92
+ See [docs/traffic.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/traffic.md).
93
+
94
+ ## For coding agents
95
+
96
+ An agent edits code, but the consequences land in processes it cannot see.
97
+ procdeck already owns those processes, so it can answer:
83
98
 
84
99
  ```sh
85
- pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
100
+ procdeck mark before-fix # a marker at "now"
101
+ procdeck restart api && procdeck wait-for api
102
+ procdeck logs --since-mark before-fix # only what the change caused
103
+ procdeck http --since-mark before-fix # only the requests it caused
86
104
  ```
87
105
 
106
+ Everything takes `--json` and is bounded by default. The same verbs are served
107
+ over MCP — `claude mcp add procdeck -- procdeck mcp`, once, globally: the
108
+ instance registry finds the right deck per project. `procdeck agents` writes
109
+ the discovery section into CLAUDE.md / AGENTS.md.
110
+
111
+ See [docs/agents.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/agents.md).
112
+
88
113
  ## Commands
89
114
 
90
115
  ```sh
91
- procdeck init # write procdeck.config.json: one proc per workspace dev script
116
+ procdeck init # write procdeck.config.json from what the project already says
92
117
  procdeck up [config] # start (detached) and open the UI — idempotent
93
118
  procdeck down # stop: every process tree is terminated
94
- procdeck restart # down + up — after editing the config or updating procdeck
95
- procdeck status # this project's deck: address, uptime, every proc's state
119
+ procdeck restart [proc] # one proc, or the whole deck; --all for every deck on this machine
120
+ procdeck status # address, uptime, every proc's state (--json for agents)
96
121
  procdeck ls # every running deck on this machine
97
122
  procdeck open # open the UI — no port to remember
98
- procdeck logs [-f] # procdeck's own log (startup, shutdown, errors)
123
+ procdeck logs [proc] # pane output as plain lines; --grep, --since, --since-mark
124
+ procdeck http [proc] # captured HTTP and WebSocket traffic
125
+ procdeck errors [proc] # stack traces, deduplicated
99
126
  procdeck up --fg # foreground instead: Ctrl-C stops the deck
100
127
  ```
101
128
 
102
129
  `procdeck` alone is `procdeck up`. Without a config path, the nearest
103
- `procdeck.config.{json,ts,js,mjs}` up from the current directory is used, so every
104
- command works from any subdirectory of the project. Running decks register in
105
- `~/.procdeck/instances/` (one JSON per deck — that is how `down`, `ls` and the UI's
106
- deck switcher find them; stale entries are pruned by pid), and a detached deck's own
107
- output goes to `~/.procdeck/logs/`. `--no-open` skips the browser. Updating procdeck
108
- needs a `procdeck restart` — the running deck keeps the old code until then.
130
+ `procdeck.config.{json,ts,js,mjs}` above the current directory is used.
109
131
 
110
- The server binds **127.0.0.1 only** — the UI types into real terminals, so it is not
111
- something to put on the LAN by accident. `"host": "0.0.0.0"` in the config opens it up
112
- (a devcontainer whose browser is on the host, a VM). No telemetry, no service worker.
132
+ Running decks register in `~/.procdeck/instances/` (one JSON per deck, pruned
133
+ by pid) and a detached deck's own output goes to `~/.procdeck/logs/`. Updating
134
+ procdeck needs a `procdeck restart` — the running deck keeps the old code until
135
+ then, and `status` says so out loud when the versions differ.
113
136
 
114
- ## Try the example
115
-
116
- In a clone of this repo:
117
-
118
- ```sh
119
- pnpm install
120
- pnpm dev # builds the UI, starts the example stack in the foreground
121
- ```
122
-
123
- > **Install footprint.** ~2 MB, one dependency. The server is shipped as a bundle, so
124
- > nothing but the PTY bindings is installed — and those are
125
- > [`@lydell/node-pty`](https://github.com/lydell/node-pty): the same sources as
126
- > [node-pty](https://github.com/microsoft/node-pty), distributed as per-platform packages.
127
- > Nothing compiles and no install script runs, so there is no `pnpm approve-builds` /
128
- > `trustedDependencies` detour, and no compiler is needed on Linux.
129
-
130
- Open <http://localhost:4820> for the panes, then <http://web.localhost:4820> and
131
- <http://api.localhost:4820> — the example's two servers found each other (and their own
132
- ports) entirely through `${port}` templates. See [`example/`](example/).
137
+ The server binds **127.0.0.1 only**: the UI types into real terminals, so it is
138
+ not something to put on the LAN by accident. `"host": "0.0.0.0"` opens it up
139
+ for a devcontainer or a VM. No telemetry, no service worker.
133
140
 
134
141
  ## Config
135
142
 
136
- `procdeck init` writes a first one from what is already there, trying in order: a
137
- **Procfile** (already a list of processes); **workspaces** (`pnpm-workspace.yaml`,
138
- `workspaces` in `package.json`) — one proc per package with a `dev` (or `start:dev`,
139
- `serve`, `watch`, `start`) script, run through the package manager your lockfile
140
- points at (`pnpm --filter web dev`, `yarn workspace web dev`, …); **plain
141
- subdirectories** — `backend/`, `frontend/`, each with its own `package.json` (its own
142
- lockfile decides the manager) or a recognisable non-JS project (Django, Go, Rust, Rails,
143
- Phoenix, docker compose), run with `cwd` set; and finally the **root itself**. Ports and
144
- dependencies are yours to add; the tips it prints say how.
145
-
146
- Two formats, one schema. **JSON** needs nothing from your toolchain — no TypeScript, and
147
- procdeck itself doesn't have to be a dependency. Point `$schema` at the published schema
148
- and the editor completes and validates every field:
149
-
150
143
  ```jsonc
151
144
  {
152
145
  "$schema": "https://unpkg.com/procdeck/schema.json",
153
- "port": 4820,
154
146
  "procs": [
155
147
  { "id": "api", "shell": "pnpm --filter api dev", "env": { "PORT": "${port}" } },
156
148
  {
@@ -163,125 +155,59 @@ and the editor completes and validates every field:
163
155
  }
164
156
  ```
165
157
 
166
- (Installed locally, `"./node_modules/procdeck/schema.json"` works too and needs no
167
- network.)
168
-
169
- **TypeScript** buys comments and computed configs, at the price of Node ≥ 22.18 (native
170
- type stripping) and procdeck in your dependencies:
158
+ JSON needs nothing from your toolchain — no TypeScript, and procdeck does not
159
+ have to be a dependency. TypeScript configs buy comments and computed values:
171
160
 
172
161
  ```ts
173
162
  import { defineConfig } from "procdeck"
174
163
 
175
164
  export default defineConfig({
176
- port: 4820, // UI + proxy port
177
- procs: [
178
- {
179
- id: "api", // pane name and the proxy subdomain
180
- shell: "pnpm --filter api dev", // or cmd: ["node", "server.js"]
181
- env: { PORT: "${port}" }, // ask procdeck for a free port
182
- preflight: { // gate: must pass before spawning
183
- shell: "wrangler whoami",
184
- expect: "You are logged in",
185
- hint: "run `wrangler login`, then Start",
186
- },
187
- alerts: [{ pattern: "ERROR", label: "check me" }],
188
- },
189
- {
190
- id: "web",
191
- shell: "pnpm --filter web dev",
192
- env: { PORT: "${port}", API_URL: "http://localhost:${port:api}" },
193
- needs: ["api"], // wait until api is listening
194
- },
195
- {
196
- id: "worker",
197
- shell: "node worker.js",
198
- readyWhen: "started", // never listens; running = ready
199
- },
200
- ],
165
+ procs: [{ id: "api", shell: "pnpm dev", env: { PORT: "${port}" } }]
201
166
  })
202
167
  ```
203
168
 
204
- Full field reference: [`packages/procdeck/src/config.ts`](packages/procdeck/src/config.ts)
205
- (the schema is the documentation — `schema.json` is generated from it at build time, so
206
- the two formats validate identically). Config files are looked up in this order when no
207
- path is given: `procdeck.config.json`, `.ts`, `.js`, `.mjs`.
208
-
209
- Tips:
210
-
211
- - Give tools that auto-increment on a busy port (vite) a `strictPort` flag, so a
212
- violated port assignment fails loudly instead of drifting.
213
- - A good pattern for app dev scripts: `vite dev --port ${PORT:-3000} --strictPort` —
214
- fixed default when run by hand, assigned port under procdeck.
215
- - The proxy is for humans and browsers. Scripts and server-to-server calls should use
216
- the injected env (`${port:api}`) — system resolvers don't all know `*.localhost`.
217
-
218
- ## How it fits together
219
-
220
- | Path | What |
221
- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
222
- | `packages/procdeck/src` | The server: plain-Node PTY layer, Effect supervisor, HTTP/SSE server with the reverse proxy. |
223
- | `packages/procdeck/ui` | [Foldkit](https://foldkit.dev) (Elm-on-Effect) app; built by vite, served statically by the server. |
224
- | `example/` | Self-contained demo stack (Node one-liners) exercising every feature. |
225
-
226
- | File | What |
227
- | --------------- | ---------------------------------------------------------------------------------------------------- |
228
- | `proc.ts` | Plain Node: PTY spawn, process-group kill, SIGTERM → SIGKILL escalation. |
229
- | `config.ts` | Effect `Schema` for the config, `${port}` templating, loader. |
230
- | `supervisor.ts` | Effect layer: scoped lifecycle, port assignment, `PubSub` fan-out, per-proc state. |
231
- | `ports.ts` | Free-port allocation and `pgrep`+`lsof` listening-port discovery. |
232
- | `server.ts` | `effect/unstable/http` router behind a small Node↔web-handler bridge; SSE downstream, POST upstream; `Host`-routed reverse proxy with WebSocket pass-through. |
233
- | `registry.ts` | `~/.procdeck/instances/<id>.json` per running deck: how `down`/`ls`/the deck switcher find decks. |
234
- | `lifecycle.ts` | Detaching (`up` re-spawns itself as `up --fg` and waits for the registry entry), `down`, port probe. |
235
- | `init.ts` | `procdeck init`: workspace scan → a first config (one proc per package with a dev script). |
236
- | `cli.ts` | `effect/unstable/cli` commands; `up --fg` is the server, everything else talks to the registry/API. |
237
-
238
- Design notes worth keeping:
239
-
240
- - **Effect end to end.** The server is Effect; the UI is a Foldkit app — one Model (a
241
- Schema), pure `update`, side effects as Commands. Terminals are imperative islands via
242
- `Mount.defineStream`; PTY log chunks bypass the Model (`ReceivedLog` → `WriteTerminal`
243
- Command into a module-level terminal registry — the view never renders them); the SSE
244
- subscription is gated on a Model condition ("every pane mounted"), so the replayed
245
- backlog ordering is declarative, not sequenced by hand.
246
- - **The server has no bundler.** It runs as `.ts` via Node type stripping (Node ≥ 22.18).
247
- - **SSE, not WebSocket.** Logs only flow towards the browser; commands are ordinary
248
- POSTs. No upgrade handshake, no extra dependency, and `EventSource` reconnects by
249
- itself.
250
- - **Per-proc backlog, one stream per subscriber.** Each proc keeps its last 256 KB of
251
- output; a subscriber (an SSE connection) gets every proc's backlog and status, a
252
- `synced` marker, then live events from a `PubSub` it subscribed to *before* the
253
- snapshot — no gap, and a per-proc chunk counter cuts the duplicate at the seam. A
254
- tab opened on a deck that has run for days still shows every pane's history, and
255
- the UI knows exactly which chunks are news (unread tallies, notifications).
256
- - **The supervisor is `Effect.acquireRelease`.** Shutdown is not a code path anyone has
257
- to remember to call: closing the scope terminates every process tree. The SSE response
258
- stream lives in the request scope, so a dropped tab cleans up its subscription too.
259
- Ctrl-C, `procdeck down` and the UI's ⏻ are all the same SIGTERM into the same scope.
260
- - **Detached is just `up --fg` in the background.** No daemon, no IPC: `up` spawns
261
- itself detached with its output in a log file, and waits for the child's registry
262
- entry to appear — written only after `listen` succeeds — so "up" means reachable.
263
- The registry is plain files pruned by pid; every deck serves its own UI, and the
264
- deck switcher is a list of links.
265
-
266
- ## Tests
169
+ Both formats validate against the same schema. Full field reference:
170
+ [docs/config.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/config.md).
171
+
172
+ ## Try the example
173
+
174
+ In a clone of this repo:
267
175
 
268
176
  ```sh
269
- pnpm test
177
+ pnpm install
178
+ pnpm dev # builds the UI, starts the example stack in the foreground
270
179
  ```
271
180
 
272
- Integration tests with real (tiny) processes — `node -e` one-liners and shells with
273
- background children — because the risks live at the OS boundary: PTY detection, group
274
- kills, SIGTERM-ignoring survivors, scope teardown. Mocking any of that would test the
275
- mock. They need a real PTY (`/dev/ptmx`), so they won't run inside sandboxes that block
276
- PTY allocation.
181
+ Then open <http://localhost:4820> for the panes, and
182
+ <http://web.localhost:4820> / <http://api.localhost:4820> — the example's
183
+ servers found each other, and their own ports, entirely through `${port}`
184
+ templates. See [`example/`](https://github.com/kondaurovDev/procdeck/tree/main/example).
185
+
186
+ ## Docs
277
187
 
278
- Gotcha: if every spawn fails with `posix_spawnp failed.`, the usual culprit is the
279
- node-pty `spawn-helper` prebuild arriving without its execute bit (pnpm/Bun skipping
280
- its install script). `proc.ts` restores the bit at import time, so this should
281
- self-heal — if it still fails, PTY allocation itself is probably blocked (sandboxes).
188
+ | Doc | What |
189
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
190
+ | [config.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/config.md) | Every config field, port templating, readiness, gates. |
191
+ | [agents.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/agents.md) | The verify loop, the CLI verbs, the MCP tools. |
192
+ | [traffic.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/traffic.md) | What the HTTP observer captures, and what it cannot. |
193
+ | [architecture.md](https://github.com/kondaurovDev/procdeck/blob/main/docs/architecture.md) | How the code is put together. |
194
+ | [design/](https://github.com/kondaurovDev/procdeck/tree/main/docs/design/) | Engineering notes behind the bigger features. |
282
195
 
283
196
  ## Status
284
197
 
285
- macOS/Linux only (PTYs, `pgrep`, `lsof`). Known shortcut: statuses are a mutated map
286
- rather than `SubscriptionRef`s. Not done yet: an injected FAB overlay inside the developed apps, and a dependency-death
287
- policy (a dependency dying does _not_ cascade — deliberately).
198
+ **macOS and Linux.** Port discovery asks the OS through `pgrep` + `lsof`, and
199
+ a whole-tree restart signals the process group (`kill(-pid)`) — both POSIX.
200
+ Windows works through WSL, not natively.
201
+
202
+ **Missing on purpose.** A dependency dying does _not_ cascade to its
203
+ dependents: procdeck tells you and leaves the decision to you. And nothing is
204
+ injected into the apps you develop — no overlay, no script tag, no agent in
205
+ your page.
206
+
207
+ Patches welcome — see
208
+ [contributing.md](https://github.com/kondaurovDev/procdeck/blob/main/contributing.md).
209
+ The UI types into real terminals, so it is worth knowing what that implies and
210
+ what the traffic observer keeps:
211
+ [security.md](https://github.com/kondaurovDev/procdeck/blob/main/security.md).
212
+
213
+ MIT.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
+ * https://github.com/chjj/term.js
5
+ * @license MIT
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ * Originally forked from (with the author's permission):
26
+ * Fabrice Bellard's javascript vt100 for jslinux:
27
+ * http://bellard.org/jslinux/
28
+ * Copyright (c) 2011 Fabrice Bellard
29
+ * The original design remains. The terminal itself
30
+ * has been extended to include xterm CSI codes, among
31
+ * other features.
32
+ */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{font-family:monospace;-webkit-user-select:text;user-select:text;white-space:pre}.xterm .xterm-accessibility-tree>div{transform-origin:left;width:fit-content}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;background:#0000;transition:opacity .1s linear;z-index:11}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{position:absolute;display:none}.xterm .xterm-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}:root{--bg: #0e1116;--panel: #151a21;--hover: #1b212a;--control: #1f2630;--border: #232a34;--text: #d8dee9;--muted: #7b8794;--accent: #4c9aff;--accent-bg: #16202e;--accent-border: #2a3a52;--ok: #3fb950;--warn: #d29922;--on-warn: #1a1005;--bad: #f85149;--overlay: rgba(31, 38, 48, .85);color-scheme:dark}@media(prefers-color-scheme:light){:root:not([data-theme=dark]){--bg: #ffffff;--panel: #f6f8fa;--hover: #eaeef2;--control: #f6f8fa;--border: #d0d7de;--text: #1f2328;--muted: #656d76;--accent: #0969da;--accent-bg: #ddf4ff;--accent-border: #b6e3ff;--ok: #1a7f37;--warn: #bf8700;--on-warn: #ffffff;--bad: #cf222e;--overlay: rgba(246, 248, 250, .9);color-scheme:light}}:root[data-theme=light]{--bg: #ffffff;--panel: #f6f8fa;--hover: #eaeef2;--control: #f6f8fa;--border: #d0d7de;--text: #1f2328;--muted: #656d76;--accent: #0969da;--accent-bg: #ddf4ff;--accent-border: #b6e3ff;--ok: #1a7f37;--warn: #bf8700;--on-warn: #ffffff;--bad: #cf222e;--overlay: rgba(246, 248, 250, .9);color-scheme:light}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:13px/1.45 ui-sans-serif,system-ui,sans-serif}.layout{height:100vh;display:grid;grid-template-columns:260px 1fr;grid-template-rows:auto minmax(0,1fr)}.layout.grid{grid-template-columns:1fr}.layout.grid aside{display:none}.layout aside{grid-area:2 / 1}.layout main{grid-area:2 / 2}.layout.grid main{grid-area:2 / 1}.banner{grid-area:2 / 1 / 3 / -1;align-self:start;z-index:2;padding:4px 14px;font-size:12px;color:var(--on-warn);background:var(--warn);animation:banner-pulse 1.6s ease-in-out infinite}.layout.down aside,.layout.down main{opacity:.55;transition:opacity .4s}.banner.down{color:var(--text);background:var(--control);border-bottom:1px solid var(--border);animation:none}@keyframes banner-pulse{50%{opacity:.75}}header{grid-column:1 / -1;display:flex;gap:8px;align-items:center;padding:8px 14px;border-bottom:1px solid var(--border);background:var(--panel)}header h1{margin:0 8px 0 0;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}header .meta{margin-left:0}header .meta.error{color:var(--bad)}.deck{position:relative;margin-right:6px}button.deck-name{color:var(--text);font-size:12px;font-weight:600;background:none;border-color:transparent;padding:3px 6px}button.deck-name .caret{margin-left:5px;font-size:10px;color:var(--muted)}.deck.open button.deck-name{background:var(--control);border-color:var(--border)}.switcher{position:absolute;top:calc(100% + 4px);left:0;z-index:5;min-width:220px;max-width:360px;padding:4px;display:flex;flex-direction:column;gap:2px;background:var(--panel);border:1px solid var(--border);border-radius:6px;box-shadow:0 8px 24px #00000059;font-size:12px}.switcher>.meta{padding:6px 8px;white-space:normal;line-height:1.4}.switcher>.version{margin-top:2px;border-top:1px solid var(--border);font-size:11px}.switcher a{display:flex;gap:8px;align-items:baseline;padding:6px 8px;border-radius:4px;color:var(--text);text-decoration:none}.switcher a:hover{background:var(--hover)}.switcher a .name{font-weight:600}.switcher a .meta{margin-left:auto}.deck-actions{margin-left:auto;display:inline-flex;gap:6px}.deck-actions .bell{padding:4px 8px;filter:grayscale(1);opacity:.5}.deck-actions .bell.on{filter:none;opacity:1;border-color:var(--accent)}.deck-actions .bell.denied{filter:none;opacity:.8;border-color:var(--warn)}.deck-actions .install{color:var(--accent);margin-right:8px}.deck-actions .shutdown{margin-left:6px;color:var(--muted)}.deck-actions .shutdown:hover:not(:disabled){color:var(--bad);border-color:var(--bad)}aside{background:var(--panel);border-right:1px solid var(--border);overflow-y:auto}.proc{padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;display:grid;grid-template-columns:8px 1fr;gap:10px;align-items:baseline}.proc:hover{background:var(--hover)}.proc.active{background:var(--hover);box-shadow:inset 2px 0 0 var(--accent)}.dot{width:8px;height:8px;border-radius:50%;background:var(--muted);align-self:center}.dot.running{background:var(--ok)}.dot.starting{background:var(--warn)}.dot.waiting{background:var(--accent)}.dot.exited{background:var(--bad)}.dot.blocked{background:transparent;border:2px solid var(--bad)}.badge{margin-left:6px;padding:1px 5px;border-radius:8px;font-size:10px;font-weight:600;color:var(--on-warn);background:var(--warn);vertical-align:1px}.badge.err{color:#fff;background:var(--bad);cursor:help}.proc .name{font-weight:600}.proc-body{min-width:0}.sub{display:flex;align-items:center;gap:5px;margin-top:4px}.up{margin-left:auto;color:var(--muted);font-size:10px;font-family:ui-monospace,SFMono-Regular,monospace;font-variant-numeric:tabular-nums}.up.exit{color:var(--bad);opacity:.85}.proc .cmd{color:var(--muted);font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}main{display:grid;grid-template-rows:minmax(0,1fr) auto;min-width:0;min-height:0}.meta{color:var(--muted);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;margin-left:auto}button{font:inherit;color:var(--text);background:var(--control);border:1px solid var(--border);border-radius:6px;padding:4px 12px;cursor:pointer}button:hover:not(:disabled){border-color:var(--accent)}button:disabled{opacity:.35;cursor:default}a{color:var(--accent)}.links{display:flex;align-items:center;gap:6px;min-width:0;overflow:hidden}a.port{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;text-decoration:none;color:var(--accent);background:var(--accent-bg);border:1px solid var(--accent-border);border-radius:5px;padding:1px 6px;white-space:nowrap}a.port:hover{border-color:var(--accent)}.links-more{color:var(--muted);font-size:11px;white-space:nowrap;cursor:help}input.search{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 9px;width:200px}input.search:focus{outline:none;border-color:var(--accent)}.hints{padding:10px 16px;color:var(--muted);font-size:10px;line-height:1.7}.to-bottom{position:absolute;right:18px;bottom:14px;z-index:5;font-size:11px;padding:3px 12px;border-radius:12px;background:var(--overlay)}.to-bottom[hidden]{display:none}.segmented{display:inline-flex;margin-left:4px}.segmented button{padding:4px 9px;border-radius:0;font-size:13px;line-height:1.2}.segmented button:first-child{border-radius:6px 0 0 6px}.segmented button+button{margin-left:-1px}.segmented button:last-child{border-radius:0 6px 6px 0}.segmented.theme{margin-right:8px}.segmented button.on{color:var(--accent);border-color:var(--accent);background:var(--accent-bg);position:relative;z-index:1}.terminals{position:relative;min-height:0;background:var(--bg)}.tile{display:flex;flex-direction:column;min-height:0;min-width:0}.tile[hidden]{display:none}.term{position:relative;flex:1;min-height:0}.terminals.single .tile{position:absolute;top:0;right:0;bottom:0;left:0}.terminals.single .term{margin:8px 4px 4px 10px}.terminals.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-auto-rows:minmax(180px,1fr);gap:6px;padding:6px;overflow-y:auto}.terminals.grid.cols-1{grid-template-columns:minmax(0,1fr)}.terminals.grid.cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.terminals.grid.cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}@media(max-width:1100px){.terminals.grid.cols-3,.terminals.grid.cols-4{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.terminals.grid{grid-template-columns:minmax(0,1fr)}}.tray{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;padding:4px 10px;border-top:1px solid var(--border);background:var(--panel);font-size:11px}.tray-item{display:inline-flex;align-items:center}.tray .peek{display:inline-flex;align-items:center;gap:6px;padding:2px 6px;background:transparent;border-color:transparent}.tray .peek:hover{background:var(--hover);border-color:var(--border)}.tray .peek.active{border-color:var(--accent)}.tray .peek .name{font-weight:600}.tray .pin{opacity:0}.tray-item:hover .pin{opacity:.6}button.pin{padding:0 4px;font-size:11px;line-height:1.4;background:transparent;border-color:transparent;filter:grayscale(1);opacity:.3}button.pin:hover{opacity:.8;border-color:var(--accent)}button.pin.on{opacity:1;filter:none}.proc .name button.pin{margin-left:4px;vertical-align:1px;opacity:0}.proc:hover .name button.pin,.proc .name button.pin.on{opacity:.3}.proc .name button.pin.on{opacity:1}.terminals.grid .tile{border:1px solid var(--border);border-radius:6px;overflow:hidden}.terminals.grid .tile.active{border-color:var(--accent)}.terminals.grid .term{margin:4px 2px 2px 6px}.tile-head{display:flex;align-items:center;gap:8px;padding:4px 8px;font-size:11px;background:var(--panel);border-bottom:1px solid var(--border);-webkit-user-select:none;user-select:none;min-width:0}.terminals.grid .tile-head{cursor:pointer}.tile-head .name{font-weight:600;white-space:nowrap}.tile-head .links{flex-shrink:0;overflow:visible}.tile-head .meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1;min-width:3ch}.pane-actions{display:inline-flex;gap:2px;flex-shrink:0;opacity:.4;transition:opacity .12s}.tile:hover .pane-actions,.tile.active .pane-actions{opacity:1}.pane-actions button{padding:1px 7px;font-size:12px;line-height:1.3;background:transparent;border-color:transparent}.pane-actions button:hover:not(:disabled){background:var(--control);border-color:var(--accent)}.traffic{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0;height:100%}.traffic-bar{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:8px 12px;border-bottom:1px solid var(--border)}.traffic-bar>button{padding:4px 9px;font-size:13px}.traffic-bar button.on{color:var(--accent);border-color:var(--accent);background:var(--accent-bg)}.traffic-spacer{flex:1}.traffic-list{overflow:auto;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.traffic-empty{padding:24px;color:var(--muted);font:13px/1.6 ui-sans-serif,system-ui,sans-serif;max-width:60ch}.traffic-row{display:grid;grid-template-columns:92px 8ch 5ch minmax(0,1fr) auto 6ch 7ch;gap:10px;align-items:baseline;padding:2px 12px;border-bottom:1px solid var(--border);cursor:pointer;white-space:nowrap}.traffic-row:hover{background:var(--hover)}.traffic-row.open{background:var(--accent-bg)}.traffic-row .proc-tag{color:var(--accent);overflow:hidden;text-overflow:ellipsis}.traffic-row .method{font-weight:600}.traffic-row .method.ws{color:var(--accent)}.traffic-row .path{overflow:hidden;text-overflow:ellipsis}.traffic-row .status.ok{color:var(--ok)}.traffic-row .status.warn{color:var(--warn)}.traffic-row .status.bad{color:var(--bad)}.traffic-row .status.ws{color:var(--muted)}.traffic-row .meta:last-child,.traffic-row .meta:nth-last-child(2){text-align:right}.traffic-detail{padding:6px 12px 12px 114px;border-bottom:1px solid var(--border);background:var(--panel)}.traffic-detail .detail-label{color:var(--muted);font:11px/2 ui-sans-serif,system-ui,sans-serif;text-transform:uppercase;letter-spacing:.04em}.traffic-detail pre{margin:0 0 8px;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word;max-height:40vh;overflow:auto}