procdeck 0.1.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-SBVZ6G4G.js +187 -0
- package/dist/cli.js +688 -0
- package/dist/config.d.ts +147 -0
- package/dist/config.js +24 -0
- package/license +21 -0
- package/package.json +73 -0
- package/readme.md +203 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/assets/index-D4mI4ojj.js +83 -0
- package/ui/dist/assets/index-DYfMdGhb.css +32 -0
- package/ui/dist/icon-192.png +0 -0
- package/ui/dist/icon-512.png +0 -0
- package/ui/dist/icon.svg +22 -0
- package/ui/dist/index.html +20 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
export declare const PreflightSchema: Schema.Struct<{
|
|
3
|
+
/** Shell command; exit 0 lets the proc spawn. */
|
|
4
|
+
readonly shell: Schema.String;
|
|
5
|
+
/**
|
|
6
|
+
* Regex that must also match the check's output. For checks that exit 0
|
|
7
|
+
* either way — `wrangler whoami` reports "not authenticated" with exit 0 —
|
|
8
|
+
* so the config doesn't need `| grep` pipelines.
|
|
9
|
+
*/
|
|
10
|
+
readonly expect: Schema.optionalKey<Schema.String>;
|
|
11
|
+
/** What the human should do when the check fails. */
|
|
12
|
+
readonly hint: Schema.optionalKey<Schema.String>;
|
|
13
|
+
}>;
|
|
14
|
+
export type Preflight = typeof PreflightSchema.Type;
|
|
15
|
+
/** A regex watched in the pane output; a match raises a badge in the UI. */
|
|
16
|
+
export declare const AlertSchema: Schema.Struct<{
|
|
17
|
+
readonly pattern: Schema.String;
|
|
18
|
+
/** Badge text. */
|
|
19
|
+
readonly label: Schema.String;
|
|
20
|
+
}>;
|
|
21
|
+
export type Alert = typeof AlertSchema.Type;
|
|
22
|
+
export declare const ProcSpecSchema: Schema.Struct<{
|
|
23
|
+
/** Stable identifier, also the tab label. */
|
|
24
|
+
readonly id: Schema.String;
|
|
25
|
+
/** Command line handed to `$SHELL -c`. */
|
|
26
|
+
readonly shell: Schema.optionalKey<Schema.String>;
|
|
27
|
+
/** argv, executed without a shell. */
|
|
28
|
+
readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
29
|
+
/** Working directory; defaults to the config file's directory. */
|
|
30
|
+
readonly cwd: Schema.optionalKey<Schema.String>;
|
|
31
|
+
/** Extra environment on top of the inherited one. */
|
|
32
|
+
readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
33
|
+
/** Spawn on startup. Defaults to true. */
|
|
34
|
+
readonly autostart: Schema.optionalKey<Schema.Boolean>;
|
|
35
|
+
/** URL this process serves, shown in the UI as a link. */
|
|
36
|
+
readonly url: Schema.optionalKey<Schema.String>;
|
|
37
|
+
/**
|
|
38
|
+
* Names of procs that must be ready before this one spawns. Declarative:
|
|
39
|
+
* readiness is auto-detected, no port numbers in the config.
|
|
40
|
+
*/
|
|
41
|
+
readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
42
|
+
/**
|
|
43
|
+
* What "ready" means for dependents: "listening" (default) — the process
|
|
44
|
+
* tree opened a TCP port; "started" — it merely spawned (for procs that
|
|
45
|
+
* never listen).
|
|
46
|
+
*/
|
|
47
|
+
readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
|
|
48
|
+
/** Check that must pass before this proc spawns; failure shows as "blocked". */
|
|
49
|
+
readonly preflight: Schema.optionalKey<Schema.Struct<{
|
|
50
|
+
/** Shell command; exit 0 lets the proc spawn. */
|
|
51
|
+
readonly shell: Schema.String;
|
|
52
|
+
/**
|
|
53
|
+
* Regex that must also match the check's output. For checks that exit 0
|
|
54
|
+
* either way — `wrangler whoami` reports "not authenticated" with exit 0 —
|
|
55
|
+
* so the config doesn't need `| grep` pipelines.
|
|
56
|
+
*/
|
|
57
|
+
readonly expect: Schema.optionalKey<Schema.String>;
|
|
58
|
+
/** What the human should do when the check fails. */
|
|
59
|
+
readonly hint: Schema.optionalKey<Schema.String>;
|
|
60
|
+
}>>;
|
|
61
|
+
/** Output patterns that raise a badge when they appear in the pane. */
|
|
62
|
+
readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
63
|
+
readonly pattern: Schema.String;
|
|
64
|
+
/** Badge text. */
|
|
65
|
+
readonly label: Schema.String;
|
|
66
|
+
}>>>;
|
|
67
|
+
}>;
|
|
68
|
+
export type ProcSpec = typeof ProcSpecSchema.Type;
|
|
69
|
+
/** Default port of the UI + proxy server. */
|
|
70
|
+
export declare const DEFAULT_UI_PORT = 4820;
|
|
71
|
+
/** Whether the spec asks procdeck to assign it a port (`${port}` anywhere). */
|
|
72
|
+
export declare const usesOwnPort: (spec: ProcSpec) => boolean;
|
|
73
|
+
/** Substitute `${port}` / `${port:id}` with the ports assigned at startup. */
|
|
74
|
+
export declare const resolveSpec: (spec: ProcSpec, assigned: Map<string, number>) => ProcSpec;
|
|
75
|
+
export declare const ProcdeckConfigSchema: Schema.Struct<{
|
|
76
|
+
/**
|
|
77
|
+
* Deck name — the tab title and the name of the installed web app.
|
|
78
|
+
* Defaults to the config directory's basename.
|
|
79
|
+
*/
|
|
80
|
+
readonly name: Schema.optionalKey<Schema.String>;
|
|
81
|
+
/** Port for the web UI. Defaults to 4820. */
|
|
82
|
+
readonly port: Schema.optionalKey<Schema.Number>;
|
|
83
|
+
readonly procs: Schema.$Array<Schema.Struct<{
|
|
84
|
+
/** Stable identifier, also the tab label. */
|
|
85
|
+
readonly id: Schema.String;
|
|
86
|
+
/** Command line handed to `$SHELL -c`. */
|
|
87
|
+
readonly shell: Schema.optionalKey<Schema.String>;
|
|
88
|
+
/** argv, executed without a shell. */
|
|
89
|
+
readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
90
|
+
/** Working directory; defaults to the config file's directory. */
|
|
91
|
+
readonly cwd: Schema.optionalKey<Schema.String>;
|
|
92
|
+
/** Extra environment on top of the inherited one. */
|
|
93
|
+
readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
94
|
+
/** Spawn on startup. Defaults to true. */
|
|
95
|
+
readonly autostart: Schema.optionalKey<Schema.Boolean>;
|
|
96
|
+
/** URL this process serves, shown in the UI as a link. */
|
|
97
|
+
readonly url: Schema.optionalKey<Schema.String>;
|
|
98
|
+
/**
|
|
99
|
+
* Names of procs that must be ready before this one spawns. Declarative:
|
|
100
|
+
* readiness is auto-detected, no port numbers in the config.
|
|
101
|
+
*/
|
|
102
|
+
readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
103
|
+
/**
|
|
104
|
+
* What "ready" means for dependents: "listening" (default) — the process
|
|
105
|
+
* tree opened a TCP port; "started" — it merely spawned (for procs that
|
|
106
|
+
* never listen).
|
|
107
|
+
*/
|
|
108
|
+
readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
|
|
109
|
+
/** Check that must pass before this proc spawns; failure shows as "blocked". */
|
|
110
|
+
readonly preflight: Schema.optionalKey<Schema.Struct<{
|
|
111
|
+
/** Shell command; exit 0 lets the proc spawn. */
|
|
112
|
+
readonly shell: Schema.String;
|
|
113
|
+
/**
|
|
114
|
+
* Regex that must also match the check's output. For checks that exit 0
|
|
115
|
+
* either way — `wrangler whoami` reports "not authenticated" with exit 0 —
|
|
116
|
+
* so the config doesn't need `| grep` pipelines.
|
|
117
|
+
*/
|
|
118
|
+
readonly expect: Schema.optionalKey<Schema.String>;
|
|
119
|
+
/** What the human should do when the check fails. */
|
|
120
|
+
readonly hint: Schema.optionalKey<Schema.String>;
|
|
121
|
+
}>>;
|
|
122
|
+
/** Output patterns that raise a badge when they appear in the pane. */
|
|
123
|
+
readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
124
|
+
readonly pattern: Schema.String;
|
|
125
|
+
/** Badge text. */
|
|
126
|
+
readonly label: Schema.String;
|
|
127
|
+
}>>>;
|
|
128
|
+
}>>;
|
|
129
|
+
}>;
|
|
130
|
+
export type ProcdeckConfig = typeof ProcdeckConfigSchema.Type;
|
|
131
|
+
/** Identity helper that gives config files their types. */
|
|
132
|
+
export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
|
|
133
|
+
export type LoadedConfig = {
|
|
134
|
+
config: ProcdeckConfig;
|
|
135
|
+
/** Directory of the config file — the default cwd for every process. */
|
|
136
|
+
root: string;
|
|
137
|
+
/** `config.name`, or the root directory's basename. */
|
|
138
|
+
name: string;
|
|
139
|
+
};
|
|
140
|
+
declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"ConfigError", {
|
|
141
|
+
readonly file: Schema.String;
|
|
142
|
+
readonly message: Schema.String;
|
|
143
|
+
}>, import("effect/Cause").YieldableError>;
|
|
144
|
+
export declare class ConfigError extends ConfigError_base {
|
|
145
|
+
}
|
|
146
|
+
export declare const loadConfig: (file: string) => Effect.Effect<LoadedConfig, ConfigError, never>;
|
|
147
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertSchema,
|
|
3
|
+
ConfigError,
|
|
4
|
+
DEFAULT_UI_PORT,
|
|
5
|
+
PreflightSchema,
|
|
6
|
+
ProcSpecSchema,
|
|
7
|
+
ProcdeckConfigSchema,
|
|
8
|
+
defineConfig,
|
|
9
|
+
loadConfig,
|
|
10
|
+
resolveSpec,
|
|
11
|
+
usesOwnPort
|
|
12
|
+
} from "./chunk-SBVZ6G4G.js";
|
|
13
|
+
export {
|
|
14
|
+
AlertSchema,
|
|
15
|
+
ConfigError,
|
|
16
|
+
DEFAULT_UI_PORT,
|
|
17
|
+
PreflightSchema,
|
|
18
|
+
ProcSpecSchema,
|
|
19
|
+
ProcdeckConfigSchema,
|
|
20
|
+
defineConfig,
|
|
21
|
+
loadConfig,
|
|
22
|
+
resolveSpec,
|
|
23
|
+
usesOwnPort
|
|
24
|
+
};
|
package/license
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksandr Kondaurov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "procdeck",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"dev-server",
|
|
8
|
+
"multiplexer",
|
|
9
|
+
"process-manager",
|
|
10
|
+
"pty",
|
|
11
|
+
"reverse-proxy",
|
|
12
|
+
"terminal",
|
|
13
|
+
"xterm"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/kondaurovDev/procdeck",
|
|
16
|
+
"bugs": "https://github.com/kondaurovDev/procdeck/issues",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Aleksandr Kondaurov",
|
|
20
|
+
"email": "kondaurov.dev@gmail.com"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/kondaurovDev/procdeck",
|
|
25
|
+
"directory": "packages/procdeck"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.18"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"ui/dist"
|
|
34
|
+
],
|
|
35
|
+
"bin": {
|
|
36
|
+
"procdeck": "./dist/cli.js"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/config.d.ts",
|
|
41
|
+
"import": "./dist/config.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"effect": "4.0.0-beta.106",
|
|
49
|
+
"node-pty": "^1.1.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@effect/platform-browser": "4.0.0-beta.106",
|
|
53
|
+
"@types/node": "^22.10.1",
|
|
54
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
55
|
+
"@xterm/addon-search": "^0.16.0",
|
|
56
|
+
"@xterm/xterm": "^6.0.0",
|
|
57
|
+
"foldkit": "^0.141.2",
|
|
58
|
+
"tsup": "^8.5.1",
|
|
59
|
+
"typescript": "^7.0.2",
|
|
60
|
+
"vite": "^6.2.0",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"ui:dev": "vite ui",
|
|
65
|
+
"build": "pnpm run build:meta && pnpm run build:ui && pnpm run build:js && pnpm run build:types",
|
|
66
|
+
"build:meta": "cp ../../readme.md ../../license .",
|
|
67
|
+
"build:ui": "vite build ui",
|
|
68
|
+
"build:js": "tsup",
|
|
69
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
70
|
+
"check:types": "tsc --noEmit && tsc -p ui --noEmit",
|
|
71
|
+
"test": "vitest run"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# procdeck
|
|
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**.
|
|
7
|
+
|
|
8
|
+
```
|
|
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
|
+
└──────────┴───────────────────────────────────────────────┘
|
|
16
|
+
```
|
|
17
|
+
|
|
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
|
+
- **Whole-tree restarts.** `pnpm` → `wrangler` → `workerd`: killing the pid would leave
|
|
48
|
+
the grandchild holding the port. procdeck signals the process group, escalating
|
|
49
|
+
SIGTERM → SIGKILL, and Ctrl-C tears down every tree it spawned.
|
|
50
|
+
- **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.
|
|
52
|
+
- **Installable as an app.** The UI ships a web-app manifest named after the deck
|
|
53
|
+
(`name` in the config, else the directory), so "⤓ install" in the bar — or File →
|
|
54
|
+
Add to Dock in Safari — gives each project its own window and Dock icon. Works over
|
|
55
|
+
plain `http://localhost`, no HTTPS needed.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires **Node ≥ 22.18**: `procdeck.config.ts` is a TypeScript file, loaded through
|
|
64
|
+
Node's built-in type stripping.
|
|
65
|
+
|
|
66
|
+
Write a `procdeck.config.ts` next to your `package.json` (see [Config](#config)), then:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx procdeck # defaults to ./procdeck.config.ts
|
|
70
|
+
npx procdeck decks/backend.ts # or point it at any config file
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Open <http://localhost:4820> for the panes.
|
|
74
|
+
|
|
75
|
+
## Try the example
|
|
76
|
+
|
|
77
|
+
In a clone of this repo:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
pnpm install
|
|
81
|
+
pnpm dev # builds the UI, starts the example stack
|
|
82
|
+
```
|
|
83
|
+
|
|
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).
|
|
92
|
+
|
|
93
|
+
Open <http://localhost:4820> for the panes, then <http://web.localhost:4820> and
|
|
94
|
+
<http://api.localhost:4820> — the example's two servers found each other (and their own
|
|
95
|
+
ports) entirely through `${port}` templates. See [`example/`](example/).
|
|
96
|
+
|
|
97
|
+
## Config
|
|
98
|
+
|
|
99
|
+
`procdeck.config.ts` — a TypeScript file, validated by an Effect schema:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { defineConfig } from "procdeck"
|
|
103
|
+
|
|
104
|
+
export default defineConfig({
|
|
105
|
+
port: 4820, // UI + proxy port
|
|
106
|
+
procs: [
|
|
107
|
+
{
|
|
108
|
+
id: "api", // pane name and the proxy subdomain
|
|
109
|
+
shell: "pnpm --filter api dev", // or cmd: ["node", "server.js"]
|
|
110
|
+
env: { PORT: "${port}" }, // ask procdeck for a free port
|
|
111
|
+
preflight: { // gate: must pass before spawning
|
|
112
|
+
shell: "wrangler whoami",
|
|
113
|
+
expect: "You are logged in",
|
|
114
|
+
hint: "run `wrangler login`, then Start",
|
|
115
|
+
},
|
|
116
|
+
alerts: [{ pattern: "ERROR", label: "check me" }],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "web",
|
|
120
|
+
shell: "pnpm --filter web dev",
|
|
121
|
+
env: { PORT: "${port}", API_URL: "http://localhost:${port:api}" },
|
|
122
|
+
needs: ["api"], // wait until api is listening
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "worker",
|
|
126
|
+
shell: "node worker.js",
|
|
127
|
+
readyWhen: "started", // never listens; running = ready
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Full field reference: [`packages/procdeck/src/config.ts`](packages/procdeck/src/config.ts)
|
|
134
|
+
(the schema is the documentation).
|
|
135
|
+
|
|
136
|
+
Tips:
|
|
137
|
+
|
|
138
|
+
- Give tools that auto-increment on a busy port (vite) a `strictPort` flag, so a
|
|
139
|
+
violated port assignment fails loudly instead of drifting.
|
|
140
|
+
- A good pattern for app dev scripts: `vite dev --port ${PORT:-3000} --strictPort` —
|
|
141
|
+
fixed default when run by hand, assigned port under procdeck.
|
|
142
|
+
- The proxy is for humans and browsers. Scripts and server-to-server calls should use
|
|
143
|
+
the injected env (`${port:api}`) — system resolvers don't all know `*.localhost`.
|
|
144
|
+
|
|
145
|
+
## How it fits together
|
|
146
|
+
|
|
147
|
+
| Path | What |
|
|
148
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| `packages/procdeck/src` | The server: plain-Node PTY layer, Effect supervisor, HTTP/SSE server with the reverse proxy. |
|
|
150
|
+
| `packages/procdeck/ui` | [Foldkit](https://foldkit.dev) (Elm-on-Effect) app; built by vite, served statically by the server. |
|
|
151
|
+
| `example/` | Self-contained demo stack (Node one-liners) exercising every feature. |
|
|
152
|
+
|
|
153
|
+
| File | What |
|
|
154
|
+
| --------------- | ---------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| `proc.ts` | Plain Node: PTY spawn, process-group kill, SIGTERM → SIGKILL escalation. |
|
|
156
|
+
| `config.ts` | Effect `Schema` for the config, `${port}` templating, loader. |
|
|
157
|
+
| `supervisor.ts` | Effect layer: scoped lifecycle, port assignment, `PubSub` fan-out, per-proc state. |
|
|
158
|
+
| `ports.ts` | Free-port allocation and `pgrep`+`lsof` listening-port discovery. |
|
|
159
|
+
| `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. |
|
|
160
|
+
| `cli.ts` | Entry point and signal handling. |
|
|
161
|
+
|
|
162
|
+
Design notes worth keeping:
|
|
163
|
+
|
|
164
|
+
- **Effect end to end.** The server is Effect; the UI is a Foldkit app — one Model (a
|
|
165
|
+
Schema), pure `update`, side effects as Commands. Terminals are imperative islands via
|
|
166
|
+
`Mount.defineStream`; PTY log chunks bypass the Model (`ReceivedLog` → `WriteTerminal`
|
|
167
|
+
Command into a module-level terminal registry — the view never renders them); the SSE
|
|
168
|
+
subscription is gated on a Model condition ("every pane mounted"), so the replayed
|
|
169
|
+
backlog ordering is declarative, not sequenced by hand.
|
|
170
|
+
- **The server has no bundler.** It runs as `.ts` via Node type stripping (Node ≥ 22.18).
|
|
171
|
+
- **SSE, not WebSocket.** Logs only flow towards the browser; commands are ordinary
|
|
172
|
+
POSTs. No upgrade handshake, no extra dependency, and `EventSource` reconnects by
|
|
173
|
+
itself.
|
|
174
|
+
- **`PubSub.sliding({ capacity, replay })`** doubles as the fan-out to every open tab
|
|
175
|
+
_and_ the scrollback ring buffer — a tab opened late replays the backlog for free.
|
|
176
|
+
- **The supervisor is `Effect.acquireRelease`.** Shutdown is not a code path anyone has
|
|
177
|
+
to remember to call: closing the scope terminates every process tree. The SSE response
|
|
178
|
+
stream lives in the request scope, so a dropped tab cleans up its subscription too.
|
|
179
|
+
|
|
180
|
+
## Tests
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
pnpm test
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Integration tests with real (tiny) processes — `node -e` one-liners and shells with
|
|
187
|
+
background children — because the risks live at the OS boundary: PTY detection, group
|
|
188
|
+
kills, SIGTERM-ignoring survivors, scope teardown. Mocking any of that would test the
|
|
189
|
+
mock. They need a real PTY (`/dev/ptmx`), so they won't run inside sandboxes that block
|
|
190
|
+
PTY allocation.
|
|
191
|
+
|
|
192
|
+
Gotcha: if every spawn fails with `posix_spawnp failed.`, the usual culprit is the
|
|
193
|
+
node-pty `spawn-helper` prebuild arriving without its execute bit (pnpm/Bun skipping
|
|
194
|
+
its install script). `proc.ts` restores the bit at import time, so this should
|
|
195
|
+
self-heal — if it still fails, PTY allocation itself is probably blocked (sandboxes).
|
|
196
|
+
|
|
197
|
+
## Status
|
|
198
|
+
|
|
199
|
+
A working prototype, macOS/Linux only (PTYs, `pgrep`, `lsof`). Known prototype-grade
|
|
200
|
+
shortcuts: the replay buffer counts events rather than bytes (a chatty proc can evict a
|
|
201
|
+
quiet one's history), and statuses are a mutated map rather than `SubscriptionRef`s.
|
|
202
|
+
Not done yet: an injected FAB overlay inside the developed apps, dependency-death
|
|
203
|
+
policy (a dependency dying does _not_ cascade — deliberately), and an npm release.
|
|
Binary file
|