procdeck 0.2.0 → 0.4.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The package's public entry point — everything a `procdeck.config.ts` needs.
3
+ *
4
+ * The types are written out structurally instead of being derived from the
5
+ * Effect schema in `config.ts`, because a derived type makes the emitted `.d.ts`
6
+ * import `effect`, and effect is bundled into the CLI rather than installed:
7
+ * consumers would be typechecking against a package they don't have.
8
+ *
9
+ * `tests/public.test.ts` asserts at the type level that this mirrors the schema
10
+ * exactly, so the two cannot drift apart silently.
11
+ */
12
+ /** A check that must pass before the proc spawns; failure parks it in `blocked`. */
13
+ export type Preflight = {
14
+ /** Shell command; exit 0 lets the proc spawn. */
15
+ readonly shell: string;
16
+ /** Regex the check's output must also match, for checks that exit 0 either way. */
17
+ readonly expect?: string;
18
+ /** What the human should do when the check fails. */
19
+ readonly hint?: string;
20
+ };
21
+ /** A regex watched in the pane output; a match raises a badge in the UI. */
22
+ export type Alert = {
23
+ readonly pattern: string;
24
+ /** Badge text. */
25
+ readonly label: string;
26
+ };
27
+ /** A single supervised process. Exactly one of `shell` / `cmd` is used. */
28
+ export type ProcSpec = {
29
+ /** Pane label and proxy subdomain (`api` → http://api.localhost:4820). */
30
+ readonly id: string;
31
+ /** Command line handed to `$SHELL -c`. */
32
+ readonly shell?: string;
33
+ /** argv, executed without a shell. */
34
+ readonly cmd?: ReadonlyArray<string>;
35
+ /** Working directory; defaults to the config file's directory. */
36
+ readonly cwd?: string;
37
+ /** Extra environment; values may use `${port}` / `${port:other}`. */
38
+ readonly env?: {
39
+ readonly [key: string]: string;
40
+ };
41
+ /** Spawn on startup. Defaults to true. */
42
+ readonly autostart?: boolean;
43
+ /** URL this process serves, shown in the UI as a link. */
44
+ readonly url?: string;
45
+ /** Ids of procs that must be ready before this one spawns. */
46
+ readonly needs?: ReadonlyArray<string>;
47
+ /** What "ready" means for dependents. Defaults to "listening". */
48
+ readonly readyWhen?: "listening" | "started";
49
+ readonly preflight?: Preflight;
50
+ readonly alerts?: ReadonlyArray<Alert>;
51
+ };
52
+ /** A procdeck deck: the web UI's settings and the processes it supervises. */
53
+ export type ProcdeckConfig = {
54
+ /** Only meaningful in a JSON config; accepted here for symmetry. */
55
+ readonly $schema?: string;
56
+ /** Deck name; defaults to the config directory's basename. */
57
+ readonly name?: string;
58
+ /** Port for the web UI and the `*.localhost` proxy. Defaults to 4820. */
59
+ readonly port?: number;
60
+ /**
61
+ * Interface the UI and proxy bind to. Defaults to "127.0.0.1" (loopback
62
+ * only — the UI types into real terminals). "0.0.0.0" to reach the deck
63
+ * from another machine or from a container's host.
64
+ */
65
+ readonly host?: string;
66
+ readonly procs: ReadonlyArray<ProcSpec>;
67
+ };
68
+ /** Identity helper that gives config files their types. */
69
+ export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
package/dist/public.js ADDED
@@ -0,0 +1,7 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
3
+ // src/public.ts
4
+ var defineConfig = (config) => config;
5
+ export {
6
+ defineConfig
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "procdeck",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",
@@ -38,23 +38,25 @@
38
38
  },
39
39
  "exports": {
40
40
  ".": {
41
- "types": "./dist/config.d.ts",
42
- "import": "./dist/config.js"
41
+ "types": "./dist/public.d.ts",
42
+ "import": "./dist/public.js"
43
43
  }
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "effect": "4.0.0-beta.106",
50
- "node-pty": "^1.1.0"
49
+ "@lydell/node-pty": "1.2.0-beta.15"
51
50
  },
52
51
  "devDependencies": {
53
52
  "@effect/platform-browser": "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 restart/stop/clear.
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 — write a `procdeck.config.json` (see
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
- Open <http://localhost:4820> for the panes. Requires **Node ≥ 22**.
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,23 +85,47 @@ 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
- > **Native dependency note.** procdeck runs panes through [node-pty](https://github.com/microsoft/node-pty),
85
- > which ships a native prebuild. npm and yarn set it up automatically. pnpm ≥ 10 and Bun
86
- > block dependency install scripts by default — in your own project run
87
- > `pnpm approve-builds` and allow `node-pty` (Bun: add it to `trustedDependencies`).
88
- > procdeck also repairs the most common fallout by itself at startup (the darwin
89
- > `spawn-helper` prebuild arriving without its execute bit), so approving is only
90
- > strictly required when node-pty must compile from source (no prebuild for your
91
- > platform).
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.
92
129
 
93
130
  Open <http://localhost:4820> for the panes, then <http://web.localhost:4820> and
94
131
  <http://api.localhost:4820> — the example's two servers found each other (and their own
@@ -96,6 +133,16 @@ ports) entirely through `${port}` templates. See [`example/`](example/).
96
133
 
97
134
  ## Config
98
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
+
99
146
  Two formats, one schema. **JSON** needs nothing from your toolchain — no TypeScript, and
100
147
  procdeck itself doesn't have to be a dependency. Point `$schema` at the published schema
101
148
  and the editor completes and validates every field:
@@ -183,7 +230,10 @@ Tips:
183
230
  | `supervisor.ts` | Effect layer: scoped lifecycle, port assignment, `PubSub` fan-out, per-proc state. |
184
231
  | `ports.ts` | Free-port allocation and `pgrep`+`lsof` listening-port discovery. |
185
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. |
186
- | `cli.ts` | Entry point and signal handling. |
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. |
187
237
 
188
238
  Design notes worth keeping:
189
239
 
@@ -197,11 +247,21 @@ Design notes worth keeping:
197
247
  - **SSE, not WebSocket.** Logs only flow towards the browser; commands are ordinary
198
248
  POSTs. No upgrade handshake, no extra dependency, and `EventSource` reconnects by
199
249
  itself.
200
- - **`PubSub.sliding({ capacity, replay })`** doubles as the fan-out to every open tab
201
- _and_ the scrollback ring buffer — a tab opened late replays the backlog for free.
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).
202
256
  - **The supervisor is `Effect.acquireRelease`.** Shutdown is not a code path anyone has
203
257
  to remember to call: closing the scope terminates every process tree. The SSE response
204
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.
205
265
 
206
266
  ## Tests
207
267
 
@@ -222,8 +282,6 @@ self-heal — if it still fails, PTY allocation itself is probably blocked (sand
222
282
 
223
283
  ## Status
224
284
 
225
- A working prototype, macOS/Linux only (PTYs, `pgrep`, `lsof`). Known prototype-grade
226
- shortcuts: the replay buffer counts events rather than bytes (a chatty proc can evict a
227
- quiet one's history), and statuses are a mutated map rather than `SubscriptionRef`s.
228
- Not done yet: an injected FAB overlay inside the developed apps, dependency-death
229
- 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": {