procdeck 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
+ * https://github.com/chjj/term.js
5
+ * @license MIT
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ * Originally forked from (with the author's permission):
26
+ * Fabrice Bellard's javascript vt100 for jslinux:
27
+ * http://bellard.org/jslinux/
28
+ * Copyright (c) 2011 Fabrice Bellard
29
+ * The original design remains. The terminal itself
30
+ * has been extended to include xterm CSI codes, among
31
+ * other features.
32
+ */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{font-family:monospace;-webkit-user-select:text;user-select:text;white-space:pre}.xterm .xterm-accessibility-tree>div{transform-origin:left;width:fit-content}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;background:#0000;transition:opacity .1s linear;z-index:11}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{position:absolute;display:none}.xterm .xterm-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}:root{--bg: #0e1116;--panel: #151a21;--hover: #1b212a;--control: #1f2630;--border: #232a34;--text: #d8dee9;--muted: #7b8794;--accent: #4c9aff;--accent-bg: #16202e;--accent-border: #2a3a52;--ok: #3fb950;--warn: #d29922;--on-warn: #1a1005;--bad: #f85149;--overlay: rgba(31, 38, 48, .85);color-scheme:dark}@media(prefers-color-scheme:light){:root:not([data-theme=dark]){--bg: #ffffff;--panel: #f6f8fa;--hover: #eaeef2;--control: #f6f8fa;--border: #d0d7de;--text: #1f2328;--muted: #656d76;--accent: #0969da;--accent-bg: #ddf4ff;--accent-border: #b6e3ff;--ok: #1a7f37;--warn: #bf8700;--on-warn: #ffffff;--bad: #cf222e;--overlay: rgba(246, 248, 250, .9);color-scheme:light}}:root[data-theme=light]{--bg: #ffffff;--panel: #f6f8fa;--hover: #eaeef2;--control: #f6f8fa;--border: #d0d7de;--text: #1f2328;--muted: #656d76;--accent: #0969da;--accent-bg: #ddf4ff;--accent-border: #b6e3ff;--ok: #1a7f37;--warn: #bf8700;--on-warn: #ffffff;--bad: #cf222e;--overlay: rgba(246, 248, 250, .9);color-scheme:light}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:13px/1.45 ui-sans-serif,system-ui,sans-serif}.layout{height:100vh;display:grid;grid-template-columns:260px 1fr;grid-template-rows:auto minmax(0,1fr)}.layout.grid{grid-template-columns:1fr}.layout.grid aside{display:none}.layout aside{grid-area:2 / 1}.layout main{grid-area:2 / 2}.layout.grid main{grid-area:2 / 1}.banner{grid-area:2 / 1 / 3 / -1;align-self:start;z-index:2;padding:4px 14px;font-size:12px;color:var(--on-warn);background:var(--warn);animation:banner-pulse 1.6s ease-in-out infinite}.layout.down aside,.layout.down main{opacity:.55;transition:opacity .4s}.banner.down{color:var(--text);background:var(--control);border-bottom:1px solid var(--border);animation:none}@keyframes banner-pulse{50%{opacity:.75}}header{grid-column:1 / -1;display:flex;gap:8px;align-items:center;padding:8px 14px;border-bottom:1px solid var(--border);background:var(--panel)}header h1{margin:0 8px 0 0;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}header .meta{margin-left:0}header .meta.error{color:var(--bad)}.deck{position:relative;margin-right:6px}button.deck-name{color:var(--text);font-size:12px;font-weight:600;background:none;border-color:transparent;padding:3px 6px}button.deck-name .caret{margin-left:5px;font-size:10px;color:var(--muted)}.deck.open button.deck-name{background:var(--control);border-color:var(--border)}.switcher{position:absolute;top:calc(100% + 4px);left:0;z-index:5;min-width:220px;max-width:360px;padding:4px;display:flex;flex-direction:column;gap:2px;background:var(--panel);border:1px solid var(--border);border-radius:6px;box-shadow:0 8px 24px #00000059;font-size:12px}.switcher>.meta{padding:6px 8px;white-space:normal;line-height:1.4}.switcher a{display:flex;gap:8px;align-items:baseline;padding:6px 8px;border-radius:4px;color:var(--text);text-decoration:none}.switcher a:hover{background:var(--hover)}.switcher a .name{font-weight:600}.switcher a .meta{margin-left:auto}.deck-actions{margin-left:auto;display:inline-flex;gap:6px}.deck-actions .bell{padding:4px 8px;filter:grayscale(1);opacity:.5}.deck-actions .bell.on{filter:none;opacity:1;border-color:var(--accent)}.deck-actions .bell.denied{filter:none;opacity:.8;border-color:var(--warn)}.deck-actions .install{color:var(--accent);margin-right:8px}.deck-actions .shutdown{margin-left:6px;color:var(--muted)}.deck-actions .shutdown:hover:not(:disabled){color:var(--bad);border-color:var(--bad)}aside{background:var(--panel);border-right:1px solid var(--border);overflow-y:auto}.proc{padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;display:grid;grid-template-columns:8px 1fr;gap:10px;align-items:baseline}.proc:hover{background:var(--hover)}.proc.active{background:var(--hover);box-shadow:inset 2px 0 0 var(--accent)}.dot{width:8px;height:8px;border-radius:50%;background:var(--muted);align-self:center}.dot.running{background:var(--ok)}.dot.starting{background:var(--warn)}.dot.waiting{background:var(--accent)}.dot.exited{background:var(--bad)}.dot.blocked{background:transparent;border:2px solid var(--bad)}.badge{margin-left:6px;padding:1px 5px;border-radius:8px;font-size:10px;font-weight:600;color:var(--on-warn);background:var(--warn);vertical-align:1px}.badge.err{color:#fff;background:var(--bad);cursor:help}.proc .name{font-weight:600}.proc-body{min-width:0}.sub{display:flex;align-items:center;gap:5px;margin-top:4px}.up{margin-left:auto;color:var(--muted);font-size:10px;font-family:ui-monospace,SFMono-Regular,monospace;font-variant-numeric:tabular-nums}.up.exit{color:var(--bad);opacity:.85}.proc .cmd{color:var(--muted);font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}main{display:grid;grid-template-rows:minmax(0,1fr) auto;min-width:0;min-height:0}.meta{color:var(--muted);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;margin-left:auto}button{font:inherit;color:var(--text);background:var(--control);border:1px solid var(--border);border-radius:6px;padding:4px 12px;cursor:pointer}button:hover:not(:disabled){border-color:var(--accent)}button:disabled{opacity:.35;cursor:default}a{color:var(--accent)}.links{display:flex;align-items:center;gap:6px;min-width:0;overflow:hidden}a.port{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;text-decoration:none;color:var(--accent);background:var(--accent-bg);border:1px solid var(--accent-border);border-radius:5px;padding:1px 6px;white-space:nowrap}a.port:hover{border-color:var(--accent)}.links-more{color:var(--muted);font-size:11px;white-space:nowrap;cursor:help}input.search{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 9px;width:200px}input.search:focus{outline:none;border-color:var(--accent)}.hints{padding:10px 16px;color:var(--muted);font-size:10px;line-height:1.7}.to-bottom{position:absolute;right:18px;bottom:14px;z-index:5;font-size:11px;padding:3px 12px;border-radius:12px;background:var(--overlay)}.to-bottom[hidden]{display:none}.segmented{display:inline-flex;margin-left:4px}.segmented button{padding:4px 9px;border-radius:0;font-size:13px;line-height:1.2}.segmented button:first-child{border-radius:6px 0 0 6px}.segmented button+button{margin-left:-1px}.segmented button:last-child{border-radius:0 6px 6px 0}.segmented.theme{margin-right:8px}.segmented button.on{color:var(--accent);border-color:var(--accent);background:var(--accent-bg);position:relative;z-index:1}.terminals{position:relative;min-height:0;background:var(--bg)}.tile{display:flex;flex-direction:column;min-height:0;min-width:0}.tile[hidden]{display:none}.term{position:relative;flex:1;min-height:0}.terminals.single .tile{position:absolute;top:0;right:0;bottom:0;left:0}.terminals.single .term{margin:8px 4px 4px 10px}.terminals.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-auto-rows:minmax(180px,1fr);gap:6px;padding:6px;overflow-y:auto}.terminals.grid.cols-1{grid-template-columns:minmax(0,1fr)}.terminals.grid.cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.terminals.grid.cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}@media(max-width:1100px){.terminals.grid.cols-3,.terminals.grid.cols-4{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.terminals.grid{grid-template-columns:minmax(0,1fr)}}.tray{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;padding:4px 10px;border-top:1px solid var(--border);background:var(--panel);font-size:11px}.tray-item{display:inline-flex;align-items:center}.tray .peek{display:inline-flex;align-items:center;gap:6px;padding:2px 6px;background:transparent;border-color:transparent}.tray .peek:hover{background:var(--hover);border-color:var(--border)}.tray .peek.active{border-color:var(--accent)}.tray .peek .name{font-weight:600}.tray .pin{opacity:0}.tray-item:hover .pin{opacity:.6}button.pin{padding:0 4px;font-size:11px;line-height:1.4;background:transparent;border-color:transparent;filter:grayscale(1);opacity:.3}button.pin:hover{opacity:.8;border-color:var(--accent)}button.pin.on{opacity:1;filter:none}.proc .name button.pin{margin-left:4px;vertical-align:1px;opacity:0}.proc:hover .name button.pin,.proc .name button.pin.on{opacity:.3}.proc .name button.pin.on{opacity:1}.terminals.grid .tile{border:1px solid var(--border);border-radius:6px;overflow:hidden}.terminals.grid .tile.active{border-color:var(--accent)}.terminals.grid .term{margin:4px 2px 2px 6px}.tile-head{display:flex;align-items:center;gap:8px;padding:4px 8px;font-size:11px;background:var(--panel);border-bottom:1px solid var(--border);-webkit-user-select:none;user-select:none;min-width:0}.terminals.grid .tile-head{cursor:pointer}.tile-head .name{font-weight:600;white-space:nowrap}.tile-head .links{flex-shrink:0;overflow:visible}.tile-head .meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1;min-width:3ch}.pane-actions{display:inline-flex;gap:2px;flex-shrink:0;opacity:.4;transition:opacity .12s}.tile:hover .pane-actions,.tile.active .pane-actions{opacity:1}.pane-actions button{padding:1px 7px;font-size:12px;line-height:1.3;background:transparent;border-color:transparent}.pane-actions button:hover:not(:disabled){background:var(--control);border-color:var(--accent)}
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
+ <!-- icon.svg with an attention badge: swapped in as the favicon while a proc
3
+ has crashed, is blocked, or has unread errors (see ui/src/command.ts). -->
4
+ <!-- procdeck app icon: four panes on a dark deck. Source for the PNGs next
5
+ to it (rsvg-convert -w N icon.svg > icon-N.png). Kept inside the
6
+ maskable safe zone (centre 80%). -->
7
+ <rect width="512" height="512" rx="112" fill="#0e1116"/>
8
+ <rect x="88" y="88" width="160" height="160" rx="22" fill="#151a21" stroke="#232a34" stroke-width="6"/>
9
+ <rect x="264" y="88" width="160" height="160" rx="22" fill="#151a21" stroke="#232a34" stroke-width="6"/>
10
+ <rect x="88" y="264" width="160" height="160" rx="22" fill="#151a21" stroke="#232a34" stroke-width="6"/>
11
+ <rect x="264" y="264" width="160" height="160" rx="22" fill="#151a21" stroke="#232a34" stroke-width="6"/>
12
+ <g fill="#3fb950">
13
+ <circle cx="122" cy="122" r="12"/>
14
+ <circle cx="298" cy="122" r="12"/>
15
+ <circle cx="122" cy="298" r="12"/>
16
+ </g>
17
+ <circle cx="298" cy="298" r="12" fill="#f85149"/>
18
+ <g stroke="#7b8794" stroke-width="10" stroke-linecap="round">
19
+ <path d="M150 122h60M118 160h96M118 190h60"/>
20
+ <path d="M326 122h60M294 160h96M294 190h72"/>
21
+ <path d="M150 298h60M118 336h72M118 366h96"/>
22
+ <path d="M326 298h60M294 336h96M294 366h48"/>
23
+ </g>
24
+ <circle cx="416" cy="96" r="88" fill="#f85149" stroke="#0e1116" stroke-width="20"/>
25
+ </svg>
@@ -5,14 +5,27 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>procdeck</title>
7
7
  <meta name="theme-color" content="#151a21" />
8
+ <!-- Paint the stored theme before the first frame (no light/dark flash).
9
+ Reads the same localStorage record as ui/src/storage.ts; the app takes
10
+ over from here (ApplyTheme), including the theme-color meta. -->
11
+ <script>
12
+ try {
13
+ var theme = JSON.parse(localStorage.getItem("procdeck.ui") || "{}").theme
14
+ if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme
15
+ var dark =
16
+ theme === "dark" ||
17
+ (theme !== "light" && matchMedia("(prefers-color-scheme: dark)").matches)
18
+ if (!dark) document.querySelector('meta[name="theme-color"]').content = "#f6f8fa"
19
+ } catch (_) {}
20
+ </script>
8
21
  <link rel="icon" href="/icon.svg" type="image/svg+xml" />
9
22
  <!-- Installable web app: the manifest is generated by the server so it
10
23
  carries the deck's name (see src/server.ts). -->
11
24
  <link rel="manifest" href="/manifest.webmanifest" />
12
25
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
13
26
  <meta name="apple-mobile-web-app-title" content="procdeck" />
14
- <script type="module" crossorigin src="/assets/index-D4mI4ojj.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DYfMdGhb.css">
27
+ <script type="module" crossorigin src="/assets/index-CLZK1NCD.js"></script>
28
+ <link rel="stylesheet" crossorigin href="/assets/index-D-mf-sKs.css">
16
29
  </head>
17
30
  <body>
18
31
  <div id="root"></div>
@@ -1,266 +0,0 @@
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/config.d.ts DELETED
@@ -1,90 +0,0 @@
1
- import { Effect, Schema } from "effect";
2
- export declare const PreflightSchema: Schema.Struct<{
3
- readonly shell: Schema.String;
4
- readonly expect: Schema.optionalKey<Schema.String>;
5
- readonly hint: Schema.optionalKey<Schema.String>;
6
- }>;
7
- export type Preflight = typeof PreflightSchema.Type;
8
- export declare const AlertSchema: Schema.Struct<{
9
- readonly pattern: Schema.String;
10
- readonly label: Schema.String;
11
- }>;
12
- export type Alert = typeof AlertSchema.Type;
13
- export declare const ProcSpecSchema: Schema.Struct<{
14
- readonly id: Schema.String;
15
- readonly shell: Schema.optionalKey<Schema.String>;
16
- readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
17
- readonly cwd: Schema.optionalKey<Schema.String>;
18
- readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
19
- readonly autostart: Schema.optionalKey<Schema.Boolean>;
20
- readonly url: Schema.optionalKey<Schema.String>;
21
- readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
22
- readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
23
- readonly preflight: Schema.optionalKey<Schema.Struct<{
24
- readonly shell: Schema.String;
25
- readonly expect: Schema.optionalKey<Schema.String>;
26
- readonly hint: Schema.optionalKey<Schema.String>;
27
- }>>;
28
- readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
29
- readonly pattern: Schema.String;
30
- readonly label: Schema.String;
31
- }>>>;
32
- }>;
33
- export type ProcSpec = typeof ProcSpecSchema.Type;
34
- /** Default port of the UI + proxy server. */
35
- export declare const DEFAULT_UI_PORT = 4820;
36
- /** Whether the spec asks procdeck to assign it a port (`${port}` anywhere). */
37
- export declare const usesOwnPort: (spec: ProcSpec) => boolean;
38
- /** Substitute `${port}` / `${port:id}` with the ports assigned at startup. */
39
- export declare const resolveSpec: (spec: ProcSpec, assigned: Map<string, number>) => ProcSpec;
40
- export declare const ProcdeckConfigSchema: Schema.Struct<{
41
- readonly $schema: Schema.optionalKey<Schema.String>;
42
- readonly name: Schema.optionalKey<Schema.String>;
43
- readonly port: Schema.optionalKey<Schema.Number>;
44
- readonly procs: Schema.$Array<Schema.Struct<{
45
- readonly id: Schema.String;
46
- readonly shell: Schema.optionalKey<Schema.String>;
47
- readonly cmd: Schema.optionalKey<Schema.$Array<Schema.String>>;
48
- readonly cwd: Schema.optionalKey<Schema.String>;
49
- readonly env: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
50
- readonly autostart: Schema.optionalKey<Schema.Boolean>;
51
- readonly url: Schema.optionalKey<Schema.String>;
52
- readonly needs: Schema.optionalKey<Schema.$Array<Schema.String>>;
53
- readonly readyWhen: Schema.optionalKey<Schema.Literals<readonly ["listening", "started"]>>;
54
- readonly preflight: Schema.optionalKey<Schema.Struct<{
55
- readonly shell: Schema.String;
56
- readonly expect: Schema.optionalKey<Schema.String>;
57
- readonly hint: Schema.optionalKey<Schema.String>;
58
- }>>;
59
- readonly alerts: Schema.optionalKey<Schema.$Array<Schema.Struct<{
60
- readonly pattern: Schema.String;
61
- readonly label: Schema.String;
62
- }>>>;
63
- }>>;
64
- }>;
65
- export type ProcdeckConfig = typeof ProcdeckConfigSchema.Type;
66
- /** Identity helper that gives config files their types. */
67
- export declare const defineConfig: (config: ProcdeckConfig) => ProcdeckConfig;
68
- export type LoadedConfig = {
69
- config: ProcdeckConfig;
70
- /** Directory of the config file — the default cwd for every process. */
71
- root: string;
72
- /** `config.name`, or the root directory's basename. */
73
- name: string;
74
- };
75
- declare const ConfigError_base: Schema.Class<ConfigError, Schema.TaggedStruct<"ConfigError", {
76
- readonly file: Schema.String;
77
- readonly message: Schema.String;
78
- }>, import("effect/Cause").YieldableError>;
79
- export declare class ConfigError extends ConfigError_base {
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;
89
- export declare const loadConfig: (file: string) => Effect.Effect<LoadedConfig, ConfigError, never>;
90
- export {};
package/dist/config.js DELETED
@@ -1,28 +0,0 @@
1
- import {
2
- AlertSchema,
3
- CONFIG_FILENAMES,
4
- ConfigError,
5
- DEFAULT_UI_PORT,
6
- PreflightSchema,
7
- ProcSpecSchema,
8
- ProcdeckConfigSchema,
9
- defineConfig,
10
- discoverConfig,
11
- loadConfig,
12
- resolveSpec,
13
- usesOwnPort
14
- } from "./chunk-LHJTDX2F.js";
15
- export {
16
- AlertSchema,
17
- CONFIG_FILENAMES,
18
- ConfigError,
19
- DEFAULT_UI_PORT,
20
- PreflightSchema,
21
- ProcSpecSchema,
22
- ProcdeckConfigSchema,
23
- defineConfig,
24
- discoverConfig,
25
- loadConfig,
26
- resolveSpec,
27
- usesOwnPort
28
- };