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
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
};
|