procdeck 0.1.0 → 0.3.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-MLKGABMK.js +9 -0
- package/dist/cli.js +28989 -445
- package/dist/public.d.ts +63 -0
- package/dist/public.js +7 -0
- package/package.json +9 -7
- package/readme.md +42 -18
- package/schema.json +212 -0
- package/dist/chunk-SBVZ6G4G.js +0 -187
- package/dist/config.d.ts +0 -147
- package/dist/config.js +0 -24
package/dist/public.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
readonly procs: ReadonlyArray<ProcSpec>;
|
|
61
|
+
};
|
|
62
|
+
/** Identity helper that gives config files their types. */
|
|
63
|
+
export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
|
package/dist/public.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": ">=22
|
|
29
|
+
"node": ">=22"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
|
+
"schema.json",
|
|
33
34
|
"ui/dist"
|
|
34
35
|
],
|
|
35
36
|
"bin": {
|
|
@@ -37,19 +38,19 @@
|
|
|
37
38
|
},
|
|
38
39
|
"exports": {
|
|
39
40
|
".": {
|
|
40
|
-
"types": "./dist/
|
|
41
|
-
"import": "./dist/
|
|
41
|
+
"types": "./dist/public.d.ts",
|
|
42
|
+
"import": "./dist/public.js"
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
"publishConfig": {
|
|
45
46
|
"access": "public"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"
|
|
49
|
-
"node-pty": "^1.1.0"
|
|
49
|
+
"@lydell/node-pty": "1.2.0-beta.15"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@effect/platform-browser": "4.0.0-beta.106",
|
|
53
|
+
"effect": "4.0.0-beta.106",
|
|
53
54
|
"@types/node": "^22.10.1",
|
|
54
55
|
"@xterm/addon-fit": "^0.11.0",
|
|
55
56
|
"@xterm/addon-search": "^0.16.0",
|
|
@@ -62,8 +63,9 @@
|
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
65
|
"ui:dev": "vite ui",
|
|
65
|
-
"build": "pnpm run build:meta && pnpm run build:ui && pnpm run build:js && pnpm run build:types",
|
|
66
|
+
"build": "pnpm run build:meta && pnpm run build:schema && pnpm run build:ui && pnpm run build:js && pnpm run build:types",
|
|
66
67
|
"build:meta": "cp ../../readme.md ../../license .",
|
|
68
|
+
"build:schema": "node scripts/gen-schema.ts",
|
|
67
69
|
"build:ui": "vite build ui",
|
|
68
70
|
"build:js": "tsup",
|
|
69
71
|
"build:types": "tsc -p tsconfig.build.json",
|
package/readme.md
CHANGED
|
@@ -56,22 +56,22 @@ which port is which**.
|
|
|
56
56
|
|
|
57
57
|
## Install
|
|
58
58
|
|
|
59
|
+
Nothing to install, if you don't want to — write a `procdeck.config.json` (see
|
|
60
|
+
[Config](#config)) and run:
|
|
61
|
+
|
|
59
62
|
```sh
|
|
60
|
-
|
|
63
|
+
npx procdeck # picks up ./procdeck.config.json
|
|
64
|
+
npx procdeck decks/backend.json # or point it at any config file
|
|
61
65
|
```
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
Node's built-in type stripping.
|
|
67
|
+
Open <http://localhost:4820> for the panes. Requires **Node ≥ 22**.
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
Or add it to the project, which is what you want if you keep the config in TypeScript:
|
|
67
70
|
|
|
68
71
|
```sh
|
|
69
|
-
|
|
70
|
-
npx procdeck decks/backend.ts # or point it at any config file
|
|
72
|
+
pnpm add -D procdeck # npm i -D procdeck · bun add -d procdeck
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
Open <http://localhost:4820> for the panes.
|
|
74
|
-
|
|
75
75
|
## Try the example
|
|
76
76
|
|
|
77
77
|
In a clone of this repo:
|
|
@@ -81,14 +81,12 @@ pnpm install
|
|
|
81
81
|
pnpm dev # builds the UI, starts the example stack
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
> **
|
|
85
|
-
>
|
|
86
|
-
>
|
|
87
|
-
>
|
|
88
|
-
>
|
|
89
|
-
> `
|
|
90
|
-
> strictly required when node-pty must compile from source (no prebuild for your
|
|
91
|
-
> platform).
|
|
84
|
+
> **Install footprint.** ~1.8 MB, one dependency. The server is shipped as a bundle, so
|
|
85
|
+
> nothing but the PTY bindings is installed — and those are
|
|
86
|
+
> [`@lydell/node-pty`](https://github.com/lydell/node-pty): the same sources as
|
|
87
|
+
> [node-pty](https://github.com/microsoft/node-pty), distributed as per-platform packages.
|
|
88
|
+
> Nothing compiles and no install script runs, so there is no `pnpm approve-builds` /
|
|
89
|
+
> `trustedDependencies` detour, and no compiler is needed on Linux.
|
|
92
90
|
|
|
93
91
|
Open <http://localhost:4820> for the panes, then <http://web.localhost:4820> and
|
|
94
92
|
<http://api.localhost:4820> — the example's two servers found each other (and their own
|
|
@@ -96,7 +94,31 @@ ports) entirely through `${port}` templates. See [`example/`](example/).
|
|
|
96
94
|
|
|
97
95
|
## Config
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
Two formats, one schema. **JSON** needs nothing from your toolchain — no TypeScript, and
|
|
98
|
+
procdeck itself doesn't have to be a dependency. Point `$schema` at the published schema
|
|
99
|
+
and the editor completes and validates every field:
|
|
100
|
+
|
|
101
|
+
```jsonc
|
|
102
|
+
{
|
|
103
|
+
"$schema": "https://unpkg.com/procdeck/schema.json",
|
|
104
|
+
"port": 4820,
|
|
105
|
+
"procs": [
|
|
106
|
+
{ "id": "api", "shell": "pnpm --filter api dev", "env": { "PORT": "${port}" } },
|
|
107
|
+
{
|
|
108
|
+
"id": "web",
|
|
109
|
+
"shell": "pnpm --filter web dev",
|
|
110
|
+
"env": { "PORT": "${port}", "API_URL": "http://localhost:${port:api}" },
|
|
111
|
+
"needs": ["api"]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
(Installed locally, `"./node_modules/procdeck/schema.json"` works too and needs no
|
|
118
|
+
network.)
|
|
119
|
+
|
|
120
|
+
**TypeScript** buys comments and computed configs, at the price of Node ≥ 22.18 (native
|
|
121
|
+
type stripping) and procdeck in your dependencies:
|
|
100
122
|
|
|
101
123
|
```ts
|
|
102
124
|
import { defineConfig } from "procdeck"
|
|
@@ -131,7 +153,9 @@ export default defineConfig({
|
|
|
131
153
|
```
|
|
132
154
|
|
|
133
155
|
Full field reference: [`packages/procdeck/src/config.ts`](packages/procdeck/src/config.ts)
|
|
134
|
-
(the schema is the documentation
|
|
156
|
+
(the schema is the documentation — `schema.json` is generated from it at build time, so
|
|
157
|
+
the two formats validate identically). Config files are looked up in this order when no
|
|
158
|
+
path is given: `procdeck.config.json`, `.ts`, `.js`, `.mjs`.
|
|
135
159
|
|
|
136
160
|
Tips:
|
|
137
161
|
|
package/schema.json
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://unpkg.com/procdeck/schema.json",
|
|
4
|
+
"title": "procdeck config",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"$schema": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "URL of the procdeck JSON schema, for editor completion and validation.",
|
|
10
|
+
"examples": [
|
|
11
|
+
"https://unpkg.com/procdeck/schema.json"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"name": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Deck name — the tab title and the name of the installed web app. Defaults to the config directory's basename.",
|
|
17
|
+
"examples": [
|
|
18
|
+
"my-app"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"port": {
|
|
22
|
+
"description": "Port for the web UI and the `*.localhost` proxy. Defaults to 4820.",
|
|
23
|
+
"examples": [
|
|
24
|
+
4820
|
|
25
|
+
],
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"allOf": [
|
|
28
|
+
{
|
|
29
|
+
"minimum": 1,
|
|
30
|
+
"maximum": 65535
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"procs": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"id": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Stable identifier: the pane label, and the proxy subdomain (`api` → http://api.localhost:4820).",
|
|
42
|
+
"examples": [
|
|
43
|
+
"api",
|
|
44
|
+
"web"
|
|
45
|
+
],
|
|
46
|
+
"allOf": [
|
|
47
|
+
{
|
|
48
|
+
"minLength": 1
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"shell": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Command line handed to `$SHELL -c` — pipes, `&&` and globs work. Exactly one of `shell` or `cmd` must be set.",
|
|
55
|
+
"examples": [
|
|
56
|
+
"pnpm --filter api dev"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"cmd": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": {
|
|
62
|
+
"type": "string"
|
|
63
|
+
},
|
|
64
|
+
"description": "argv, executed directly without a shell. Exactly one of `shell` or `cmd` must be set.",
|
|
65
|
+
"examples": [
|
|
66
|
+
[
|
|
67
|
+
"node",
|
|
68
|
+
"server.js"
|
|
69
|
+
]
|
|
70
|
+
],
|
|
71
|
+
"allOf": [
|
|
72
|
+
{
|
|
73
|
+
"minItems": 1
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"cwd": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Working directory; defaults to the config file's directory.",
|
|
80
|
+
"examples": [
|
|
81
|
+
"packages/api"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"env": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"additionalProperties": {
|
|
87
|
+
"type": "string"
|
|
88
|
+
},
|
|
89
|
+
"description": "Extra environment on top of the inherited one. Values may use `${port}` / `${port:other}` templates.",
|
|
90
|
+
"examples": [
|
|
91
|
+
{
|
|
92
|
+
"PORT": "${port}",
|
|
93
|
+
"API_URL": "http://localhost:${port:api}"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"autostart": {
|
|
98
|
+
"type": "boolean",
|
|
99
|
+
"description": "Spawn on startup. Defaults to true; false leaves the pane idle until Start."
|
|
100
|
+
},
|
|
101
|
+
"url": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "URL this process serves, shown in the UI as a link. Also pins readiness to that URL's port.",
|
|
104
|
+
"examples": [
|
|
105
|
+
"http://localhost:${port}"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"needs": {
|
|
109
|
+
"type": "array",
|
|
110
|
+
"items": {
|
|
111
|
+
"type": "string"
|
|
112
|
+
},
|
|
113
|
+
"description": "Ids of procs that must be ready before this one spawns. Declarative: readiness is auto-detected, no port numbers in the config.",
|
|
114
|
+
"examples": [
|
|
115
|
+
[
|
|
116
|
+
"api"
|
|
117
|
+
]
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"readyWhen": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"enum": [
|
|
123
|
+
"listening",
|
|
124
|
+
"started"
|
|
125
|
+
],
|
|
126
|
+
"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
|
+
},
|
|
128
|
+
"preflight": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"shell": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "Shell command; exit 0 lets the proc spawn.",
|
|
134
|
+
"examples": [
|
|
135
|
+
"wrangler whoami"
|
|
136
|
+
],
|
|
137
|
+
"allOf": [
|
|
138
|
+
{
|
|
139
|
+
"minLength": 1
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"expect": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Regex the check's output must also match. For checks that exit 0 either way — `wrangler whoami` reports \"not authenticated\" with exit 0 — so the config needs no `| grep` pipelines.",
|
|
146
|
+
"examples": [
|
|
147
|
+
"You are logged in"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"hint": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"description": "What the human should do when the check fails; shown on the blocked pane.",
|
|
153
|
+
"examples": [
|
|
154
|
+
"run `wrangler login`, then Start"
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"required": [
|
|
159
|
+
"shell"
|
|
160
|
+
],
|
|
161
|
+
"additionalProperties": false,
|
|
162
|
+
"description": "A gate that must pass before the process spawns — e.g. `wrangler whoami` before a proc whose tree needs a Cloudflare token. Keeps interactive auth flows out of supervised panes, where a restart would kill their callback servers mid-handshake."
|
|
163
|
+
},
|
|
164
|
+
"alerts": {
|
|
165
|
+
"type": "array",
|
|
166
|
+
"items": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"properties": {
|
|
169
|
+
"pattern": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"description": "Regex matched against a rolling tail of the pane's output.",
|
|
172
|
+
"examples": [
|
|
173
|
+
"ERROR",
|
|
174
|
+
"not authenticated"
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
"label": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"description": "Badge text shown in the UI when the pattern matches.",
|
|
180
|
+
"examples": [
|
|
181
|
+
"needs login"
|
|
182
|
+
],
|
|
183
|
+
"allOf": [
|
|
184
|
+
{
|
|
185
|
+
"minLength": 1
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"required": [
|
|
191
|
+
"pattern",
|
|
192
|
+
"label"
|
|
193
|
+
],
|
|
194
|
+
"additionalProperties": false,
|
|
195
|
+
"description": "A regex watched in the pane output; a match raises a badge in the UI."
|
|
196
|
+
},
|
|
197
|
+
"description": "Output patterns that raise a badge when they appear in the pane."
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": [
|
|
201
|
+
"id"
|
|
202
|
+
],
|
|
203
|
+
"additionalProperties": false
|
|
204
|
+
},
|
|
205
|
+
"description": "The processes this deck supervises."
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"required": [
|
|
209
|
+
"procs"
|
|
210
|
+
],
|
|
211
|
+
"additionalProperties": false
|
|
212
|
+
}
|
package/dist/chunk-SBVZ6G4G.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
import { pathToFileURL } from "url";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import { Effect, Schema } from "effect";
|
|
5
|
-
var regexSource = Schema.String.pipe(
|
|
6
|
-
Schema.check(
|
|
7
|
-
Schema.makeFilter((pattern) => {
|
|
8
|
-
try {
|
|
9
|
-
new RegExp(pattern);
|
|
10
|
-
return void 0;
|
|
11
|
-
} catch {
|
|
12
|
-
return "expected a valid regular expression";
|
|
13
|
-
}
|
|
14
|
-
})
|
|
15
|
-
)
|
|
16
|
-
);
|
|
17
|
-
var PreflightSchema = Schema.Struct({
|
|
18
|
-
/** Shell command; exit 0 lets the proc spawn. */
|
|
19
|
-
shell: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
20
|
-
/**
|
|
21
|
-
* Regex that must also match the check's output. For checks that exit 0
|
|
22
|
-
* either way — `wrangler whoami` reports "not authenticated" with exit 0 —
|
|
23
|
-
* so the config doesn't need `| grep` pipelines.
|
|
24
|
-
*/
|
|
25
|
-
expect: Schema.optionalKey(regexSource),
|
|
26
|
-
/** What the human should do when the check fails. */
|
|
27
|
-
hint: Schema.optionalKey(Schema.String)
|
|
28
|
-
});
|
|
29
|
-
var AlertSchema = Schema.Struct({
|
|
30
|
-
pattern: regexSource,
|
|
31
|
-
/** Badge text. */
|
|
32
|
-
label: Schema.String.pipe(Schema.check(Schema.isMinLength(1)))
|
|
33
|
-
});
|
|
34
|
-
var ProcSpecSchema = Schema.Struct({
|
|
35
|
-
/** Stable identifier, also the tab label. */
|
|
36
|
-
id: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
37
|
-
/** Command line handed to `$SHELL -c`. */
|
|
38
|
-
shell: Schema.optionalKey(Schema.String),
|
|
39
|
-
/** argv, executed without a shell. */
|
|
40
|
-
cmd: Schema.optionalKey(Schema.Array(Schema.String).pipe(Schema.check(Schema.isMinLength(1)))),
|
|
41
|
-
/** Working directory; defaults to the config file's directory. */
|
|
42
|
-
cwd: Schema.optionalKey(Schema.String),
|
|
43
|
-
/** Extra environment on top of the inherited one. */
|
|
44
|
-
env: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
45
|
-
/** Spawn on startup. Defaults to true. */
|
|
46
|
-
autostart: Schema.optionalKey(Schema.Boolean),
|
|
47
|
-
/** URL this process serves, shown in the UI as a link. */
|
|
48
|
-
url: Schema.optionalKey(Schema.String),
|
|
49
|
-
/**
|
|
50
|
-
* Names of procs that must be ready before this one spawns. Declarative:
|
|
51
|
-
* readiness is auto-detected, no port numbers in the config.
|
|
52
|
-
*/
|
|
53
|
-
needs: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
54
|
-
/**
|
|
55
|
-
* What "ready" means for dependents: "listening" (default) — the process
|
|
56
|
-
* tree opened a TCP port; "started" — it merely spawned (for procs that
|
|
57
|
-
* never listen).
|
|
58
|
-
*/
|
|
59
|
-
readyWhen: Schema.optionalKey(Schema.Literals(["listening", "started"])),
|
|
60
|
-
/** Check that must pass before this proc spawns; failure shows as "blocked". */
|
|
61
|
-
preflight: Schema.optionalKey(PreflightSchema),
|
|
62
|
-
/** Output patterns that raise a badge when they appear in the pane. */
|
|
63
|
-
alerts: Schema.optionalKey(Schema.Array(AlertSchema))
|
|
64
|
-
}).pipe(
|
|
65
|
-
Schema.check(
|
|
66
|
-
Schema.makeFilter(
|
|
67
|
-
(spec) => spec.shell === void 0 === (spec.cmd === void 0) ? "expected exactly one of `shell` or `cmd`" : void 0
|
|
68
|
-
)
|
|
69
|
-
)
|
|
70
|
-
);
|
|
71
|
-
var DEFAULT_UI_PORT = 4820;
|
|
72
|
-
var PORT_TEMPLATE = /\$\{port(?::([^}]+))?\}/g;
|
|
73
|
-
var templatedStrings = (spec) => [spec.shell, ...spec.cmd ?? [], ...Object.values(spec.env ?? {}), spec.url].filter(
|
|
74
|
-
(text) => text !== void 0
|
|
75
|
-
);
|
|
76
|
-
var usesOwnPort = (spec) => templatedStrings(spec).some(
|
|
77
|
-
(text) => [...text.matchAll(PORT_TEMPLATE)].some((match) => match[1] === void 0)
|
|
78
|
-
);
|
|
79
|
-
var portRefs = (spec) => templatedStrings(spec).flatMap(
|
|
80
|
-
(text) => [...text.matchAll(PORT_TEMPLATE)].flatMap((match) => match[1] === void 0 ? [] : [match[1]])
|
|
81
|
-
);
|
|
82
|
-
var resolveSpec = (spec, assigned) => {
|
|
83
|
-
const sub = (text) => text.replace(PORT_TEMPLATE, (whole, ref) => {
|
|
84
|
-
const port = assigned.get(ref ?? spec.id);
|
|
85
|
-
return port === void 0 ? whole : String(port);
|
|
86
|
-
});
|
|
87
|
-
return {
|
|
88
|
-
...spec,
|
|
89
|
-
...spec.shell === void 0 ? {} : { shell: sub(spec.shell) },
|
|
90
|
-
...spec.cmd === void 0 ? {} : { cmd: spec.cmd.map(sub) },
|
|
91
|
-
...spec.env === void 0 ? {} : { env: Object.fromEntries(Object.entries(spec.env).map(([key, value]) => [key, sub(value)])) },
|
|
92
|
-
...spec.url === void 0 ? {} : { url: sub(spec.url) }
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
|
-
var validateGraph = (procs) => {
|
|
96
|
-
const ids = new Set(procs.map((proc) => proc.id));
|
|
97
|
-
if (ids.size !== procs.length) return "duplicate proc ids";
|
|
98
|
-
for (const proc of procs) {
|
|
99
|
-
for (const need of proc.needs ?? []) {
|
|
100
|
-
if (!ids.has(need)) return `proc "${proc.id}" needs unknown proc "${need}"`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const needsOf = new Map(procs.map((proc) => [proc.id, proc.needs ?? []]));
|
|
104
|
-
const done = /* @__PURE__ */ new Set();
|
|
105
|
-
const onPath = /* @__PURE__ */ new Set();
|
|
106
|
-
const visit = (id) => {
|
|
107
|
-
if (done.has(id)) return void 0;
|
|
108
|
-
if (onPath.has(id)) return `dependency cycle through "${id}"`;
|
|
109
|
-
onPath.add(id);
|
|
110
|
-
for (const need of needsOf.get(id) ?? []) {
|
|
111
|
-
const cycle = visit(need);
|
|
112
|
-
if (cycle !== void 0) return cycle;
|
|
113
|
-
}
|
|
114
|
-
onPath.delete(id);
|
|
115
|
-
done.add(id);
|
|
116
|
-
return void 0;
|
|
117
|
-
};
|
|
118
|
-
for (const proc of procs) {
|
|
119
|
-
const cycle = visit(proc.id);
|
|
120
|
-
if (cycle !== void 0) return cycle;
|
|
121
|
-
}
|
|
122
|
-
return void 0;
|
|
123
|
-
};
|
|
124
|
-
var validatePortTemplates = (procs) => {
|
|
125
|
-
const byId = new Map(procs.map((proc) => [proc.id, proc]));
|
|
126
|
-
for (const proc of procs) {
|
|
127
|
-
for (const ref of portRefs(proc)) {
|
|
128
|
-
const target = byId.get(ref);
|
|
129
|
-
if (target === void 0) {
|
|
130
|
-
return `proc "${proc.id}" references \${port:${ref}} but there is no proc "${ref}"`;
|
|
131
|
-
}
|
|
132
|
-
if (!usesOwnPort(target)) {
|
|
133
|
-
return `proc "${proc.id}" references \${port:${ref}} but "${ref}" does not use \${port} itself`;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return void 0;
|
|
138
|
-
};
|
|
139
|
-
var ProcdeckConfigSchema = Schema.Struct({
|
|
140
|
-
/**
|
|
141
|
-
* Deck name — the tab title and the name of the installed web app.
|
|
142
|
-
* Defaults to the config directory's basename.
|
|
143
|
-
*/
|
|
144
|
-
name: Schema.optionalKey(Schema.String),
|
|
145
|
-
/** Port for the web UI. Defaults to 4820. */
|
|
146
|
-
port: Schema.optionalKey(Schema.Number),
|
|
147
|
-
procs: Schema.Array(ProcSpecSchema)
|
|
148
|
-
}).pipe(
|
|
149
|
-
Schema.check(
|
|
150
|
-
Schema.makeFilter(
|
|
151
|
-
(config) => validateGraph(config.procs) ?? validatePortTemplates(config.procs)
|
|
152
|
-
)
|
|
153
|
-
)
|
|
154
|
-
);
|
|
155
|
-
var defineConfig = (config) => config;
|
|
156
|
-
var ConfigError = class extends Schema.TaggedError()("ConfigError", {
|
|
157
|
-
file: Schema.String,
|
|
158
|
-
message: Schema.String
|
|
159
|
-
}) {
|
|
160
|
-
};
|
|
161
|
-
var loadConfig = Effect.fn("procdeck.loadConfig")(function* (file) {
|
|
162
|
-
const absolute = path.resolve(file);
|
|
163
|
-
const module = yield* Effect.tryPromise({
|
|
164
|
-
try: () => import(pathToFileURL(absolute).href),
|
|
165
|
-
catch: (cause) => new ConfigError({ file: absolute, message: String(cause) })
|
|
166
|
-
});
|
|
167
|
-
const config = yield* Effect.try({
|
|
168
|
-
try: () => Schema.decodeUnknownSync(ProcdeckConfigSchema)(module.default),
|
|
169
|
-
catch: (cause) => new ConfigError({ file: absolute, message: String(cause) })
|
|
170
|
-
});
|
|
171
|
-
const root = path.dirname(absolute);
|
|
172
|
-
const loaded = { config, root, name: config.name ?? path.basename(root) };
|
|
173
|
-
return loaded;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
export {
|
|
177
|
-
PreflightSchema,
|
|
178
|
-
AlertSchema,
|
|
179
|
-
ProcSpecSchema,
|
|
180
|
-
DEFAULT_UI_PORT,
|
|
181
|
-
usesOwnPort,
|
|
182
|
-
resolveSpec,
|
|
183
|
-
ProcdeckConfigSchema,
|
|
184
|
-
defineConfig,
|
|
185
|
-
ConfigError,
|
|
186
|
-
loadConfig
|
|
187
|
-
};
|