procdeck 0.3.0 → 0.5.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/dist/chunk-SIAA4J6H.js +21 -0
- package/dist/cli.js +26107 -6996
- package/dist/public.d.ts +12 -0
- package/dist/public.js +1 -1
- package/package.json +4 -2
- package/readme.md +75 -15
- package/schema.json +17 -0
- package/ui/dist/assets/index-BwFe3unL.css +32 -0
- package/ui/dist/assets/index-CTUfrVhX.js +95 -0
- package/ui/dist/icon-alert.svg +25 -0
- package/ui/dist/index.html +15 -2
- package/dist/chunk-MLKGABMK.js +0 -9
- package/ui/dist/assets/index-D4mI4ojj.js +0 -83
- package/ui/dist/assets/index-DYfMdGhb.css +0 -32
package/dist/public.d.ts
CHANGED
|
@@ -46,6 +46,12 @@ export type ProcSpec = {
|
|
|
46
46
|
readonly needs?: ReadonlyArray<string>;
|
|
47
47
|
/** What "ready" means for dependents. Defaults to "listening". */
|
|
48
48
|
readonly readyWhen?: "listening" | "started";
|
|
49
|
+
/**
|
|
50
|
+
* Route this proc's assigned `${port}` through procdeck's HTTP observer
|
|
51
|
+
* (`procdeck http` captures requests/responses). Defaults to true for
|
|
52
|
+
* procs that use `${port}`; false hands the proc the public port directly.
|
|
53
|
+
*/
|
|
54
|
+
readonly observe?: boolean;
|
|
49
55
|
readonly preflight?: Preflight;
|
|
50
56
|
readonly alerts?: ReadonlyArray<Alert>;
|
|
51
57
|
};
|
|
@@ -57,6 +63,12 @@ export type ProcdeckConfig = {
|
|
|
57
63
|
readonly name?: string;
|
|
58
64
|
/** Port for the web UI and the `*.localhost` proxy. Defaults to 4820. */
|
|
59
65
|
readonly port?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Interface the UI and proxy bind to. Defaults to "127.0.0.1" (loopback
|
|
68
|
+
* only — the UI types into real terminals). "0.0.0.0" to reach the deck
|
|
69
|
+
* from another machine or from a container's host.
|
|
70
|
+
*/
|
|
71
|
+
readonly host?: string;
|
|
60
72
|
readonly procs: ReadonlyArray<ProcSpec>;
|
|
61
73
|
};
|
|
62
74
|
/** Identity helper that gives config files their types. */
|
package/dist/public.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Interactive dev-process multiplexer with a web UI: terminal panes in a browser tab, declarative dependencies, assigned ports and a *.localhost reverse proxy.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -50,11 +50,13 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@effect/platform-browser": "4.0.0-beta.106",
|
|
53
|
-
"effect": "4.0.0-beta.106",
|
|
53
|
+
"@effect/platform-node-shared": "4.0.0-beta.106",
|
|
54
54
|
"@types/node": "^22.10.1",
|
|
55
55
|
"@xterm/addon-fit": "^0.11.0",
|
|
56
56
|
"@xterm/addon-search": "^0.16.0",
|
|
57
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
57
58
|
"@xterm/xterm": "^6.0.0",
|
|
59
|
+
"effect": "4.0.0-beta.106",
|
|
58
60
|
"foldkit": "^0.141.2",
|
|
59
61
|
"tsup": "^8.5.1",
|
|
60
62
|
"typescript": "^7.0.2",
|
package/readme.md
CHANGED
|
@@ -44,27 +44,40 @@ which port is which**.
|
|
|
44
44
|
callback server mid-handshake.
|
|
45
45
|
- **Alerts.** Regexes matched against a rolling tail of pane output; a match raises a
|
|
46
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.
|
|
47
50
|
- **Whole-tree restarts.** `pnpm` → `wrangler` → `workerd`: killing the pid would leave
|
|
48
51
|
the grandchild holding the port. procdeck signals the process group, escalating
|
|
49
52
|
SIGTERM → SIGKILL, and Ctrl-C tears down every tree it spawned.
|
|
50
53
|
- **Single or grid layout.** One pane at a time with a sidebar, or every pane tiled
|
|
51
|
-
(⌥G); each pane header carries its address, status and
|
|
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`.
|
|
52
63
|
- **Installable as an app.** The UI ships a web-app manifest named after the deck
|
|
53
64
|
(`name` in the config, else the directory), so "⤓ install" in the bar — or File →
|
|
54
65
|
Add to Dock in Safari — gives each project its own window and Dock icon. Works over
|
|
55
|
-
plain `http://localhost`, no HTTPS needed.
|
|
66
|
+
plain `http://localhost`, no HTTPS needed. Several projects? The deck name in the
|
|
67
|
+
bar lists the other running decks, one click away.
|
|
56
68
|
|
|
57
69
|
## Install
|
|
58
70
|
|
|
59
|
-
Nothing to install, if you don't want to
|
|
60
|
-
[Config](#config)) and run:
|
|
71
|
+
Nothing to install, if you don't want to:
|
|
61
72
|
|
|
62
73
|
```sh
|
|
74
|
+
npx procdeck init # writes procdeck.config.json from your workspace's dev scripts
|
|
63
75
|
npx procdeck # picks up ./procdeck.config.json
|
|
64
76
|
npx procdeck decks/backend.json # or point it at any config file
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
|
|
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**.
|
|
68
81
|
|
|
69
82
|
Or add it to the project, which is what you want if you keep the config in TypeScript:
|
|
70
83
|
|
|
@@ -72,16 +85,42 @@ Or add it to the project, which is what you want if you keep the config in TypeS
|
|
|
72
85
|
pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
|
|
73
86
|
```
|
|
74
87
|
|
|
88
|
+
## Commands
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
procdeck init # write procdeck.config.json: one proc per workspace dev script
|
|
92
|
+
procdeck up [config] # start (detached) and open the UI — idempotent
|
|
93
|
+
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
|
|
96
|
+
procdeck ls # every running deck on this machine
|
|
97
|
+
procdeck open # open the UI — no port to remember
|
|
98
|
+
procdeck logs [-f] # procdeck's own log (startup, shutdown, errors)
|
|
99
|
+
procdeck up --fg # foreground instead: Ctrl-C stops the deck
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`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.
|
|
109
|
+
|
|
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.
|
|
113
|
+
|
|
75
114
|
## Try the example
|
|
76
115
|
|
|
77
116
|
In a clone of this repo:
|
|
78
117
|
|
|
79
118
|
```sh
|
|
80
119
|
pnpm install
|
|
81
|
-
pnpm dev # builds the UI, starts the example stack
|
|
120
|
+
pnpm dev # builds the UI, starts the example stack in the foreground
|
|
82
121
|
```
|
|
83
122
|
|
|
84
|
-
> **Install footprint.** ~
|
|
123
|
+
> **Install footprint.** ~2 MB, one dependency. The server is shipped as a bundle, so
|
|
85
124
|
> nothing but the PTY bindings is installed — and those are
|
|
86
125
|
> [`@lydell/node-pty`](https://github.com/lydell/node-pty): the same sources as
|
|
87
126
|
> [node-pty](https://github.com/microsoft/node-pty), distributed as per-platform packages.
|
|
@@ -94,6 +133,16 @@ ports) entirely through `${port}` templates. See [`example/`](example/).
|
|
|
94
133
|
|
|
95
134
|
## Config
|
|
96
135
|
|
|
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
|
+
|
|
97
146
|
Two formats, one schema. **JSON** needs nothing from your toolchain — no TypeScript, and
|
|
98
147
|
procdeck itself doesn't have to be a dependency. Point `$schema` at the published schema
|
|
99
148
|
and the editor completes and validates every field:
|
|
@@ -181,7 +230,10 @@ Tips:
|
|
|
181
230
|
| `supervisor.ts` | Effect layer: scoped lifecycle, port assignment, `PubSub` fan-out, per-proc state. |
|
|
182
231
|
| `ports.ts` | Free-port allocation and `pgrep`+`lsof` listening-port discovery. |
|
|
183
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. |
|
|
184
|
-
| `
|
|
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. |
|
|
185
237
|
|
|
186
238
|
Design notes worth keeping:
|
|
187
239
|
|
|
@@ -195,11 +247,21 @@ Design notes worth keeping:
|
|
|
195
247
|
- **SSE, not WebSocket.** Logs only flow towards the browser; commands are ordinary
|
|
196
248
|
POSTs. No upgrade handshake, no extra dependency, and `EventSource` reconnects by
|
|
197
249
|
itself.
|
|
198
|
-
-
|
|
199
|
-
|
|
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).
|
|
200
256
|
- **The supervisor is `Effect.acquireRelease`.** Shutdown is not a code path anyone has
|
|
201
257
|
to remember to call: closing the scope terminates every process tree. The SSE response
|
|
202
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.
|
|
203
265
|
|
|
204
266
|
## Tests
|
|
205
267
|
|
|
@@ -220,8 +282,6 @@ self-heal — if it still fails, PTY allocation itself is probably blocked (sand
|
|
|
220
282
|
|
|
221
283
|
## Status
|
|
222
284
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
Not done yet: an injected FAB overlay inside the developed apps, dependency-death
|
|
227
|
-
policy (a dependency dying does _not_ cascade — deliberately), and an npm release.
|
|
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).
|
package/schema.json
CHANGED
|
@@ -31,6 +31,19 @@
|
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
|
+
"host": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Interface the UI and proxy bind to. Defaults to \"127.0.0.1\" — loopback only, because the UI types into real terminals. Set \"0.0.0.0\" to reach the deck from another machine or from a container's host.",
|
|
37
|
+
"examples": [
|
|
38
|
+
"127.0.0.1",
|
|
39
|
+
"0.0.0.0"
|
|
40
|
+
],
|
|
41
|
+
"allOf": [
|
|
42
|
+
{
|
|
43
|
+
"minLength": 1
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
34
47
|
"procs": {
|
|
35
48
|
"type": "array",
|
|
36
49
|
"items": {
|
|
@@ -125,6 +138,10 @@
|
|
|
125
138
|
],
|
|
126
139
|
"description": "What \"ready\" means for dependents: \"listening\" (default) — the process tree opened a TCP port; \"started\" — it merely spawned, for procs that never listen."
|
|
127
140
|
},
|
|
141
|
+
"observe": {
|
|
142
|
+
"type": "boolean",
|
|
143
|
+
"description": "Route this proc's assigned `${port}` through procdeck's HTTP observer, capturing requests and responses for `procdeck http`. Defaults to true for procs that use `${port}`; false hands the proc the public port directly (no traffic capture)."
|
|
144
|
+
},
|
|
128
145
|
"preflight": {
|
|
129
146
|
"type": "object",
|
|
130
147
|
"properties": {
|
|
@@ -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 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}
|