frizz 0.3.0 → 0.5.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/README.md +26 -4
- package/dist/claude-agent-broker.js +9 -4
- package/dist/dev-child.js +1868 -529
- package/dist/frizz.js +814 -234
- package/package.json +5 -3
- package/runtime/cc-worker/DECISIONS.md +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +159 -174
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/web-dist/assets/{TerminalPane-CTdetDJJ.js → TerminalPane-WcpgmCA-.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BDKrjCMs.js → abnfDiagram-VRR7QNED-Caecklbz.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-BTy2nvYL.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-BJebpUUM.js → architectureDiagram-ZJ3FMSHR-BEk0fg8C.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BNbuk25k.js → blockDiagram-677ZJIJ3-B4yq3xiy.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-lCfyotdU.js → c4Diagram-LMCZKHZV-DaA5NBkz.js} +1 -1
- package/web-dist/assets/channel-WghlT4VS.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-DP72SEkr.js → chunk-32BRIVSS-2XsSs8F_.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-DxO3gN-p.js → chunk-52WLFC77-DWJfECP3.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-BXFx9Vlr.js → chunk-C7G6YPKG-Bz07eaGt.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-CR3sHpPf.js → chunk-EX3LRPZG-D78kZEMM.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-CJ8L__oG.js → chunk-FWX5IMBZ-CeCa17CW.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-BCl3JSWr.js → chunk-HOUHSVGY-9H4GFuBa.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CZxFMci1.js → chunk-ICXQ74PX--Aa17r4X.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ck3_47dB.js → chunk-MOJQB5TN-C3vXhsPV.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-CVXjES-F.js → chunk-OGEWGWER-G4kZRBkX.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-JjrVgaR7.js → chunk-PUDLZKDR-DWd5kvyM.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-C5YMRAQ0.js → chunk-Q4XR5HBZ-u55p39Up.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DK0mfUHU.js → chunk-V7JOEXUC-BU4YBmV4.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-BKBntpid.js → chunk-VAUOI2AC-VXcmT20q.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-zgE1dG6U.js → chunk-VR4S4FIN-CSjEXLOC.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CZhh1IBq.js → chunk-WYO6CB5R-4WGk2dpL.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CrNjIEem.js → chunk-ZGVPDNZ5-D5Xl8ZV6.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-D21-Dy4r.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-D21-Dy4r.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-Ca_BPfTG.js → cynefin-VYW2F7L2-O6wB49wK.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-CsINQf6g.js → cynefinDiagram-TSTJHNR4-pd6ZogNC.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-CE0DYMFy.js → dagre-VKFMJZFB-D8W1S6fw.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-BqTduEYE.js → diagram-FQU43EPY-Bm1NOkHd.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-Cho718s6.js → diagram-G47NLZAW-CjwnXTIc.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-D0Z8t9UC.js → diagram-NH7WQ7WH-DH1jZ_mu.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-CBacyeDx.js → diagram-OA4YK3LP-B-s969Q_.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-CozpXa4j.js → diagram-WEI45ONY-BU3lv6cL.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-ba4NbQQo.js → ebnfDiagram-CCIWWBDH-FsAeyqcC.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-B3BciOYa.js → erDiagram-Q63AITRT-BRvg5Y9d.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-RhX2WA7g.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-C7N14tdV.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-lPmrRit4.js → ganttDiagram-NO4QXBWP-eATKNh9U.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-BjyclOq0.js → gitGraph-TEB2WS4Q-CNYqsua8.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js → gitGraphDiagram-IHSO6WYX-BUxgluf8.js} +1 -1
- package/web-dist/assets/index-BFuReKr0.js +368 -0
- package/web-dist/assets/index-BH9U3Zlg.css +1 -0
- package/web-dist/assets/{info-DKCQHKI2-Bwycegvf.js → info-DKCQHKI2-HsVLwo1c.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BCvRGj_5.js → infoDiagram-FWYZ7A6U-D7U65xDp.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js → ishikawaDiagram-FXEZZL3T-BsfIYXjX.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-C5ROwFio.js → journeyDiagram-5HDEW3XC-CpkREKol.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-YLPLkpeT.js → kanban-definition-HUTT4EX6-2M3KW6Gw.js} +1 -1
- package/web-dist/assets/{line-KtkNqRgI.js → line-Cs7zWs24.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-D_FfqBe7.js → mermaid-parser.core-Dw-IECV5.js} +3 -3
- package/web-dist/assets/{mermaid.core-Ffv8anVf.js → mermaid.core-C1lrddjT.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-B2jj4vfm.js → mindmap-definition-LN4V7U3C-gVS5On1g.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-BjmWHwra.js → packet-7NZHBO7P-Tti1-UWA.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-Dq3iJDyq.js → pegDiagram-2B236MQR-DE5in93G.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-jBbH1lv9.js → pie-RZYD4A2V-DfLNkvHl.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-C3ETW9lq.js → pieDiagram-ENE6RG2P-DcqSqJqb.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DItSmme7.js → quadrantDiagram-ABIIQ3AL-DR-nRPgN.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-6ey6crgP.js → radar-I7S5WNFK-DGLDBfPd.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-Cii-Mn0E.js → railroad-3IZDKUUU-BaiGEHlV.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-BCeoIOWA.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-lZpXJLD8.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-CaUBrHTL.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-M363ils_.js → railroadDiagram-RFXS5EU6-82aEev5_.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-CTs2_V6T.js → requirementDiagram-TGXJPOKE-Bs8opBRj.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-QTLLcDPD.js → sankeyDiagram-HTMAVEWB-gOb3FIr8.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js → sequenceDiagram-DBY2YBRQ-6DvjtRHR.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-BecB6roG.js → stateDiagram-2N3HPSRC-Cg2LWdUf.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-Digf9CWZ.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-COiYgS0w.js → swimlanes-5IMT3BWC-BDpPdYEl.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-DEZRDWHA.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-Ci22coeH.js → timeline-definition-FHXFAJF6-BSjfeutv.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-4DW35czh.js → treeView-QDETBFTQ-DwgPy6V3.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-Dq_-Z6-Y.js → treemap-6X3UGDF4-7eiifTKW.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-CHQSIEYq.js → vennDiagram-L72KCM5P-9u4AhcnV.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-Bwlh7HCY.js → wardley-OPB4EBWU-DqNb5x9U.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-DctjuPYn.js → wardleyDiagram-EHGQE667-DlJ7h3vg.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-ZoIGosv8.js → xychartDiagram-FW5EYKEG-B_9ukg2S.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +0 -1
- package/web-dist/assets/channel-pr7r6raB.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +0 -1
- package/web-dist/assets/index-BQtjYMpV.css +0 -1
- package/web-dist/assets/index-CLW1Q49U.js +0 -360
- package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV--1Wv7FQQ.js +0 -8
package/dist/frizz.js
CHANGED
|
@@ -10,6 +10,180 @@ var __export = (target2, all) => {
|
|
|
10
10
|
__defProp(target2, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// packages/server/src/local-origin.ts
|
|
14
|
+
import { isIP } from "node:net";
|
|
15
|
+
function publicAuthority(policy) {
|
|
16
|
+
const raw = policy?.publicOrigin;
|
|
17
|
+
if (!raw) return null;
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (url.origin !== raw) return null;
|
|
25
|
+
return {
|
|
26
|
+
hostname: normalizedHostname(url.hostname),
|
|
27
|
+
port: explicitPort(url),
|
|
28
|
+
authority: url.host.toLowerCase(),
|
|
29
|
+
scheme: url.protocol === "https:" ? "https" : "http"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function authoritySendsFetchMetadata(authority) {
|
|
33
|
+
return LOCAL_HOSTNAMES.has(authority.hostname) || authority.scheme === "https";
|
|
34
|
+
}
|
|
35
|
+
function acceptsHostname(hostname, policy) {
|
|
36
|
+
if (LOCAL_HOSTNAMES.has(hostname)) return true;
|
|
37
|
+
if (!policy?.exposed) return false;
|
|
38
|
+
if (isIP(hostname) !== 0) return true;
|
|
39
|
+
return (policy.allowedHosts ?? []).some((allowed) => allowed === "*" || allowed.toLowerCase() === hostname);
|
|
40
|
+
}
|
|
41
|
+
function oneHeader(value) {
|
|
42
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
43
|
+
}
|
|
44
|
+
function normalizedHostname(hostname) {
|
|
45
|
+
const lower = hostname.toLowerCase();
|
|
46
|
+
return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
|
47
|
+
}
|
|
48
|
+
function explicitPort(url) {
|
|
49
|
+
if (url.port) return Number(url.port);
|
|
50
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
51
|
+
}
|
|
52
|
+
function validExpectedPort(port) {
|
|
53
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
54
|
+
}
|
|
55
|
+
function parseLocalHttpOrigin(value, expectedPort, policy) {
|
|
56
|
+
const raw = oneHeader(value);
|
|
57
|
+
if (!raw || !validExpectedPort(expectedPort) || raw !== raw.trim()) return null;
|
|
58
|
+
const declared = publicAuthority(policy);
|
|
59
|
+
if (declared && raw === policy?.publicOrigin) return declared;
|
|
60
|
+
let url;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(raw);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (url.protocol !== "http:" || url.origin !== raw) return null;
|
|
67
|
+
const hostname = normalizedHostname(url.hostname);
|
|
68
|
+
if (!acceptsHostname(hostname, policy) || explicitPort(url) !== expectedPort) return null;
|
|
69
|
+
return { hostname, port: expectedPort, authority: url.host, scheme: "http" };
|
|
70
|
+
}
|
|
71
|
+
function parseLocalHost(value, expectedPort, policy) {
|
|
72
|
+
const raw = oneHeader(value);
|
|
73
|
+
if (!raw || !validExpectedPort(expectedPort) || raw !== raw.trim() || raw.includes(",")) return null;
|
|
74
|
+
let url;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(`http://${raw}`);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
|
|
81
|
+
if (url.host.toLowerCase() !== raw.toLowerCase()) return null;
|
|
82
|
+
const hostname = normalizedHostname(url.hostname);
|
|
83
|
+
const declared = publicAuthority(policy);
|
|
84
|
+
if (declared && hostname === declared.hostname) {
|
|
85
|
+
return url.port === "" || Number(url.port) === declared.port ? declared : null;
|
|
86
|
+
}
|
|
87
|
+
if (!acceptsHostname(hostname, policy) || explicitPort(url) !== expectedPort) return null;
|
|
88
|
+
return { hostname, port: expectedPort, authority: url.host.toLowerCase(), scheme: "http" };
|
|
89
|
+
}
|
|
90
|
+
function hasForwardedAuthority(headers) {
|
|
91
|
+
return FORWARDED_HEADERS.some((name) => headers[name] !== void 0);
|
|
92
|
+
}
|
|
93
|
+
function forwardedAuthorityAllowed(headers, host, policy) {
|
|
94
|
+
if (!hasForwardedAuthority(headers)) return true;
|
|
95
|
+
const declared = publicAuthority(policy);
|
|
96
|
+
return !!declared && declared.hostname === host.hostname;
|
|
97
|
+
}
|
|
98
|
+
function isTrustedLocalHttpRequest(headers, expectedPort, allowMissingOrigin = false, policy) {
|
|
99
|
+
const host = parseLocalHost(headers.host, expectedPort, policy);
|
|
100
|
+
if (!host) return false;
|
|
101
|
+
if (!forwardedAuthorityAllowed(headers, host, policy)) return false;
|
|
102
|
+
if (headers.origin === void 0) return allowMissingOrigin;
|
|
103
|
+
const origin = parseLocalHttpOrigin(headers.origin, expectedPort, policy);
|
|
104
|
+
return !!origin && host.hostname === origin.hostname && host.port === origin.port;
|
|
105
|
+
}
|
|
106
|
+
function allowedLocalCorsOrigin(origin, expectedPort, policy) {
|
|
107
|
+
return parseLocalHttpOrigin(origin, expectedPort, policy) ? origin : void 0;
|
|
108
|
+
}
|
|
109
|
+
function normalizeBindHost(value) {
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
if (!trimmed) throw new Error("--host requires an address");
|
|
112
|
+
const bare = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
113
|
+
const lower = bare.toLowerCase();
|
|
114
|
+
if (lower === "localhost") return LOOPBACK_BIND_HOST;
|
|
115
|
+
if (isIP(bare) === 0) {
|
|
116
|
+
throw new Error(`invalid --host address: ${value} (use an IP address such as 0.0.0.0, :: or 127.0.0.1)`);
|
|
117
|
+
}
|
|
118
|
+
return bare;
|
|
119
|
+
}
|
|
120
|
+
function bindHostIsExposed(host) {
|
|
121
|
+
return !LOOPBACK_BIND_HOSTS.has(host.trim().toLowerCase());
|
|
122
|
+
}
|
|
123
|
+
function normalizePublicOrigin(value) {
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
const invalid = (why) => new Error(`invalid --public-origin: ${value} (${why})`);
|
|
126
|
+
if (!trimmed) throw new Error("--public-origin requires a URL");
|
|
127
|
+
let url;
|
|
128
|
+
try {
|
|
129
|
+
url = new URL(trimmed);
|
|
130
|
+
} catch {
|
|
131
|
+
throw invalid("use a full URL such as https://frizz.example.com");
|
|
132
|
+
}
|
|
133
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw invalid("only http and https are supported");
|
|
134
|
+
if (url.username || url.password) throw invalid("an origin carries no credentials");
|
|
135
|
+
if (url.pathname !== "/" || url.search || url.hash) throw invalid("an origin is a scheme and host only, with no path");
|
|
136
|
+
if (!url.hostname) throw invalid("no hostname");
|
|
137
|
+
return url.origin;
|
|
138
|
+
}
|
|
139
|
+
function normalizeAllowedHosts(values) {
|
|
140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
141
|
+
for (const value of values) {
|
|
142
|
+
for (const entry of value.split(",")) {
|
|
143
|
+
const host = entry.trim().toLowerCase();
|
|
144
|
+
if (host) seen.add(host);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...seen];
|
|
148
|
+
}
|
|
149
|
+
function rejectWebSocketUpgrade(socket, status = 403, reason = "Forbidden") {
|
|
150
|
+
const body = `${reason}
|
|
151
|
+
`;
|
|
152
|
+
const response = [
|
|
153
|
+
`HTTP/1.1 ${status} ${reason}`,
|
|
154
|
+
"Connection: close",
|
|
155
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
156
|
+
`Content-Length: ${Buffer.byteLength(body)}`,
|
|
157
|
+
"",
|
|
158
|
+
body
|
|
159
|
+
].join("\r\n");
|
|
160
|
+
try {
|
|
161
|
+
socket.end(response);
|
|
162
|
+
} catch {
|
|
163
|
+
try {
|
|
164
|
+
socket.destroy();
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST, ALL_INTERFACES_BIND_HOST, LOOPBACK_BIND_HOSTS;
|
|
170
|
+
var init_local_origin = __esm({
|
|
171
|
+
"packages/server/src/local-origin.ts"() {
|
|
172
|
+
"use strict";
|
|
173
|
+
LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
174
|
+
FORWARDED_HEADERS = [
|
|
175
|
+
"forwarded",
|
|
176
|
+
"x-forwarded-for",
|
|
177
|
+
"x-forwarded-host",
|
|
178
|
+
"x-forwarded-port",
|
|
179
|
+
"x-forwarded-proto"
|
|
180
|
+
];
|
|
181
|
+
LOOPBACK_BIND_HOST = "127.0.0.1";
|
|
182
|
+
ALL_INTERFACES_BIND_HOST = "0.0.0.0";
|
|
183
|
+
LOOPBACK_BIND_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "[::1]"]);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
13
187
|
// packages/server/src/frizz-paths.ts
|
|
14
188
|
import { existsSync } from "node:fs";
|
|
15
189
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
@@ -1250,160 +1424,6 @@ var init_project_launch = __esm({
|
|
|
1250
1424
|
}
|
|
1251
1425
|
});
|
|
1252
1426
|
|
|
1253
|
-
// packages/server/src/local-origin.ts
|
|
1254
|
-
import { isIP } from "node:net";
|
|
1255
|
-
function publicAuthority(policy) {
|
|
1256
|
-
const raw = policy?.publicOrigin;
|
|
1257
|
-
if (!raw) return null;
|
|
1258
|
-
let url;
|
|
1259
|
-
try {
|
|
1260
|
-
url = new URL(raw);
|
|
1261
|
-
} catch {
|
|
1262
|
-
return null;
|
|
1263
|
-
}
|
|
1264
|
-
if (url.origin !== raw) return null;
|
|
1265
|
-
return {
|
|
1266
|
-
hostname: normalizedHostname(url.hostname),
|
|
1267
|
-
port: explicitPort(url),
|
|
1268
|
-
authority: url.host.toLowerCase(),
|
|
1269
|
-
scheme: url.protocol === "https:" ? "https" : "http"
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
function authoritySendsFetchMetadata(authority) {
|
|
1273
|
-
return LOCAL_HOSTNAMES.has(authority.hostname) || authority.scheme === "https";
|
|
1274
|
-
}
|
|
1275
|
-
function acceptsHostname(hostname, policy) {
|
|
1276
|
-
if (LOCAL_HOSTNAMES.has(hostname)) return true;
|
|
1277
|
-
if (!policy?.exposed) return false;
|
|
1278
|
-
if (isIP(hostname) !== 0) return true;
|
|
1279
|
-
return (policy.allowedHosts ?? []).some((allowed) => allowed === "*" || allowed.toLowerCase() === hostname);
|
|
1280
|
-
}
|
|
1281
|
-
function oneHeader(value) {
|
|
1282
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1283
|
-
}
|
|
1284
|
-
function normalizedHostname(hostname) {
|
|
1285
|
-
const lower = hostname.toLowerCase();
|
|
1286
|
-
return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
|
1287
|
-
}
|
|
1288
|
-
function explicitPort(url) {
|
|
1289
|
-
if (url.port) return Number(url.port);
|
|
1290
|
-
return url.protocol === "https:" ? 443 : 80;
|
|
1291
|
-
}
|
|
1292
|
-
function validExpectedPort(port) {
|
|
1293
|
-
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
1294
|
-
}
|
|
1295
|
-
function parseLocalHttpOrigin(value, expectedPort, policy) {
|
|
1296
|
-
const raw = oneHeader(value);
|
|
1297
|
-
if (!raw || !validExpectedPort(expectedPort) || raw !== raw.trim()) return null;
|
|
1298
|
-
const declared = publicAuthority(policy);
|
|
1299
|
-
if (declared && raw === policy?.publicOrigin) return declared;
|
|
1300
|
-
let url;
|
|
1301
|
-
try {
|
|
1302
|
-
url = new URL(raw);
|
|
1303
|
-
} catch {
|
|
1304
|
-
return null;
|
|
1305
|
-
}
|
|
1306
|
-
if (url.protocol !== "http:" || url.origin !== raw) return null;
|
|
1307
|
-
const hostname = normalizedHostname(url.hostname);
|
|
1308
|
-
if (!acceptsHostname(hostname, policy) || explicitPort(url) !== expectedPort) return null;
|
|
1309
|
-
return { hostname, port: expectedPort, authority: url.host, scheme: "http" };
|
|
1310
|
-
}
|
|
1311
|
-
function parseLocalHost(value, expectedPort, policy) {
|
|
1312
|
-
const raw = oneHeader(value);
|
|
1313
|
-
if (!raw || !validExpectedPort(expectedPort) || raw !== raw.trim() || raw.includes(",")) return null;
|
|
1314
|
-
let url;
|
|
1315
|
-
try {
|
|
1316
|
-
url = new URL(`http://${raw}`);
|
|
1317
|
-
} catch {
|
|
1318
|
-
return null;
|
|
1319
|
-
}
|
|
1320
|
-
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
|
|
1321
|
-
if (url.host.toLowerCase() !== raw.toLowerCase()) return null;
|
|
1322
|
-
const hostname = normalizedHostname(url.hostname);
|
|
1323
|
-
const declared = publicAuthority(policy);
|
|
1324
|
-
if (declared && hostname === declared.hostname) {
|
|
1325
|
-
return url.port === "" || Number(url.port) === declared.port ? declared : null;
|
|
1326
|
-
}
|
|
1327
|
-
if (!acceptsHostname(hostname, policy) || explicitPort(url) !== expectedPort) return null;
|
|
1328
|
-
return { hostname, port: expectedPort, authority: url.host.toLowerCase(), scheme: "http" };
|
|
1329
|
-
}
|
|
1330
|
-
function hasForwardedAuthority(headers) {
|
|
1331
|
-
return FORWARDED_HEADERS.some((name) => headers[name] !== void 0);
|
|
1332
|
-
}
|
|
1333
|
-
function forwardedAuthorityAllowed(headers, host, policy) {
|
|
1334
|
-
if (!hasForwardedAuthority(headers)) return true;
|
|
1335
|
-
const declared = publicAuthority(policy);
|
|
1336
|
-
return !!declared && declared.hostname === host.hostname;
|
|
1337
|
-
}
|
|
1338
|
-
function isTrustedLocalHttpRequest(headers, expectedPort, allowMissingOrigin = false, policy) {
|
|
1339
|
-
const host = parseLocalHost(headers.host, expectedPort, policy);
|
|
1340
|
-
if (!host) return false;
|
|
1341
|
-
if (!forwardedAuthorityAllowed(headers, host, policy)) return false;
|
|
1342
|
-
if (headers.origin === void 0) return allowMissingOrigin;
|
|
1343
|
-
const origin = parseLocalHttpOrigin(headers.origin, expectedPort, policy);
|
|
1344
|
-
return !!origin && host.hostname === origin.hostname && host.port === origin.port;
|
|
1345
|
-
}
|
|
1346
|
-
function allowedLocalCorsOrigin(origin, expectedPort, policy) {
|
|
1347
|
-
return parseLocalHttpOrigin(origin, expectedPort, policy) ? origin : void 0;
|
|
1348
|
-
}
|
|
1349
|
-
function normalizeBindHost(value) {
|
|
1350
|
-
const trimmed = value.trim();
|
|
1351
|
-
if (!trimmed) throw new Error("--host requires an address");
|
|
1352
|
-
const bare = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
1353
|
-
const lower = bare.toLowerCase();
|
|
1354
|
-
if (lower === "localhost") return LOOPBACK_BIND_HOST;
|
|
1355
|
-
if (isIP(bare) === 0) {
|
|
1356
|
-
throw new Error(`invalid --host address: ${value} (use an IP address such as 0.0.0.0, :: or 127.0.0.1)`);
|
|
1357
|
-
}
|
|
1358
|
-
return bare;
|
|
1359
|
-
}
|
|
1360
|
-
function bindHostIsExposed(host) {
|
|
1361
|
-
return !LOOPBACK_BIND_HOSTS.has(host.trim().toLowerCase());
|
|
1362
|
-
}
|
|
1363
|
-
function normalizePublicOrigin(value) {
|
|
1364
|
-
const trimmed = value.trim();
|
|
1365
|
-
const invalid = (why) => new Error(`invalid --public-origin: ${value} (${why})`);
|
|
1366
|
-
if (!trimmed) throw new Error("--public-origin requires a URL");
|
|
1367
|
-
let url;
|
|
1368
|
-
try {
|
|
1369
|
-
url = new URL(trimmed);
|
|
1370
|
-
} catch {
|
|
1371
|
-
throw invalid("use a full URL such as https://frizz.example.com");
|
|
1372
|
-
}
|
|
1373
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw invalid("only http and https are supported");
|
|
1374
|
-
if (url.username || url.password) throw invalid("an origin carries no credentials");
|
|
1375
|
-
if (url.pathname !== "/" || url.search || url.hash) throw invalid("an origin is a scheme and host only, with no path");
|
|
1376
|
-
if (!url.hostname) throw invalid("no hostname");
|
|
1377
|
-
return url.origin;
|
|
1378
|
-
}
|
|
1379
|
-
function normalizeAllowedHosts(values) {
|
|
1380
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1381
|
-
for (const value of values) {
|
|
1382
|
-
for (const entry of value.split(",")) {
|
|
1383
|
-
const host = entry.trim().toLowerCase();
|
|
1384
|
-
if (host) seen.add(host);
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
return [...seen];
|
|
1388
|
-
}
|
|
1389
|
-
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST, ALL_INTERFACES_BIND_HOST, LOOPBACK_BIND_HOSTS;
|
|
1390
|
-
var init_local_origin = __esm({
|
|
1391
|
-
"packages/server/src/local-origin.ts"() {
|
|
1392
|
-
"use strict";
|
|
1393
|
-
LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1394
|
-
FORWARDED_HEADERS = [
|
|
1395
|
-
"forwarded",
|
|
1396
|
-
"x-forwarded-for",
|
|
1397
|
-
"x-forwarded-host",
|
|
1398
|
-
"x-forwarded-port",
|
|
1399
|
-
"x-forwarded-proto"
|
|
1400
|
-
];
|
|
1401
|
-
LOOPBACK_BIND_HOST = "127.0.0.1";
|
|
1402
|
-
ALL_INTERFACES_BIND_HOST = "0.0.0.0";
|
|
1403
|
-
LOOPBACK_BIND_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "[::1]"]);
|
|
1404
|
-
}
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
1427
|
// packages/server/src/boot-progress.ts
|
|
1408
1428
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1409
1429
|
import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
|
|
@@ -6255,7 +6275,7 @@ var init_receipt_bus = __esm({
|
|
|
6255
6275
|
function fallbackPort(base) {
|
|
6256
6276
|
return base + 1e4;
|
|
6257
6277
|
}
|
|
6258
|
-
var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind,
|
|
6278
|
+
var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, ThreadWatchView, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, ProjectCard, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
|
|
6259
6279
|
var init_src = __esm({
|
|
6260
6280
|
"packages/shared/src/index.ts"() {
|
|
6261
6281
|
"use strict";
|
|
@@ -6467,7 +6487,15 @@ var init_src = __esm({
|
|
|
6467
6487
|
command: external_exports.string().optional(),
|
|
6468
6488
|
// ISO8601 of the shell output file's last write — "last active 6 min ago" for a quiet-but-live
|
|
6469
6489
|
// watcher. Optional (see SubAgentView.lastActivityAt).
|
|
6470
|
-
lastActivityAt: external_exports.string().optional()
|
|
6490
|
+
lastActivityAt: external_exports.string().optional(),
|
|
6491
|
+
// The PROVIDER's session-wide background-task handle (`bzvtnt3ig`), as distinct from `id`, which is
|
|
6492
|
+
// the launch tool_use id. Both name the same shell and neither is a substitute for the other:
|
|
6493
|
+
// `id` is what the two copies of a row reconcile on, and this is the handle the runtime hands the
|
|
6494
|
+
// MODEL — "Command running in background with ID: bzvtnt3ig" is the only id a worker ever sees, so
|
|
6495
|
+
// it is the one it registers a `shell` watcher against. Matching on `id`/`label` alone meant every
|
|
6496
|
+
// such watcher was unfireable (scheduler.evalWatchers, 2026-08-14). Absent for a CODEX row, whose
|
|
6497
|
+
// single `processId` IS its `id`, and for a Claude row between its tool_use and its launch ack.
|
|
6498
|
+
taskId: external_exports.string().optional()
|
|
6471
6499
|
});
|
|
6472
6500
|
CompletionHoldOp = external_exports.object({
|
|
6473
6501
|
label: external_exports.string(),
|
|
@@ -6504,9 +6532,12 @@ var init_src = __esm({
|
|
|
6504
6532
|
title: external_exports.string().max(120)
|
|
6505
6533
|
});
|
|
6506
6534
|
AwaitingHint = external_exports.object({
|
|
6507
|
-
kind: external_exports.enum(["
|
|
6535
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "for", "reason"]),
|
|
6508
6536
|
value: external_exports.string()
|
|
6509
6537
|
});
|
|
6538
|
+
RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session"];
|
|
6539
|
+
RETIRED_LINE_RE = new RegExp(`^\\s*(${RETIRED_AWAITING_KINDS.join("|")}):\\s*\\S`, "im");
|
|
6540
|
+
AWAITING_FOR_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
6510
6541
|
SnoozeUntil = external_exports.string().regex(
|
|
6511
6542
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
6512
6543
|
"Snooze time must be an ISO-8601 UTC instant"
|
|
@@ -6529,12 +6560,6 @@ var init_src = __esm({
|
|
|
6529
6560
|
stopHook: external_exports.boolean(),
|
|
6530
6561
|
heartbeat: external_exports.boolean(),
|
|
6531
6562
|
postCompaction: external_exports.boolean(),
|
|
6532
|
-
/** NOT a fourth trigger — a HOLD over all three. While it is on, nothing is sent for as long as the
|
|
6533
|
-
* thread is blocked on the human: an unanswered ```question fence, a native ask, or a permission
|
|
6534
|
-
* prompt. The footer panel seeds it ON for a fresh arming, with the stop hook, because they are one
|
|
6535
|
-
* intent — see scheduler.ts, "WHAT A PENDING QUESTION DOES TO ALL THREE TRIGGERS". The stored column
|
|
6536
|
-
* still defaults OFF, so an existing row and an older caller both keep the behaviour they had. */
|
|
6537
|
-
pauseOnQuestions: external_exports.boolean(),
|
|
6538
6563
|
intervalSeconds: external_exports.number().int().positive().optional(),
|
|
6539
6564
|
armedAt: external_exports.string(),
|
|
6540
6565
|
/** Last delivery per trigger; stamped separately so each reads its own clock. */
|
|
@@ -6547,33 +6572,156 @@ var init_src = __esm({
|
|
|
6547
6572
|
TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
|
|
6548
6573
|
SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
|
|
6549
6574
|
SIGNOFF_NUDGE_MESSAGE = [
|
|
6550
|
-
`${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and
|
|
6575
|
+
`${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
|
|
6551
6576
|
"",
|
|
6552
|
-
"You rested without a fence, so this thread cannot be triaged.
|
|
6577
|
+
"You rested without a fence, so this thread cannot be triaged.",
|
|
6578
|
+
"",
|
|
6579
|
+
"**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
|
|
6580
|
+
"original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
|
|
6581
|
+
"is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
|
|
6582
|
+
"neither is naming the next step or writing it into a scratch file.",
|
|
6583
|
+
"",
|
|
6584
|
+
"**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
|
|
6585
|
+
"FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
|
|
6586
|
+
"the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
|
|
6587
|
+
"thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
|
|
6588
|
+
"changes they now have to review. If it should be done, name it in one line and let the human dispatch",
|
|
6589
|
+
"it.",
|
|
6590
|
+
"",
|
|
6591
|
+
"**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
|
|
6592
|
+
"investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
|
|
6593
|
+
"work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
|
|
6594
|
+
"the write-up.",
|
|
6595
|
+
"",
|
|
6596
|
+
"**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
|
|
6597
|
+
"right now: ask that one in a question fence. Every other open choice INSIDE the task \u2014 a name, a",
|
|
6598
|
+
"default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
|
|
6599
|
+
"what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
|
|
6600
|
+
"unanswered question is not permission to go build the answer.",
|
|
6601
|
+
"",
|
|
6602
|
+
"Otherwise, add a fence at the END of your next message:",
|
|
6553
6603
|
"",
|
|
6554
6604
|
"- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
|
|
6555
6605
|
"- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
|
|
6556
6606
|
" if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
|
|
6557
6607
|
" **bolded verb phrase**.",
|
|
6558
|
-
"-
|
|
6608
|
+
"- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. It is PURE STRUCTURE: one line",
|
|
6609
|
+
" per thing you are waiting on, a REQUIRED `for:` duration, and one line of `reason:` prose.",
|
|
6610
|
+
"",
|
|
6611
|
+
" ```awaiting",
|
|
6612
|
+
" shell: <the id your runtime gave you>",
|
|
6613
|
+
" pr: owner/repo#123",
|
|
6614
|
+
" for: 2h",
|
|
6615
|
+
" reason: what you are waiting for, in one line",
|
|
6616
|
+
" ```",
|
|
6617
|
+
"",
|
|
6618
|
+
" Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
|
|
6619
|
+
" A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
|
|
6620
|
+
" awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
|
|
6621
|
+
" `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
|
|
6622
|
+
"",
|
|
6623
|
+
"**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
|
|
6624
|
+
"draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
|
|
6625
|
+
"the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
|
|
6626
|
+
"or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
|
|
6627
|
+
"`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
|
|
6628
|
+
"None of them is a `done`. And what is not",
|
|
6629
|
+
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
6630
|
+
"in the handoff.",
|
|
6559
6631
|
"",
|
|
6560
6632
|
"**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
|
|
6561
6633
|
"fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
|
|
6634
|
+
"The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
|
|
6635
|
+
"ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
|
|
6562
6636
|
"",
|
|
6563
6637
|
"Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
|
|
6564
6638
|
"seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
|
|
6565
6639
|
"frizz \u2014 so anything you assumed they had followed, they have not."
|
|
6566
6640
|
].join("\n");
|
|
6567
6641
|
ThreadWatchKind = external_exports.enum(["shell", "github"]);
|
|
6568
|
-
|
|
6569
|
-
|
|
6642
|
+
GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
|
|
6643
|
+
GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
|
|
6644
|
+
GithubWatchStatus = external_exports.object({
|
|
6645
|
+
checks: GithubChecksState,
|
|
6646
|
+
/** The counts behind the verdict, so the card can say "3 running, 12 passed" the way GitHub does
|
|
6647
|
+
* rather than only "checks are running". */
|
|
6648
|
+
running: external_exports.number().int().nonnegative(),
|
|
6649
|
+
passed: external_exports.number().int().nonnegative(),
|
|
6650
|
+
failed: external_exports.number().int().nonnegative(),
|
|
6651
|
+
/** The failing job NAMES, capped — what a human actually needs to decide whether to look. */
|
|
6652
|
+
failing: external_exports.array(external_exports.string()).max(8).default([]),
|
|
6653
|
+
merge: GithubMergeState,
|
|
6654
|
+
/** OPEN | CLOSED | MERGED, lowercased. A merged or closed PR ends the wait outright. */
|
|
6655
|
+
state: external_exports.enum(["open", "closed", "merged"]),
|
|
6656
|
+
/** When frizz last heard from GitHub. A poll can fail or be rate-limited, and a stale reading stated
|
|
6657
|
+
* as current is worse than no reading. */
|
|
6658
|
+
polledAt: external_exports.string(),
|
|
6659
|
+
/** The head commit this verdict was reached on. A PR watcher reports a CI verdict when it is NEWS, and
|
|
6660
|
+
* "red again on a new commit" is news that the bare word `failing` cannot express. Optional because a
|
|
6661
|
+
* reading taken before 2026-08-17 carries none. */
|
|
6662
|
+
head: external_exports.string().optional(),
|
|
6663
|
+
/** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
|
|
6664
|
+
* same job, or a slower job going red after the first — both of which are a second failure the worker
|
|
6665
|
+
* must hear about. Empty when nothing is failing. */
|
|
6666
|
+
failureSig: external_exports.string().optional()
|
|
6667
|
+
}).strict();
|
|
6570
6668
|
ThreadWatchView = external_exports.object({
|
|
6571
6669
|
id: external_exports.string(),
|
|
6572
6670
|
kind: ThreadWatchKind,
|
|
6573
6671
|
target: external_exports.string(),
|
|
6574
6672
|
state: external_exports.enum(["armed", "fired", "dropped"]),
|
|
6575
|
-
createdAt: external_exports.string()
|
|
6673
|
+
createdAt: external_exports.string(),
|
|
6674
|
+
/** `github` rows only, and absent until the first successful poll. */
|
|
6675
|
+
github: GithubWatchStatus.optional()
|
|
6676
|
+
}).strict();
|
|
6677
|
+
PR_WATCH_DEFAULT_FOR_MS = 6 * 60 * 60 * 1e3;
|
|
6678
|
+
PrWatchView = external_exports.object({
|
|
6679
|
+
id: external_exports.string(),
|
|
6680
|
+
/** `owner/repo#N`, normalized — the same string the board's row and the status book are keyed by. */
|
|
6681
|
+
target: external_exports.string(),
|
|
6682
|
+
state: external_exports.enum(["armed", "dropped", "settled"]),
|
|
6683
|
+
createdAt: external_exports.string(),
|
|
6684
|
+
/** The PR's checks/mergeability as the poller last saw it. Absent until the first successful poll. */
|
|
6685
|
+
github: GithubWatchStatus.optional()
|
|
6686
|
+
}).strict();
|
|
6687
|
+
AddOwnPrWatchInput = external_exports.object({
|
|
6688
|
+
slug: ThreadSlug,
|
|
6689
|
+
/** `owner/repo#123` or a PR URL. Parsed server-side; an unparseable ref is refused rather than stored,
|
|
6690
|
+
* because a watcher that can never fire is worse than none — the worker rests believing it is covered. */
|
|
6691
|
+
target: external_exports.string().trim().min(1).max(200),
|
|
6692
|
+
/** How long to watch, as a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration).
|
|
6693
|
+
*
|
|
6694
|
+
* A PR nobody ever reviews would otherwise be polled forever, and a thread parked on it would wait
|
|
6695
|
+
* forever with it — the same unbounded wait the awaiting fence's `for:` closes, one level down. A
|
|
6696
|
+
* duration rather than an instant for the same reason it is one there: it cannot be written in the
|
|
6697
|
+
* past (maintainer 2026-08-15, asking for it explicitly).
|
|
6698
|
+
*
|
|
6699
|
+
* REQUIRED BY THE TOOL, OPTIONAL ON THE WIRE, and the asymmetry is deliberate. A worker's MCP server
|
|
6700
|
+
* outlives every frizz restart, so a session dispatched before this existed still holds a binary that
|
|
6701
|
+
* cannot send it — and making the RPC reject that would break `watch_pr` outright for every thread
|
|
6702
|
+
* already running, with no recourse from inside those threads. Absent ⇒ PR_WATCH_DEFAULT_FOR, which
|
|
6703
|
+
* still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
|
|
6704
|
+
* is better bounded than broken. */
|
|
6705
|
+
for: external_exports.string().trim().min(1).max(16).optional()
|
|
6706
|
+
}).strict();
|
|
6707
|
+
AddOwnPrWatchResult = external_exports.object({
|
|
6708
|
+
id: external_exports.string(),
|
|
6709
|
+
target: external_exports.string(),
|
|
6710
|
+
/** True when this exact PR was ALREADY watched by this thread, so the call registered nothing new.
|
|
6711
|
+
* Re-registering after a compaction is the common case, and a duplicate would double every wake. */
|
|
6712
|
+
alreadyArmed: external_exports.boolean(),
|
|
6713
|
+
watches: external_exports.array(PrWatchView)
|
|
6576
6714
|
}).strict();
|
|
6715
|
+
DropOwnPrWatchInput = external_exports.object({
|
|
6716
|
+
slug: ThreadSlug,
|
|
6717
|
+
id: external_exports.string().min(1).max(64)
|
|
6718
|
+
}).strict();
|
|
6719
|
+
DropOwnPrWatchResult = external_exports.object({
|
|
6720
|
+
dropped: external_exports.boolean(),
|
|
6721
|
+
watches: external_exports.array(PrWatchView)
|
|
6722
|
+
}).strict();
|
|
6723
|
+
ListOwnPrWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
6724
|
+
OwnPrWatchesResult = external_exports.object({ watches: external_exports.array(PrWatchView) }).strict();
|
|
6577
6725
|
ThreadTimerView = external_exports.object({
|
|
6578
6726
|
id: external_exports.string(),
|
|
6579
6727
|
prompt: external_exports.string(),
|
|
@@ -6731,6 +6879,16 @@ var init_src = __esm({
|
|
|
6731
6879
|
// AUTO-bump (the scheduler resumes the agent with exactly this text) rather than a reminder, which is
|
|
6732
6880
|
// the distinction the held row's tooltip renders. Absent ⇒ the card merely re-surfaces.
|
|
6733
6881
|
snoozePrompt: external_exports.string().optional(),
|
|
6882
|
+
/** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
|
|
6883
|
+
* until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
|
|
6884
|
+
* thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
|
|
6885
|
+
*
|
|
6886
|
+
* It travels because the chat has to honour it too (2026-08-14). Until then only the QUEUE did, on
|
|
6887
|
+
* the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
|
|
6888
|
+
* reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
|
|
6889
|
+
* explicitly parked THIS rest, showing them the same card with the same button one surface over is
|
|
6890
|
+
* not information, and they said so. */
|
|
6891
|
+
bgSnoozed: external_exports.boolean().optional(),
|
|
6734
6892
|
// Which Claude transport serves this thread: "broker" = a session-broker-owned Agent SDK session
|
|
6735
6893
|
// (typed control channel), "tmux" = the interactive TUI in a pane. Only the broker can be asked to
|
|
6736
6894
|
// reload its plugin closure in place, so the board needs it to decide whether to offer that verb at
|
|
@@ -6848,6 +7006,13 @@ var init_src = __esm({
|
|
|
6848
7006
|
// Optional so a pre-restart server keeps working; absent means "fall back to `/`", i.e. the old
|
|
6849
7007
|
// behaviour. Registry-derived, so it is the same slug every other surface links to.
|
|
6850
7008
|
projectSlug: external_exports.string().optional(),
|
|
7009
|
+
// The server's home directory — the expansion of a `~` a worker wrote in prose. Agents reference
|
|
7010
|
+
// files that way constantly (`~/.claude/CLAUDE.md`), and the browser has no way to derive it, so a
|
|
7011
|
+
// `~`-anchored Markdown link had no absolute path to become and stayed a same-origin anchor that
|
|
7012
|
+
// navigated out of Frizz. The client only ever uses it to build a path it then hands BACK to the
|
|
7013
|
+
// server, which realpath-gates it exactly as it gates one the author typed in full. Optional so a
|
|
7014
|
+
// pre-restart server keeps working; absent means `~` links stay unresolved, i.e. the old behaviour.
|
|
7015
|
+
homeDir: external_exports.string().optional(),
|
|
6851
7016
|
// (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
|
|
6852
7017
|
// board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
|
|
6853
7018
|
// was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
|
|
@@ -6954,13 +7119,23 @@ var init_src = __esm({
|
|
|
6954
7119
|
// Default action for a vetted non-image local path in agent markdown. Image clicks always use the
|
|
6955
7120
|
// OS default viewer so screenshots retain their expected behavior.
|
|
6956
7121
|
localFileOpener: LocalFileOpener.optional(),
|
|
6957
|
-
// GitHub batch-dispatch prompt
|
|
6958
|
-
// unset OR blank the server falls back to its exported
|
|
6959
|
-
//
|
|
6960
|
-
//
|
|
6961
|
-
//
|
|
6962
|
-
|
|
6963
|
-
|
|
7122
|
+
// The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
|
|
7123
|
+
// unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
|
|
7124
|
+
// the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
|
|
7125
|
+
// prepended by the server (not part of the editable template) so a custom prompt can never break the
|
|
7126
|
+
// thread↔.frizz-file binding. Optional so old settings blobs parse.
|
|
7127
|
+
//
|
|
7128
|
+
// ONE field, not one per kind. Issue and PR each had their own template until 2026-08-15, and the two
|
|
7129
|
+
// said the same thing twice: read the whole thread, be skeptical, cite what you checked, post nothing.
|
|
7130
|
+
// A person tuning "be more dubious" had to make the same edit in two boxes and keep them in step. The
|
|
7131
|
+
// ONE thing that genuinely differs per kind — which `gh` command reads the item — is a line in the
|
|
7132
|
+
// metadata block, so the merged template just carries both, and the worker picks the one that applies.
|
|
7133
|
+
//
|
|
7134
|
+
// There is no migration off the two old keys, and that is deliberate (the maintainer's call): Settings
|
|
7135
|
+
// is a non-strict z.object, so `githubIssuePrompt`/`githubPrPrompt` are STRIPPED the moment an old
|
|
7136
|
+
// blob is parsed. Any existing override is dropped and the reader gets the new shipped default —
|
|
7137
|
+
// exactly the intended backfill. Merging two customized templates into one has no correct answer.
|
|
7138
|
+
githubPrompt: external_exports.string().optional()
|
|
6964
7139
|
});
|
|
6965
7140
|
DispatchProviderPreferences = external_exports.object({
|
|
6966
7141
|
model: external_exports.string().trim().min(1).max(200).optional(),
|
|
@@ -7089,9 +7264,6 @@ var init_src = __esm({
|
|
|
7089
7264
|
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
7090
7265
|
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
7091
7266
|
postCompaction: external_exports.boolean().default(false),
|
|
7092
|
-
// The QUESTION HOLD (2026-08-11). Defaulted for the same reason as the trigger above: a caller that
|
|
7093
|
-
// has never heard of it means "don't hold", which is also this option's own default.
|
|
7094
|
-
pauseOnQuestions: external_exports.boolean().default(false),
|
|
7095
7267
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
7096
7268
|
}).strict();
|
|
7097
7269
|
SetOwnThreadRecurringPromptInput = external_exports.object({
|
|
@@ -7103,8 +7275,14 @@ var init_src = __esm({
|
|
|
7103
7275
|
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
7104
7276
|
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
7105
7277
|
postCompaction: external_exports.boolean().default(false),
|
|
7106
|
-
|
|
7107
|
-
|
|
7278
|
+
/** ACCEPTED AND IGNORED. The question hold was deleted 2026-08-16 (see scheduler.ts) and no caller in
|
|
7279
|
+
* this repo sends it any more — but this object is `.strict()`, and the caller on the other end is a
|
|
7280
|
+
* DETACHED worker daemon holding the `frizz-mcp.mjs` it was spawned with. Those outlive a server
|
|
7281
|
+
* restart by design, so for as long as any pre-2026-08-16 worker is alive a `start` would otherwise
|
|
7282
|
+
* come back as a validation error on a field the model cannot see it is sending. Tolerated here rather
|
|
7283
|
+
* than in the router so the shape stays one declaration; delete it once no such worker can be running.
|
|
7284
|
+
* The BROWSER input above needs no such clause — a stale tab is one reload away. */
|
|
7285
|
+
pauseOnQuestions: external_exports.boolean().optional(),
|
|
7108
7286
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
7109
7287
|
}).strict();
|
|
7110
7288
|
SetOwnThreadRecurringPromptResult = external_exports.object({
|
|
@@ -7131,6 +7309,23 @@ var init_src = __esm({
|
|
|
7131
7309
|
OwnThreadTimersResult = external_exports.object({
|
|
7132
7310
|
timers: external_exports.array(ThreadTimerView)
|
|
7133
7311
|
}).strict();
|
|
7312
|
+
ThreadActivityItem = external_exports.object({
|
|
7313
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr"]),
|
|
7314
|
+
/** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
|
|
7315
|
+
* was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
|
|
7316
|
+
id: external_exports.string(),
|
|
7317
|
+
label: external_exports.string(),
|
|
7318
|
+
/** ISO8601 of when it started or was armed — absent when frizz has no instant for it. */
|
|
7319
|
+
since: external_exports.string().optional(),
|
|
7320
|
+
/** A timer's fire instant, or a PR's expiry. Absent for shells and sub-agents. */
|
|
7321
|
+
until: external_exports.string().optional()
|
|
7322
|
+
}).strict();
|
|
7323
|
+
ListOwnThreadActivityInput = external_exports.object({
|
|
7324
|
+
slug: ThreadSlug
|
|
7325
|
+
}).strict();
|
|
7326
|
+
OwnThreadActivityResult = external_exports.object({
|
|
7327
|
+
activity: external_exports.array(ThreadActivityItem)
|
|
7328
|
+
}).strict();
|
|
7134
7329
|
SetOwnThreadTimerResult = external_exports.object({
|
|
7135
7330
|
id: external_exports.string(),
|
|
7136
7331
|
fireAt: external_exports.string(),
|
|
@@ -7140,49 +7335,6 @@ var init_src = __esm({
|
|
|
7140
7335
|
cancelled: external_exports.boolean(),
|
|
7141
7336
|
timers: external_exports.array(ThreadTimerView)
|
|
7142
7337
|
}).strict();
|
|
7143
|
-
AddOwnThreadWatchInput = external_exports.object({
|
|
7144
|
-
slug: ThreadSlug,
|
|
7145
|
-
// REGISTERABLE kinds only, which is `shell` and nothing else — deliberately narrower than
|
|
7146
|
-
// `ThreadWatchKind`, whose `github` member is a board-derived VIEW row with no registration behind it.
|
|
7147
|
-
// The MCP tool already refuses a PR here in words ("a pr wait does not belong here — use an ```awaiting
|
|
7148
|
-
// fence"); this is the same refusal at the wire, where it cannot be talked past.
|
|
7149
|
-
kind: RegisterableThreadWatchKind,
|
|
7150
|
-
target: ThreadWatchTarget,
|
|
7151
|
-
/** The worker is BLOCKING on this one inside its tool call, so the scheduler settles it silently
|
|
7152
|
-
* instead of delivering a wake — see the column comment in storage.ts. Defaulted, so a caller that
|
|
7153
|
-
* has never heard of it registers an ordinary background watch. */
|
|
7154
|
-
foreground: external_exports.boolean().default(false)
|
|
7155
|
-
}).strict();
|
|
7156
|
-
PromoteOwnThreadWatchInput = external_exports.object({
|
|
7157
|
-
slug: ThreadSlug,
|
|
7158
|
-
id: external_exports.string().min(1).max(64)
|
|
7159
|
-
}).strict();
|
|
7160
|
-
PromoteOwnThreadWatchResult = external_exports.object({
|
|
7161
|
-
promoted: external_exports.boolean(),
|
|
7162
|
-
watches: external_exports.array(ThreadWatchView)
|
|
7163
|
-
}).strict();
|
|
7164
|
-
DropOwnThreadWatchInput = external_exports.object({
|
|
7165
|
-
slug: ThreadSlug,
|
|
7166
|
-
id: external_exports.string().min(1).max(64)
|
|
7167
|
-
}).strict();
|
|
7168
|
-
ListOwnThreadWatchesInput = external_exports.object({
|
|
7169
|
-
slug: ThreadSlug
|
|
7170
|
-
}).strict();
|
|
7171
|
-
OwnThreadWatchesResult = external_exports.object({
|
|
7172
|
-
watches: external_exports.array(ThreadWatchView)
|
|
7173
|
-
}).strict();
|
|
7174
|
-
AddOwnThreadWatchResult = external_exports.object({
|
|
7175
|
-
id: external_exports.string(),
|
|
7176
|
-
/** True when this exact (kind, target) was ALREADY armed on the thread, so the call registered
|
|
7177
|
-
* nothing new. A worker re-registering after a compaction is the common case, and silently minting a
|
|
7178
|
-
* duplicate would mean two wakes for one event. */
|
|
7179
|
-
alreadyArmed: external_exports.boolean(),
|
|
7180
|
-
watches: external_exports.array(ThreadWatchView)
|
|
7181
|
-
}).strict();
|
|
7182
|
-
DropOwnThreadWatchResult = external_exports.object({
|
|
7183
|
-
dropped: external_exports.boolean(),
|
|
7184
|
-
watches: external_exports.array(ThreadWatchView)
|
|
7185
|
-
}).strict();
|
|
7186
7338
|
SetOwnThreadStopHookInput = external_exports.object({
|
|
7187
7339
|
slug: ThreadSlug,
|
|
7188
7340
|
prompt: RecurringPromptText.nullable(),
|
|
@@ -7279,12 +7431,52 @@ ${DISPATCH_TASK_BANNER}
|
|
|
7279
7431
|
WAKE_MULTI_HEAD = new RegExp(
|
|
7280
7432
|
String.raw`^(👤|🤖) (\d+) new GitHub items on (${WAKE_REF})\. Read exactly these — ` + WAKE_SCOPE + String.raw` — and continue:$`
|
|
7281
7433
|
);
|
|
7434
|
+
PR_WATCH_FINISHED = new RegExp(String.raw`^⏰ (${WAKE_REF}) was (MERGED|CLOSED)\.$`);
|
|
7435
|
+
PR_WATCH_CI_PASSED = new RegExp(String.raw`^✅ CI PASSED on (${WAKE_REF}) — (\d+) checks? green\.$`);
|
|
7436
|
+
PR_WATCH_CI_FAILED = new RegExp(String.raw`^❌ CI FAILED on (${WAKE_REF})(?:: (.+?))?\.$`);
|
|
7282
7437
|
GithubStatus = external_exports.object({
|
|
7283
7438
|
installed: external_exports.boolean(),
|
|
7284
7439
|
inRepo: external_exports.boolean(),
|
|
7285
7440
|
nameWithOwner: external_exports.string().nullable(),
|
|
7286
7441
|
authed: external_exports.boolean()
|
|
7287
7442
|
});
|
|
7443
|
+
GithubRefCard = external_exports.object({
|
|
7444
|
+
ref: external_exports.string(),
|
|
7445
|
+
kind: external_exports.enum(["issue", "pr", "commit"]),
|
|
7446
|
+
repo: external_exports.string(),
|
|
7447
|
+
// owner/repo, for the card's header line
|
|
7448
|
+
url: external_exports.string(),
|
|
7449
|
+
title: external_exports.string(),
|
|
7450
|
+
body: external_exports.string(),
|
|
7451
|
+
// already truncated server-side to the card's excerpt budget
|
|
7452
|
+
state: external_exports.string(),
|
|
7453
|
+
// OPEN | CLOSED | MERGED | DRAFT — empty for a commit, which has no state
|
|
7454
|
+
stateReason: external_exports.string().optional(),
|
|
7455
|
+
// COMPLETED | NOT_PLANNED | REOPENED — GitHub's closed-issue nuance
|
|
7456
|
+
at: external_exports.string().optional(),
|
|
7457
|
+
// ISO: opened-at for an issue/PR, committed-at for a commit
|
|
7458
|
+
authorLogin: external_exports.string().optional(),
|
|
7459
|
+
authorName: external_exports.string().optional(),
|
|
7460
|
+
// commits carry a git author name with no GitHub account behind it
|
|
7461
|
+
authorAvatar: external_exports.string().optional(),
|
|
7462
|
+
labels: external_exports.array(external_exports.object({ name: external_exports.string(), color: external_exports.string() })).default([]),
|
|
7463
|
+
additions: external_exports.number().int().nonnegative().optional(),
|
|
7464
|
+
deletions: external_exports.number().int().nonnegative().optional(),
|
|
7465
|
+
changedFiles: external_exports.number().int().nonnegative().optional(),
|
|
7466
|
+
comments: external_exports.number().int().nonnegative().optional(),
|
|
7467
|
+
// Epoch ms of the fetch this card came from. The CLIENT owns the freshness decision (render the
|
|
7468
|
+
// cached card instantly, then revalidate if it is old), so it has to be able to see the age.
|
|
7469
|
+
fetchedAt: external_exports.number().int().nonnegative()
|
|
7470
|
+
});
|
|
7471
|
+
GithubRefPreviewInput = external_exports.object({
|
|
7472
|
+
refs: external_exports.array(external_exports.string().min(3).max(120)).min(1).max(100),
|
|
7473
|
+
refresh: external_exports.boolean().default(false)
|
|
7474
|
+
});
|
|
7475
|
+
GithubRefPreviewResult = external_exports.object({
|
|
7476
|
+
cards: external_exports.array(GithubRefCard),
|
|
7477
|
+
missing: external_exports.array(external_exports.string()),
|
|
7478
|
+
error: external_exports.string().optional()
|
|
7479
|
+
});
|
|
7288
7480
|
GithubItem = external_exports.object({
|
|
7289
7481
|
kind: external_exports.enum(["issue", "pr"]),
|
|
7290
7482
|
number: external_exports.number().int().positive(),
|
|
@@ -7951,6 +8143,7 @@ var init_local_image = __esm({
|
|
|
7951
8143
|
});
|
|
7952
8144
|
|
|
7953
8145
|
// src/production.ts
|
|
8146
|
+
init_local_origin();
|
|
7954
8147
|
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7955
8148
|
import { homedir as homedir8 } from "node:os";
|
|
7956
8149
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -8421,6 +8614,10 @@ var ANSI = {
|
|
|
8421
8614
|
cyan: "\x1B[36m",
|
|
8422
8615
|
red: "\x1B[31m"
|
|
8423
8616
|
};
|
|
8617
|
+
function clockTime(ms) {
|
|
8618
|
+
const at = new Date(ms);
|
|
8619
|
+
return [at.getHours(), at.getMinutes(), at.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
8620
|
+
}
|
|
8424
8621
|
function formatDuration(ms) {
|
|
8425
8622
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
8426
8623
|
if (ms < 1e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
@@ -8448,6 +8645,8 @@ var Readout = class {
|
|
|
8448
8645
|
settled = false;
|
|
8449
8646
|
/** Non-TTY only: suppress repeating an identical status row. */
|
|
8450
8647
|
lastPlainLine = "";
|
|
8648
|
+
/** Has a post-boot notice printed yet? The first one leads with a blank line, the rest stack. */
|
|
8649
|
+
noticed = false;
|
|
8451
8650
|
constructor(options2 = {}) {
|
|
8452
8651
|
this.out = options2.output ?? process.stdout;
|
|
8453
8652
|
this.tty = this.out.isTTY === true;
|
|
@@ -8577,8 +8776,33 @@ ${SYNC_END}`);
|
|
|
8577
8776
|
this.out.write(`${line}
|
|
8578
8777
|
`);
|
|
8579
8778
|
}
|
|
8779
|
+
/**
|
|
8780
|
+
* A lifecycle line printed AFTER the ready block — the board restarting, updating, or falling over.
|
|
8781
|
+
*
|
|
8782
|
+
* The block above it is a snapshot of one moment; these are a timeline, and they arrive minutes or
|
|
8783
|
+
* hours later because somebody clicked Restart in a browser or a control-plane child crashed. So
|
|
8784
|
+
* this row carries a wall-clock time, which no other row here does: without one, a beat found on
|
|
8785
|
+
* a scrolled-back terminal says what happened and gives no way to tell when.
|
|
8786
|
+
*/
|
|
8787
|
+
notice(tone, label, detail) {
|
|
8788
|
+
const glyph = tone === "done" ? this.c(ANSI.green, "\u2713") : tone === "failed" ? this.c(ANSI.red, "\u2717") : this.c(ANSI.cyan, "\u21BB");
|
|
8789
|
+
if (!this.tty || this.debug) {
|
|
8790
|
+
this.out.write(`frizz: ${label.toLowerCase()}${detail ? ` \u2014 ${detail}` : ""}
|
|
8791
|
+
`);
|
|
8792
|
+
return;
|
|
8793
|
+
}
|
|
8794
|
+
const lead = this.noticed ? "" : "\n";
|
|
8795
|
+
this.noticed = true;
|
|
8796
|
+
const time = this.c(ANSI.dim, clockTime(this.now()));
|
|
8797
|
+
const tail = detail ? this.c(ANSI.dim, detail) : "";
|
|
8798
|
+
this.write(`${lead} ${glyph} ${time} ${tail ? label.padEnd(12) : label}${tail}`);
|
|
8799
|
+
}
|
|
8580
8800
|
/** Print a line without disturbing the repaint region (used for the debug feed and warnings). */
|
|
8581
8801
|
note(line) {
|
|
8802
|
+
this.write(line);
|
|
8803
|
+
}
|
|
8804
|
+
/** Put one line on screen, above the repaint region if there still is one. */
|
|
8805
|
+
write(line) {
|
|
8582
8806
|
if (this.settled || !this.tty || this.debug) {
|
|
8583
8807
|
this.out.write(`${line}
|
|
8584
8808
|
`);
|
|
@@ -8625,6 +8849,7 @@ ${SYNC_END}`);
|
|
|
8625
8849
|
}),
|
|
8626
8850
|
// Yellow, above the dim hint: exposing the board off loopback is the one launch outcome the
|
|
8627
8851
|
// operator must not skim past, so it may not share the hint's low-contrast styling.
|
|
8852
|
+
...options2.qr?.length ? ["", ...options2.qr.map((row) => ` ${row}`)] : [],
|
|
8628
8853
|
...options2.warning ? ["", ` ${this.c(ANSI.yellow, options2.warning)}`] : [],
|
|
8629
8854
|
...hint ? ["", ` ${this.c(ANSI.dim, hint)}`] : [],
|
|
8630
8855
|
""
|
|
@@ -8691,6 +8916,19 @@ function sliceVisible(line, limit) {
|
|
|
8691
8916
|
}
|
|
8692
8917
|
return `${out}${ANSI.reset}`;
|
|
8693
8918
|
}
|
|
8919
|
+
function renderSupervisorActivity(readout2, event) {
|
|
8920
|
+
if (!readout2) return;
|
|
8921
|
+
switch (event.kind) {
|
|
8922
|
+
case "restarting":
|
|
8923
|
+
return readout2.notice("progress", "Restarting", event.message);
|
|
8924
|
+
case "updating":
|
|
8925
|
+
return readout2.notice("progress", "Updating", event.message);
|
|
8926
|
+
case "ready":
|
|
8927
|
+
return readout2.notice("done", "Restarted", event.ms === void 0 ? event.message : `in ${formatDuration(event.ms)}`);
|
|
8928
|
+
case "failed":
|
|
8929
|
+
return readout2.notice("failed", "Failed", event.message);
|
|
8930
|
+
}
|
|
8931
|
+
}
|
|
8694
8932
|
|
|
8695
8933
|
// src/production.ts
|
|
8696
8934
|
init_logging();
|
|
@@ -8801,6 +9039,8 @@ function parseCliArgs(argv) {
|
|
|
8801
9039
|
"--detach",
|
|
8802
9040
|
"--stop",
|
|
8803
9041
|
"--status",
|
|
9042
|
+
"--link",
|
|
9043
|
+
"--cloud",
|
|
8804
9044
|
"--help",
|
|
8805
9045
|
"-h",
|
|
8806
9046
|
"--dev",
|
|
@@ -8832,6 +9072,8 @@ function parseCliArgs(argv) {
|
|
|
8832
9072
|
// Retain the option in the parsed shape for callers, but normal frizz-dev is always attached.
|
|
8833
9073
|
foreground: true,
|
|
8834
9074
|
stop: args2.has("--stop"),
|
|
9075
|
+
link: args2.has("--link"),
|
|
9076
|
+
cloud: args2.has("--cloud"),
|
|
8835
9077
|
status: args2.has("--status"),
|
|
8836
9078
|
help: args2.has("--help") || args2.has("-h"),
|
|
8837
9079
|
dev: args2.has("--dev"),
|
|
@@ -8844,6 +9086,7 @@ function parseCliArgs(argv) {
|
|
|
8844
9086
|
};
|
|
8845
9087
|
}
|
|
8846
9088
|
var EXPOSED_WARNING = "Anyone who can reach this address can run commands on this machine as you. Frizz has no login \u2014 only do this on a network you trust.";
|
|
9089
|
+
var REUSED_NETWORK_FLAGS_WARNING = "This joined the Frizz already running on this machine, so --host/--public-origin were ignored. Stop it first (frizz --stop) and relaunch with the flag to change how the board is reached.";
|
|
8847
9090
|
var PUBLIC_ORIGIN_WARNING = "Frizz has no login of its own, so whatever sits in front of this origin IS the access control. Require authentication there (Cloudflare Access, Tailscale) \u2014 an unauthenticated tunnel lets anyone with the URL run commands on this machine as you.";
|
|
8848
9091
|
function resolveBindSelection(options2, env = process.env) {
|
|
8849
9092
|
const fromEnv = env.FRIZZ_HOST?.trim();
|
|
@@ -8854,7 +9097,14 @@ function resolveBindSelection(options2, env = process.env) {
|
|
|
8854
9097
|
]);
|
|
8855
9098
|
const publicOriginRaw = options2.publicOrigin ?? env.FRIZZ_PUBLIC_ORIGIN?.trim();
|
|
8856
9099
|
const publicOrigin = publicOriginRaw ? normalizePublicOrigin(publicOriginRaw) : void 0;
|
|
8857
|
-
|
|
9100
|
+
const publicToken = publicOrigin ? env.FRIZZ_PUBLIC_TOKEN?.trim() || void 0 : void 0;
|
|
9101
|
+
return {
|
|
9102
|
+
host,
|
|
9103
|
+
exposed: bindHostIsExposed(host),
|
|
9104
|
+
allowedHosts,
|
|
9105
|
+
...publicOrigin ? { publicOrigin } : {},
|
|
9106
|
+
...publicToken ? { publicToken } : {}
|
|
9107
|
+
};
|
|
8858
9108
|
}
|
|
8859
9109
|
function boardAddress(url) {
|
|
8860
9110
|
return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
|
|
@@ -9140,16 +9390,129 @@ import { basename as basename6, dirname as dirname7, extname as extname2, isAbso
|
|
|
9140
9390
|
import { fileURLToPath } from "node:url";
|
|
9141
9391
|
import watcher from "@parcel/watcher";
|
|
9142
9392
|
|
|
9393
|
+
// packages/server/src/restart-supervisor.ts
|
|
9394
|
+
import { request as requestHttp, createServer as createServer2 } from "node:http";
|
|
9395
|
+
import { connect } from "node:net";
|
|
9396
|
+
|
|
9397
|
+
// packages/server/src/access-codes.ts
|
|
9398
|
+
import { randomBytes, timingSafeEqual, createHmac } from "node:crypto";
|
|
9399
|
+
var CODE_BYTES = 16;
|
|
9400
|
+
var SESSION_BYTES = 32;
|
|
9401
|
+
var DEFAULT_CODE_TTL_MS = 5 * 6e4;
|
|
9402
|
+
var DEFAULT_SESSION_TTL_MS = 365 * 24 * 60 * 6e4;
|
|
9403
|
+
function defaultRandomToken(bytes) {
|
|
9404
|
+
return randomBytes(bytes).toString("base64url");
|
|
9405
|
+
}
|
|
9406
|
+
function secretsMatch(a, b) {
|
|
9407
|
+
const left = Buffer.from(a);
|
|
9408
|
+
const right = Buffer.from(b);
|
|
9409
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
9410
|
+
}
|
|
9411
|
+
var AccessStore = class {
|
|
9412
|
+
codes = /* @__PURE__ */ new Map();
|
|
9413
|
+
options;
|
|
9414
|
+
/** Signing key for sessions. Persisted by the caller; a fresh one signs every device out. */
|
|
9415
|
+
signingKey;
|
|
9416
|
+
constructor(options2 = {}) {
|
|
9417
|
+
this.signingKey = options2.signingKey ?? randomBytes(32);
|
|
9418
|
+
this.options = {
|
|
9419
|
+
codeTtlMs: options2.codeTtlMs ?? DEFAULT_CODE_TTL_MS,
|
|
9420
|
+
sessionTtlMs: options2.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,
|
|
9421
|
+
now: options2.now ?? Date.now,
|
|
9422
|
+
randomToken: options2.randomToken ?? defaultRandomToken,
|
|
9423
|
+
onConsumed: options2.onConsumed
|
|
9424
|
+
};
|
|
9425
|
+
}
|
|
9426
|
+
/** Mint a code to show as a QR or a link. Minting does not invalidate codes already outstanding. */
|
|
9427
|
+
issue() {
|
|
9428
|
+
this.sweep();
|
|
9429
|
+
const now = this.options.now();
|
|
9430
|
+
const code = {
|
|
9431
|
+
code: this.options.randomToken(CODE_BYTES),
|
|
9432
|
+
createdAt: now,
|
|
9433
|
+
expiresAt: now + this.options.codeTtlMs
|
|
9434
|
+
};
|
|
9435
|
+
this.codes.set(code.code, code);
|
|
9436
|
+
return { code: code.code, createdAt: code.createdAt, expiresAt: code.expiresAt };
|
|
9437
|
+
}
|
|
9438
|
+
/**
|
|
9439
|
+
* Trade a code for a session, at most once, ever.
|
|
9440
|
+
*
|
|
9441
|
+
* The consumed marker is set BEFORE anything else can observe success, and JS runs this method to
|
|
9442
|
+
* completion without interleaving, so two browsers racing the same code cannot both win. That
|
|
9443
|
+
* atomicity is what makes "single-use" true rather than aspirational — it is the first thing the
|
|
9444
|
+
* tests attack.
|
|
9445
|
+
*/
|
|
9446
|
+
redeem(code) {
|
|
9447
|
+
if (!code) return { ok: false, reason: "unknown" };
|
|
9448
|
+
const stored = this.codes.get(code);
|
|
9449
|
+
if (!stored || !secretsMatch(stored.code, code)) return { ok: false, reason: "unknown" };
|
|
9450
|
+
const now = this.options.now();
|
|
9451
|
+
if (stored.consumedAt !== void 0) return { ok: false, reason: "already-used" };
|
|
9452
|
+
if (now >= stored.expiresAt) {
|
|
9453
|
+
this.sweep();
|
|
9454
|
+
return { ok: false, reason: "expired" };
|
|
9455
|
+
}
|
|
9456
|
+
stored.consumedAt = now;
|
|
9457
|
+
this.sweep();
|
|
9458
|
+
this.options.onConsumed?.(code);
|
|
9459
|
+
return { ok: true, session: this.mintSession(now), expiresAt: now + this.options.sessionTtlMs };
|
|
9460
|
+
}
|
|
9461
|
+
/**
|
|
9462
|
+
* A session is `<expiry>.<hmac>` — stateless, so it needs no server-side table, and signed with a key
|
|
9463
|
+
* the caller persists so it survives a restart. Self-describing expiry means a stale cookie is
|
|
9464
|
+
* rejected without a lookup, and rotating the key revokes every outstanding session at once.
|
|
9465
|
+
*/
|
|
9466
|
+
mintSession(now) {
|
|
9467
|
+
const expiresAt = now + this.options.sessionTtlMs;
|
|
9468
|
+
const nonce = this.options.randomToken(SESSION_BYTES);
|
|
9469
|
+
const payload = `${expiresAt}.${nonce}`;
|
|
9470
|
+
return `${payload}.${this.sign(payload)}`;
|
|
9471
|
+
}
|
|
9472
|
+
sign(payload) {
|
|
9473
|
+
return createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
9474
|
+
}
|
|
9475
|
+
/** Is this cookie a session signed by this board's key, and still in date? */
|
|
9476
|
+
verifySession(session) {
|
|
9477
|
+
if (!session) return false;
|
|
9478
|
+
const cut = session.lastIndexOf(".");
|
|
9479
|
+
if (cut <= 0) return false;
|
|
9480
|
+
const payload = session.slice(0, cut);
|
|
9481
|
+
const signature = session.slice(cut + 1);
|
|
9482
|
+
if (!secretsMatch(signature, this.sign(payload))) return false;
|
|
9483
|
+
const expiresAt = Number(payload.slice(0, payload.indexOf(".")));
|
|
9484
|
+
return Number.isFinite(expiresAt) && this.options.now() < expiresAt;
|
|
9485
|
+
}
|
|
9486
|
+
/**
|
|
9487
|
+
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
9488
|
+
*
|
|
9489
|
+
* A CONSUMED code is deliberately kept until that same expiry rather than deleted on use. Deleting it
|
|
9490
|
+
* immediately would make a replay indistinguishable from a typo — both "unknown" — and the difference
|
|
9491
|
+
* matters: "already-used" is the message that tells someone their link was used by somebody else.
|
|
9492
|
+
* It costs one small entry for at most the code TTL.
|
|
9493
|
+
*/
|
|
9494
|
+
sweep() {
|
|
9495
|
+
const now = this.options.now();
|
|
9496
|
+
for (const [key, stored] of this.codes) {
|
|
9497
|
+
if (now >= stored.expiresAt) this.codes.delete(key);
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
/** Outstanding codes that could still be redeemed — neither consumed nor expired. */
|
|
9501
|
+
outstanding() {
|
|
9502
|
+
this.sweep();
|
|
9503
|
+
return [...this.codes.values()].filter((stored) => stored.consumedAt === void 0).map(({ code, createdAt, expiresAt }) => ({ code, createdAt, expiresAt }));
|
|
9504
|
+
}
|
|
9505
|
+
};
|
|
9506
|
+
|
|
9143
9507
|
// packages/server/src/restart-supervisor.ts
|
|
9144
9508
|
init_local_origin();
|
|
9145
9509
|
init_local_image();
|
|
9146
9510
|
init_src();
|
|
9147
|
-
import { request as requestHttp, createServer as createServer2 } from "node:http";
|
|
9148
|
-
import { connect } from "node:net";
|
|
9149
9511
|
var SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
|
|
9150
9512
|
var SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
9151
9513
|
var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restart`;
|
|
9152
9514
|
var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
9515
|
+
var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
9153
9516
|
var SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
9154
9517
|
function responseJson(res, status, value) {
|
|
9155
9518
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -9168,9 +9531,27 @@ function proxyHeaders(req, childPort, vouchSameOrigin = false) {
|
|
|
9168
9531
|
delete headers.connection;
|
|
9169
9532
|
return headers;
|
|
9170
9533
|
}
|
|
9534
|
+
var PUBLIC_TOKEN_PARAM = "frizz_token";
|
|
9535
|
+
var ACCESS_CODE_PARAM = "frizz_code";
|
|
9536
|
+
var PUBLIC_TOKEN_COOKIE = "frizz_public";
|
|
9537
|
+
var SESSION_COOKIE = "frizz_session";
|
|
9538
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
9539
|
+
function readCookie(header, name) {
|
|
9540
|
+
if (!header) return void 0;
|
|
9541
|
+
for (const pair of header.split(";")) {
|
|
9542
|
+
const eq = pair.indexOf("=");
|
|
9543
|
+
if (eq === -1) continue;
|
|
9544
|
+
if (pair.slice(0, eq).trim() === name) return pair.slice(eq + 1).trim();
|
|
9545
|
+
}
|
|
9546
|
+
return void 0;
|
|
9547
|
+
}
|
|
9548
|
+
function unauthorizedPage(reason) {
|
|
9549
|
+
const detail = reason === "already-used" ? "That link has already been used. Generate a fresh one." : reason === "expired" ? "That link expired. Generate a fresh one." : "This address needs a current access link.";
|
|
9550
|
+
return `<!doctype html><meta charset="utf-8"><title>Unauthorized</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:3rem;font:16px system-ui;color:#e7e7e7;background:#171717}main{max-width:30rem;padding:1.5rem;border:1px solid #444;border-radius:.75rem;background:#222}</style><main><h1>Unauthorized</h1>${detail ? `<p>${detail}</p>` : ""}</main>`;
|
|
9551
|
+
}
|
|
9171
9552
|
function isControlRequest(req) {
|
|
9172
9553
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9173
|
-
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH;
|
|
9554
|
+
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH;
|
|
9174
9555
|
}
|
|
9175
9556
|
function isLocalImageRequest(req) {
|
|
9176
9557
|
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
@@ -9192,17 +9573,40 @@ var RestartSupervisorProxy = class {
|
|
|
9192
9573
|
* judged here first.
|
|
9193
9574
|
*/
|
|
9194
9575
|
policy;
|
|
9576
|
+
/**
|
|
9577
|
+
* Codes and sessions for the public origin, or null when no public origin is declared (loopback-only
|
|
9578
|
+
* boards are never gated, so there is nothing to store).
|
|
9579
|
+
*/
|
|
9580
|
+
access;
|
|
9195
9581
|
/** Both halves of every live upgraded pair, so close() can actually finish. See close(). */
|
|
9196
9582
|
upgradedSockets = /* @__PURE__ */ new Set();
|
|
9197
9583
|
constructor(options2) {
|
|
9198
9584
|
this.options = options2;
|
|
9199
9585
|
this.host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
9586
|
+
this.access = options2.publicOrigin ? new AccessStore({
|
|
9587
|
+
onConsumed: () => options2.onCodeConsumed?.(),
|
|
9588
|
+
...options2.sessionKey ? { signingKey: options2.sessionKey } : {}
|
|
9589
|
+
}) : null;
|
|
9200
9590
|
this.policy = {
|
|
9201
9591
|
exposed: bindHostIsExposed(this.host),
|
|
9202
9592
|
allowedHosts: options2.allowedHosts ?? [],
|
|
9203
9593
|
...options2.publicOrigin ? { publicOrigin: options2.publicOrigin } : {}
|
|
9204
9594
|
};
|
|
9205
9595
|
}
|
|
9596
|
+
/**
|
|
9597
|
+
* Mint a single-use code for this board, or null when no public origin is declared.
|
|
9598
|
+
*
|
|
9599
|
+
* Issuing does NOT invalidate codes already outstanding — pressing the key twice, or two people
|
|
9600
|
+
* asking at once, must not silently break the first QR somebody is still walking towards.
|
|
9601
|
+
*/
|
|
9602
|
+
issueAccessCode() {
|
|
9603
|
+
return this.access?.issue() ?? null;
|
|
9604
|
+
}
|
|
9605
|
+
/** The full URL to show as a link or a QR. */
|
|
9606
|
+
accessUrl(code) {
|
|
9607
|
+
const origin = this.options.publicOrigin;
|
|
9608
|
+
return origin ? `${origin}/?${ACCESS_CODE_PARAM}=${encodeURIComponent(code)}` : null;
|
|
9609
|
+
}
|
|
9206
9610
|
async listen() {
|
|
9207
9611
|
if (this.server) return;
|
|
9208
9612
|
const server = createServer2((req, res) => this.handle(req, res));
|
|
@@ -9316,6 +9720,26 @@ var RestartSupervisorProxy = class {
|
|
|
9316
9720
|
res.end("Forbidden");
|
|
9317
9721
|
return;
|
|
9318
9722
|
}
|
|
9723
|
+
if (pathname === SUPERVISOR_ACCESS_CODE_PATH) {
|
|
9724
|
+
if (this.arrivedPublicly(req)) {
|
|
9725
|
+
res.writeHead(403);
|
|
9726
|
+
res.end("Forbidden");
|
|
9727
|
+
return;
|
|
9728
|
+
}
|
|
9729
|
+
if (req.method !== "POST") {
|
|
9730
|
+
res.writeHead(405, { allow: "POST" });
|
|
9731
|
+
res.end();
|
|
9732
|
+
return;
|
|
9733
|
+
}
|
|
9734
|
+
const link = this.issueAccessCode();
|
|
9735
|
+
const url = link && this.accessUrl(link.code);
|
|
9736
|
+
if (!link || !url) {
|
|
9737
|
+
responseJson(res, 409, { error: "this board has no public origin, so there is nothing to link to" });
|
|
9738
|
+
return;
|
|
9739
|
+
}
|
|
9740
|
+
responseJson(res, 200, { url, expiresAt: link.expiresAt });
|
|
9741
|
+
return;
|
|
9742
|
+
}
|
|
9319
9743
|
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
9320
9744
|
responseJson(res, 200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...this.status() });
|
|
9321
9745
|
return;
|
|
@@ -9365,11 +9789,93 @@ var RestartSupervisorProxy = class {
|
|
|
9365
9789
|
});
|
|
9366
9790
|
res.end(req.method === "HEAD" ? void 0 : result.body);
|
|
9367
9791
|
}
|
|
9792
|
+
/**
|
|
9793
|
+
* Is this request arriving as the declared public origin rather than over loopback?
|
|
9794
|
+
*
|
|
9795
|
+
* The token gate keys on this and not on "is the port exposed", because the two are independent:
|
|
9796
|
+
* a tunnelled board stays bound to 127.0.0.1 while being reachable from the internet by name.
|
|
9797
|
+
*/
|
|
9798
|
+
arrivedPublicly(req) {
|
|
9799
|
+
const declared = this.options.publicOrigin;
|
|
9800
|
+
if (!declared) return false;
|
|
9801
|
+
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
9802
|
+
return !!host && !LOOPBACK_HOSTNAMES.has(host.hostname);
|
|
9803
|
+
}
|
|
9804
|
+
/**
|
|
9805
|
+
* Bearer check for the public origin. Two ways in: the cookie the exchange below sets, or the
|
|
9806
|
+
* one-time `?frizz_token=` in the URL the operator was handed at launch.
|
|
9807
|
+
*
|
|
9808
|
+
* Compared with `timingSafeEqual` on equal-length buffers — a plain `===` on a secret leaks its
|
|
9809
|
+
* prefix to a patient attacker, and this secret is the only thing between the internet and a shell.
|
|
9810
|
+
*/
|
|
9811
|
+
/**
|
|
9812
|
+
* Is this request carrying a session this process minted?
|
|
9813
|
+
*
|
|
9814
|
+
* Sessions only. A raw `?frizz_code=` is NOT accepted here — it is redeemed once by exchangeCode()
|
|
9815
|
+
* below and traded for a session, so a code that has already been spent cannot keep working just by
|
|
9816
|
+
* staying in somebody's URL bar.
|
|
9817
|
+
*/
|
|
9818
|
+
sessionAccepted(req) {
|
|
9819
|
+
if (!this.access) return true;
|
|
9820
|
+
if (this.options.publicToken && this.legacyTokenAccepted(req)) return true;
|
|
9821
|
+
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
9822
|
+
}
|
|
9823
|
+
/**
|
|
9824
|
+
* The standing `FRIZZ_PUBLIC_TOKEN`, for headless boxes where nobody can press a key to mint a code.
|
|
9825
|
+
* Deliberately kept as a separate, opt-in path rather than folded into sessions: it has the weaker
|
|
9826
|
+
* properties (never rotates, every copy stays valid) and the readout says so.
|
|
9827
|
+
*/
|
|
9828
|
+
legacyTokenAccepted(req) {
|
|
9829
|
+
const expected2 = this.options.publicToken;
|
|
9830
|
+
if (!expected2) return false;
|
|
9831
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9832
|
+
const supplied = url.searchParams.get(PUBLIC_TOKEN_PARAM) ?? readCookie(req.headers.cookie, PUBLIC_TOKEN_COOKIE);
|
|
9833
|
+
return !!supplied && secretsMatch(supplied, expected2);
|
|
9834
|
+
}
|
|
9835
|
+
/**
|
|
9836
|
+
* Trade `?frizz_code=…` (or the legacy `?frizz_token=…`) for a cookie, then bounce to the same URL
|
|
9837
|
+
* without the secret, so it never lands in history, a Referer header, or a screenshot.
|
|
9838
|
+
*/
|
|
9839
|
+
exchangeCode(req, res) {
|
|
9840
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9841
|
+
const code = url.searchParams.get(ACCESS_CODE_PARAM);
|
|
9842
|
+
const legacy = url.searchParams.get(PUBLIC_TOKEN_PARAM);
|
|
9843
|
+
if (code === null && legacy === null) return false;
|
|
9844
|
+
let cookie;
|
|
9845
|
+
if (code !== null) {
|
|
9846
|
+
const redeemed = this.access?.redeem(code);
|
|
9847
|
+
if (!redeemed?.ok) {
|
|
9848
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
9849
|
+
res.end(unauthorizedPage(redeemed?.reason));
|
|
9850
|
+
return true;
|
|
9851
|
+
}
|
|
9852
|
+
cookie = `${SESSION_COOKIE}=${redeemed.session}`;
|
|
9853
|
+
} else {
|
|
9854
|
+
if (!this.legacyTokenAccepted(req)) return false;
|
|
9855
|
+
cookie = `${PUBLIC_TOKEN_COOKIE}=${this.options.publicToken}`;
|
|
9856
|
+
}
|
|
9857
|
+
url.searchParams.delete(ACCESS_CODE_PARAM);
|
|
9858
|
+
url.searchParams.delete(PUBLIC_TOKEN_PARAM);
|
|
9859
|
+
const target2 = `${url.pathname}${url.search}${url.hash}` || "/";
|
|
9860
|
+
res.writeHead(302, {
|
|
9861
|
+
location: target2,
|
|
9862
|
+
"set-cookie": `${cookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`,
|
|
9863
|
+
"cache-control": "no-store"
|
|
9864
|
+
});
|
|
9865
|
+
res.end();
|
|
9866
|
+
return true;
|
|
9867
|
+
}
|
|
9368
9868
|
handle(req, res) {
|
|
9369
9869
|
if (isControlRequest(req)) {
|
|
9370
9870
|
void this.handleControl(req, res);
|
|
9371
9871
|
return;
|
|
9372
9872
|
}
|
|
9873
|
+
if (this.arrivedPublicly(req) && this.exchangeCode(req, res)) return;
|
|
9874
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
9875
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
9876
|
+
res.end(unauthorizedPage());
|
|
9877
|
+
return;
|
|
9878
|
+
}
|
|
9373
9879
|
if (!this.authorityAccepted(req)) {
|
|
9374
9880
|
res.writeHead(403, { "content-type": "text/plain; charset=UTF-8" });
|
|
9375
9881
|
res.end("Forbidden");
|
|
@@ -9403,6 +9909,10 @@ var RestartSupervisorProxy = class {
|
|
|
9403
9909
|
req.pipe(upstream);
|
|
9404
9910
|
}
|
|
9405
9911
|
handleUpgrade(req, socket, head) {
|
|
9912
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
9913
|
+
rejectWebSocketUpgrade(socket, 401, "Unauthorized");
|
|
9914
|
+
return;
|
|
9915
|
+
}
|
|
9406
9916
|
if (!this.authorityAccepted(req, false)) {
|
|
9407
9917
|
socket.destroy();
|
|
9408
9918
|
return;
|
|
@@ -9664,6 +10174,13 @@ var Supervisor = class {
|
|
|
9664
10174
|
launchTarget;
|
|
9665
10175
|
logLine;
|
|
9666
10176
|
errorLine;
|
|
10177
|
+
onActivity;
|
|
10178
|
+
/** Suppresses activity beats until the first boot has settled; until then the readout owns the terminal. */
|
|
10179
|
+
booted = false;
|
|
10180
|
+
/** When the restart currently in flight began, so its "ready" beat can report a duration. */
|
|
10181
|
+
restartStartedAt;
|
|
10182
|
+
/** Last beat emitted, so a transition reported twice is printed once. */
|
|
10183
|
+
lastActivity;
|
|
9667
10184
|
publicProxy;
|
|
9668
10185
|
updateRestart;
|
|
9669
10186
|
rollbackUpdate;
|
|
@@ -9697,6 +10214,7 @@ var Supervisor = class {
|
|
|
9697
10214
|
this.ownerToken = opts.launchOwnerToken;
|
|
9698
10215
|
this.logLine = opts.log ?? ((line) => log.info("supervisor", stripPrefix(line)));
|
|
9699
10216
|
this.errorLine = opts.error ?? ((line) => log.error("supervisor", stripPrefix(line)));
|
|
10217
|
+
this.onActivity = opts.onActivity;
|
|
9700
10218
|
this.updateRestart = opts.updateRestart;
|
|
9701
10219
|
this.rollbackUpdate = opts.rollbackUpdate;
|
|
9702
10220
|
this.durableReexec = opts.durableReexec;
|
|
@@ -9705,6 +10223,9 @@ var Supervisor = class {
|
|
|
9705
10223
|
host: opts.host,
|
|
9706
10224
|
allowedHosts: opts.allowedHosts,
|
|
9707
10225
|
publicOrigin: opts.publicOrigin,
|
|
10226
|
+
publicToken: opts.publicToken,
|
|
10227
|
+
onCodeConsumed: opts.onCodeConsumed,
|
|
10228
|
+
sessionKey: opts.sessionKey,
|
|
9708
10229
|
childPort: () => this.childPort,
|
|
9709
10230
|
restart: () => this.restartFromBrowser(),
|
|
9710
10231
|
updateRestart: this.updateRestart ? () => this.updateFromBrowser() : void 0,
|
|
@@ -9727,6 +10248,13 @@ var Supervisor = class {
|
|
|
9727
10248
|
currentBoot() {
|
|
9728
10249
|
return this.boot;
|
|
9729
10250
|
}
|
|
10251
|
+
/** Mint a single-use access link for the public origin, or null when none is declared. */
|
|
10252
|
+
issueAccessLink() {
|
|
10253
|
+
const code = this.publicProxy.issueAccessCode();
|
|
10254
|
+
if (!code) return null;
|
|
10255
|
+
const url = this.publicProxy.accessUrl(code.code);
|
|
10256
|
+
return url ? { code: code.code, url, expiresAt: code.expiresAt } : null;
|
|
10257
|
+
}
|
|
9730
10258
|
async start() {
|
|
9731
10259
|
if (this.supervisorLock) {
|
|
9732
10260
|
mkdirSync8(dirname7(this.supervisorLock), { recursive: true });
|
|
@@ -9854,8 +10382,13 @@ var Supervisor = class {
|
|
|
9854
10382
|
}
|
|
9855
10383
|
async updateFromBrowser() {
|
|
9856
10384
|
if (!this.updateRestart) return { state: "failed", message: "Update & Restart is unavailable in this launcher mode" };
|
|
10385
|
+
this.emitActivity("updating", "preparing the new build \u2014 the running one is untouched until it is ready");
|
|
9857
10386
|
const candidate = await this.updateRestart();
|
|
9858
|
-
if (candidate.state !== "ready")
|
|
10387
|
+
if (candidate.state !== "ready") {
|
|
10388
|
+
this.emitActivity("failed", candidate.message ?? "the update could not be prepared");
|
|
10389
|
+
return candidate;
|
|
10390
|
+
}
|
|
10391
|
+
if (candidate.message) this.emitActivity("updating", candidate.message);
|
|
9859
10392
|
if (!this.durableReexec) {
|
|
9860
10393
|
return this.failDurableUpdate(
|
|
9861
10394
|
"Update & Restart requires a durable supervisor handoff; the current supervisor was left running",
|
|
@@ -10020,7 +10553,38 @@ var Supervisor = class {
|
|
|
10020
10553
|
});
|
|
10021
10554
|
});
|
|
10022
10555
|
}
|
|
10556
|
+
/**
|
|
10557
|
+
* One beat to the foreground terminal, deduped.
|
|
10558
|
+
*
|
|
10559
|
+
* Every transition already funnels through writeStatus, so this hangs off that single point rather
|
|
10560
|
+
* than off the dozen call sites that report one — a path added later cannot forget to announce
|
|
10561
|
+
* itself. The dedupe matters because a failure is often written twice (the crash handler, then the
|
|
10562
|
+
* browser-restart waiter that observed it), and the operator should read that once.
|
|
10563
|
+
*/
|
|
10564
|
+
emitActivity(kind, message, ms) {
|
|
10565
|
+
if (!this.onActivity) return;
|
|
10566
|
+
const signature = `${kind}\0${message}`;
|
|
10567
|
+
if (signature === this.lastActivity) return;
|
|
10568
|
+
this.lastActivity = signature;
|
|
10569
|
+
try {
|
|
10570
|
+
this.onActivity({ kind, message, ...ms === void 0 ? {} : { ms } });
|
|
10571
|
+
} catch {
|
|
10572
|
+
}
|
|
10573
|
+
}
|
|
10023
10574
|
writeStatus(state, message, boot = this.boot) {
|
|
10575
|
+
if (state === "ready" && !this.booted) this.booted = true;
|
|
10576
|
+
else if (this.booted) {
|
|
10577
|
+
if (state === "restarting") {
|
|
10578
|
+
this.restartStartedAt = Date.now();
|
|
10579
|
+
this.emitActivity("restarting", message);
|
|
10580
|
+
} else if (state === "ready") {
|
|
10581
|
+
const startedAt = this.restartStartedAt;
|
|
10582
|
+
this.restartStartedAt = void 0;
|
|
10583
|
+
this.emitActivity("ready", message, startedAt === void 0 ? void 0 : Date.now() - startedAt);
|
|
10584
|
+
} else if (state === "failed" || state === "degraded") {
|
|
10585
|
+
this.emitActivity("failed", message);
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10024
10588
|
if (!this.supervisorLock) return;
|
|
10025
10589
|
try {
|
|
10026
10590
|
writeProjectStatus(this.supervisorLock, {
|
|
@@ -10322,6 +10886,7 @@ if (options.help) {
|
|
|
10322
10886
|
`Frizz production launcher
|
|
10323
10887
|
|
|
10324
10888
|
Usage: npx ${PACKAGE_NAME} [options] [repository]
|
|
10889
|
+
npx ${PACKAGE_NAME} up serve this repo publicly, board + tunnel together
|
|
10325
10890
|
|
|
10326
10891
|
Runs the npm-resolved immutable Frizz package, then opens it in your default browser. Use frizz-dev
|
|
10327
10892
|
only for a source checkout.
|
|
@@ -10333,6 +10898,7 @@ Options:
|
|
|
10333
10898
|
--host [address] serve on a network address instead of loopback (bare --host means 0.0.0.0)
|
|
10334
10899
|
--allowed-host <name> with --host, also accept this DNS name as the board's address (repeatable)
|
|
10335
10900
|
--public-origin <url> serve behind a proxy/tunnel reachable at this exact origin
|
|
10901
|
+
--link print a fresh single-use access link for the running board
|
|
10336
10902
|
--debug stream the full event feed to the terminal instead of the compact readout
|
|
10337
10903
|
-h, --help show this help
|
|
10338
10904
|
|
|
@@ -10340,14 +10906,16 @@ Environment:
|
|
|
10340
10906
|
FRIZZ_HOST same as --host
|
|
10341
10907
|
FRIZZ_ALLOWED_HOSTS same as --allowed-host, comma separated
|
|
10342
10908
|
FRIZZ_PUBLIC_ORIGIN same as --public-origin
|
|
10909
|
+
FRIZZ_PUBLIC_TOKEN standing secret for HEADLESS boxes that cannot show a QR
|
|
10343
10910
|
|
|
10344
10911
|
--host puts a board that can run shell commands as you on the network, and Frizz has no login: anyone
|
|
10345
10912
|
who reaches the port controls it. Only do this on a network you trust. An IP address works as-is; to
|
|
10346
10913
|
reach the board by DNS name you must list that name with --allowed-host ("*" allows any).
|
|
10347
10914
|
|
|
10348
10915
|
--public-origin serves the board through a tunnel or reverse proxy without putting it on the LAN
|
|
10349
|
-
at all \u2014 Frizz stays on loopback and the tunnel dials it.
|
|
10350
|
-
|
|
10916
|
+
at all \u2014 Frizz stays on loopback and the tunnel dials it. It prints a SINGLE-USE access link, and
|
|
10917
|
+
shows it as a QR so you can scan it from a phone; press L for a fresh one at any time. Scanning it
|
|
10918
|
+
trades the code for a session cookie, so the link itself stops working the moment it is used.`
|
|
10351
10919
|
);
|
|
10352
10920
|
process.exit(0);
|
|
10353
10921
|
}
|
|
@@ -10459,15 +11027,17 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
10459
11027
|
}
|
|
10460
11028
|
const network = reused ? [] : networkUrls(port, bind.host);
|
|
10461
11029
|
const publicOrigin = reused ? void 0 : bind.publicOrigin;
|
|
11030
|
+
const networkFlagsIgnored = reused && (bind.publicOrigin !== void 0 || bindHostIsExposed(bind.host));
|
|
10462
11031
|
const warnings = [
|
|
10463
11032
|
...network.length > 0 ? [EXPOSED_WARNING] : [],
|
|
10464
|
-
...publicOrigin ? [PUBLIC_ORIGIN_WARNING] : []
|
|
11033
|
+
...publicOrigin ? [PUBLIC_ORIGIN_WARNING] : [],
|
|
11034
|
+
...networkFlagsIgnored ? [REUSED_NETWORK_FLAGS_WARNING] : []
|
|
10465
11035
|
];
|
|
10466
11036
|
if (!readout) {
|
|
10467
11037
|
console.log(`${reused ? "reusing" : "started"} Frizz ${PACKAGE_VERSION} for ${workspace.root}`);
|
|
10468
11038
|
console.log(url);
|
|
10469
11039
|
for (const address of network) console.log(address);
|
|
10470
|
-
if (publicOrigin) console.log(publicOrigin);
|
|
11040
|
+
if (publicOrigin) console.log(`${publicOrigin}/${bind.publicToken ? `?frizz_token=${bind.publicToken}` : ""}`);
|
|
10471
11041
|
for (const warning of warnings) console.log(warning);
|
|
10472
11042
|
return;
|
|
10473
11043
|
}
|
|
@@ -10476,7 +11046,7 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
10476
11046
|
[
|
|
10477
11047
|
{ label: "Local", value: boardAddress(url), accent: true },
|
|
10478
11048
|
...network.map((address) => ({ label: "Network", value: `${address}/`, accent: true })),
|
|
10479
|
-
...publicOrigin ? [{ label: "Public", value: `${publicOrigin}
|
|
11049
|
+
...publicOrigin ? [{ label: "Public", value: `${publicOrigin}/${bind.publicToken ? `?frizz_token=${bind.publicToken}` : ""}`, accent: true }] : [],
|
|
10480
11050
|
{ label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
|
|
10481
11051
|
...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
|
|
10482
11052
|
],
|
|
@@ -10525,6 +11095,7 @@ async function runSupervisor(port, token) {
|
|
|
10525
11095
|
host: bind.host,
|
|
10526
11096
|
allowedHosts: bind.allowedHosts,
|
|
10527
11097
|
...bind.publicOrigin ? { publicOrigin: bind.publicOrigin } : {},
|
|
11098
|
+
...bind.publicToken ? { publicToken: bind.publicToken } : {},
|
|
10528
11099
|
cwd: workspace.root,
|
|
10529
11100
|
stateDir: workspace.stateDir,
|
|
10530
11101
|
launchTarget: target,
|
|
@@ -10534,6 +11105,10 @@ async function runSupervisor(port, token) {
|
|
|
10534
11105
|
childEntry,
|
|
10535
11106
|
childEnvironment: () => ({ FRIZZ_STABLE_WEB_DIST: webDist, FRIZZ_STABLE_ARTIFACT: `npm:${PACKAGE_NAME}@${PACKAGE_VERSION}`, FRIZZ_SCRIPTS_DIR: scriptsDir, FRIZZ_WORKER_PLUGIN_DIR: workerPluginDir }),
|
|
10536
11107
|
updateAvailable: () => updateAvailable,
|
|
11108
|
+
// The terminal that owns the board says so when the board goes down and comes back. Everything
|
|
11109
|
+
// here is triggered from a browser tab or by a crash, so without this the foreground process is
|
|
11110
|
+
// the last place to learn what happened to it.
|
|
11111
|
+
onActivity: (event) => renderSupervisorActivity(readout, event),
|
|
10537
11112
|
updateRestart: async () => {
|
|
10538
11113
|
try {
|
|
10539
11114
|
const plan = await planRegistryUpdate(PACKAGE_NAME, PACKAGE_VERSION, npmRegistryReleaseAdapter);
|
|
@@ -10551,6 +11126,10 @@ async function runSupervisor(port, token) {
|
|
|
10551
11126
|
const plan = plannedUpdate ?? await planRegistryUpdate(PACKAGE_NAME, PACKAGE_VERSION, npmRegistryReleaseAdapter);
|
|
10552
11127
|
if (!plan) throw new Error("Frizz is already current");
|
|
10553
11128
|
handoffToRegistrySuccessor(plan, { port, projectDir: workspace.root, cwd: workspace.root, env }, npmRegistryReleaseAdapter);
|
|
11129
|
+
readout?.notice("done", "Updated", `Frizz ${plan.latestVersion} is taking over on port ${port}`);
|
|
11130
|
+
readout?.note(`
|
|
11131
|
+
Frizz ${plan.latestVersion} now runs in the background \u2014 ctrl-c here no longer reaches it. Stop it with ${PACKAGE_NAME} --stop.
|
|
11132
|
+
`);
|
|
10554
11133
|
process.exit(0);
|
|
10555
11134
|
}
|
|
10556
11135
|
});
|
|
@@ -10577,6 +11156,7 @@ async function runSupervisor(port, token) {
|
|
|
10577
11156
|
});
|
|
10578
11157
|
process.on("SIGINT", stop);
|
|
10579
11158
|
process.on("SIGTERM", stop);
|
|
11159
|
+
process.on("SIGHUP", stop);
|
|
10580
11160
|
void supervisor.stopRequested.then(stop);
|
|
10581
11161
|
await supervisor.firstBoot;
|
|
10582
11162
|
return await new Promise(() => {
|