procdeck 0.1.0 → 0.2.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-LHJTDX2F.js +266 -0
- package/dist/cli.js +8 -2
- package/dist/config.d.ts +9 -66
- package/dist/config.js +5 -1
- package/package.json +5 -3
- package/readme.md +36 -10
- package/schema.json +212 -0
- package/dist/chunk-SBVZ6G4G.js +0 -187
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { Effect, Schema } from "effect";
|
|
7
|
+
var isRegex = 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
|
+
var regexSource = (annotations) => Schema.String.annotate(annotations).pipe(Schema.check(isRegex));
|
|
16
|
+
var PreflightSchema = Schema.Struct({
|
|
17
|
+
shell: Schema.String.annotate({
|
|
18
|
+
description: "Shell command; exit 0 lets the proc spawn.",
|
|
19
|
+
examples: ["wrangler whoami"]
|
|
20
|
+
}).pipe(Schema.check(Schema.isMinLength(1))),
|
|
21
|
+
expect: Schema.optionalKey(
|
|
22
|
+
regexSource({
|
|
23
|
+
description: 'Regex the check\'s output must also match. For checks that exit 0 either way \u2014 `wrangler whoami` reports "not authenticated" with exit 0 \u2014 so the config needs no `| grep` pipelines.',
|
|
24
|
+
examples: ["You are logged in"]
|
|
25
|
+
})
|
|
26
|
+
),
|
|
27
|
+
hint: Schema.optionalKey(
|
|
28
|
+
Schema.String.annotate({
|
|
29
|
+
description: "What the human should do when the check fails; shown on the blocked pane.",
|
|
30
|
+
examples: ["run `wrangler login`, then Start"]
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
}).annotate({
|
|
34
|
+
description: "A gate that must pass before the process spawns \u2014 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."
|
|
35
|
+
});
|
|
36
|
+
var AlertSchema = Schema.Struct({
|
|
37
|
+
pattern: regexSource({
|
|
38
|
+
description: "Regex matched against a rolling tail of the pane's output.",
|
|
39
|
+
examples: ["ERROR", "not authenticated"]
|
|
40
|
+
}),
|
|
41
|
+
label: Schema.String.annotate({
|
|
42
|
+
description: "Badge text shown in the UI when the pattern matches.",
|
|
43
|
+
examples: ["needs login"]
|
|
44
|
+
}).pipe(Schema.check(Schema.isMinLength(1)))
|
|
45
|
+
}).annotate({
|
|
46
|
+
description: "A regex watched in the pane output; a match raises a badge in the UI."
|
|
47
|
+
});
|
|
48
|
+
var ProcSpecSchema = Schema.Struct({
|
|
49
|
+
id: Schema.String.annotate({
|
|
50
|
+
description: "Stable identifier: the pane label, and the proxy subdomain (`api` \u2192 http://api.localhost:4820).",
|
|
51
|
+
examples: ["api", "web"]
|
|
52
|
+
}).pipe(Schema.check(Schema.isMinLength(1))),
|
|
53
|
+
shell: Schema.optionalKey(
|
|
54
|
+
Schema.String.annotate({
|
|
55
|
+
description: "Command line handed to `$SHELL -c` \u2014 pipes, `&&` and globs work. Exactly one of `shell` or `cmd` must be set.",
|
|
56
|
+
examples: ["pnpm --filter api dev"]
|
|
57
|
+
})
|
|
58
|
+
),
|
|
59
|
+
cmd: Schema.optionalKey(
|
|
60
|
+
Schema.Array(Schema.String).annotate({
|
|
61
|
+
description: "argv, executed directly without a shell. Exactly one of `shell` or `cmd` must be set.",
|
|
62
|
+
examples: [["node", "server.js"]]
|
|
63
|
+
}).pipe(Schema.check(Schema.isMinLength(1)))
|
|
64
|
+
),
|
|
65
|
+
cwd: Schema.optionalKey(
|
|
66
|
+
Schema.String.annotate({
|
|
67
|
+
description: "Working directory; defaults to the config file's directory.",
|
|
68
|
+
examples: ["packages/api"]
|
|
69
|
+
})
|
|
70
|
+
),
|
|
71
|
+
env: Schema.optionalKey(
|
|
72
|
+
Schema.Record(Schema.String, Schema.String).annotate({
|
|
73
|
+
description: "Extra environment on top of the inherited one. Values may use `${port}` / `${port:other}` templates.",
|
|
74
|
+
examples: [{ PORT: "${port}", API_URL: "http://localhost:${port:api}" }]
|
|
75
|
+
})
|
|
76
|
+
),
|
|
77
|
+
autostart: Schema.optionalKey(
|
|
78
|
+
Schema.Boolean.annotate({
|
|
79
|
+
description: "Spawn on startup. Defaults to true; false leaves the pane idle until Start."
|
|
80
|
+
})
|
|
81
|
+
),
|
|
82
|
+
url: Schema.optionalKey(
|
|
83
|
+
Schema.String.annotate({
|
|
84
|
+
description: "URL this process serves, shown in the UI as a link. Also pins readiness to that URL's port.",
|
|
85
|
+
examples: ["http://localhost:${port}"]
|
|
86
|
+
})
|
|
87
|
+
),
|
|
88
|
+
needs: Schema.optionalKey(
|
|
89
|
+
Schema.Array(Schema.String).annotate({
|
|
90
|
+
description: "Ids of procs that must be ready before this one spawns. Declarative: readiness is auto-detected, no port numbers in the config.",
|
|
91
|
+
examples: [["api"]]
|
|
92
|
+
})
|
|
93
|
+
),
|
|
94
|
+
readyWhen: Schema.optionalKey(
|
|
95
|
+
Schema.Literals(["listening", "started"]).annotate({
|
|
96
|
+
description: 'What "ready" means for dependents: "listening" (default) \u2014 the process tree opened a TCP port; "started" \u2014 it merely spawned, for procs that never listen.'
|
|
97
|
+
})
|
|
98
|
+
),
|
|
99
|
+
preflight: Schema.optionalKey(PreflightSchema),
|
|
100
|
+
alerts: Schema.optionalKey(
|
|
101
|
+
Schema.Array(AlertSchema).annotate({
|
|
102
|
+
description: "Output patterns that raise a badge when they appear in the pane."
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
}).pipe(
|
|
106
|
+
Schema.check(
|
|
107
|
+
Schema.makeFilter(
|
|
108
|
+
(spec) => spec.shell === void 0 === (spec.cmd === void 0) ? "expected exactly one of `shell` or `cmd`" : void 0
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
).annotate({
|
|
112
|
+
description: "A single supervised process. Exactly one of `shell` / `cmd` is used: `shell` goes through `sh -c` (pipes, `&&`, globs work, at the cost of one extra process in the tree), `cmd` is an argv array executed directly."
|
|
113
|
+
});
|
|
114
|
+
var DEFAULT_UI_PORT = 4820;
|
|
115
|
+
var PORT_TEMPLATE = /\$\{port(?::([^}]+))?\}/g;
|
|
116
|
+
var templatedStrings = (spec) => [spec.shell, ...spec.cmd ?? [], ...Object.values(spec.env ?? {}), spec.url].filter(
|
|
117
|
+
(text) => text !== void 0
|
|
118
|
+
);
|
|
119
|
+
var usesOwnPort = (spec) => templatedStrings(spec).some(
|
|
120
|
+
(text) => [...text.matchAll(PORT_TEMPLATE)].some((match) => match[1] === void 0)
|
|
121
|
+
);
|
|
122
|
+
var portRefs = (spec) => templatedStrings(spec).flatMap(
|
|
123
|
+
(text) => [...text.matchAll(PORT_TEMPLATE)].flatMap((match) => match[1] === void 0 ? [] : [match[1]])
|
|
124
|
+
);
|
|
125
|
+
var resolveSpec = (spec, assigned) => {
|
|
126
|
+
const sub = (text) => text.replace(PORT_TEMPLATE, (whole, ref) => {
|
|
127
|
+
const port = assigned.get(ref ?? spec.id);
|
|
128
|
+
return port === void 0 ? whole : String(port);
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
...spec,
|
|
132
|
+
...spec.shell === void 0 ? {} : { shell: sub(spec.shell) },
|
|
133
|
+
...spec.cmd === void 0 ? {} : { cmd: spec.cmd.map(sub) },
|
|
134
|
+
...spec.env === void 0 ? {} : { env: Object.fromEntries(Object.entries(spec.env).map(([key, value]) => [key, sub(value)])) },
|
|
135
|
+
...spec.url === void 0 ? {} : { url: sub(spec.url) }
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
var validateGraph = (procs) => {
|
|
139
|
+
const ids = new Set(procs.map((proc) => proc.id));
|
|
140
|
+
if (ids.size !== procs.length) return "duplicate proc ids";
|
|
141
|
+
for (const proc of procs) {
|
|
142
|
+
for (const need of proc.needs ?? []) {
|
|
143
|
+
if (!ids.has(need)) return `proc "${proc.id}" needs unknown proc "${need}"`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const needsOf = new Map(procs.map((proc) => [proc.id, proc.needs ?? []]));
|
|
147
|
+
const done = /* @__PURE__ */ new Set();
|
|
148
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
149
|
+
const visit = (id) => {
|
|
150
|
+
if (done.has(id)) return void 0;
|
|
151
|
+
if (onPath.has(id)) return `dependency cycle through "${id}"`;
|
|
152
|
+
onPath.add(id);
|
|
153
|
+
for (const need of needsOf.get(id) ?? []) {
|
|
154
|
+
const cycle = visit(need);
|
|
155
|
+
if (cycle !== void 0) return cycle;
|
|
156
|
+
}
|
|
157
|
+
onPath.delete(id);
|
|
158
|
+
done.add(id);
|
|
159
|
+
return void 0;
|
|
160
|
+
};
|
|
161
|
+
for (const proc of procs) {
|
|
162
|
+
const cycle = visit(proc.id);
|
|
163
|
+
if (cycle !== void 0) return cycle;
|
|
164
|
+
}
|
|
165
|
+
return void 0;
|
|
166
|
+
};
|
|
167
|
+
var validatePortTemplates = (procs) => {
|
|
168
|
+
const byId = new Map(procs.map((proc) => [proc.id, proc]));
|
|
169
|
+
for (const proc of procs) {
|
|
170
|
+
for (const ref of portRefs(proc)) {
|
|
171
|
+
const target = byId.get(ref);
|
|
172
|
+
if (target === void 0) {
|
|
173
|
+
return `proc "${proc.id}" references \${port:${ref}} but there is no proc "${ref}"`;
|
|
174
|
+
}
|
|
175
|
+
if (!usesOwnPort(target)) {
|
|
176
|
+
return `proc "${proc.id}" references \${port:${ref}} but "${ref}" does not use \${port} itself`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return void 0;
|
|
181
|
+
};
|
|
182
|
+
var ProcdeckConfigSchema = Schema.Struct({
|
|
183
|
+
// JSON configs point at the published schema for editor completion; the key
|
|
184
|
+
// is meaningless at runtime but must be accepted, because Struct is exact.
|
|
185
|
+
$schema: Schema.optionalKey(
|
|
186
|
+
Schema.String.annotate({
|
|
187
|
+
description: "URL of the procdeck JSON schema, for editor completion and validation.",
|
|
188
|
+
examples: ["https://unpkg.com/procdeck/schema.json"]
|
|
189
|
+
})
|
|
190
|
+
),
|
|
191
|
+
name: Schema.optionalKey(
|
|
192
|
+
Schema.String.annotate({
|
|
193
|
+
description: "Deck name \u2014 the tab title and the name of the installed web app. Defaults to the config directory's basename.",
|
|
194
|
+
examples: ["my-app"]
|
|
195
|
+
})
|
|
196
|
+
),
|
|
197
|
+
port: Schema.optionalKey(
|
|
198
|
+
Schema.Number.annotate({
|
|
199
|
+
description: "Port for the web UI and the `*.localhost` proxy. Defaults to 4820.",
|
|
200
|
+
examples: [4820]
|
|
201
|
+
}).pipe(Schema.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 65535 })))
|
|
202
|
+
),
|
|
203
|
+
procs: Schema.Array(ProcSpecSchema).annotate({
|
|
204
|
+
description: "The processes this deck supervises."
|
|
205
|
+
})
|
|
206
|
+
}).pipe(
|
|
207
|
+
Schema.check(
|
|
208
|
+
Schema.makeFilter(
|
|
209
|
+
(config) => validateGraph(config.procs) ?? validatePortTemplates(config.procs)
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
).annotate({
|
|
213
|
+
description: "A procdeck deck: the web UI's settings and the processes it supervises."
|
|
214
|
+
});
|
|
215
|
+
var defineConfig = (config) => config;
|
|
216
|
+
var ConfigError = class extends Schema.TaggedError()("ConfigError", {
|
|
217
|
+
file: Schema.String,
|
|
218
|
+
message: Schema.String
|
|
219
|
+
}) {
|
|
220
|
+
};
|
|
221
|
+
var CONFIG_FILENAMES = [
|
|
222
|
+
"procdeck.config.json",
|
|
223
|
+
"procdeck.config.ts",
|
|
224
|
+
"procdeck.config.js",
|
|
225
|
+
"procdeck.config.mjs"
|
|
226
|
+
];
|
|
227
|
+
var discoverConfig = (dir) => CONFIG_FILENAMES.map((name) => path.join(dir, name)).find((candidate) => existsSync(candidate));
|
|
228
|
+
var readSource = async (absolute) => {
|
|
229
|
+
if (path.extname(absolute) === ".json") {
|
|
230
|
+
return JSON.parse(await readFile(absolute, "utf8"));
|
|
231
|
+
}
|
|
232
|
+
const module = await import(pathToFileURL(absolute).href);
|
|
233
|
+
return module.default;
|
|
234
|
+
};
|
|
235
|
+
var loadConfig = Effect.fn("procdeck.loadConfig")(function* (file) {
|
|
236
|
+
const absolute = path.resolve(file);
|
|
237
|
+
if (!existsSync(absolute)) {
|
|
238
|
+
return yield* new ConfigError({ file: absolute, message: "config file not found" });
|
|
239
|
+
}
|
|
240
|
+
const source = yield* Effect.tryPromise({
|
|
241
|
+
try: () => readSource(absolute),
|
|
242
|
+
catch: (cause) => new ConfigError({ file: absolute, message: String(cause) })
|
|
243
|
+
});
|
|
244
|
+
const config = yield* Effect.try({
|
|
245
|
+
try: () => Schema.decodeUnknownSync(ProcdeckConfigSchema)(source),
|
|
246
|
+
catch: (cause) => new ConfigError({ file: absolute, message: String(cause) })
|
|
247
|
+
});
|
|
248
|
+
const root = path.dirname(absolute);
|
|
249
|
+
const loaded = { config, root, name: config.name ?? path.basename(root) };
|
|
250
|
+
return loaded;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
PreflightSchema,
|
|
255
|
+
AlertSchema,
|
|
256
|
+
ProcSpecSchema,
|
|
257
|
+
DEFAULT_UI_PORT,
|
|
258
|
+
usesOwnPort,
|
|
259
|
+
resolveSpec,
|
|
260
|
+
ProcdeckConfigSchema,
|
|
261
|
+
defineConfig,
|
|
262
|
+
ConfigError,
|
|
263
|
+
CONFIG_FILENAMES,
|
|
264
|
+
discoverConfig,
|
|
265
|
+
loadConfig
|
|
266
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONFIG_FILENAMES,
|
|
3
4
|
DEFAULT_UI_PORT,
|
|
5
|
+
discoverConfig,
|
|
4
6
|
loadConfig,
|
|
5
7
|
resolveSpec,
|
|
6
8
|
usesOwnPort
|
|
7
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LHJTDX2F.js";
|
|
8
10
|
|
|
9
11
|
// src/cli.ts
|
|
10
12
|
import { Effect as Effect3, Fiber as Fiber2 } from "effect";
|
|
@@ -657,7 +659,11 @@ var serve = (supervisor, port, deck) => Effect2.acquireRelease(
|
|
|
657
659
|
).pipe(Effect2.as(port));
|
|
658
660
|
|
|
659
661
|
// src/cli.ts
|
|
660
|
-
var configPath = process.argv[2] ??
|
|
662
|
+
var configPath = process.argv[2] ?? discoverConfig(process.cwd());
|
|
663
|
+
if (configPath === void 0) {
|
|
664
|
+
console.error(`procdeck: no config file here \u2014 expected one of ${CONFIG_FILENAMES.join(", ")}`);
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
661
667
|
var main = Effect3.gen(function* () {
|
|
662
668
|
const loaded = yield* loadConfig(configPath);
|
|
663
669
|
const supervisor = yield* makeSupervisor(loaded);
|
package/dist/config.d.ts
CHANGED
|
@@ -1,67 +1,32 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
export declare const PreflightSchema: Schema.Struct<{
|
|
3
|
-
/** Shell command; exit 0 lets the proc spawn. */
|
|
4
3
|
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
4
|
readonly expect: Schema.optionalKey<Schema.String>;
|
|
11
|
-
/** What the human should do when the check fails. */
|
|
12
5
|
readonly hint: Schema.optionalKey<Schema.String>;
|
|
13
6
|
}>;
|
|
14
7
|
export type Preflight = typeof PreflightSchema.Type;
|
|
15
|
-
/** A regex watched in the pane output; a match raises a badge in the UI. */
|
|
16
8
|
export declare const AlertSchema: Schema.Struct<{
|
|
17
9
|
readonly pattern: Schema.String;
|
|
18
|
-
/** Badge text. */
|
|
19
10
|
readonly label: Schema.String;
|
|
20
11
|
}>;
|
|
21
12
|
export type Alert = typeof AlertSchema.Type;
|
|
22
13
|
export declare const ProcSpecSchema: Schema.Struct<{
|
|
23
|
-
/** Stable identifier, also the tab label. */
|
|
24
14
|
readonly id: Schema.String;
|
|
25
|
-
/** Command line handed to `$SHELL -c`. */
|
|
26
15
|
readonly shell: Schema.optionalKey<Schema.String>;
|
|
27
|
-
/** argv, executed without a shell. */
|
|
28
16
|
readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
29
|
-
/** Working directory; defaults to the config file's directory. */
|
|
30
17
|
readonly cwd: Schema.optionalKey<Schema.String>;
|
|
31
|
-
/** Extra environment on top of the inherited one. */
|
|
32
18
|
readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
33
|
-
/** Spawn on startup. Defaults to true. */
|
|
34
19
|
readonly autostart: Schema.optionalKey<Schema.Boolean>;
|
|
35
|
-
/** URL this process serves, shown in the UI as a link. */
|
|
36
20
|
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
21
|
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
22
|
readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
|
|
48
|
-
/** Check that must pass before this proc spawns; failure shows as "blocked". */
|
|
49
23
|
readonly preflight: Schema.optionalKey<Schema.Struct<{
|
|
50
|
-
/** Shell command; exit 0 lets the proc spawn. */
|
|
51
24
|
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
25
|
readonly expect: Schema.optionalKey<Schema.String>;
|
|
58
|
-
/** What the human should do when the check fails. */
|
|
59
26
|
readonly hint: Schema.optionalKey<Schema.String>;
|
|
60
27
|
}>>;
|
|
61
|
-
/** Output patterns that raise a badge when they appear in the pane. */
|
|
62
28
|
readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
63
29
|
readonly pattern: Schema.String;
|
|
64
|
-
/** Badge text. */
|
|
65
30
|
readonly label: Schema.String;
|
|
66
31
|
}>>>;
|
|
67
32
|
}>;
|
|
@@ -73,56 +38,26 @@ export declare const usesOwnPort: (spec: ProcSpec) => boolean;
|
|
|
73
38
|
/** Substitute `${port}` / `${port:id}` with the ports assigned at startup. */
|
|
74
39
|
export declare const resolveSpec: (spec: ProcSpec, assigned: Map<string, number>) => ProcSpec;
|
|
75
40
|
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
|
-
*/
|
|
41
|
+
readonly $schema: Schema.optionalKey<Schema.String>;
|
|
80
42
|
readonly name: Schema.optionalKey<Schema.String>;
|
|
81
|
-
/** Port for the web UI. Defaults to 4820. */
|
|
82
43
|
readonly port: Schema.optionalKey<Schema.Number>;
|
|
83
44
|
readonly procs: Schema.$Array<Schema.Struct<{
|
|
84
|
-
/** Stable identifier, also the tab label. */
|
|
85
45
|
readonly id: Schema.String;
|
|
86
|
-
/** Command line handed to `$SHELL -c`. */
|
|
87
46
|
readonly shell: Schema.optionalKey<Schema.String>;
|
|
88
|
-
/** argv, executed without a shell. */
|
|
89
47
|
readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
90
|
-
/** Working directory; defaults to the config file's directory. */
|
|
91
48
|
readonly cwd: Schema.optionalKey<Schema.String>;
|
|
92
|
-
/** Extra environment on top of the inherited one. */
|
|
93
49
|
readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
94
|
-
/** Spawn on startup. Defaults to true. */
|
|
95
50
|
readonly autostart: Schema.optionalKey<Schema.Boolean>;
|
|
96
|
-
/** URL this process serves, shown in the UI as a link. */
|
|
97
51
|
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
52
|
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
53
|
readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
|
|
109
|
-
/** Check that must pass before this proc spawns; failure shows as "blocked". */
|
|
110
54
|
readonly preflight: Schema.optionalKey<Schema.Struct<{
|
|
111
|
-
/** Shell command; exit 0 lets the proc spawn. */
|
|
112
55
|
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
56
|
readonly expect: Schema.optionalKey<Schema.String>;
|
|
119
|
-
/** What the human should do when the check fails. */
|
|
120
57
|
readonly hint: Schema.optionalKey<Schema.String>;
|
|
121
58
|
}>>;
|
|
122
|
-
/** Output patterns that raise a badge when they appear in the pane. */
|
|
123
59
|
readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
124
60
|
readonly pattern: Schema.String;
|
|
125
|
-
/** Badge text. */
|
|
126
61
|
readonly label: Schema.String;
|
|
127
62
|
}>>>;
|
|
128
63
|
}>>;
|
|
@@ -143,5 +78,13 @@ declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"C
|
|
|
143
78
|
}>, import("effect/Cause").YieldableError>;
|
|
144
79
|
export declare class ConfigError extends ConfigError_base {
|
|
145
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Config file names the CLI looks for when started without an argument, in
|
|
83
|
+
* order. JSON comes first because it needs nothing from the toolchain: no
|
|
84
|
+
* TypeScript, and no procdeck in the project's dependencies.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CONFIG_FILENAMES: readonly ["procdeck.config.json", "procdeck.config.ts", "procdeck.config.js", "procdeck.config.mjs"];
|
|
87
|
+
/** The first config file present in `dir`, if any. */
|
|
88
|
+
export declare const discoverConfig: (dir: string) => string | undefined;
|
|
146
89
|
export declare const loadConfig: (file: string) => Effect.Effect<LoadedConfig, ConfigError, never>;
|
|
147
90
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AlertSchema,
|
|
3
|
+
CONFIG_FILENAMES,
|
|
3
4
|
ConfigError,
|
|
4
5
|
DEFAULT_UI_PORT,
|
|
5
6
|
PreflightSchema,
|
|
6
7
|
ProcSpecSchema,
|
|
7
8
|
ProcdeckConfigSchema,
|
|
8
9
|
defineConfig,
|
|
10
|
+
discoverConfig,
|
|
9
11
|
loadConfig,
|
|
10
12
|
resolveSpec,
|
|
11
13
|
usesOwnPort
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-LHJTDX2F.js";
|
|
13
15
|
export {
|
|
14
16
|
AlertSchema,
|
|
17
|
+
CONFIG_FILENAMES,
|
|
15
18
|
ConfigError,
|
|
16
19
|
DEFAULT_UI_PORT,
|
|
17
20
|
PreflightSchema,
|
|
18
21
|
ProcSpecSchema,
|
|
19
22
|
ProcdeckConfigSchema,
|
|
20
23
|
defineConfig,
|
|
24
|
+
discoverConfig,
|
|
21
25
|
loadConfig,
|
|
22
26
|
resolveSpec,
|
|
23
27
|
usesOwnPort
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": {
|
|
@@ -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:
|
|
@@ -96,7 +96,31 @@ ports) entirely through `${port}` templates. See [`example/`](example/).
|
|
|
96
96
|
|
|
97
97
|
## Config
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
Two formats, one schema. **JSON** needs nothing from your toolchain — no TypeScript, and
|
|
100
|
+
procdeck itself doesn't have to be a dependency. Point `$schema` at the published schema
|
|
101
|
+
and the editor completes and validates every field:
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
{
|
|
105
|
+
"$schema": "https://unpkg.com/procdeck/schema.json",
|
|
106
|
+
"port": 4820,
|
|
107
|
+
"procs": [
|
|
108
|
+
{ "id": "api", "shell": "pnpm --filter api dev", "env": { "PORT": "${port}" } },
|
|
109
|
+
{
|
|
110
|
+
"id": "web",
|
|
111
|
+
"shell": "pnpm --filter web dev",
|
|
112
|
+
"env": { "PORT": "${port}", "API_URL": "http://localhost:${port:api}" },
|
|
113
|
+
"needs": ["api"]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
(Installed locally, `"./node_modules/procdeck/schema.json"` works too and needs no
|
|
120
|
+
network.)
|
|
121
|
+
|
|
122
|
+
**TypeScript** buys comments and computed configs, at the price of Node ≥ 22.18 (native
|
|
123
|
+
type stripping) and procdeck in your dependencies:
|
|
100
124
|
|
|
101
125
|
```ts
|
|
102
126
|
import { defineConfig } from "procdeck"
|
|
@@ -131,7 +155,9 @@ export default defineConfig({
|
|
|
131
155
|
```
|
|
132
156
|
|
|
133
157
|
Full field reference: [`packages/procdeck/src/config.ts`](packages/procdeck/src/config.ts)
|
|
134
|
-
(the schema is the documentation
|
|
158
|
+
(the schema is the documentation — `schema.json` is generated from it at build time, so
|
|
159
|
+
the two formats validate identically). Config files are looked up in this order when no
|
|
160
|
+
path is given: `procdeck.config.json`, `.ts`, `.js`, `.mjs`.
|
|
135
161
|
|
|
136
162
|
Tips:
|
|
137
163
|
|
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
|
-
};
|