frizz 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -20
- package/dist/claude-agent-broker.js +72 -27
- package/dist/dev-child.js +8302 -4628
- package/dist/frizz.js +1302 -701
- package/package.json +10 -4
- package/runtime/cc-worker/.claude-plugin/plugin.json +1 -1
- package/runtime/cc-worker/DECISIONS.md +81 -4
- package/runtime/cc-worker/bin/frizz-mcp.mjs +472 -58
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +1 -1
- package/runtime/cc-worker/hooks/deny-plan.mjs +1 -1
- package/runtime/cc-worker/hooks/hooks.json +0 -7
- package/runtime/cc-worker/hooks/precompact-instructions.mjs +39 -28
- package/runtime/cc-worker/hooks/scratchpad.mjs +142 -169
- package/runtime/cc-worker/hooks/session-seed.mjs +16 -15
- package/runtime/cc-worker/skills/waits/SKILL.md +2 -2
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/{TerminalPane-ROKHp1ib.js → TerminalPane-sd3XXjx5.js} +2 -2
- package/web-dist/assets/{abnfDiagram-VRR7QNED-DcpdhBs3.js → abnfDiagram-VRR7QNED-Bry_FIqg.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-CoMJQ5Ya.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CUAKf0mn.js → architectureDiagram-ZJ3FMSHR-Baok4OeI.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BPwpJIzx.js → blockDiagram-677ZJIJ3-BgHIO9lI.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-1lptuHzZ.js → c4Diagram-LMCZKHZV-rfj9Z17Z.js} +1 -1
- package/web-dist/assets/channel-D6uS1y7I.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-CFR9AKjY.js → chunk-32BRIVSS-BwaHfK1Y.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-CM9uct7m.js → chunk-52WLFC77-BmdQDZOA.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-DiveJARw.js → chunk-C7G6YPKG-ByQFjoZe.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-BE1CBw8F.js → chunk-EX3LRPZG-DQFudBKr.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-DL42uXiO.js → chunk-FWX5IMBZ-OW9qEPiq.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-DPhJWgDw.js → chunk-HOUHSVGY-BdKLF8nO.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-CwYy-6AP.js → chunk-ICXQ74PX-C1roqKR1.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-Ds5I9wxq.js → chunk-MOJQB5TN-Cx0u1ZDK.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-DHiZJwQD.js → chunk-OGEWGWER-rxv9xtNR.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-B-eyQTsF.js → chunk-PUDLZKDR-BXWPPPPi.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-DK7dB3Ti.js → chunk-Q4XR5HBZ-CpqbD101.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-Dn59m74L.js → chunk-V7JOEXUC-CP8_3qVt.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-DK7x36hd.js → chunk-VAUOI2AC-Dl7xahpk.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-D7-CI3Yl.js → chunk-VR4S4FIN-BKtMX64l.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-B3l-mLCs.js → chunk-WYO6CB5R-DmLOdJcu.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-Dobxlxie.js → chunk-ZGVPDNZ5-5wN7eXvS.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-BW5YiBf4.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-BW5YiBf4.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-Dh7RuEUJ.js → cynefin-VYW2F7L2-DUYahBXb.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-ClPi2mZZ.js → cynefinDiagram-TSTJHNR4-Rm0i30nE.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-52_WP1QV.js → dagre-VKFMJZFB-3uhsB-W3.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-D_1zVsTL.js → diagram-FQU43EPY-BUvflNRQ.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-CdZxuGUy.js → diagram-G47NLZAW-DUluIZel.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-C8pSFu0P.js → diagram-NH7WQ7WH-BBrwsbed.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-C5bjZLre.js → diagram-OA4YK3LP-DWnTQBw4.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-Bxzhiuzn.js → diagram-WEI45ONY-BjEavoC0.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-g-Z0J2wP.js → ebnfDiagram-CCIWWBDH-CW4xZAZH.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-DVNkgIHp.js → erDiagram-Q63AITRT-BemQ1R4B.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-BF5KB7ja.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-BYo4Eytx.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-_71pQYEK.js → ganttDiagram-NO4QXBWP-lDdG5I_z.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-ChIZiGZS.js → gitGraph-TEB2WS4Q-Bw87a_Se.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DCHAFI0l.js → gitGraphDiagram-IHSO6WYX-B762r0yi.js} +1 -1
- package/web-dist/assets/index-B3QArXu2.css +1 -0
- package/web-dist/assets/index-CAapfhMv.js +365 -0
- package/web-dist/assets/{info-DKCQHKI2-BW-n_T1j.js → info-DKCQHKI2-DGOv6Gx0.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-CgDYsKi9.js → infoDiagram-FWYZ7A6U-DL92kAzf.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-ClzGNt9N.js → ishikawaDiagram-FXEZZL3T-CFYd4OtU.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-DSCQxkHC.js → journeyDiagram-5HDEW3XC-QHuYL5zq.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-CdrdX9N8.js → kanban-definition-HUTT4EX6-DwmMluQ8.js} +1 -1
- package/web-dist/assets/{line-ha38Dc-1.js → line-Je_PFFHm.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-Z4uMcpip.js → mermaid-parser.core-BeiGWsn3.js} +3 -3
- package/web-dist/assets/{mermaid.core-iZRq3hbu.js → mermaid.core-BP8JrW48.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-DmhInJO_.js → mindmap-definition-LN4V7U3C-BsmjCIrM.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-DBPB36Kl.js → packet-7NZHBO7P-D6ekYYGJ.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-CAH3ljfj.js → pegDiagram-2B236MQR-CQc2Kt0u.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-_h_eX4Ca.js → pie-RZYD4A2V-C_45fO8J.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-DFBPus8j.js → pieDiagram-ENE6RG2P-Dg5z6iWt.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DMvOCjt8.js → quadrantDiagram-ABIIQ3AL-c7gZxuCX.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-2EzoPHEZ.js → radar-I7S5WNFK-D80XNJQq.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-BPJnn-hm.js → railroad-3IZDKUUU-Brz7eZQ0.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-D44pcGhJ.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-CiWVH5SP.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cy1B6-pN.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-DKq5FagA.js → railroadDiagram-RFXS5EU6-Db9K7bty.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-BJ5tGazp.js → requirementDiagram-TGXJPOKE-m1ipMrDe.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-XSJjcBhX.js → sankeyDiagram-HTMAVEWB-4Zz9ry0k.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-CBb8emSe.js → sequenceDiagram-DBY2YBRQ-CZeGHw4X.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-DDfRW94V.js → stateDiagram-2N3HPSRC-rLUVs1Pg.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-w7QDFEvx.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-DvRYbkZi.js → swimlanes-5IMT3BWC-BAeBdscz.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-UhdgWpNZ.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-BNUa_DwI.js → timeline-definition-FHXFAJF6-DL0eWMF-.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-I6-IW6nJ.js → treeView-QDETBFTQ-gEtzLMA8.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-CWWmEUYJ.js → treemap-6X3UGDF4-CCym1Gz6.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-DTDrPGLk.js → vennDiagram-L72KCM5P-cF97-nb5.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-CNsdgXXA.js → wardley-OPB4EBWU-DQJHCGbM.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-YE0tq3Kh.js → wardleyDiagram-EHGQE667-0CnHzXLX.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-D0ofMX8C.js → xychartDiagram-FW5EYKEG-9YDOp4m5.js} +1 -1
- package/web-dist/favicon-16.png +0 -0
- package/web-dist/favicon-32.png +0 -0
- package/web-dist/favicon.svg +17 -61
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/icon-maskable-512.png +0 -0
- package/web-dist/index.html +19 -8
- package/web-dist/manifest.webmanifest +3 -3
- package/runtime/cc-worker/hooks/scratchpad-stop.mjs +0 -125
- package/runtime/cc-worker/skills/handoff/SKILL.md +0 -209
- package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +0 -1
- package/web-dist/assets/channel-CqKDIFQF.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +0 -1
- package/web-dist/assets/index-w4v-GZEc.js +0 -358
- package/web-dist/assets/index-zyi22LPz.css +0 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-BZyGdgSG.js +0 -8
package/dist/frizz.js
CHANGED
|
@@ -10,10 +10,184 @@ 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
|
-
import { homedir as homedir2 } from "node:os";
|
|
16
|
-
import { join as join2 } from "node:path";
|
|
189
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
190
|
+
import { dirname, join as join2 } from "node:path";
|
|
17
191
|
function legacyFrizzRoot(home = homedir2()) {
|
|
18
192
|
return join2(home, LEGACY_DIR_NAME);
|
|
19
193
|
}
|
|
@@ -387,7 +561,7 @@ import {
|
|
|
387
561
|
writeFileSync as writeFileSync2
|
|
388
562
|
} from "node:fs";
|
|
389
563
|
import { homedir as homedir4 } from "node:os";
|
|
390
|
-
import { dirname, join as join4, resolve } from "node:path";
|
|
564
|
+
import { dirname as dirname2, join as join4, resolve } from "node:path";
|
|
391
565
|
import { setTimeout as delay } from "node:timers/promises";
|
|
392
566
|
function errorCode2(error) {
|
|
393
567
|
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
@@ -492,7 +666,7 @@ function quarantineLock(lockPath) {
|
|
|
492
666
|
return true;
|
|
493
667
|
}
|
|
494
668
|
function tryAcquireLock(lockPath, adapter) {
|
|
495
|
-
mkdirSync2(
|
|
669
|
+
mkdirSync2(dirname2(lockPath), { recursive: true, mode: 448 });
|
|
496
670
|
let fd;
|
|
497
671
|
try {
|
|
498
672
|
fd = openSync2(lockPath, "wx", 384);
|
|
@@ -513,7 +687,7 @@ function tryAcquireLock(lockPath, adapter) {
|
|
|
513
687
|
`, "utf8");
|
|
514
688
|
fsyncSync(fd);
|
|
515
689
|
closeSync(fd);
|
|
516
|
-
syncDirectory(
|
|
690
|
+
syncDirectory(dirname2(lockPath));
|
|
517
691
|
} catch (error) {
|
|
518
692
|
try {
|
|
519
693
|
closeSync(fd);
|
|
@@ -663,7 +837,7 @@ function syncGitConfigPath(path) {
|
|
|
663
837
|
} finally {
|
|
664
838
|
closeSync(fd);
|
|
665
839
|
}
|
|
666
|
-
syncDirectory(
|
|
840
|
+
syncDirectory(dirname2(path));
|
|
667
841
|
}
|
|
668
842
|
function syncGitConfig(dir) {
|
|
669
843
|
let raw;
|
|
@@ -744,6 +918,35 @@ function resolveGitProjectIdentity(dir, home = homedir4()) {
|
|
|
744
918
|
release();
|
|
745
919
|
}
|
|
746
920
|
}
|
|
921
|
+
function parseRepoLabel(remoteUrl) {
|
|
922
|
+
const url = remoteUrl.trim();
|
|
923
|
+
if (!url) return null;
|
|
924
|
+
const scp = url.match(/^[^/@]+@[^:/]+:(.+?)(?:\.git)?\/?$/);
|
|
925
|
+
if (scp) return normalizeOwnerRepo(scp[1]);
|
|
926
|
+
const m = url.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\/(.+?)(?:\.git)?\/?$/i);
|
|
927
|
+
if (m) return normalizeOwnerRepo(m[1]);
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
function normalizeOwnerRepo(path) {
|
|
931
|
+
const parts = path.split("/").filter(Boolean);
|
|
932
|
+
if (parts.length < 2) return null;
|
|
933
|
+
return parts.slice(-2).join("/");
|
|
934
|
+
}
|
|
935
|
+
function originRemoteUrl(dir) {
|
|
936
|
+
try {
|
|
937
|
+
return execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
938
|
+
cwd: dir,
|
|
939
|
+
encoding: "utf8",
|
|
940
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
941
|
+
}).trim() || null;
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function resolveProjectLabel(dir) {
|
|
947
|
+
const url = originRemoteUrl(dir);
|
|
948
|
+
return url ? parseRepoLabel(url) : null;
|
|
949
|
+
}
|
|
747
950
|
var GLOBAL_LOCK_NAME, GLOBAL_LOCK_OWNER, PORT_LOCK_DIR, INCOMPLETE_LOCK_GRACE_MS, LOCK_POLL_MS, PROJECT_ID_RE, SYNC_WAIT2;
|
|
748
951
|
var init_project_identity = __esm({
|
|
749
952
|
"packages/server/src/project-identity.ts"() {
|
|
@@ -773,7 +976,7 @@ import {
|
|
|
773
976
|
statSync as statSync3,
|
|
774
977
|
writeFileSync as writeFileSync3
|
|
775
978
|
} from "node:fs";
|
|
776
|
-
import { basename as basename2, dirname as
|
|
979
|
+
import { basename as basename2, dirname as dirname3, isAbsolute, join as join5 } from "node:path";
|
|
777
980
|
function errorCode3(error) {
|
|
778
981
|
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
779
982
|
}
|
|
@@ -796,8 +999,8 @@ function syncDirectory2(path) {
|
|
|
796
999
|
}
|
|
797
1000
|
}
|
|
798
1001
|
function atomicJson(path, value) {
|
|
799
|
-
mkdirSync3(
|
|
800
|
-
const temp = join5(
|
|
1002
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
|
|
1003
|
+
const temp = join5(dirname3(path), `.${basename2(path)}.${process.pid}.${randomUUID3()}.tmp`);
|
|
801
1004
|
let fd;
|
|
802
1005
|
try {
|
|
803
1006
|
fd = openSync3(temp, "wx", 384);
|
|
@@ -807,7 +1010,7 @@ function atomicJson(path, value) {
|
|
|
807
1010
|
closeSync2(fd);
|
|
808
1011
|
fd = void 0;
|
|
809
1012
|
renameSync2(temp, path);
|
|
810
|
-
syncDirectory2(
|
|
1013
|
+
syncDirectory2(dirname3(path));
|
|
811
1014
|
} catch (error) {
|
|
812
1015
|
if (fd !== void 0) {
|
|
813
1016
|
try {
|
|
@@ -984,7 +1187,7 @@ function writeNewOwner(path, record) {
|
|
|
984
1187
|
fsyncSync2(fd);
|
|
985
1188
|
closeSync2(fd);
|
|
986
1189
|
fd = void 0;
|
|
987
|
-
syncDirectory2(
|
|
1190
|
+
syncDirectory2(dirname3(path));
|
|
988
1191
|
} catch (error) {
|
|
989
1192
|
if (fd !== void 0) {
|
|
990
1193
|
try {
|
|
@@ -1200,6 +1403,7 @@ var OWNER_NAME, MUTATION_GUARD_NAME, OWNER_VERSION, PARTIAL_GRACE_MS, GUARD_TIME
|
|
|
1200
1403
|
var init_project_launch = __esm({
|
|
1201
1404
|
"packages/server/src/project-launch.ts"() {
|
|
1202
1405
|
"use strict";
|
|
1406
|
+
init_project_identity();
|
|
1203
1407
|
init_process_generation();
|
|
1204
1408
|
init_process_generation();
|
|
1205
1409
|
OWNER_NAME = "project-launch.owner";
|
|
@@ -1220,163 +1424,9 @@ var init_project_launch = __esm({
|
|
|
1220
1424
|
}
|
|
1221
1425
|
});
|
|
1222
1426
|
|
|
1223
|
-
// packages/server/src/local-origin.ts
|
|
1224
|
-
import { isIP } from "node:net";
|
|
1225
|
-
function publicAuthority(policy) {
|
|
1226
|
-
const raw = policy?.publicOrigin;
|
|
1227
|
-
if (!raw) return null;
|
|
1228
|
-
let url;
|
|
1229
|
-
try {
|
|
1230
|
-
url = new URL(raw);
|
|
1231
|
-
} catch {
|
|
1232
|
-
return null;
|
|
1233
|
-
}
|
|
1234
|
-
if (url.origin !== raw) return null;
|
|
1235
|
-
return {
|
|
1236
|
-
hostname: normalizedHostname(url.hostname),
|
|
1237
|
-
port: explicitPort(url),
|
|
1238
|
-
authority: url.host.toLowerCase(),
|
|
1239
|
-
scheme: url.protocol === "https:" ? "https" : "http"
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
function authoritySendsFetchMetadata(authority) {
|
|
1243
|
-
return LOCAL_HOSTNAMES.has(authority.hostname) || authority.scheme === "https";
|
|
1244
|
-
}
|
|
1245
|
-
function acceptsHostname(hostname, policy) {
|
|
1246
|
-
if (LOCAL_HOSTNAMES.has(hostname)) return true;
|
|
1247
|
-
if (!policy?.exposed) return false;
|
|
1248
|
-
if (isIP(hostname) !== 0) return true;
|
|
1249
|
-
return (policy.allowedHosts ?? []).some((allowed) => allowed === "*" || allowed.toLowerCase() === hostname);
|
|
1250
|
-
}
|
|
1251
|
-
function oneHeader(value) {
|
|
1252
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1253
|
-
}
|
|
1254
|
-
function normalizedHostname(hostname) {
|
|
1255
|
-
const lower = hostname.toLowerCase();
|
|
1256
|
-
return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
|
1257
|
-
}
|
|
1258
|
-
function explicitPort(url) {
|
|
1259
|
-
if (url.port) return Number(url.port);
|
|
1260
|
-
return url.protocol === "https:" ? 443 : 80;
|
|
1261
|
-
}
|
|
1262
|
-
function validExpectedPort(port) {
|
|
1263
|
-
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
1264
|
-
}
|
|
1265
|
-
function parseLocalHttpOrigin(value, expectedPort, policy) {
|
|
1266
|
-
const raw = oneHeader(value);
|
|
1267
|
-
if (!raw || !validExpectedPort(expectedPort) || raw !== raw.trim()) return null;
|
|
1268
|
-
const declared = publicAuthority(policy);
|
|
1269
|
-
if (declared && raw === policy?.publicOrigin) return declared;
|
|
1270
|
-
let url;
|
|
1271
|
-
try {
|
|
1272
|
-
url = new URL(raw);
|
|
1273
|
-
} catch {
|
|
1274
|
-
return null;
|
|
1275
|
-
}
|
|
1276
|
-
if (url.protocol !== "http:" || url.origin !== raw) return null;
|
|
1277
|
-
const hostname = normalizedHostname(url.hostname);
|
|
1278
|
-
if (!acceptsHostname(hostname, policy) || explicitPort(url) !== expectedPort) return null;
|
|
1279
|
-
return { hostname, port: expectedPort, authority: url.host, scheme: "http" };
|
|
1280
|
-
}
|
|
1281
|
-
function parseLocalHost(value, expectedPort, policy) {
|
|
1282
|
-
const raw = oneHeader(value);
|
|
1283
|
-
if (!raw || !validExpectedPort(expectedPort) || raw !== raw.trim() || raw.includes(",")) return null;
|
|
1284
|
-
let url;
|
|
1285
|
-
try {
|
|
1286
|
-
url = new URL(`http://${raw}`);
|
|
1287
|
-
} catch {
|
|
1288
|
-
return null;
|
|
1289
|
-
}
|
|
1290
|
-
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
|
|
1291
|
-
if (url.host.toLowerCase() !== raw.toLowerCase()) return null;
|
|
1292
|
-
const hostname = normalizedHostname(url.hostname);
|
|
1293
|
-
const declared = publicAuthority(policy);
|
|
1294
|
-
if (declared && hostname === declared.hostname) {
|
|
1295
|
-
return url.port === "" || Number(url.port) === declared.port ? declared : null;
|
|
1296
|
-
}
|
|
1297
|
-
if (!acceptsHostname(hostname, policy) || explicitPort(url) !== expectedPort) return null;
|
|
1298
|
-
return { hostname, port: expectedPort, authority: url.host.toLowerCase(), scheme: "http" };
|
|
1299
|
-
}
|
|
1300
|
-
function hasForwardedAuthority(headers) {
|
|
1301
|
-
return FORWARDED_HEADERS.some((name) => headers[name] !== void 0);
|
|
1302
|
-
}
|
|
1303
|
-
function forwardedAuthorityAllowed(headers, host, policy) {
|
|
1304
|
-
if (!hasForwardedAuthority(headers)) return true;
|
|
1305
|
-
const declared = publicAuthority(policy);
|
|
1306
|
-
return !!declared && declared.hostname === host.hostname;
|
|
1307
|
-
}
|
|
1308
|
-
function isTrustedLocalHttpRequest(headers, expectedPort, allowMissingOrigin = false, policy) {
|
|
1309
|
-
const host = parseLocalHost(headers.host, expectedPort, policy);
|
|
1310
|
-
if (!host) return false;
|
|
1311
|
-
if (!forwardedAuthorityAllowed(headers, host, policy)) return false;
|
|
1312
|
-
if (headers.origin === void 0) return allowMissingOrigin;
|
|
1313
|
-
const origin = parseLocalHttpOrigin(headers.origin, expectedPort, policy);
|
|
1314
|
-
return !!origin && host.hostname === origin.hostname && host.port === origin.port;
|
|
1315
|
-
}
|
|
1316
|
-
function allowedLocalCorsOrigin(origin, expectedPort, policy) {
|
|
1317
|
-
return parseLocalHttpOrigin(origin, expectedPort, policy) ? origin : void 0;
|
|
1318
|
-
}
|
|
1319
|
-
function normalizeBindHost(value) {
|
|
1320
|
-
const trimmed = value.trim();
|
|
1321
|
-
if (!trimmed) throw new Error("--host requires an address");
|
|
1322
|
-
const bare = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
1323
|
-
const lower = bare.toLowerCase();
|
|
1324
|
-
if (lower === "localhost") return LOOPBACK_BIND_HOST;
|
|
1325
|
-
if (isIP(bare) === 0) {
|
|
1326
|
-
throw new Error(`invalid --host address: ${value} (use an IP address such as 0.0.0.0, :: or 127.0.0.1)`);
|
|
1327
|
-
}
|
|
1328
|
-
return bare;
|
|
1329
|
-
}
|
|
1330
|
-
function bindHostIsExposed(host) {
|
|
1331
|
-
return !LOOPBACK_BIND_HOSTS.has(host.trim().toLowerCase());
|
|
1332
|
-
}
|
|
1333
|
-
function normalizePublicOrigin(value) {
|
|
1334
|
-
const trimmed = value.trim();
|
|
1335
|
-
const invalid = (why) => new Error(`invalid --public-origin: ${value} (${why})`);
|
|
1336
|
-
if (!trimmed) throw new Error("--public-origin requires a URL");
|
|
1337
|
-
let url;
|
|
1338
|
-
try {
|
|
1339
|
-
url = new URL(trimmed);
|
|
1340
|
-
} catch {
|
|
1341
|
-
throw invalid("use a full URL such as https://frizz.example.com");
|
|
1342
|
-
}
|
|
1343
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw invalid("only http and https are supported");
|
|
1344
|
-
if (url.username || url.password) throw invalid("an origin carries no credentials");
|
|
1345
|
-
if (url.pathname !== "/" || url.search || url.hash) throw invalid("an origin is a scheme and host only, with no path");
|
|
1346
|
-
if (!url.hostname) throw invalid("no hostname");
|
|
1347
|
-
return url.origin;
|
|
1348
|
-
}
|
|
1349
|
-
function normalizeAllowedHosts(values) {
|
|
1350
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1351
|
-
for (const value of values) {
|
|
1352
|
-
for (const entry of value.split(",")) {
|
|
1353
|
-
const host = entry.trim().toLowerCase();
|
|
1354
|
-
if (host) seen.add(host);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
return [...seen];
|
|
1358
|
-
}
|
|
1359
|
-
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST, ALL_INTERFACES_BIND_HOST, LOOPBACK_BIND_HOSTS;
|
|
1360
|
-
var init_local_origin = __esm({
|
|
1361
|
-
"packages/server/src/local-origin.ts"() {
|
|
1362
|
-
"use strict";
|
|
1363
|
-
LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1364
|
-
FORWARDED_HEADERS = [
|
|
1365
|
-
"forwarded",
|
|
1366
|
-
"x-forwarded-for",
|
|
1367
|
-
"x-forwarded-host",
|
|
1368
|
-
"x-forwarded-port",
|
|
1369
|
-
"x-forwarded-proto"
|
|
1370
|
-
];
|
|
1371
|
-
LOOPBACK_BIND_HOST = "127.0.0.1";
|
|
1372
|
-
ALL_INTERFACES_BIND_HOST = "0.0.0.0";
|
|
1373
|
-
LOOPBACK_BIND_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "[::1]"]);
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
1427
|
// packages/server/src/boot-progress.ts
|
|
1378
1428
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1379
|
-
import { basename as basename3, dirname as
|
|
1429
|
+
import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
|
|
1380
1430
|
function bootProgressPath(stateDir) {
|
|
1381
1431
|
return join6(stateDir, BOOT_PROGRESS_NAME);
|
|
1382
1432
|
}
|
|
@@ -1397,419 +1447,13 @@ var init_boot_progress = __esm({
|
|
|
1397
1447
|
}
|
|
1398
1448
|
});
|
|
1399
1449
|
|
|
1400
|
-
// packages/server/src/sqlite-quiet.ts
|
|
1401
|
-
function isSqliteExperimentalNotice(warning, type) {
|
|
1402
|
-
const name = typeof warning === "string" ? type : warning?.name;
|
|
1403
|
-
if (name !== "ExperimentalWarning") return false;
|
|
1404
|
-
const text = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
1405
|
-
return text.includes("SQLite");
|
|
1406
|
-
}
|
|
1407
|
-
var emitWarning;
|
|
1408
|
-
var init_sqlite_quiet = __esm({
|
|
1409
|
-
"packages/server/src/sqlite-quiet.ts"() {
|
|
1410
|
-
"use strict";
|
|
1411
|
-
emitWarning = process.emitWarning.bind(process);
|
|
1412
|
-
process.emitWarning = ((warning, ...rest) => {
|
|
1413
|
-
const type = typeof rest[0] === "string" ? rest[0] : void 0;
|
|
1414
|
-
if (isSqliteExperimentalNotice(warning, type)) return;
|
|
1415
|
-
return emitWarning(warning, ...rest);
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
// packages/server/src/sqlite.ts
|
|
1421
|
-
import { createRequire } from "node:module";
|
|
1422
|
-
function namedParameters(sql) {
|
|
1423
|
-
const found = /* @__PURE__ */ new Set();
|
|
1424
|
-
for (let index = 0; index < sql.length; index++) {
|
|
1425
|
-
const char = sql[index];
|
|
1426
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
1427
|
-
const quote = char;
|
|
1428
|
-
index++;
|
|
1429
|
-
while (index < sql.length) {
|
|
1430
|
-
if (sql[index] === quote) {
|
|
1431
|
-
if (sql[index + 1] === quote) index++;
|
|
1432
|
-
else break;
|
|
1433
|
-
}
|
|
1434
|
-
index++;
|
|
1435
|
-
}
|
|
1436
|
-
continue;
|
|
1437
|
-
}
|
|
1438
|
-
if (char === "[") {
|
|
1439
|
-
while (index < sql.length && sql[index] !== "]") index++;
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
if (char === "-" && sql[index + 1] === "-") {
|
|
1443
|
-
while (index < sql.length && sql[index] !== "\n") index++;
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
if (char === "/" && sql[index + 1] === "*") {
|
|
1447
|
-
index += 2;
|
|
1448
|
-
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) index++;
|
|
1449
|
-
index++;
|
|
1450
|
-
continue;
|
|
1451
|
-
}
|
|
1452
|
-
if (char === "@" || char === ":" || char === "$") {
|
|
1453
|
-
let end = index + 1;
|
|
1454
|
-
while (end < sql.length && /[A-Za-z0-9_]/.test(sql[end])) end++;
|
|
1455
|
-
if (end > index + 1) found.add(sql.slice(index + 1, end));
|
|
1456
|
-
index = end - 1;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
return [...found];
|
|
1460
|
-
}
|
|
1461
|
-
function isNamedBagCandidate(value) {
|
|
1462
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer) && !(value instanceof Date);
|
|
1463
|
-
}
|
|
1464
|
-
function plainRow(row) {
|
|
1465
|
-
return row === void 0 || row === null ? row : Object.assign({}, row);
|
|
1466
|
-
}
|
|
1467
|
-
function hasBinding(bag, name) {
|
|
1468
|
-
return name in bag || `@${name}` in bag || `:${name}` in bag || `$${name}` in bag;
|
|
1469
|
-
}
|
|
1470
|
-
function transactionControl(sql) {
|
|
1471
|
-
let last = null;
|
|
1472
|
-
for (const statement of sql.split(";")) {
|
|
1473
|
-
const text = statement.trim().toUpperCase();
|
|
1474
|
-
if (/^BEGIN\b/.test(text)) last = "open";
|
|
1475
|
-
else if (/^(COMMIT|END)\b/.test(text)) last = "close";
|
|
1476
|
-
else if (/^ROLLBACK\b/.test(text) && !/^ROLLBACK\s+TO\b/.test(text)) last = "close";
|
|
1477
|
-
}
|
|
1478
|
-
return last;
|
|
1479
|
-
}
|
|
1480
|
-
var DatabaseSync, PreparedStatement, Database;
|
|
1481
|
-
var init_sqlite = __esm({
|
|
1482
|
-
"packages/server/src/sqlite.ts"() {
|
|
1483
|
-
"use strict";
|
|
1484
|
-
init_sqlite_quiet();
|
|
1485
|
-
({ DatabaseSync } = createRequire(import.meta.url)("node:sqlite"));
|
|
1486
|
-
PreparedStatement = class {
|
|
1487
|
-
#statement;
|
|
1488
|
-
#required;
|
|
1489
|
-
#sql;
|
|
1490
|
-
constructor(statement, sql) {
|
|
1491
|
-
this.#statement = statement;
|
|
1492
|
-
this.#sql = sql;
|
|
1493
|
-
this.#required = namedParameters(sql);
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Build exactly the bindings this statement asks for, and nothing else.
|
|
1497
|
-
*
|
|
1498
|
-
* Frizz hands whole row objects to statements that bind a subset of their fields, which raw
|
|
1499
|
-
* node:sqlite rejects with "Unknown named parameter". The obvious fix is
|
|
1500
|
-
* `setAllowUnknownNamedParameters(true)` — but that method only exists from Node 23.11, and using it
|
|
1501
|
-
* would have raised Frizz's floor above the release that first shipped `node:sqlite` at all, for no
|
|
1502
|
-
* reason. Selecting the required names from the bag needs no runtime API, and gets the same result.
|
|
1503
|
-
*
|
|
1504
|
-
* A name the caller never supplied throws, rather than binding NULL the way node:sqlite would.
|
|
1505
|
-
* See the file header: silently writing NULL where a value was expected is the one difference that
|
|
1506
|
-
* could quietly corrupt data instead of failing.
|
|
1507
|
-
*/
|
|
1508
|
-
#bind(params) {
|
|
1509
|
-
if (params.length === 1 && this.#required.length > 0 && isNamedBagCandidate(params[0])) {
|
|
1510
|
-
const bag = params[0];
|
|
1511
|
-
const bound = {};
|
|
1512
|
-
for (const name of this.#required) {
|
|
1513
|
-
if (!hasBinding(bag, name)) {
|
|
1514
|
-
throw new RangeError(
|
|
1515
|
-
`Missing named parameter "${name}" for statement: ${this.#sql.trim().slice(0, 120)}`
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1518
|
-
const value = name in bag ? bag[name] : bag[`@${name}`] ?? bag[`:${name}`] ?? bag[`$${name}`];
|
|
1519
|
-
bound[name] = value === void 0 ? null : value;
|
|
1520
|
-
}
|
|
1521
|
-
return [bound];
|
|
1522
|
-
}
|
|
1523
|
-
return params.map((value) => value === void 0 ? null : value);
|
|
1524
|
-
}
|
|
1525
|
-
run(...params) {
|
|
1526
|
-
const result = this.#statement.run(...this.#bind(params));
|
|
1527
|
-
return { changes: Number(result.changes), lastInsertRowid: Number(result.lastInsertRowid) };
|
|
1528
|
-
}
|
|
1529
|
-
get(...params) {
|
|
1530
|
-
return plainRow(this.#statement.get(...this.#bind(params)));
|
|
1531
|
-
}
|
|
1532
|
-
all(...params) {
|
|
1533
|
-
return this.#statement.all(...this.#bind(params)).map(plainRow);
|
|
1534
|
-
}
|
|
1535
|
-
};
|
|
1536
|
-
Database = class {
|
|
1537
|
-
#db;
|
|
1538
|
-
/** Transaction nesting depth, so a nested transaction becomes a SAVEPOINT rather than an error. */
|
|
1539
|
-
#depth = 0;
|
|
1540
|
-
/** Fallback transaction state for runtimes without `isTransaction` (added in Node 24). */
|
|
1541
|
-
#openTransaction = false;
|
|
1542
|
-
/** better-sqlite3 exposes the database file here; the scheduler tests reopen a store through it. */
|
|
1543
|
-
name;
|
|
1544
|
-
constructor(path, options2 = {}) {
|
|
1545
|
-
this.name = path;
|
|
1546
|
-
this.#db = new DatabaseSync(path, options2.readonly ? { readOnly: true } : {});
|
|
1547
|
-
}
|
|
1548
|
-
/** True while a transaction is open, however it was opened. */
|
|
1549
|
-
get inTransaction() {
|
|
1550
|
-
const native = this.#db.isTransaction;
|
|
1551
|
-
return typeof native === "boolean" ? native : this.#openTransaction;
|
|
1552
|
-
}
|
|
1553
|
-
/**
|
|
1554
|
-
* Type parameters are `<BindParameters, Row>`, matching better-sqlite3 — 53 call sites already
|
|
1555
|
-
* spell them that way (`prepare<[string], SessionRow>`), and silently swapping the order would
|
|
1556
|
-
* type every one of them as `any` rather than failing loudly.
|
|
1557
|
-
*/
|
|
1558
|
-
prepare(sql) {
|
|
1559
|
-
return new PreparedStatement(this.#db.prepare(sql), sql);
|
|
1560
|
-
}
|
|
1561
|
-
/** Returns the database, matching better-sqlite3, so existing `db.exec(a).exec(b)` chains hold. */
|
|
1562
|
-
exec(sql) {
|
|
1563
|
-
this.#db.exec(sql);
|
|
1564
|
-
const control = transactionControl(sql);
|
|
1565
|
-
if (control === "open") this.#openTransaction = true;
|
|
1566
|
-
else if (control === "close") this.#openTransaction = false;
|
|
1567
|
-
return this;
|
|
1568
|
-
}
|
|
1569
|
-
/**
|
|
1570
|
-
* `PRAGMA` with better-sqlite3's calling convention. Frizz only ever SETS pragmas (`journal_mode`,
|
|
1571
|
-
* `busy_timeout`, `foreign_keys`) and ignores the result, but returning the rows keeps the shape
|
|
1572
|
-
* honest for a read like `pragma("journal_mode")`.
|
|
1573
|
-
*/
|
|
1574
|
-
pragma(source) {
|
|
1575
|
-
const sql = `PRAGMA ${source}`;
|
|
1576
|
-
try {
|
|
1577
|
-
return this.#db.prepare(sql).all();
|
|
1578
|
-
} catch {
|
|
1579
|
-
this.#db.exec(sql);
|
|
1580
|
-
return void 0;
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
/**
|
|
1584
|
-
* better-sqlite3's `transaction()`, including the part that is easy to miss: it is RE-ENTRANT.
|
|
1585
|
-
* Calling one transaction-wrapped function from inside another must not throw "cannot start a
|
|
1586
|
-
* transaction within a transaction" — better-sqlite3 quietly switches to a SAVEPOINT, and Frizz's
|
|
1587
|
-
* storage layer composes these freely, so the nesting has to survive the port.
|
|
1588
|
-
*/
|
|
1589
|
-
transaction(fn) {
|
|
1590
|
-
const runWith = (mode) => (...args2) => {
|
|
1591
|
-
const depth = this.#depth;
|
|
1592
|
-
const savepoint = `frizz_txn_${depth}`;
|
|
1593
|
-
this.#depth = depth + 1;
|
|
1594
|
-
try {
|
|
1595
|
-
this.exec(depth === 0 ? `BEGIN ${mode}` : `SAVEPOINT ${savepoint}`);
|
|
1596
|
-
let result;
|
|
1597
|
-
try {
|
|
1598
|
-
result = fn(...args2);
|
|
1599
|
-
} catch (error) {
|
|
1600
|
-
this.exec(depth === 0 ? "ROLLBACK" : `ROLLBACK TO ${savepoint}`);
|
|
1601
|
-
if (depth > 0) this.exec(`RELEASE ${savepoint}`);
|
|
1602
|
-
throw error;
|
|
1603
|
-
}
|
|
1604
|
-
this.exec(depth === 0 ? "COMMIT" : `RELEASE ${savepoint}`);
|
|
1605
|
-
return result;
|
|
1606
|
-
} finally {
|
|
1607
|
-
this.#depth = depth;
|
|
1608
|
-
}
|
|
1609
|
-
};
|
|
1610
|
-
const wrapper = runWith("IMMEDIATE");
|
|
1611
|
-
wrapper.default = wrapper;
|
|
1612
|
-
wrapper.deferred = runWith("DEFERRED");
|
|
1613
|
-
wrapper.immediate = runWith("IMMEDIATE");
|
|
1614
|
-
wrapper.exclusive = runWith("EXCLUSIVE");
|
|
1615
|
-
return wrapper;
|
|
1616
|
-
}
|
|
1617
|
-
close() {
|
|
1618
|
-
this.#db.close();
|
|
1619
|
-
}
|
|
1620
|
-
};
|
|
1621
|
-
}
|
|
1622
|
-
});
|
|
1623
|
-
|
|
1624
|
-
// packages/server/src/migrate-fray.ts
|
|
1625
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1626
|
-
import { existsSync as existsSync2, lstatSync, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync4, rmdirSync } from "node:fs";
|
|
1627
|
-
import { homedir as homedir5 } from "node:os";
|
|
1628
|
-
import { join as join7, resolve as resolve2 } from "node:path";
|
|
1629
|
-
function xdg2(env, name) {
|
|
1630
|
-
const value = env[name];
|
|
1631
|
-
return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
|
|
1632
|
-
}
|
|
1633
|
-
function movesFor(env, platform, home) {
|
|
1634
|
-
const moves = [{ from: join7(home, ".fray"), to: join7(home, ".frizz") }];
|
|
1635
|
-
if (platform === "win32") {
|
|
1636
|
-
const local = env.LOCALAPPDATA || join7(env.USERPROFILE || home, "AppData", "Local");
|
|
1637
|
-
moves.push({ from: join7(local, "Fray"), to: join7(local, "Frizz") });
|
|
1638
|
-
} else if (platform === "darwin") {
|
|
1639
|
-
const support = join7(home, "Library", "Application Support");
|
|
1640
|
-
moves.push({ from: join7(support, "Fray"), to: join7(support, "Frizz") });
|
|
1641
|
-
moves.push({ from: join7(home, "Library", "Caches", "Fray"), to: join7(home, "Library", "Caches", "Frizz") });
|
|
1642
|
-
} else {
|
|
1643
|
-
moves.push({ from: join7(home, ".local", "share", "fray"), to: join7(home, ".local", "share", "frizz") });
|
|
1644
|
-
moves.push({ from: join7(home, ".local", "state", "fray"), to: join7(home, ".local", "state", "frizz") });
|
|
1645
|
-
moves.push({ from: join7(home, ".cache", "fray"), to: join7(home, ".cache", "frizz") });
|
|
1646
|
-
}
|
|
1647
|
-
for (const name of ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]) {
|
|
1648
|
-
const base = xdg2(env, name);
|
|
1649
|
-
if (base) moves.push({ from: join7(base, "fray"), to: join7(base, "frizz") });
|
|
1650
|
-
}
|
|
1651
|
-
return moves;
|
|
1652
|
-
}
|
|
1653
|
-
function migrateFrayGlobalRoots(options2 = {}) {
|
|
1654
|
-
const env = options2.env ?? process.env;
|
|
1655
|
-
const platform = options2.platform ?? process.platform;
|
|
1656
|
-
const home = options2.home ?? homedir5();
|
|
1657
|
-
const exists = options2.exists ?? existsSync2;
|
|
1658
|
-
const rename = options2.rename ?? renameSync4;
|
|
1659
|
-
const merge = options2.merge ?? mergeInto;
|
|
1660
|
-
const moved = [];
|
|
1661
|
-
for (const move of movesFor(env, platform, home)) {
|
|
1662
|
-
if (!exists(move.from)) continue;
|
|
1663
|
-
try {
|
|
1664
|
-
if (exists(move.to)) {
|
|
1665
|
-
if (merge(move.from, move.to)) moved.push(move);
|
|
1666
|
-
continue;
|
|
1667
|
-
}
|
|
1668
|
-
rename(move.from, move.to);
|
|
1669
|
-
moved.push(move);
|
|
1670
|
-
} catch {
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
return moved;
|
|
1674
|
-
}
|
|
1675
|
-
function mergeInto(from, to) {
|
|
1676
|
-
let moved = false;
|
|
1677
|
-
for (const entry of readdirSync2(from, { withFileTypes: true })) {
|
|
1678
|
-
const src = join7(from, entry.name);
|
|
1679
|
-
const dst = join7(to, entry.name);
|
|
1680
|
-
if (!existsSync2(dst)) {
|
|
1681
|
-
renameSync4(src, dst);
|
|
1682
|
-
moved = true;
|
|
1683
|
-
continue;
|
|
1684
|
-
}
|
|
1685
|
-
if (entry.isDirectory() && lstatSync(dst).isDirectory() && mergeInto(src, dst)) moved = true;
|
|
1686
|
-
}
|
|
1687
|
-
try {
|
|
1688
|
-
rmdirSync(from);
|
|
1689
|
-
} catch {
|
|
1690
|
-
}
|
|
1691
|
-
return moved;
|
|
1692
|
-
}
|
|
1693
|
-
function migrateFrayProjectDir(projectDir) {
|
|
1694
|
-
const from = join7(projectDir, ".fray");
|
|
1695
|
-
const to = join7(projectDir, ".frizz");
|
|
1696
|
-
try {
|
|
1697
|
-
if (!lstatSync(from).isDirectory()) return false;
|
|
1698
|
-
if (!existsSync2(to)) {
|
|
1699
|
-
renameSync4(from, to);
|
|
1700
|
-
return true;
|
|
1701
|
-
}
|
|
1702
|
-
return lstatSync(to).isDirectory() ? mergeInto(from, to) : false;
|
|
1703
|
-
} catch {
|
|
1704
|
-
return false;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
function readId(dir, scope, key) {
|
|
1708
|
-
let output;
|
|
1709
|
-
try {
|
|
1710
|
-
output = execFileSync3("git", ["config", ...scope, "--get-all", key], {
|
|
1711
|
-
cwd: dir,
|
|
1712
|
-
encoding: "utf8",
|
|
1713
|
-
env: { ...process.env, LC_ALL: "C" },
|
|
1714
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1715
|
-
});
|
|
1716
|
-
} catch {
|
|
1717
|
-
return void 0;
|
|
1718
|
-
}
|
|
1719
|
-
const values = output.replace(/\r?\n$/u, "").split(/\r?\n/u).filter((value) => value.length > 0);
|
|
1720
|
-
if (values.length !== 1) return void 0;
|
|
1721
|
-
const id = (values[0] ?? "").trim();
|
|
1722
|
-
return PROJECT_ID_RE2.test(id) ? id : void 0;
|
|
1723
|
-
}
|
|
1724
|
-
function writeId(dir, scope, id) {
|
|
1725
|
-
try {
|
|
1726
|
-
execFileSync3("git", ["config", ...scope, "--replace-all", CURRENT_KEY, id], {
|
|
1727
|
-
cwd: dir,
|
|
1728
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
1729
|
-
});
|
|
1730
|
-
} catch {
|
|
1731
|
-
return false;
|
|
1732
|
-
}
|
|
1733
|
-
return readId(dir, scope, CURRENT_KEY) === id;
|
|
1734
|
-
}
|
|
1735
|
-
function hasBoard(id, home) {
|
|
1736
|
-
const db = join7(projectStateDir(id, home), "ui.db");
|
|
1737
|
-
if (!existsSync2(db)) return false;
|
|
1738
|
-
let sql;
|
|
1739
|
-
try {
|
|
1740
|
-
sql = new Database(db, { readonly: true });
|
|
1741
|
-
const row = sql.prepare("select count(*) as n from session").get();
|
|
1742
|
-
return Number(row?.n ?? 0) > 0;
|
|
1743
|
-
} catch {
|
|
1744
|
-
return true;
|
|
1745
|
-
} finally {
|
|
1746
|
-
try {
|
|
1747
|
-
sql?.close();
|
|
1748
|
-
} catch {
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
function linkedWorktreeScopes(dir) {
|
|
1753
|
-
const gitPath = (flag) => {
|
|
1754
|
-
try {
|
|
1755
|
-
const raw = execFileSync3("git", ["rev-parse", flag], {
|
|
1756
|
-
cwd: dir,
|
|
1757
|
-
encoding: "utf8",
|
|
1758
|
-
env: { ...process.env, LC_ALL: "C" },
|
|
1759
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1760
|
-
}).trim();
|
|
1761
|
-
return realpathSync2(resolve2(dir, raw));
|
|
1762
|
-
} catch {
|
|
1763
|
-
return void 0;
|
|
1764
|
-
}
|
|
1765
|
-
};
|
|
1766
|
-
const gitDir = gitPath("--git-dir");
|
|
1767
|
-
const commonGitDir = gitPath("--git-common-dir");
|
|
1768
|
-
if (!gitDir || !commonGitDir || gitDir === commonGitDir) return void 0;
|
|
1769
|
-
return {
|
|
1770
|
-
legacy: ["--file", join7(gitDir, "fray.config")],
|
|
1771
|
-
current: ["--file", join7(gitDir, "frizz.config")]
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
function adopt(dir, legacy, current, home) {
|
|
1775
|
-
const legacyId = readId(dir, legacy, LEGACY_KEY);
|
|
1776
|
-
if (!legacyId) return void 0;
|
|
1777
|
-
const currentId = readId(dir, current, CURRENT_KEY);
|
|
1778
|
-
if (currentId === legacyId) return void 0;
|
|
1779
|
-
if (currentId && hasBoard(currentId, home)) return void 0;
|
|
1780
|
-
if (!writeId(dir, current, legacyId)) return void 0;
|
|
1781
|
-
return currentId ? { id: legacyId, replaced: currentId } : { id: legacyId };
|
|
1782
|
-
}
|
|
1783
|
-
function migrateFrayProjectId(projectDir, { home } = {}) {
|
|
1784
|
-
const adopted = [];
|
|
1785
|
-
const repository = adopt(projectDir, ["--local"], ["--local"], home);
|
|
1786
|
-
if (repository) adopted.push(repository);
|
|
1787
|
-
const worktree = linkedWorktreeScopes(projectDir);
|
|
1788
|
-
if (worktree) {
|
|
1789
|
-
const own = adopt(projectDir, worktree.legacy, worktree.current, home);
|
|
1790
|
-
if (own) adopted.push({ ...own, worktree: true });
|
|
1791
|
-
}
|
|
1792
|
-
return adopted;
|
|
1793
|
-
}
|
|
1794
|
-
var PROJECT_ID_RE2, LEGACY_KEY, CURRENT_KEY;
|
|
1795
|
-
var init_migrate_fray = __esm({
|
|
1796
|
-
"packages/server/src/migrate-fray.ts"() {
|
|
1797
|
-
"use strict";
|
|
1798
|
-
init_frizz_paths();
|
|
1799
|
-
init_sqlite();
|
|
1800
|
-
PROJECT_ID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
1801
|
-
LEGACY_KEY = "fray.id";
|
|
1802
|
-
CURRENT_KEY = "frizz.id";
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
|
|
1806
1450
|
// packages/server/src/project-root.ts
|
|
1807
1451
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
1808
|
-
import { closeSync as closeSync3, existsSync as
|
|
1809
|
-
import { homedir as
|
|
1810
|
-
import { dirname as
|
|
1452
|
+
import { closeSync as closeSync3, existsSync as existsSync2, fsyncSync as fsyncSync3, mkdirSync as mkdirSync5, openSync as openSync4, readFileSync as readFileSync5, realpathSync as realpathSync2, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1453
|
+
import { homedir as homedir5 } from "node:os";
|
|
1454
|
+
import { dirname as dirname5, join as join7, parse, resolve as resolve2 } from "node:path";
|
|
1811
1455
|
function projectIdPath(root) {
|
|
1812
|
-
return
|
|
1456
|
+
return join7(root, FRIZZ_DIR, ID_FILE);
|
|
1813
1457
|
}
|
|
1814
1458
|
function readProjectIdFile(root) {
|
|
1815
1459
|
let raw;
|
|
@@ -1825,17 +1469,17 @@ function readProjectIdFile(root) {
|
|
|
1825
1469
|
}
|
|
1826
1470
|
}
|
|
1827
1471
|
function writeProjectIdFile(root, id) {
|
|
1828
|
-
const dir =
|
|
1472
|
+
const dir = join7(root, FRIZZ_DIR);
|
|
1829
1473
|
mkdirSync5(dir, { recursive: true });
|
|
1830
|
-
const ignore =
|
|
1831
|
-
if (!
|
|
1474
|
+
const ignore = join7(dir, SELF_IGNORE);
|
|
1475
|
+
if (!existsSync2(ignore)) {
|
|
1832
1476
|
try {
|
|
1833
1477
|
writeFileSync5(ignore, "*\n", { flag: "wx" });
|
|
1834
1478
|
} catch {
|
|
1835
1479
|
}
|
|
1836
1480
|
}
|
|
1837
1481
|
const path = projectIdPath(root);
|
|
1838
|
-
const temp =
|
|
1482
|
+
const temp = join7(dir, `.${ID_FILE}.${process.pid}.${randomUUID4()}.tmp`);
|
|
1839
1483
|
let fd;
|
|
1840
1484
|
try {
|
|
1841
1485
|
fd = openSync4(temp, "wx", 384);
|
|
@@ -1844,7 +1488,7 @@ function writeProjectIdFile(root, id) {
|
|
|
1844
1488
|
fsyncSync3(fd);
|
|
1845
1489
|
closeSync3(fd);
|
|
1846
1490
|
fd = void 0;
|
|
1847
|
-
|
|
1491
|
+
renameSync4(temp, path);
|
|
1848
1492
|
} catch (error) {
|
|
1849
1493
|
if (fd !== void 0) {
|
|
1850
1494
|
try {
|
|
@@ -1863,7 +1507,7 @@ function writeProjectIdFile(root, id) {
|
|
|
1863
1507
|
function projectRootLockName(root) {
|
|
1864
1508
|
return `identity-path-${createHash3("sha256").update(root).digest("hex")}.lock`;
|
|
1865
1509
|
}
|
|
1866
|
-
function ensureProjectIdFile(root, home =
|
|
1510
|
+
function ensureProjectIdFile(root, home = homedir5(), seed) {
|
|
1867
1511
|
const existing = readProjectIdFile(root);
|
|
1868
1512
|
if (existing) return existing;
|
|
1869
1513
|
const release = acquireNamedLaunchLockSync(home, projectRootLockName(root));
|
|
@@ -1883,23 +1527,52 @@ function isNotAGitWorktree(error) {
|
|
|
1883
1527
|
return /not a git repository/iu.test(stderr) || /must be run in a work ?tree/iu.test(stderr);
|
|
1884
1528
|
}
|
|
1885
1529
|
function hasAny(dir, names) {
|
|
1886
|
-
return names.some((name) =>
|
|
1530
|
+
return names.some((name) => existsSync2(join7(dir, name)));
|
|
1531
|
+
}
|
|
1532
|
+
function isHomeDirectory(dir, home = homedir5()) {
|
|
1533
|
+
const canonical = (value) => {
|
|
1534
|
+
try {
|
|
1535
|
+
return realpathSync2(resolve2(value));
|
|
1536
|
+
} catch {
|
|
1537
|
+
return resolve2(value);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
try {
|
|
1541
|
+
return canonical(dir) === canonical(home);
|
|
1542
|
+
} catch {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function existingProjectId(dir) {
|
|
1547
|
+
const fromFile = readProjectIdFile(dir);
|
|
1548
|
+
if (fromFile) return fromFile;
|
|
1549
|
+
try {
|
|
1550
|
+
return readGitProjectId(dir);
|
|
1551
|
+
} catch {
|
|
1552
|
+
return void 0;
|
|
1553
|
+
}
|
|
1887
1554
|
}
|
|
1888
|
-
function
|
|
1555
|
+
function isExistingProjectRoot(dir) {
|
|
1556
|
+
return existingProjectId(dir) !== void 0;
|
|
1557
|
+
}
|
|
1558
|
+
function hasProjectMarker(dir) {
|
|
1559
|
+
return hasAny(dir, REPO_MARKERS) || hasAny(dir, PROJECT_MARKERS);
|
|
1560
|
+
}
|
|
1561
|
+
function discoverProjectRoot(cwd = process.cwd(), home = homedir5()) {
|
|
1889
1562
|
let dir;
|
|
1890
1563
|
try {
|
|
1891
|
-
dir =
|
|
1564
|
+
dir = resolve2(cwd);
|
|
1892
1565
|
} catch {
|
|
1893
|
-
return
|
|
1566
|
+
return resolve2(cwd);
|
|
1894
1567
|
}
|
|
1895
|
-
const stop =
|
|
1568
|
+
const stop = resolve2(home);
|
|
1896
1569
|
const filesystemRoot = parse(dir).root;
|
|
1897
|
-
for (let at = dir; ; at =
|
|
1570
|
+
for (let at = dir; ; at = dirname5(at)) {
|
|
1898
1571
|
if (at === stop || at === filesystemRoot) break;
|
|
1899
|
-
if (
|
|
1572
|
+
if (existsSync2(projectIdPath(at))) return at;
|
|
1900
1573
|
if (hasAny(at, REPO_MARKERS)) return at;
|
|
1901
1574
|
if (hasAny(at, PROJECT_MARKERS)) return at;
|
|
1902
|
-
if (
|
|
1575
|
+
if (dirname5(at) === at) break;
|
|
1903
1576
|
}
|
|
1904
1577
|
return dir;
|
|
1905
1578
|
}
|
|
@@ -6035,6 +5708,10 @@ var init_zod = __esm({
|
|
|
6035
5708
|
});
|
|
6036
5709
|
|
|
6037
5710
|
// packages/shared/src/thread-slug.ts
|
|
5711
|
+
function slugify(title) {
|
|
5712
|
+
const s = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5713
|
+
return s.slice(0, THREAD_SLUG_MAX_CHARS).replace(/-+$/g, "") || "thread";
|
|
5714
|
+
}
|
|
6038
5715
|
var THREAD_SLUG_MAX_CHARS, ThreadSlug;
|
|
6039
5716
|
var init_thread_slug = __esm({
|
|
6040
5717
|
"packages/shared/src/thread-slug.ts"() {
|
|
@@ -6595,7 +6272,10 @@ var init_receipt_bus = __esm({
|
|
|
6595
6272
|
});
|
|
6596
6273
|
|
|
6597
6274
|
// packages/shared/src/index.ts
|
|
6598
|
-
|
|
6275
|
+
function fallbackPort(base) {
|
|
6276
|
+
return base + 1e4;
|
|
6277
|
+
}
|
|
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, 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;
|
|
6599
6279
|
var init_src = __esm({
|
|
6600
6280
|
"packages/shared/src/index.ts"() {
|
|
6601
6281
|
"use strict";
|
|
@@ -6807,7 +6487,15 @@ var init_src = __esm({
|
|
|
6807
6487
|
command: external_exports.string().optional(),
|
|
6808
6488
|
// ISO8601 of the shell output file's last write — "last active 6 min ago" for a quiet-but-live
|
|
6809
6489
|
// watcher. Optional (see SubAgentView.lastActivityAt).
|
|
6810
|
-
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()
|
|
6811
6499
|
});
|
|
6812
6500
|
CompletionHoldOp = external_exports.object({
|
|
6813
6501
|
label: external_exports.string(),
|
|
@@ -6844,9 +6532,12 @@ var init_src = __esm({
|
|
|
6844
6532
|
title: external_exports.string().max(120)
|
|
6845
6533
|
});
|
|
6846
6534
|
AwaitingHint = external_exports.object({
|
|
6847
|
-
kind: external_exports.enum(["
|
|
6535
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "for", "reason"]),
|
|
6848
6536
|
value: external_exports.string()
|
|
6849
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;
|
|
6850
6541
|
SnoozeUntil = external_exports.string().regex(
|
|
6851
6542
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
6852
6543
|
"Snooze time must be an ISO-8601 UTC instant"
|
|
@@ -6863,21 +6554,172 @@ var init_src = __esm({
|
|
|
6863
6554
|
RecurringIntervalSeconds = external_exports.number().int().min(RECURRING_MIN_INTERVAL_SECONDS).max(RECURRING_MAX_INTERVAL_SECONDS);
|
|
6864
6555
|
ThreadRecurringPrompt = external_exports.object({
|
|
6865
6556
|
prompt: external_exports.string(),
|
|
6866
|
-
/** The
|
|
6867
|
-
* fires on `intervalSeconds`
|
|
6557
|
+
/** The three mechanisms, named as the panel labels them. `stopHook` fires at every rest; `heartbeat`
|
|
6558
|
+
* fires on `intervalSeconds`; `postCompaction` fires whenever the harness summarizes the thread's
|
|
6559
|
+
* context away. The latter two both reach the agent mid-turn. */
|
|
6868
6560
|
stopHook: external_exports.boolean(),
|
|
6869
6561
|
heartbeat: external_exports.boolean(),
|
|
6562
|
+
postCompaction: external_exports.boolean(),
|
|
6870
6563
|
intervalSeconds: external_exports.number().int().positive().optional(),
|
|
6871
6564
|
armedAt: external_exports.string(),
|
|
6872
|
-
/** Last delivery
|
|
6565
|
+
/** Last delivery per trigger; stamped separately so each reads its own clock. */
|
|
6873
6566
|
lastRestFiredAt: external_exports.string().optional(),
|
|
6874
|
-
lastScheduleFiredAt: external_exports.string().optional()
|
|
6567
|
+
lastScheduleFiredAt: external_exports.string().optional(),
|
|
6568
|
+
lastCompactFiredAt: external_exports.string().optional()
|
|
6875
6569
|
}).strict();
|
|
6876
|
-
ALLDONE_SENTINEL = "ALLDONE";
|
|
6877
|
-
OPT_OUT_NOTE = `If there is genuinely no further work, reply ${ALLDONE_SENTINEL} on its own line to stop these prompts \u2014 but be sure, because it permanently stalls this run.`;
|
|
6878
6570
|
TIMER_PROMPT_MAX = SNOOZE_PROMPT_MAX;
|
|
6879
6571
|
TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
|
|
6880
6572
|
TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
|
|
6573
|
+
SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
|
|
6574
|
+
SIGNOFF_NUDGE_MESSAGE = [
|
|
6575
|
+
`${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
|
|
6576
|
+
"",
|
|
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:",
|
|
6603
|
+
"",
|
|
6604
|
+
"- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
|
|
6605
|
+
"- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
|
|
6606
|
+
" if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
|
|
6607
|
+
" **bolded verb phrase**.",
|
|
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 gets its own card first via `mcp__frizz__spawn_thread`; none of them is a `done`. And what is not",
|
|
6627
|
+
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
6628
|
+
"in the handoff.",
|
|
6629
|
+
"",
|
|
6630
|
+
"**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
|
|
6631
|
+
"fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
|
|
6632
|
+
"The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
|
|
6633
|
+
"ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
|
|
6634
|
+
"",
|
|
6635
|
+
"Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
|
|
6636
|
+
"seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
|
|
6637
|
+
"frizz \u2014 so anything you assumed they had followed, they have not."
|
|
6638
|
+
].join("\n");
|
|
6639
|
+
ThreadWatchKind = external_exports.enum(["shell", "github"]);
|
|
6640
|
+
GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
|
|
6641
|
+
GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
|
|
6642
|
+
GithubWatchStatus = external_exports.object({
|
|
6643
|
+
checks: GithubChecksState,
|
|
6644
|
+
/** The counts behind the verdict, so the card can say "3 running, 12 passed" the way GitHub does
|
|
6645
|
+
* rather than only "checks are running". */
|
|
6646
|
+
running: external_exports.number().int().nonnegative(),
|
|
6647
|
+
passed: external_exports.number().int().nonnegative(),
|
|
6648
|
+
failed: external_exports.number().int().nonnegative(),
|
|
6649
|
+
/** The failing job NAMES, capped — what a human actually needs to decide whether to look. */
|
|
6650
|
+
failing: external_exports.array(external_exports.string()).max(8).default([]),
|
|
6651
|
+
merge: GithubMergeState,
|
|
6652
|
+
/** OPEN | CLOSED | MERGED, lowercased. A merged or closed PR ends the wait outright. */
|
|
6653
|
+
state: external_exports.enum(["open", "closed", "merged"]),
|
|
6654
|
+
/** When frizz last heard from GitHub. A poll can fail or be rate-limited, and a stale reading stated
|
|
6655
|
+
* as current is worse than no reading. */
|
|
6656
|
+
polledAt: external_exports.string(),
|
|
6657
|
+
/** The head commit this verdict was reached on. A PR watcher reports a CI verdict when it is NEWS, and
|
|
6658
|
+
* "red again on a new commit" is news that the bare word `failing` cannot express. Optional because a
|
|
6659
|
+
* reading taken before 2026-08-17 carries none. */
|
|
6660
|
+
head: external_exports.string().optional(),
|
|
6661
|
+
/** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
|
|
6662
|
+
* same job, or a slower job going red after the first — both of which are a second failure the worker
|
|
6663
|
+
* must hear about. Empty when nothing is failing. */
|
|
6664
|
+
failureSig: external_exports.string().optional()
|
|
6665
|
+
}).strict();
|
|
6666
|
+
ThreadWatchView = external_exports.object({
|
|
6667
|
+
id: external_exports.string(),
|
|
6668
|
+
kind: ThreadWatchKind,
|
|
6669
|
+
target: external_exports.string(),
|
|
6670
|
+
state: external_exports.enum(["armed", "fired", "dropped"]),
|
|
6671
|
+
createdAt: external_exports.string(),
|
|
6672
|
+
/** `github` rows only, and absent until the first successful poll. */
|
|
6673
|
+
github: GithubWatchStatus.optional()
|
|
6674
|
+
}).strict();
|
|
6675
|
+
PR_WATCH_DEFAULT_FOR_MS = 6 * 60 * 60 * 1e3;
|
|
6676
|
+
PrWatchView = external_exports.object({
|
|
6677
|
+
id: external_exports.string(),
|
|
6678
|
+
/** `owner/repo#N`, normalized — the same string the board's row and the status book are keyed by. */
|
|
6679
|
+
target: external_exports.string(),
|
|
6680
|
+
state: external_exports.enum(["armed", "dropped", "settled"]),
|
|
6681
|
+
createdAt: external_exports.string(),
|
|
6682
|
+
/** The PR's checks/mergeability as the poller last saw it. Absent until the first successful poll. */
|
|
6683
|
+
github: GithubWatchStatus.optional()
|
|
6684
|
+
}).strict();
|
|
6685
|
+
AddOwnPrWatchInput = external_exports.object({
|
|
6686
|
+
slug: ThreadSlug,
|
|
6687
|
+
/** `owner/repo#123` or a PR URL. Parsed server-side; an unparseable ref is refused rather than stored,
|
|
6688
|
+
* because a watcher that can never fire is worse than none — the worker rests believing it is covered. */
|
|
6689
|
+
target: external_exports.string().trim().min(1).max(200),
|
|
6690
|
+
/** How long to watch, as a DURATION (`30m`, `2h`, `3d` — parseAwaitingDuration).
|
|
6691
|
+
*
|
|
6692
|
+
* A PR nobody ever reviews would otherwise be polled forever, and a thread parked on it would wait
|
|
6693
|
+
* forever with it — the same unbounded wait the awaiting fence's `for:` closes, one level down. A
|
|
6694
|
+
* duration rather than an instant for the same reason it is one there: it cannot be written in the
|
|
6695
|
+
* past (maintainer 2026-08-15, asking for it explicitly).
|
|
6696
|
+
*
|
|
6697
|
+
* REQUIRED BY THE TOOL, OPTIONAL ON THE WIRE, and the asymmetry is deliberate. A worker's MCP server
|
|
6698
|
+
* outlives every frizz restart, so a session dispatched before this existed still holds a binary that
|
|
6699
|
+
* cannot send it — and making the RPC reject that would break `watch_pr` outright for every thread
|
|
6700
|
+
* already running, with no recourse from inside those threads. Absent ⇒ PR_WATCH_DEFAULT_FOR, which
|
|
6701
|
+
* still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
|
|
6702
|
+
* is better bounded than broken. */
|
|
6703
|
+
for: external_exports.string().trim().min(1).max(16).optional()
|
|
6704
|
+
}).strict();
|
|
6705
|
+
AddOwnPrWatchResult = external_exports.object({
|
|
6706
|
+
id: external_exports.string(),
|
|
6707
|
+
target: external_exports.string(),
|
|
6708
|
+
/** True when this exact PR was ALREADY watched by this thread, so the call registered nothing new.
|
|
6709
|
+
* Re-registering after a compaction is the common case, and a duplicate would double every wake. */
|
|
6710
|
+
alreadyArmed: external_exports.boolean(),
|
|
6711
|
+
watches: external_exports.array(PrWatchView)
|
|
6712
|
+
}).strict();
|
|
6713
|
+
DropOwnPrWatchInput = external_exports.object({
|
|
6714
|
+
slug: ThreadSlug,
|
|
6715
|
+
id: external_exports.string().min(1).max(64)
|
|
6716
|
+
}).strict();
|
|
6717
|
+
DropOwnPrWatchResult = external_exports.object({
|
|
6718
|
+
dropped: external_exports.boolean(),
|
|
6719
|
+
watches: external_exports.array(PrWatchView)
|
|
6720
|
+
}).strict();
|
|
6721
|
+
ListOwnPrWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
6722
|
+
OwnPrWatchesResult = external_exports.object({ watches: external_exports.array(PrWatchView) }).strict();
|
|
6881
6723
|
ThreadTimerView = external_exports.object({
|
|
6882
6724
|
id: external_exports.string(),
|
|
6883
6725
|
prompt: external_exports.string(),
|
|
@@ -6982,6 +6824,14 @@ var init_src = __esm({
|
|
|
6982
6824
|
// Live background SHELLS the worker launched (tailer-derived). Same default-[] discipline. Rendered
|
|
6983
6825
|
// in the anchored background-ops strip alongside sub-agents; ids make current rows drillable.
|
|
6984
6826
|
bgShells: external_exports.array(BgShellView).default([]),
|
|
6827
|
+
// The thread's ARMED WATCHERS — registry-derived, not folded from the transcript, which is what makes
|
|
6828
|
+
// them survive the worker saying one more sentence. Same default-[] discipline as the two above.
|
|
6829
|
+
//
|
|
6830
|
+
// These NEVER park the thread: a row with an armed watcher stays a visible queue handoff, and Snooze
|
|
6831
|
+
// remains the only way to hide one (maintainer 2026-08-12, choosing this over auto-Held for every
|
|
6832
|
+
// kind). So this field is for the operator to SEE what a thread is waiting on — it deliberately feeds
|
|
6833
|
+
// no queue-membership rule.
|
|
6834
|
+
watches: external_exports.array(ThreadWatchView).default([]),
|
|
6985
6835
|
// A pending native AskUserQuestion the session is frozen on (tailer-derived). Optional — absent
|
|
6986
6836
|
// when there's no unanswered ask. Feeds needsAction + the read-only question render + "Answer in Terminal".
|
|
6987
6837
|
pendingAsk: PendingAsk.optional(),
|
|
@@ -7027,6 +6877,16 @@ var init_src = __esm({
|
|
|
7027
6877
|
// AUTO-bump (the scheduler resumes the agent with exactly this text) rather than a reminder, which is
|
|
7028
6878
|
// the distinction the held row's tooltip renders. Absent ⇒ the card merely re-surfaces.
|
|
7029
6879
|
snoozePrompt: external_exports.string().optional(),
|
|
6880
|
+
/** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
|
|
6881
|
+
* until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
|
|
6882
|
+
* thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
|
|
6883
|
+
*
|
|
6884
|
+
* It travels because the chat has to honour it too (2026-08-14). Until then only the QUEUE did, on
|
|
6885
|
+
* the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
|
|
6886
|
+
* reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
|
|
6887
|
+
* explicitly parked THIS rest, showing them the same card with the same button one surface over is
|
|
6888
|
+
* not information, and they said so. */
|
|
6889
|
+
bgSnoozed: external_exports.boolean().optional(),
|
|
7030
6890
|
// Which Claude transport serves this thread: "broker" = a session-broker-owned Agent SDK session
|
|
7031
6891
|
// (typed control channel), "tmux" = the interactive TUI in a pane. Only the broker can be asked to
|
|
7032
6892
|
// reload its plugin closure in place, so the board needs it to decide whether to offer that verb at
|
|
@@ -7068,9 +6928,6 @@ var init_src = __esm({
|
|
|
7068
6928
|
// ISO8601 read/seen telemetry (threadSeen RPC — recorded when the human opens the thread). Kept for
|
|
7069
6929
|
// compatibility and analytics only; viewing never acknowledges or removes a queue handoff.
|
|
7070
6930
|
seenAt: external_exports.string().optional(),
|
|
7071
|
-
// Project-relative scratchpad path (.frizz/threads/<session-id>/scratch.md) once provisioned — the worker's
|
|
7072
|
-
// compaction-proof working memory, rendered as the thread's doc tab.
|
|
7073
|
-
scratchpadPath: external_exports.string().optional(),
|
|
7074
6931
|
// Project-relative plan artifact this thread was dispatched from (.frizz/plans/*.md), if any.
|
|
7075
6932
|
planPath: external_exports.string().optional(),
|
|
7076
6933
|
// Which agent backend runs this thread (Codex-support epic, Phase 3) — drives the subtle per-row
|
|
@@ -7087,13 +6944,13 @@ var init_src = __esm({
|
|
|
7087
6944
|
// Raw durable barrier bit. Unlike permissionPending this remains true for a future/corrupt value,
|
|
7088
6945
|
// so rolling clients fail closed instead of enabling another composer while ownership is unknown.
|
|
7089
6946
|
permissionChangePending: external_exports.boolean().optional(),
|
|
7090
|
-
// The last
|
|
7091
|
-
// perm-policy.mjs), and how many times it has
|
|
7092
|
-
//
|
|
7093
|
-
//
|
|
7094
|
-
//
|
|
6947
|
+
// The last DENIAL the worker's permission POLICY made for this thread (cc-worker/hooks/
|
|
6948
|
+
// perm-policy.mjs), and how many times it has denied. A refusal changes what the worker can do, so
|
|
6949
|
+
// it earns a card. Approvals and deferrals are deliberately absent: a deferral already shows as a
|
|
6950
|
+
// permission prompt / Needs you, and an approval blocks nobody — the quiet line that used to report
|
|
6951
|
+
// one stuck to the bottom of the thread forever, describing a command the reader had long moved past.
|
|
7095
6952
|
permPolicy: external_exports.object({
|
|
7096
|
-
decision: external_exports.
|
|
6953
|
+
decision: external_exports.literal("deny"),
|
|
7097
6954
|
rule: external_exports.string(),
|
|
7098
6955
|
reason: external_exports.string(),
|
|
7099
6956
|
tool: external_exports.string().nullable(),
|
|
@@ -7135,6 +6992,25 @@ var init_src = __esm({
|
|
|
7135
6992
|
projectName: external_exports.string(),
|
|
7136
6993
|
projectLabel: external_exports.string(),
|
|
7137
6994
|
// "owner/repo" from the git origin remote; falls back to projectName
|
|
6995
|
+
// "owner/repo" ONLY when that origin remote is github.com — the link target the rendered-markdown
|
|
6996
|
+
// autolinker turns `#123` and a bare commit hash into. Deliberately NOT projectLabel, which is a
|
|
6997
|
+
// host-agnostic DISPLAY name: a GitLab origin yields an owner/repo there too, and pointing its `#12`
|
|
6998
|
+
// at github.com would be a wrong destination rather than a missing one. Absent means the
|
|
6999
|
+
// augmentation stays off (no remote, another forge, or a pre-restart server).
|
|
7000
|
+
githubRepo: external_exports.string().optional(),
|
|
7001
|
+
// This project's URL slug — the `<slug>` in `/project/<slug>`. The client cannot derive it: a
|
|
7002
|
+
// PREFIXED page reads it off its own path, but the LAUNCHING project is served unprefixed and so has
|
|
7003
|
+
// nothing to read, which left `/` — the all-projects GRID — as the only URL its queue could name.
|
|
7004
|
+
// Optional so a pre-restart server keeps working; absent means "fall back to `/`", i.e. the old
|
|
7005
|
+
// behaviour. Registry-derived, so it is the same slug every other surface links to.
|
|
7006
|
+
projectSlug: external_exports.string().optional(),
|
|
7007
|
+
// The server's home directory — the expansion of a `~` a worker wrote in prose. Agents reference
|
|
7008
|
+
// files that way constantly (`~/.claude/CLAUDE.md`), and the browser has no way to derive it, so a
|
|
7009
|
+
// `~`-anchored Markdown link had no absolute path to become and stayed a same-origin anchor that
|
|
7010
|
+
// navigated out of Frizz. The client only ever uses it to build a path it then hands BACK to the
|
|
7011
|
+
// server, which realpath-gates it exactly as it gates one the author typed in full. Optional so a
|
|
7012
|
+
// pre-restart server keeps working; absent means `~` links stay unresolved, i.e. the old behaviour.
|
|
7013
|
+
homeDir: external_exports.string().optional(),
|
|
7138
7014
|
// (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
|
|
7139
7015
|
// board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
|
|
7140
7016
|
// was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
|
|
@@ -7223,6 +7099,17 @@ var init_src = __esm({
|
|
|
7223
7099
|
// presents it; resolveClaudeEffort (server/backend/claude-effort.ts) translates it at the spawn edge.
|
|
7224
7100
|
effort: external_exports.enum(["low", "medium", "high", "xhigh", "max", "ultra", "ultracode"]).optional(),
|
|
7225
7101
|
notifications: external_exports.boolean(),
|
|
7102
|
+
/**
|
|
7103
|
+
* The permanent column of project icons. OFF by default, on purpose.
|
|
7104
|
+
*
|
|
7105
|
+
* A rail of every project on the machine is a standing invitation to leave the thread you are in —
|
|
7106
|
+
* "just too tempting" (maintainer 2026-08-06). Frizz's home is one board; the grid is a page you go
|
|
7107
|
+
* to, not furniture you sit beside. Hidden, the way back is a breadcrumb in the status bar, which
|
|
7108
|
+
* costs a click exactly when you meant to switch and nothing when you did not.
|
|
7109
|
+
*
|
|
7110
|
+
* Machine-level, like the font: which chrome you want is a property of the person, not the repo.
|
|
7111
|
+
*/
|
|
7112
|
+
projectRail: external_exports.boolean(),
|
|
7226
7113
|
// UI type family. `mono` (default) is the mono-forward system; `sans` swaps prose/UI chrome to a
|
|
7227
7114
|
// sans stack while code / tool lines / the terminal stay mono. Optional so an old settings blob
|
|
7228
7115
|
// parses; defaultSettings pins "mono".
|
|
@@ -7230,13 +7117,23 @@ var init_src = __esm({
|
|
|
7230
7117
|
// Default action for a vetted non-image local path in agent markdown. Image clicks always use the
|
|
7231
7118
|
// OS default viewer so screenshots retain their expected behavior.
|
|
7232
7119
|
localFileOpener: LocalFileOpener.optional(),
|
|
7233
|
-
// GitHub batch-dispatch prompt
|
|
7234
|
-
// unset OR blank the server falls back to its exported
|
|
7235
|
-
//
|
|
7236
|
-
//
|
|
7237
|
-
//
|
|
7238
|
-
|
|
7239
|
-
|
|
7120
|
+
// The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
|
|
7121
|
+
// unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
|
|
7122
|
+
// the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
|
|
7123
|
+
// prepended by the server (not part of the editable template) so a custom prompt can never break the
|
|
7124
|
+
// thread↔.frizz-file binding. Optional so old settings blobs parse.
|
|
7125
|
+
//
|
|
7126
|
+
// ONE field, not one per kind. Issue and PR each had their own template until 2026-08-15, and the two
|
|
7127
|
+
// said the same thing twice: read the whole thread, be skeptical, cite what you checked, post nothing.
|
|
7128
|
+
// A person tuning "be more dubious" had to make the same edit in two boxes and keep them in step. The
|
|
7129
|
+
// ONE thing that genuinely differs per kind — which `gh` command reads the item — is a line in the
|
|
7130
|
+
// metadata block, so the merged template just carries both, and the worker picks the one that applies.
|
|
7131
|
+
//
|
|
7132
|
+
// There is no migration off the two old keys, and that is deliberate (the maintainer's call): Settings
|
|
7133
|
+
// is a non-strict z.object, so `githubIssuePrompt`/`githubPrPrompt` are STRIPPED the moment an old
|
|
7134
|
+
// blob is parsed. Any existing override is dropped and the reader gets the new shipped default —
|
|
7135
|
+
// exactly the intended backfill. Merging two customized templates into one has no correct answer.
|
|
7136
|
+
githubPrompt: external_exports.string().optional()
|
|
7240
7137
|
});
|
|
7241
7138
|
DispatchProviderPreferences = external_exports.object({
|
|
7242
7139
|
model: external_exports.string().trim().min(1).max(200).optional(),
|
|
@@ -7361,6 +7258,10 @@ var init_src = __esm({
|
|
|
7361
7258
|
prompt: RecurringPromptText.nullable(),
|
|
7362
7259
|
stopHook: external_exports.boolean(),
|
|
7363
7260
|
heartbeat: external_exports.boolean(),
|
|
7261
|
+
// The POST-COMPACTION trigger (scheduler SOURCE 7, added 2026-08-06). Defaulted rather than required
|
|
7262
|
+
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
7263
|
+
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
7264
|
+
postCompaction: external_exports.boolean().default(false),
|
|
7364
7265
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
7365
7266
|
}).strict();
|
|
7366
7267
|
SetOwnThreadRecurringPromptInput = external_exports.object({
|
|
@@ -7368,8 +7269,29 @@ var init_src = __esm({
|
|
|
7368
7269
|
prompt: RecurringPromptText.nullable(),
|
|
7369
7270
|
stopHook: external_exports.boolean(),
|
|
7370
7271
|
heartbeat: external_exports.boolean(),
|
|
7272
|
+
// The POST-COMPACTION trigger (scheduler SOURCE 7, added 2026-08-06). Defaulted rather than required
|
|
7273
|
+
// so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
|
|
7274
|
+
// with the trigger off, which is the honest reading of a caller that has never heard of it.
|
|
7275
|
+
postCompaction: external_exports.boolean().default(false),
|
|
7276
|
+
/** ACCEPTED AND IGNORED. The question hold was deleted 2026-08-16 (see scheduler.ts) and no caller in
|
|
7277
|
+
* this repo sends it any more — but this object is `.strict()`, and the caller on the other end is a
|
|
7278
|
+
* DETACHED worker daemon holding the `frizz-mcp.mjs` it was spawned with. Those outlive a server
|
|
7279
|
+
* restart by design, so for as long as any pre-2026-08-16 worker is alive a `start` would otherwise
|
|
7280
|
+
* come back as a validation error on a field the model cannot see it is sending. Tolerated here rather
|
|
7281
|
+
* than in the router so the shape stays one declaration; delete it once no such worker can be running.
|
|
7282
|
+
* The BROWSER input above needs no such clause — a stale tab is one reload away. */
|
|
7283
|
+
pauseOnQuestions: external_exports.boolean().optional(),
|
|
7371
7284
|
intervalSeconds: RecurringIntervalSeconds.optional()
|
|
7372
7285
|
}).strict();
|
|
7286
|
+
SetOwnThreadRecurringPromptResult = external_exports.object({
|
|
7287
|
+
replaced: ThreadRecurringPrompt.nullable()
|
|
7288
|
+
}).strict();
|
|
7289
|
+
GetOwnThreadRecurringPromptInput = external_exports.object({
|
|
7290
|
+
slug: ThreadSlug
|
|
7291
|
+
}).strict();
|
|
7292
|
+
OwnThreadRecurringPromptResult = external_exports.object({
|
|
7293
|
+
recurringPrompt: ThreadRecurringPrompt.nullable()
|
|
7294
|
+
}).strict();
|
|
7373
7295
|
SetOwnThreadTimerInput = external_exports.object({
|
|
7374
7296
|
slug: ThreadSlug,
|
|
7375
7297
|
prompt: TimerPromptText,
|
|
@@ -7385,6 +7307,23 @@ var init_src = __esm({
|
|
|
7385
7307
|
OwnThreadTimersResult = external_exports.object({
|
|
7386
7308
|
timers: external_exports.array(ThreadTimerView)
|
|
7387
7309
|
}).strict();
|
|
7310
|
+
ThreadActivityItem = external_exports.object({
|
|
7311
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr"]),
|
|
7312
|
+
/** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
|
|
7313
|
+
* was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
|
|
7314
|
+
id: external_exports.string(),
|
|
7315
|
+
label: external_exports.string(),
|
|
7316
|
+
/** ISO8601 of when it started or was armed — absent when frizz has no instant for it. */
|
|
7317
|
+
since: external_exports.string().optional(),
|
|
7318
|
+
/** A timer's fire instant, or a PR's expiry. Absent for shells and sub-agents. */
|
|
7319
|
+
until: external_exports.string().optional()
|
|
7320
|
+
}).strict();
|
|
7321
|
+
ListOwnThreadActivityInput = external_exports.object({
|
|
7322
|
+
slug: ThreadSlug
|
|
7323
|
+
}).strict();
|
|
7324
|
+
OwnThreadActivityResult = external_exports.object({
|
|
7325
|
+
activity: external_exports.array(ThreadActivityItem)
|
|
7326
|
+
}).strict();
|
|
7388
7327
|
SetOwnThreadTimerResult = external_exports.object({
|
|
7389
7328
|
id: external_exports.string(),
|
|
7390
7329
|
fireAt: external_exports.string(),
|
|
@@ -7434,6 +7373,11 @@ var init_src = __esm({
|
|
|
7434
7373
|
// attempted a write after the flip to danger-full-access was still refused). So the change is real and
|
|
7435
7374
|
// durable, yet it does not reach work already executing. Distinct from "next-resume", which means
|
|
7436
7375
|
// nothing was applied to the live session at all.
|
|
7376
|
+
//
|
|
7377
|
+
// It is ALSO the Claude answer, arrived at from the opposite direction: a permission mode is a LAUNCH
|
|
7378
|
+
// flag there, so frizz retires the idle worker process and the next turn cold-resumes under the new
|
|
7379
|
+
// one (router `setThreadPermission`). Same promise to the operator — stored, and true from the next
|
|
7380
|
+
// turn on — reached by restarting rather than by retuning.
|
|
7437
7381
|
effect: external_exports.enum(["applied", "next-turn", "next-resume"])
|
|
7438
7382
|
});
|
|
7439
7383
|
ThreadProfileOptionsInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
@@ -7491,6 +7435,43 @@ ${DISPATCH_TASK_BANNER}
|
|
|
7491
7435
|
nameWithOwner: external_exports.string().nullable(),
|
|
7492
7436
|
authed: external_exports.boolean()
|
|
7493
7437
|
});
|
|
7438
|
+
GithubRefCard = external_exports.object({
|
|
7439
|
+
ref: external_exports.string(),
|
|
7440
|
+
kind: external_exports.enum(["issue", "pr", "commit"]),
|
|
7441
|
+
repo: external_exports.string(),
|
|
7442
|
+
// owner/repo, for the card's header line
|
|
7443
|
+
url: external_exports.string(),
|
|
7444
|
+
title: external_exports.string(),
|
|
7445
|
+
body: external_exports.string(),
|
|
7446
|
+
// already truncated server-side to the card's excerpt budget
|
|
7447
|
+
state: external_exports.string(),
|
|
7448
|
+
// OPEN | CLOSED | MERGED | DRAFT — empty for a commit, which has no state
|
|
7449
|
+
stateReason: external_exports.string().optional(),
|
|
7450
|
+
// COMPLETED | NOT_PLANNED | REOPENED — GitHub's closed-issue nuance
|
|
7451
|
+
at: external_exports.string().optional(),
|
|
7452
|
+
// ISO: opened-at for an issue/PR, committed-at for a commit
|
|
7453
|
+
authorLogin: external_exports.string().optional(),
|
|
7454
|
+
authorName: external_exports.string().optional(),
|
|
7455
|
+
// commits carry a git author name with no GitHub account behind it
|
|
7456
|
+
authorAvatar: external_exports.string().optional(),
|
|
7457
|
+
labels: external_exports.array(external_exports.object({ name: external_exports.string(), color: external_exports.string() })).default([]),
|
|
7458
|
+
additions: external_exports.number().int().nonnegative().optional(),
|
|
7459
|
+
deletions: external_exports.number().int().nonnegative().optional(),
|
|
7460
|
+
changedFiles: external_exports.number().int().nonnegative().optional(),
|
|
7461
|
+
comments: external_exports.number().int().nonnegative().optional(),
|
|
7462
|
+
// Epoch ms of the fetch this card came from. The CLIENT owns the freshness decision (render the
|
|
7463
|
+
// cached card instantly, then revalidate if it is old), so it has to be able to see the age.
|
|
7464
|
+
fetchedAt: external_exports.number().int().nonnegative()
|
|
7465
|
+
});
|
|
7466
|
+
GithubRefPreviewInput = external_exports.object({
|
|
7467
|
+
refs: external_exports.array(external_exports.string().min(3).max(120)).min(1).max(100),
|
|
7468
|
+
refresh: external_exports.boolean().default(false)
|
|
7469
|
+
});
|
|
7470
|
+
GithubRefPreviewResult = external_exports.object({
|
|
7471
|
+
cards: external_exports.array(GithubRefCard),
|
|
7472
|
+
missing: external_exports.array(external_exports.string()),
|
|
7473
|
+
error: external_exports.string().optional()
|
|
7474
|
+
});
|
|
7494
7475
|
GithubItem = external_exports.object({
|
|
7495
7476
|
kind: external_exports.enum(["issue", "pr"]),
|
|
7496
7477
|
number: external_exports.number().int().positive(),
|
|
@@ -7853,7 +7834,25 @@ ${DISPATCH_TASK_BANNER}
|
|
|
7853
7834
|
// there and stores the DISPATCH id here. Additive + optional: absent when the ack was never seen (a
|
|
7854
7835
|
// resumed session whose dispatch scrolled out), and the line then renders as plain text, not a dead link.
|
|
7855
7836
|
peerFrom: external_exports.string().optional(),
|
|
7856
|
-
peerDispatchId: external_exports.string().optional()
|
|
7837
|
+
peerDispatchId: external_exports.string().optional(),
|
|
7838
|
+
// …and the tell that `peerFrom` is ONLY that subagent_type — that the parser could not resolve the
|
|
7839
|
+
// dispatch's own description for this sender. It matters because a profile cell is not a name: every
|
|
7840
|
+
// child dispatched at `frizz:opus-high` reports under the identical string, so a divider reading
|
|
7841
|
+
// «frizz:opus-high» names the MODEL, not the work, and two siblings are indistinguishable
|
|
7842
|
+
// (maintainer 2026-08-06: "I'm also still occasionally seeing things like 'Agent <OPUS:HIGH>
|
|
7843
|
+
// rested'"). The client renders an unnamed sender as "Sub-agent reported" and keeps the cell in the
|
|
7844
|
+
// tooltip, rather than promoting a profile to a title.
|
|
7845
|
+
//
|
|
7846
|
+
// Resolution is genuinely late-arriving, not merely missing: the description comes from the DISPATCH
|
|
7847
|
+
// record, so a report rendered while the window has not yet reached that record is unnamed and gains
|
|
7848
|
+
// its title once it has. Set only on the Claude path — a codex peer names a real task.
|
|
7849
|
+
peerUnnamed: external_exports.literal(true).optional(),
|
|
7850
|
+
// The sender's own RUNTIME agent id (`origin.senderTaskId`) — kept so a LATER pass can finish the job
|
|
7851
|
+
// the fold could not. The paged transcript RPC folds a bounded window, so a report whose dispatch
|
|
7852
|
+
// scrolled above the page start has no description available at fold time; the tailer still holds the
|
|
7853
|
+
// pairing, and `projectTranscriptPeerNames` uses this id to ask it. Never a drawer key on its own —
|
|
7854
|
+
// that is `peerDispatchId`, which the same pass can also supply once this resolves.
|
|
7855
|
+
peerSenderTaskId: external_exports.string().optional()
|
|
7857
7856
|
});
|
|
7858
7857
|
TranscriptPageCursor = external_exports.string().min(1).max(2048).regex(/^[A-Za-z0-9_-]+$/);
|
|
7859
7858
|
TranscriptPage = external_exports.object({
|
|
@@ -7872,12 +7871,240 @@ ${DISPATCH_TASK_BANNER}
|
|
|
7872
7871
|
external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict(),
|
|
7873
7872
|
external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict()
|
|
7874
7873
|
]);
|
|
7875
|
-
|
|
7874
|
+
ProjectCard = external_exports.object({
|
|
7875
|
+
id: external_exports.string(),
|
|
7876
|
+
slug: external_exports.string(),
|
|
7877
|
+
name: external_exports.string(),
|
|
7878
|
+
path: external_exports.string(),
|
|
7879
|
+
lastOpenedAt: external_exports.string(),
|
|
7880
|
+
/** The directory is gone — moved or deleted. The card stays so it can be reopened or forgotten. */
|
|
7881
|
+
stale: external_exports.boolean(),
|
|
7882
|
+
/**
|
|
7883
|
+
* When this project's icon was last established, or absent if it never has been.
|
|
7884
|
+
*
|
|
7885
|
+
* Carried purely so the client can hang it off the `/_frizz/project-icon` URL: the icon bytes are
|
|
7886
|
+
* cached hard (a rail of forty squares is forty requests, and none of them should recur), which
|
|
7887
|
+
* means a newly uploaded icon would otherwise stay invisible behind the cached old one. A changed
|
|
7888
|
+
* version is a changed URL, so the swap is immediate without weakening the caching for everyone.
|
|
7889
|
+
*
|
|
7890
|
+
* Deliberately NOT "does this project have an icon". Answering that for a project nobody has
|
|
7891
|
+
* scanned yet would mean scanning it, and this list is one file read on purpose.
|
|
7892
|
+
*/
|
|
7893
|
+
iconVersion: external_exports.string().optional(),
|
|
7894
|
+
/**
|
|
7895
|
+
* Whether this project HAS an icon — and crucially, whether we have even looked.
|
|
7896
|
+
*
|
|
7897
|
+
* Three states, not a boolean, because the two "no icon to draw right now" cases must behave
|
|
7898
|
+
* differently and a boolean collapses them:
|
|
7899
|
+
* · `icon` — one is stored; draw it.
|
|
7900
|
+
* · `none` — scanned, nothing found. Draw the monogram and DO NOT request the icon route,
|
|
7901
|
+
* which is what stops an iconless project flashing its initials and then swapping.
|
|
7902
|
+
* · `unknown` — never scanned. The monogram shows, but the request MUST still go out, because
|
|
7903
|
+
* that request is what triggers the (lazy, cached) scan in the first place.
|
|
7904
|
+
*
|
|
7905
|
+
* Collapsing `unknown` into `none` deadlocks the whole feature: no image element is rendered, so
|
|
7906
|
+
* the icon route is never called, so the scan never runs, so the project stays `unknown` forever.
|
|
7907
|
+
* Measured 2026-08-06 — a rail of 29 projects had scanned exactly ONE, and only because a probe
|
|
7908
|
+
* had fetched that one's URL by hand.
|
|
7909
|
+
*/
|
|
7910
|
+
iconStatus: external_exports.enum(["icon", "none", "unknown"]),
|
|
7911
|
+
/** An operator's uploaded icon, rather than one the scan found. Drives what the menu offers. */
|
|
7912
|
+
iconIsCustom: external_exports.boolean().optional()
|
|
7913
|
+
});
|
|
7914
|
+
PROJECT_ICON_MAX_BASE64_CHARS = 4 * 1024 * 1024;
|
|
7915
|
+
DirectoryPickResult = external_exports.discriminatedUnion("kind", [
|
|
7916
|
+
external_exports.object({ kind: external_exports.literal("picked"), project: ProjectCard }),
|
|
7917
|
+
external_exports.object({ kind: external_exports.literal("cancelled") }),
|
|
7918
|
+
external_exports.object({ kind: external_exports.literal("unavailable"), reason: external_exports.string() })
|
|
7919
|
+
]);
|
|
7920
|
+
ThreadLocation = external_exports.object({ projectSlug: external_exports.string(), projectName: external_exports.string() });
|
|
7921
|
+
FRIZZ_ROUTE_PREFIX = "/_frizz";
|
|
7922
|
+
DEFAULT_PORT = 9393;
|
|
7923
|
+
}
|
|
7924
|
+
});
|
|
7925
|
+
|
|
7926
|
+
// packages/server/src/project-registry.ts
|
|
7927
|
+
import { closeSync as closeSync4, existsSync as existsSync3, fsyncSync as fsyncSync4, mkdirSync as mkdirSync6, openSync as openSync5, readFileSync as readFileSync6, readdirSync as readdirSync2, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7928
|
+
import { homedir as homedir6 } from "node:os";
|
|
7929
|
+
import { basename as basename4, dirname as dirname6, join as join8 } from "node:path";
|
|
7930
|
+
function registryPath(home = homedir6()) {
|
|
7931
|
+
return join8(frizzPaths({ home }).data, "registry.json");
|
|
7932
|
+
}
|
|
7933
|
+
function readRegistry(home = homedir6()) {
|
|
7934
|
+
let raw;
|
|
7935
|
+
try {
|
|
7936
|
+
raw = readFileSync6(registryPath(home), "utf8");
|
|
7937
|
+
} catch {
|
|
7938
|
+
return { ...EMPTY, projects: [] };
|
|
7939
|
+
}
|
|
7940
|
+
try {
|
|
7941
|
+
const parsed = JSON.parse(raw);
|
|
7942
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.projects)) return { ...EMPTY, projects: [] };
|
|
7943
|
+
return {
|
|
7944
|
+
version: 1,
|
|
7945
|
+
projects: parsed.projects.filter(
|
|
7946
|
+
(p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string"
|
|
7947
|
+
)
|
|
7948
|
+
};
|
|
7949
|
+
} catch {
|
|
7950
|
+
return { ...EMPTY, projects: [] };
|
|
7951
|
+
}
|
|
7952
|
+
}
|
|
7953
|
+
function writeRegistry(registry, home = homedir6()) {
|
|
7954
|
+
const path = registryPath(home);
|
|
7955
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
7956
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
7957
|
+
let fd;
|
|
7958
|
+
try {
|
|
7959
|
+
fd = openSync5(temp, "w", 384);
|
|
7960
|
+
writeFileSync6(fd, `${JSON.stringify(registry, null, 2)}
|
|
7961
|
+
`, "utf8");
|
|
7962
|
+
fsyncSync4(fd);
|
|
7963
|
+
closeSync4(fd);
|
|
7964
|
+
fd = void 0;
|
|
7965
|
+
renameSync5(temp, path);
|
|
7966
|
+
} catch (error) {
|
|
7967
|
+
if (fd !== void 0) {
|
|
7968
|
+
try {
|
|
7969
|
+
closeSync4(fd);
|
|
7970
|
+
} catch {
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
try {
|
|
7974
|
+
rmSync6(temp, { force: true });
|
|
7975
|
+
} catch {
|
|
7976
|
+
}
|
|
7977
|
+
throw error;
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
function slugifyProject(value) {
|
|
7981
|
+
const s = slugify(value);
|
|
7982
|
+
return s === "thread" ? "project" : s;
|
|
7983
|
+
}
|
|
7984
|
+
function deriveSlug(dir, taken, options2 = {}) {
|
|
7985
|
+
const base = slugifyProject(basename4(dir));
|
|
7986
|
+
const parent = slugifyProject(basename4(dirname6(dir)));
|
|
7987
|
+
const qualified = GENERIC.has(base) && parent ? `${parent}-${base}` : base;
|
|
7988
|
+
const candidates = [
|
|
7989
|
+
qualified,
|
|
7990
|
+
...options2.remoteOwner ? [`${slugifyProject(options2.remoteOwner)}-${base}`] : [],
|
|
7991
|
+
...parent && parent !== qualified ? [`${parent}-${base}`] : []
|
|
7992
|
+
];
|
|
7993
|
+
for (const candidate of candidates) {
|
|
7994
|
+
if (candidate && !taken.has(candidate) && !RESERVED.has(candidate)) return candidate;
|
|
7995
|
+
}
|
|
7996
|
+
const root = candidates[0] || "project";
|
|
7997
|
+
for (let n = 2; ; n++) {
|
|
7998
|
+
const candidate = `${root}-${n}`;
|
|
7999
|
+
if (!taken.has(candidate) && !RESERVED.has(candidate)) return candidate;
|
|
8000
|
+
}
|
|
8001
|
+
}
|
|
8002
|
+
function canonicalPath(dir) {
|
|
8003
|
+
try {
|
|
8004
|
+
return realpathSync3(dir);
|
|
8005
|
+
} catch {
|
|
8006
|
+
return dir;
|
|
8007
|
+
}
|
|
8008
|
+
}
|
|
8009
|
+
function registerProject(input, home = homedir6()) {
|
|
8010
|
+
input = { ...input, dir: canonicalPath(input.dir) };
|
|
8011
|
+
const registry = readRegistry(home);
|
|
8012
|
+
const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
8013
|
+
const byId = registry.projects.find((p) => p.id === input.id);
|
|
8014
|
+
if (byId) {
|
|
8015
|
+
if (byId.path === input.dir) {
|
|
8016
|
+
byId.lastOpenedAt = at;
|
|
8017
|
+
writeRegistry(registry, home);
|
|
8018
|
+
return { entry: byId, action: "reopened" };
|
|
8019
|
+
}
|
|
8020
|
+
if (existsSync3(join8(byId.path, ".frizz", ".id"))) return { action: "duplicate" };
|
|
8021
|
+
byId.path = input.dir;
|
|
8022
|
+
byId.lastOpenedAt = at;
|
|
8023
|
+
writeRegistry(registry, home);
|
|
8024
|
+
return { entry: byId, action: "moved" };
|
|
8025
|
+
}
|
|
8026
|
+
const byPath = registry.projects.find((p) => p.path === input.dir);
|
|
8027
|
+
if (byPath) {
|
|
8028
|
+
byPath.id = input.id;
|
|
8029
|
+
byPath.lastOpenedAt = at;
|
|
8030
|
+
writeRegistry(registry, home);
|
|
8031
|
+
return { entry: byPath, action: "rekeyed" };
|
|
8032
|
+
}
|
|
8033
|
+
const taken = new Set(registry.projects.map((p) => p.slug));
|
|
8034
|
+
const entry = {
|
|
8035
|
+
id: input.id,
|
|
8036
|
+
path: input.dir,
|
|
8037
|
+
slug: deriveSlug(input.dir, taken, { remoteOwner: input.remoteOwner }),
|
|
8038
|
+
lastOpenedAt: at
|
|
8039
|
+
};
|
|
8040
|
+
registry.projects.push(entry);
|
|
8041
|
+
writeRegistry(registry, home);
|
|
8042
|
+
return { entry, action: "created" };
|
|
8043
|
+
}
|
|
8044
|
+
function listProjects(home = homedir6()) {
|
|
8045
|
+
const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
|
|
8046
|
+
return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync3(p.path) })).sort((a, b) => {
|
|
8047
|
+
if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
|
|
8048
|
+
if (a.order === void 0) return 1;
|
|
8049
|
+
if (b.order === void 0) return -1;
|
|
8050
|
+
return a.order - b.order;
|
|
8051
|
+
});
|
|
8052
|
+
}
|
|
8053
|
+
function findByPath(path, home = homedir6()) {
|
|
8054
|
+
const canonical = canonicalPath(path);
|
|
8055
|
+
return readRegistry(home).projects.find((p) => p.path === canonical || p.path === path);
|
|
8056
|
+
}
|
|
8057
|
+
var EMPTY, GENERIC, RESERVED;
|
|
8058
|
+
var init_project_registry = __esm({
|
|
8059
|
+
"packages/server/src/project-registry.ts"() {
|
|
8060
|
+
"use strict";
|
|
8061
|
+
init_src();
|
|
8062
|
+
init_frizz_paths();
|
|
8063
|
+
init_project_root();
|
|
8064
|
+
EMPTY = { version: 1, projects: [] };
|
|
8065
|
+
GENERIC = /* @__PURE__ */ new Set([
|
|
8066
|
+
"app",
|
|
8067
|
+
"src",
|
|
8068
|
+
"web",
|
|
8069
|
+
"www",
|
|
8070
|
+
"main",
|
|
8071
|
+
"repo",
|
|
8072
|
+
"code",
|
|
8073
|
+
"server",
|
|
8074
|
+
"client",
|
|
8075
|
+
"packages",
|
|
8076
|
+
"site",
|
|
8077
|
+
"scratch",
|
|
8078
|
+
"tmp",
|
|
8079
|
+
"test"
|
|
8080
|
+
]);
|
|
8081
|
+
RESERVED = /* @__PURE__ */ new Set([
|
|
8082
|
+
"_frizz",
|
|
8083
|
+
"rpc",
|
|
8084
|
+
"events",
|
|
8085
|
+
"ws",
|
|
8086
|
+
"term",
|
|
8087
|
+
"attach",
|
|
8088
|
+
"local-image",
|
|
8089
|
+
"local-visualization",
|
|
8090
|
+
// `control` answers `/control/stop` and `/control/status` — the supervisor channel the board polls
|
|
8091
|
+
// every few seconds. It was missing from this list, so a directory called `control` could have taken
|
|
8092
|
+
// the segment and routed the whole machine's supervisor polling into one project's app, which has no
|
|
8093
|
+
// such route. Only newly minted slugs consult this, so adding it strands nothing already registered.
|
|
8094
|
+
"control",
|
|
8095
|
+
"project-icon",
|
|
8096
|
+
"assets",
|
|
8097
|
+
"favicon",
|
|
8098
|
+
"manifest",
|
|
8099
|
+
"index",
|
|
8100
|
+
"api",
|
|
8101
|
+
"health"
|
|
8102
|
+
]);
|
|
7876
8103
|
}
|
|
7877
8104
|
});
|
|
7878
8105
|
|
|
7879
8106
|
// packages/server/src/local-image.ts
|
|
7880
|
-
import { readFileSync as
|
|
8107
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
|
|
7881
8108
|
import { extname, isAbsolute as isAbsolute2 } from "node:path";
|
|
7882
8109
|
function resolveLocalImage(rawPath) {
|
|
7883
8110
|
if (!rawPath || !isAbsolute2(rawPath)) return { status: 400 };
|
|
@@ -7885,13 +8112,13 @@ function resolveLocalImage(rawPath) {
|
|
|
7885
8112
|
if (!contentType) return { status: 400 };
|
|
7886
8113
|
let real;
|
|
7887
8114
|
try {
|
|
7888
|
-
real =
|
|
8115
|
+
real = realpathSync5(rawPath);
|
|
7889
8116
|
} catch {
|
|
7890
8117
|
return { status: 404 };
|
|
7891
8118
|
}
|
|
7892
8119
|
try {
|
|
7893
8120
|
if (!statSync4(real).isFile()) return { status: 404 };
|
|
7894
|
-
return { status: 200, contentType, body:
|
|
8121
|
+
return { status: 200, contentType, body: readFileSync8(real) };
|
|
7895
8122
|
} catch {
|
|
7896
8123
|
return { status: 404 };
|
|
7897
8124
|
}
|
|
@@ -7911,7 +8138,8 @@ var init_local_image = __esm({
|
|
|
7911
8138
|
});
|
|
7912
8139
|
|
|
7913
8140
|
// src/production.ts
|
|
7914
|
-
|
|
8141
|
+
init_local_origin();
|
|
8142
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7915
8143
|
import { homedir as homedir8 } from "node:os";
|
|
7916
8144
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7917
8145
|
import { join as join11 } from "node:path";
|
|
@@ -8076,12 +8304,12 @@ function manifestIdFor(url) {
|
|
|
8076
8304
|
return url.endsWith("/") ? url : `${url}/`;
|
|
8077
8305
|
}
|
|
8078
8306
|
function execFileP(cmd, args2) {
|
|
8079
|
-
return new Promise((
|
|
8307
|
+
return new Promise((resolve5, reject) => {
|
|
8080
8308
|
execFile(
|
|
8081
8309
|
cmd,
|
|
8082
8310
|
args2,
|
|
8083
8311
|
{ encoding: "utf8", timeout: 1e4 },
|
|
8084
|
-
(err, stdout) => err ? reject(err) :
|
|
8312
|
+
(err, stdout) => err ? reject(err) : resolve5(stdout)
|
|
8085
8313
|
);
|
|
8086
8314
|
});
|
|
8087
8315
|
}
|
|
@@ -8160,9 +8388,9 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
8160
8388
|
cmdPipe.on("error", failAll);
|
|
8161
8389
|
resPipe.on("error", failAll);
|
|
8162
8390
|
child.on("exit", () => failAll(new Error("chrome exited before the CDP handshake completed")));
|
|
8163
|
-
const call = (method, params = {}, timeoutMs = 2e4) => new Promise((
|
|
8391
|
+
const call = (method, params = {}, timeoutMs = 2e4) => new Promise((resolve5, reject) => {
|
|
8164
8392
|
const id = ++seq;
|
|
8165
|
-
pending.set(id, { resolve:
|
|
8393
|
+
pending.set(id, { resolve: resolve5, reject });
|
|
8166
8394
|
cmdPipe.write(JSON.stringify({ id, method, params }) + "\0");
|
|
8167
8395
|
setTimeout(() => {
|
|
8168
8396
|
if (pending.has(id)) {
|
|
@@ -8175,15 +8403,15 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
8175
8403
|
await call("Browser.close").catch(() => {
|
|
8176
8404
|
});
|
|
8177
8405
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
8178
|
-
await new Promise((
|
|
8406
|
+
await new Promise((resolve5) => {
|
|
8179
8407
|
const t = setTimeout(() => {
|
|
8180
8408
|
child.kill();
|
|
8181
|
-
|
|
8409
|
+
resolve5();
|
|
8182
8410
|
}, 1e4);
|
|
8183
8411
|
t.unref();
|
|
8184
8412
|
child.once("exit", () => {
|
|
8185
8413
|
clearTimeout(t);
|
|
8186
|
-
|
|
8414
|
+
resolve5();
|
|
8187
8415
|
});
|
|
8188
8416
|
});
|
|
8189
8417
|
};
|
|
@@ -8585,6 +8813,7 @@ ${SYNC_END}`);
|
|
|
8585
8813
|
}),
|
|
8586
8814
|
// Yellow, above the dim hint: exposing the board off loopback is the one launch outcome the
|
|
8587
8815
|
// operator must not skim past, so it may not share the hint's low-contrast styling.
|
|
8816
|
+
...options2.qr?.length ? ["", ...options2.qr.map((row) => ` ${row}`)] : [],
|
|
8588
8817
|
...options2.warning ? ["", ` ${this.c(ANSI.yellow, options2.warning)}`] : [],
|
|
8589
8818
|
...hint ? ["", ` ${this.c(ANSI.dim, hint)}`] : [],
|
|
8590
8819
|
""
|
|
@@ -8661,15 +8890,15 @@ init_project_launch();
|
|
|
8661
8890
|
init_local_origin();
|
|
8662
8891
|
init_boot_progress();
|
|
8663
8892
|
init_frizz_paths();
|
|
8664
|
-
init_migrate_fray();
|
|
8665
8893
|
init_project_root();
|
|
8666
8894
|
init_logging();
|
|
8667
8895
|
init_src();
|
|
8668
|
-
|
|
8896
|
+
init_project_registry();
|
|
8897
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
8669
8898
|
import { createServer } from "node:net";
|
|
8670
|
-
import { mkdirSync as
|
|
8899
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, realpathSync as realpathSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8671
8900
|
import { homedir as homedir7, networkInterfaces } from "node:os";
|
|
8672
|
-
import { basename as
|
|
8901
|
+
import { basename as basename5, join as join9, resolve as resolve3 } from "node:path";
|
|
8673
8902
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
8674
8903
|
var PORT_SCAN_COUNT = 100;
|
|
8675
8904
|
var LAUNCH_TIMEOUT_MS = 3e4;
|
|
@@ -8761,6 +8990,7 @@ function parseCliArgs(argv) {
|
|
|
8761
8990
|
"--detach",
|
|
8762
8991
|
"--stop",
|
|
8763
8992
|
"--status",
|
|
8993
|
+
"--link",
|
|
8764
8994
|
"--help",
|
|
8765
8995
|
"-h",
|
|
8766
8996
|
"--dev",
|
|
@@ -8792,6 +9022,7 @@ function parseCliArgs(argv) {
|
|
|
8792
9022
|
// Retain the option in the parsed shape for callers, but normal frizz-dev is always attached.
|
|
8793
9023
|
foreground: true,
|
|
8794
9024
|
stop: args2.has("--stop"),
|
|
9025
|
+
link: args2.has("--link"),
|
|
8795
9026
|
status: args2.has("--status"),
|
|
8796
9027
|
help: args2.has("--help") || args2.has("-h"),
|
|
8797
9028
|
dev: args2.has("--dev"),
|
|
@@ -8804,6 +9035,7 @@ function parseCliArgs(argv) {
|
|
|
8804
9035
|
};
|
|
8805
9036
|
}
|
|
8806
9037
|
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.";
|
|
9038
|
+
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.";
|
|
8807
9039
|
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.";
|
|
8808
9040
|
function resolveBindSelection(options2, env = process.env) {
|
|
8809
9041
|
const fromEnv = env.FRIZZ_HOST?.trim();
|
|
@@ -8814,7 +9046,17 @@ function resolveBindSelection(options2, env = process.env) {
|
|
|
8814
9046
|
]);
|
|
8815
9047
|
const publicOriginRaw = options2.publicOrigin ?? env.FRIZZ_PUBLIC_ORIGIN?.trim();
|
|
8816
9048
|
const publicOrigin = publicOriginRaw ? normalizePublicOrigin(publicOriginRaw) : void 0;
|
|
8817
|
-
|
|
9049
|
+
const publicToken = publicOrigin ? env.FRIZZ_PUBLIC_TOKEN?.trim() || void 0 : void 0;
|
|
9050
|
+
return {
|
|
9051
|
+
host,
|
|
9052
|
+
exposed: bindHostIsExposed(host),
|
|
9053
|
+
allowedHosts,
|
|
9054
|
+
...publicOrigin ? { publicOrigin } : {},
|
|
9055
|
+
...publicToken ? { publicToken } : {}
|
|
9056
|
+
};
|
|
9057
|
+
}
|
|
9058
|
+
function boardAddress(url) {
|
|
9059
|
+
return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
|
|
8818
9060
|
}
|
|
8819
9061
|
function networkUrls(port, host, interfaces = networkInterfaces) {
|
|
8820
9062
|
if (!bindHostIsExposed(host)) return [];
|
|
@@ -8837,10 +9079,30 @@ function networkUrls(port, host, interfaces = networkInterfaces) {
|
|
|
8837
9079
|
}
|
|
8838
9080
|
return urls;
|
|
8839
9081
|
}
|
|
8840
|
-
function
|
|
9082
|
+
function resolveLaunchIntent(cwd = process.cwd(), home = homedir7(), env = process.env) {
|
|
9083
|
+
const candidate = realpathSync4(discoverProjectRoot(cwd, home));
|
|
9084
|
+
const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
|
|
9085
|
+
if ((known || hasProjectMarker(candidate)) && !isHomeDirectory(candidate, home))
|
|
9086
|
+
return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
|
|
9087
|
+
const host = mostRecentProject(home, env);
|
|
9088
|
+
const reason = isHomeDirectory(candidate, home) ? "home" : "unadopted";
|
|
9089
|
+
if (!host) return { kind: "empty", directory: candidate, reason };
|
|
9090
|
+
return reason === "home" ? { kind: "grid", workspace: host } : { kind: "offer", workspace: host, directory: candidate };
|
|
9091
|
+
}
|
|
9092
|
+
function mostRecentProject(home, env) {
|
|
9093
|
+
for (const entry of listProjects(home)) {
|
|
9094
|
+
if (entry.stale || isHomeDirectory(entry.path, home)) continue;
|
|
9095
|
+
try {
|
|
9096
|
+
return resolveWorkspace(entry.path, home, env);
|
|
9097
|
+
} catch {
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
return void 0;
|
|
9101
|
+
}
|
|
9102
|
+
function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.env) {
|
|
8841
9103
|
let gitRoot;
|
|
8842
9104
|
try {
|
|
8843
|
-
gitRoot =
|
|
9105
|
+
gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
8844
9106
|
cwd,
|
|
8845
9107
|
encoding: "utf8",
|
|
8846
9108
|
// stderr is CAPTURED, not ignored: it is the only thing that distinguishes "no worktree here"
|
|
@@ -8851,15 +9113,12 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.
|
|
|
8851
9113
|
if (!isNotAGitWorktree(error)) throw new Error("unable to resolve Git repository root");
|
|
8852
9114
|
gitRoot = void 0;
|
|
8853
9115
|
}
|
|
8854
|
-
|
|
8855
|
-
const root0 = realpathSync3(gitRoot ?? discoverProjectRoot(cwd, home));
|
|
8856
|
-
if (migrate) migrateFrayProjectId(root0, { home });
|
|
9116
|
+
const root0 = realpathSync4(gitRoot ?? discoverProjectRoot(cwd, home));
|
|
8857
9117
|
const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
|
|
8858
9118
|
const root = identity?.root ?? root0;
|
|
8859
|
-
if (migrate) migrateFrayProjectDir(root);
|
|
8860
9119
|
const id = ensureProjectIdFile(root, home, identity?.id);
|
|
8861
9120
|
const stateDir = projectStateDir(id, home);
|
|
8862
|
-
|
|
9121
|
+
mkdirSync7(stateDir, { recursive: true });
|
|
8863
9122
|
const target2 = {
|
|
8864
9123
|
projectId: id,
|
|
8865
9124
|
projectDir: root,
|
|
@@ -8870,7 +9129,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.
|
|
|
8870
9129
|
root,
|
|
8871
9130
|
id,
|
|
8872
9131
|
stateDir,
|
|
8873
|
-
name:
|
|
9132
|
+
name: basename5(root),
|
|
8874
9133
|
identityScope: identity?.scope ?? "repository"
|
|
8875
9134
|
};
|
|
8876
9135
|
}
|
|
@@ -8885,7 +9144,7 @@ function workspaceLaunchTarget(workspace2) {
|
|
|
8885
9144
|
function workspaceFromLaunchTarget(target2, env = process.env) {
|
|
8886
9145
|
let root;
|
|
8887
9146
|
try {
|
|
8888
|
-
root =
|
|
9147
|
+
root = realpathSync4(target2.projectDir);
|
|
8889
9148
|
} catch {
|
|
8890
9149
|
throw new Error("pinned Frizz workspace is no longer available");
|
|
8891
9150
|
}
|
|
@@ -8895,14 +9154,14 @@ function workspaceFromLaunchTarget(target2, env = process.env) {
|
|
|
8895
9154
|
root,
|
|
8896
9155
|
id: target2.projectId,
|
|
8897
9156
|
stateDir: target2.stateDir,
|
|
8898
|
-
name:
|
|
9157
|
+
name: basename5(root),
|
|
8899
9158
|
identityScope: target2.identityScope === "worktree" ? "worktree" : "repository"
|
|
8900
9159
|
};
|
|
8901
9160
|
}
|
|
8902
9161
|
function parseStatusFile(path, authoritative, expected2, adapter = defaultProcessPlatformAdapter) {
|
|
8903
9162
|
try {
|
|
8904
9163
|
const value = JSON.parse(
|
|
8905
|
-
|
|
9164
|
+
readFileSync7(path, "utf8")
|
|
8906
9165
|
);
|
|
8907
9166
|
if (!Number.isInteger(value.pid) || value.pid <= 0 || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
|
|
8908
9167
|
return null;
|
|
@@ -8944,19 +9203,21 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
|
|
|
8944
9203
|
function readPreferredPort(stateDir) {
|
|
8945
9204
|
try {
|
|
8946
9205
|
const value = JSON.parse(
|
|
8947
|
-
|
|
9206
|
+
readFileSync7(join9(stateDir, "launcher.json"), "utf8")
|
|
8948
9207
|
);
|
|
8949
9208
|
return Number.isInteger(value.port) && value.port > 0 && value.port <= 65535 ? value.port : void 0;
|
|
8950
9209
|
} catch {
|
|
8951
9210
|
return void 0;
|
|
8952
9211
|
}
|
|
8953
9212
|
}
|
|
8954
|
-
|
|
9213
|
+
var HEALTH_PROBE_TIMEOUT_MS = 1e3;
|
|
9214
|
+
async function probeFrizz(port, expected2, fetcher = fetch, timeoutMs = HEALTH_PROBE_TIMEOUT_MS) {
|
|
8955
9215
|
const controller = new AbortController();
|
|
8956
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
9216
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
8957
9217
|
timeout.unref?.();
|
|
8958
9218
|
try {
|
|
8959
|
-
const
|
|
9219
|
+
const scope = expected2.slug ? `${FRIZZ_ROUTE_PREFIX}/${expected2.slug}` : FRIZZ_ROUTE_PREFIX;
|
|
9220
|
+
const response = await fetcher(`http://127.0.0.1:${port}${scope}/health`, {
|
|
8960
9221
|
signal: controller.signal
|
|
8961
9222
|
});
|
|
8962
9223
|
if (!response.ok) return null;
|
|
@@ -8981,26 +9242,36 @@ function expectedOwnerHealth(target2, owner) {
|
|
|
8981
9242
|
...owner ? { ownerProof: projectLaunchTokenProof(target2, owner.token) } : {}
|
|
8982
9243
|
};
|
|
8983
9244
|
}
|
|
8984
|
-
async function
|
|
8985
|
-
|
|
9245
|
+
async function probeBindPort(port, host = LOOPBACK_BIND_HOST) {
|
|
9246
|
+
return new Promise((resolveBind) => {
|
|
8986
9247
|
const server = createServer();
|
|
8987
9248
|
server.unref();
|
|
8988
|
-
server.once(
|
|
8989
|
-
|
|
9249
|
+
server.once(
|
|
9250
|
+
"error",
|
|
9251
|
+
(error) => resolveBind(error.code ?? "EADDRINUSE")
|
|
9252
|
+
);
|
|
9253
|
+
server.listen(port, host, () => server.close(() => resolveBind(void 0)));
|
|
8990
9254
|
});
|
|
8991
|
-
return bind2(port);
|
|
8992
9255
|
}
|
|
8993
|
-
function
|
|
9256
|
+
async function canBindPort(port, host = LOOPBACK_BIND_HOST) {
|
|
9257
|
+
return await probeBindPort(port, host) === void 0;
|
|
9258
|
+
}
|
|
9259
|
+
function portUnavailableMessage(port, code) {
|
|
9260
|
+
if (code === "EACCES" || code === "EPERM")
|
|
9261
|
+
return `port ${port} is reserved by the system, not held by a process (bind returned ${code}); on Windows list the reservations with: netsh int ipv4 show excludedportrange protocol=tcp`;
|
|
9262
|
+
return `port ${port} is already in use`;
|
|
9263
|
+
}
|
|
9264
|
+
function portCandidates(preferred, base = DEFAULT_PORT) {
|
|
9265
|
+
const fallback = fallbackPort(base);
|
|
8994
9266
|
return [
|
|
9267
|
+
base,
|
|
8995
9268
|
preferred,
|
|
8996
|
-
...Array.from(
|
|
8997
|
-
{ length: PORT_SCAN_COUNT },
|
|
8998
|
-
(_, index) => DEFAULT_PORT + index
|
|
8999
|
-
)
|
|
9269
|
+
...Array.from({ length: PORT_SCAN_COUNT }, (_, index) => fallback + index)
|
|
9000
9270
|
].filter((port) => !!port && port <= 65535);
|
|
9001
9271
|
}
|
|
9002
|
-
function noFreePortMessage() {
|
|
9003
|
-
|
|
9272
|
+
function noFreePortMessage(base = DEFAULT_PORT) {
|
|
9273
|
+
const fallback = fallbackPort(base);
|
|
9274
|
+
return `no free Frizz port: ${base} is taken, and so is every port in ${fallback}-${fallback + PORT_SCAN_COUNT - 1}`;
|
|
9004
9275
|
}
|
|
9005
9276
|
async function allocatePort(explicit, preferred, options2 = {}) {
|
|
9006
9277
|
const host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
@@ -9015,14 +9286,17 @@ async function allocatePort(explicit, preferred, options2 = {}) {
|
|
|
9015
9286
|
};
|
|
9016
9287
|
if (explicit !== void 0) {
|
|
9017
9288
|
const allocated = await claim(explicit);
|
|
9018
|
-
if (!allocated)
|
|
9289
|
+
if (!allocated)
|
|
9290
|
+
throw new Error(
|
|
9291
|
+
portUnavailableMessage(explicit, await probeBindPort(explicit, host))
|
|
9292
|
+
);
|
|
9019
9293
|
return allocated;
|
|
9020
9294
|
}
|
|
9021
|
-
for (const port of portCandidates(preferred)) {
|
|
9295
|
+
for (const port of portCandidates(preferred, options2.base)) {
|
|
9022
9296
|
const allocated = await claim(port);
|
|
9023
9297
|
if (allocated) return allocated;
|
|
9024
9298
|
}
|
|
9025
|
-
throw new Error(noFreePortMessage());
|
|
9299
|
+
throw new Error(noFreePortMessage(options2.base));
|
|
9026
9300
|
}
|
|
9027
9301
|
async function waitForWorkspace(port, expected2, timeoutMs = LAUNCH_TIMEOUT_MS, options2 = {}) {
|
|
9028
9302
|
const started = Date.now();
|
|
@@ -9059,21 +9333,135 @@ init_project_launch();
|
|
|
9059
9333
|
init_project_launch();
|
|
9060
9334
|
import { fork } from "node:child_process";
|
|
9061
9335
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
9062
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
9336
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
9063
9337
|
import { createServer as createNetServer } from "node:net";
|
|
9064
|
-
import { basename as
|
|
9338
|
+
import { basename as basename6, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute3, relative, resolve as resolve4, sep as sep2 } from "node:path";
|
|
9065
9339
|
import { fileURLToPath } from "node:url";
|
|
9066
9340
|
import watcher from "@parcel/watcher";
|
|
9067
9341
|
|
|
9068
9342
|
// packages/server/src/restart-supervisor.ts
|
|
9069
|
-
init_local_origin();
|
|
9070
|
-
init_local_image();
|
|
9071
9343
|
import { request as requestHttp, createServer as createServer2 } from "node:http";
|
|
9072
9344
|
import { connect } from "node:net";
|
|
9345
|
+
|
|
9346
|
+
// packages/server/src/access-codes.ts
|
|
9347
|
+
import { randomBytes, timingSafeEqual, createHmac } from "node:crypto";
|
|
9348
|
+
var CODE_BYTES = 16;
|
|
9349
|
+
var SESSION_BYTES = 32;
|
|
9350
|
+
var DEFAULT_CODE_TTL_MS = 5 * 6e4;
|
|
9351
|
+
var DEFAULT_SESSION_TTL_MS = 365 * 24 * 60 * 6e4;
|
|
9352
|
+
function defaultRandomToken(bytes) {
|
|
9353
|
+
return randomBytes(bytes).toString("base64url");
|
|
9354
|
+
}
|
|
9355
|
+
function secretsMatch(a, b) {
|
|
9356
|
+
const left = Buffer.from(a);
|
|
9357
|
+
const right = Buffer.from(b);
|
|
9358
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
9359
|
+
}
|
|
9360
|
+
var AccessStore = class {
|
|
9361
|
+
codes = /* @__PURE__ */ new Map();
|
|
9362
|
+
options;
|
|
9363
|
+
/** Signing key for sessions. Persisted by the caller; a fresh one signs every device out. */
|
|
9364
|
+
signingKey;
|
|
9365
|
+
constructor(options2 = {}) {
|
|
9366
|
+
this.signingKey = options2.signingKey ?? randomBytes(32);
|
|
9367
|
+
this.options = {
|
|
9368
|
+
codeTtlMs: options2.codeTtlMs ?? DEFAULT_CODE_TTL_MS,
|
|
9369
|
+
sessionTtlMs: options2.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,
|
|
9370
|
+
now: options2.now ?? Date.now,
|
|
9371
|
+
randomToken: options2.randomToken ?? defaultRandomToken,
|
|
9372
|
+
onConsumed: options2.onConsumed
|
|
9373
|
+
};
|
|
9374
|
+
}
|
|
9375
|
+
/** Mint a code to show as a QR or a link. Minting does not invalidate codes already outstanding. */
|
|
9376
|
+
issue() {
|
|
9377
|
+
this.sweep();
|
|
9378
|
+
const now = this.options.now();
|
|
9379
|
+
const code = {
|
|
9380
|
+
code: this.options.randomToken(CODE_BYTES),
|
|
9381
|
+
createdAt: now,
|
|
9382
|
+
expiresAt: now + this.options.codeTtlMs
|
|
9383
|
+
};
|
|
9384
|
+
this.codes.set(code.code, code);
|
|
9385
|
+
return { code: code.code, createdAt: code.createdAt, expiresAt: code.expiresAt };
|
|
9386
|
+
}
|
|
9387
|
+
/**
|
|
9388
|
+
* Trade a code for a session, at most once, ever.
|
|
9389
|
+
*
|
|
9390
|
+
* The consumed marker is set BEFORE anything else can observe success, and JS runs this method to
|
|
9391
|
+
* completion without interleaving, so two browsers racing the same code cannot both win. That
|
|
9392
|
+
* atomicity is what makes "single-use" true rather than aspirational — it is the first thing the
|
|
9393
|
+
* tests attack.
|
|
9394
|
+
*/
|
|
9395
|
+
redeem(code) {
|
|
9396
|
+
if (!code) return { ok: false, reason: "unknown" };
|
|
9397
|
+
const stored = this.codes.get(code);
|
|
9398
|
+
if (!stored || !secretsMatch(stored.code, code)) return { ok: false, reason: "unknown" };
|
|
9399
|
+
const now = this.options.now();
|
|
9400
|
+
if (stored.consumedAt !== void 0) return { ok: false, reason: "already-used" };
|
|
9401
|
+
if (now >= stored.expiresAt) {
|
|
9402
|
+
this.sweep();
|
|
9403
|
+
return { ok: false, reason: "expired" };
|
|
9404
|
+
}
|
|
9405
|
+
stored.consumedAt = now;
|
|
9406
|
+
this.sweep();
|
|
9407
|
+
this.options.onConsumed?.(code);
|
|
9408
|
+
return { ok: true, session: this.mintSession(now), expiresAt: now + this.options.sessionTtlMs };
|
|
9409
|
+
}
|
|
9410
|
+
/**
|
|
9411
|
+
* A session is `<expiry>.<hmac>` — stateless, so it needs no server-side table, and signed with a key
|
|
9412
|
+
* the caller persists so it survives a restart. Self-describing expiry means a stale cookie is
|
|
9413
|
+
* rejected without a lookup, and rotating the key revokes every outstanding session at once.
|
|
9414
|
+
*/
|
|
9415
|
+
mintSession(now) {
|
|
9416
|
+
const expiresAt = now + this.options.sessionTtlMs;
|
|
9417
|
+
const nonce = this.options.randomToken(SESSION_BYTES);
|
|
9418
|
+
const payload = `${expiresAt}.${nonce}`;
|
|
9419
|
+
return `${payload}.${this.sign(payload)}`;
|
|
9420
|
+
}
|
|
9421
|
+
sign(payload) {
|
|
9422
|
+
return createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
9423
|
+
}
|
|
9424
|
+
/** Is this cookie a session signed by this board's key, and still in date? */
|
|
9425
|
+
verifySession(session) {
|
|
9426
|
+
if (!session) return false;
|
|
9427
|
+
const cut = session.lastIndexOf(".");
|
|
9428
|
+
if (cut <= 0) return false;
|
|
9429
|
+
const payload = session.slice(0, cut);
|
|
9430
|
+
const signature = session.slice(cut + 1);
|
|
9431
|
+
if (!secretsMatch(signature, this.sign(payload))) return false;
|
|
9432
|
+
const expiresAt = Number(payload.slice(0, payload.indexOf(".")));
|
|
9433
|
+
return Number.isFinite(expiresAt) && this.options.now() < expiresAt;
|
|
9434
|
+
}
|
|
9435
|
+
/**
|
|
9436
|
+
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
9437
|
+
*
|
|
9438
|
+
* A CONSUMED code is deliberately kept until that same expiry rather than deleted on use. Deleting it
|
|
9439
|
+
* immediately would make a replay indistinguishable from a typo — both "unknown" — and the difference
|
|
9440
|
+
* matters: "already-used" is the message that tells someone their link was used by somebody else.
|
|
9441
|
+
* It costs one small entry for at most the code TTL.
|
|
9442
|
+
*/
|
|
9443
|
+
sweep() {
|
|
9444
|
+
const now = this.options.now();
|
|
9445
|
+
for (const [key, stored] of this.codes) {
|
|
9446
|
+
if (now >= stored.expiresAt) this.codes.delete(key);
|
|
9447
|
+
}
|
|
9448
|
+
}
|
|
9449
|
+
/** Outstanding codes that could still be redeemed — neither consumed nor expired. */
|
|
9450
|
+
outstanding() {
|
|
9451
|
+
this.sweep();
|
|
9452
|
+
return [...this.codes.values()].filter((stored) => stored.consumedAt === void 0).map(({ code, createdAt, expiresAt }) => ({ code, createdAt, expiresAt }));
|
|
9453
|
+
}
|
|
9454
|
+
};
|
|
9455
|
+
|
|
9456
|
+
// packages/server/src/restart-supervisor.ts
|
|
9457
|
+
init_local_origin();
|
|
9458
|
+
init_local_image();
|
|
9459
|
+
init_src();
|
|
9073
9460
|
var SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
|
|
9074
9461
|
var SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
9075
9462
|
var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restart`;
|
|
9076
9463
|
var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
9464
|
+
var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
9077
9465
|
var SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
9078
9466
|
function responseJson(res, status, value) {
|
|
9079
9467
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -9092,13 +9480,34 @@ function proxyHeaders(req, childPort, vouchSameOrigin = false) {
|
|
|
9092
9480
|
delete headers.connection;
|
|
9093
9481
|
return headers;
|
|
9094
9482
|
}
|
|
9483
|
+
var PUBLIC_TOKEN_PARAM = "frizz_token";
|
|
9484
|
+
var ACCESS_CODE_PARAM = "frizz_code";
|
|
9485
|
+
var PUBLIC_TOKEN_COOKIE = "frizz_public";
|
|
9486
|
+
var SESSION_COOKIE = "frizz_session";
|
|
9487
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
9488
|
+
function readCookie(header, name) {
|
|
9489
|
+
if (!header) return void 0;
|
|
9490
|
+
for (const pair of header.split(";")) {
|
|
9491
|
+
const eq = pair.indexOf("=");
|
|
9492
|
+
if (eq === -1) continue;
|
|
9493
|
+
if (pair.slice(0, eq).trim() === name) return pair.slice(eq + 1).trim();
|
|
9494
|
+
}
|
|
9495
|
+
return void 0;
|
|
9496
|
+
}
|
|
9497
|
+
function unauthorizedPage(reason) {
|
|
9498
|
+
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.";
|
|
9499
|
+
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>`;
|
|
9500
|
+
}
|
|
9095
9501
|
function isControlRequest(req) {
|
|
9096
9502
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9097
|
-
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH;
|
|
9503
|
+
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH;
|
|
9098
9504
|
}
|
|
9099
9505
|
function isLocalImageRequest(req) {
|
|
9100
|
-
|
|
9506
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
9507
|
+
const { pathname } = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9508
|
+
return LOCAL_IMAGE_PATH.test(pathname);
|
|
9101
9509
|
}
|
|
9510
|
+
var LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
|
|
9102
9511
|
var RestartSupervisorProxy = class {
|
|
9103
9512
|
server = null;
|
|
9104
9513
|
restartInFlight = null;
|
|
@@ -9113,17 +9522,40 @@ var RestartSupervisorProxy = class {
|
|
|
9113
9522
|
* judged here first.
|
|
9114
9523
|
*/
|
|
9115
9524
|
policy;
|
|
9525
|
+
/**
|
|
9526
|
+
* Codes and sessions for the public origin, or null when no public origin is declared (loopback-only
|
|
9527
|
+
* boards are never gated, so there is nothing to store).
|
|
9528
|
+
*/
|
|
9529
|
+
access;
|
|
9116
9530
|
/** Both halves of every live upgraded pair, so close() can actually finish. See close(). */
|
|
9117
9531
|
upgradedSockets = /* @__PURE__ */ new Set();
|
|
9118
9532
|
constructor(options2) {
|
|
9119
9533
|
this.options = options2;
|
|
9120
9534
|
this.host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
9535
|
+
this.access = options2.publicOrigin ? new AccessStore({
|
|
9536
|
+
onConsumed: () => options2.onCodeConsumed?.(),
|
|
9537
|
+
...options2.sessionKey ? { signingKey: options2.sessionKey } : {}
|
|
9538
|
+
}) : null;
|
|
9121
9539
|
this.policy = {
|
|
9122
9540
|
exposed: bindHostIsExposed(this.host),
|
|
9123
9541
|
allowedHosts: options2.allowedHosts ?? [],
|
|
9124
9542
|
...options2.publicOrigin ? { publicOrigin: options2.publicOrigin } : {}
|
|
9125
9543
|
};
|
|
9126
9544
|
}
|
|
9545
|
+
/**
|
|
9546
|
+
* Mint a single-use code for this board, or null when no public origin is declared.
|
|
9547
|
+
*
|
|
9548
|
+
* Issuing does NOT invalidate codes already outstanding — pressing the key twice, or two people
|
|
9549
|
+
* asking at once, must not silently break the first QR somebody is still walking towards.
|
|
9550
|
+
*/
|
|
9551
|
+
issueAccessCode() {
|
|
9552
|
+
return this.access?.issue() ?? null;
|
|
9553
|
+
}
|
|
9554
|
+
/** The full URL to show as a link or a QR. */
|
|
9555
|
+
accessUrl(code) {
|
|
9556
|
+
const origin = this.options.publicOrigin;
|
|
9557
|
+
return origin ? `${origin}/?${ACCESS_CODE_PARAM}=${encodeURIComponent(code)}` : null;
|
|
9558
|
+
}
|
|
9127
9559
|
async listen() {
|
|
9128
9560
|
if (this.server) return;
|
|
9129
9561
|
const server = createServer2((req, res) => this.handle(req, res));
|
|
@@ -9237,6 +9669,26 @@ var RestartSupervisorProxy = class {
|
|
|
9237
9669
|
res.end("Forbidden");
|
|
9238
9670
|
return;
|
|
9239
9671
|
}
|
|
9672
|
+
if (pathname === SUPERVISOR_ACCESS_CODE_PATH) {
|
|
9673
|
+
if (this.arrivedPublicly(req)) {
|
|
9674
|
+
res.writeHead(403);
|
|
9675
|
+
res.end("Forbidden");
|
|
9676
|
+
return;
|
|
9677
|
+
}
|
|
9678
|
+
if (req.method !== "POST") {
|
|
9679
|
+
res.writeHead(405, { allow: "POST" });
|
|
9680
|
+
res.end();
|
|
9681
|
+
return;
|
|
9682
|
+
}
|
|
9683
|
+
const link = this.issueAccessCode();
|
|
9684
|
+
const url = link && this.accessUrl(link.code);
|
|
9685
|
+
if (!link || !url) {
|
|
9686
|
+
responseJson(res, 409, { error: "this board has no public origin, so there is nothing to link to" });
|
|
9687
|
+
return;
|
|
9688
|
+
}
|
|
9689
|
+
responseJson(res, 200, { url, expiresAt: link.expiresAt });
|
|
9690
|
+
return;
|
|
9691
|
+
}
|
|
9240
9692
|
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
9241
9693
|
responseJson(res, 200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...this.status() });
|
|
9242
9694
|
return;
|
|
@@ -9286,11 +9738,93 @@ var RestartSupervisorProxy = class {
|
|
|
9286
9738
|
});
|
|
9287
9739
|
res.end(req.method === "HEAD" ? void 0 : result.body);
|
|
9288
9740
|
}
|
|
9741
|
+
/**
|
|
9742
|
+
* Is this request arriving as the declared public origin rather than over loopback?
|
|
9743
|
+
*
|
|
9744
|
+
* The token gate keys on this and not on "is the port exposed", because the two are independent:
|
|
9745
|
+
* a tunnelled board stays bound to 127.0.0.1 while being reachable from the internet by name.
|
|
9746
|
+
*/
|
|
9747
|
+
arrivedPublicly(req) {
|
|
9748
|
+
const declared = this.options.publicOrigin;
|
|
9749
|
+
if (!declared) return false;
|
|
9750
|
+
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
9751
|
+
return !!host && !LOOPBACK_HOSTNAMES.has(host.hostname);
|
|
9752
|
+
}
|
|
9753
|
+
/**
|
|
9754
|
+
* Bearer check for the public origin. Two ways in: the cookie the exchange below sets, or the
|
|
9755
|
+
* one-time `?frizz_token=` in the URL the operator was handed at launch.
|
|
9756
|
+
*
|
|
9757
|
+
* Compared with `timingSafeEqual` on equal-length buffers — a plain `===` on a secret leaks its
|
|
9758
|
+
* prefix to a patient attacker, and this secret is the only thing between the internet and a shell.
|
|
9759
|
+
*/
|
|
9760
|
+
/**
|
|
9761
|
+
* Is this request carrying a session this process minted?
|
|
9762
|
+
*
|
|
9763
|
+
* Sessions only. A raw `?frizz_code=` is NOT accepted here — it is redeemed once by exchangeCode()
|
|
9764
|
+
* below and traded for a session, so a code that has already been spent cannot keep working just by
|
|
9765
|
+
* staying in somebody's URL bar.
|
|
9766
|
+
*/
|
|
9767
|
+
sessionAccepted(req) {
|
|
9768
|
+
if (!this.access) return true;
|
|
9769
|
+
if (this.options.publicToken && this.legacyTokenAccepted(req)) return true;
|
|
9770
|
+
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
9771
|
+
}
|
|
9772
|
+
/**
|
|
9773
|
+
* The standing `FRIZZ_PUBLIC_TOKEN`, for headless boxes where nobody can press a key to mint a code.
|
|
9774
|
+
* Deliberately kept as a separate, opt-in path rather than folded into sessions: it has the weaker
|
|
9775
|
+
* properties (never rotates, every copy stays valid) and the readout says so.
|
|
9776
|
+
*/
|
|
9777
|
+
legacyTokenAccepted(req) {
|
|
9778
|
+
const expected2 = this.options.publicToken;
|
|
9779
|
+
if (!expected2) return false;
|
|
9780
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9781
|
+
const supplied = url.searchParams.get(PUBLIC_TOKEN_PARAM) ?? readCookie(req.headers.cookie, PUBLIC_TOKEN_COOKIE);
|
|
9782
|
+
return !!supplied && secretsMatch(supplied, expected2);
|
|
9783
|
+
}
|
|
9784
|
+
/**
|
|
9785
|
+
* Trade `?frizz_code=…` (or the legacy `?frizz_token=…`) for a cookie, then bounce to the same URL
|
|
9786
|
+
* without the secret, so it never lands in history, a Referer header, or a screenshot.
|
|
9787
|
+
*/
|
|
9788
|
+
exchangeCode(req, res) {
|
|
9789
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
9790
|
+
const code = url.searchParams.get(ACCESS_CODE_PARAM);
|
|
9791
|
+
const legacy = url.searchParams.get(PUBLIC_TOKEN_PARAM);
|
|
9792
|
+
if (code === null && legacy === null) return false;
|
|
9793
|
+
let cookie;
|
|
9794
|
+
if (code !== null) {
|
|
9795
|
+
const redeemed = this.access?.redeem(code);
|
|
9796
|
+
if (!redeemed?.ok) {
|
|
9797
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
9798
|
+
res.end(unauthorizedPage(redeemed?.reason));
|
|
9799
|
+
return true;
|
|
9800
|
+
}
|
|
9801
|
+
cookie = `${SESSION_COOKIE}=${redeemed.session}`;
|
|
9802
|
+
} else {
|
|
9803
|
+
if (!this.legacyTokenAccepted(req)) return false;
|
|
9804
|
+
cookie = `${PUBLIC_TOKEN_COOKIE}=${this.options.publicToken}`;
|
|
9805
|
+
}
|
|
9806
|
+
url.searchParams.delete(ACCESS_CODE_PARAM);
|
|
9807
|
+
url.searchParams.delete(PUBLIC_TOKEN_PARAM);
|
|
9808
|
+
const target2 = `${url.pathname}${url.search}${url.hash}` || "/";
|
|
9809
|
+
res.writeHead(302, {
|
|
9810
|
+
location: target2,
|
|
9811
|
+
"set-cookie": `${cookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`,
|
|
9812
|
+
"cache-control": "no-store"
|
|
9813
|
+
});
|
|
9814
|
+
res.end();
|
|
9815
|
+
return true;
|
|
9816
|
+
}
|
|
9289
9817
|
handle(req, res) {
|
|
9290
9818
|
if (isControlRequest(req)) {
|
|
9291
9819
|
void this.handleControl(req, res);
|
|
9292
9820
|
return;
|
|
9293
9821
|
}
|
|
9822
|
+
if (this.arrivedPublicly(req) && this.exchangeCode(req, res)) return;
|
|
9823
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
9824
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
9825
|
+
res.end(unauthorizedPage());
|
|
9826
|
+
return;
|
|
9827
|
+
}
|
|
9294
9828
|
if (!this.authorityAccepted(req)) {
|
|
9295
9829
|
res.writeHead(403, { "content-type": "text/plain; charset=UTF-8" });
|
|
9296
9830
|
res.end("Forbidden");
|
|
@@ -9324,6 +9858,10 @@ var RestartSupervisorProxy = class {
|
|
|
9324
9858
|
req.pipe(upstream);
|
|
9325
9859
|
}
|
|
9326
9860
|
handleUpgrade(req, socket, head) {
|
|
9861
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
9862
|
+
rejectWebSocketUpgrade(socket, 401, "Unauthorized");
|
|
9863
|
+
return;
|
|
9864
|
+
}
|
|
9327
9865
|
if (!this.authorityAccepted(req, false)) {
|
|
9328
9866
|
socket.destroy();
|
|
9329
9867
|
return;
|
|
@@ -9436,8 +9974,8 @@ function createSupervisorShutdownHandler(options2) {
|
|
|
9436
9974
|
);
|
|
9437
9975
|
};
|
|
9438
9976
|
}
|
|
9439
|
-
var packagesDir =
|
|
9440
|
-
var workspaceDir =
|
|
9977
|
+
var packagesDir = resolve4(import.meta.dirname, "..", "..");
|
|
9978
|
+
var workspaceDir = resolve4(packagesDir, "..");
|
|
9441
9979
|
var INITIAL_BOOT_REASON = "initial boot";
|
|
9442
9980
|
function defaultDevWatchRoots() {
|
|
9443
9981
|
return [workspaceDir];
|
|
@@ -9447,17 +9985,17 @@ function isWithin(path, root) {
|
|
|
9447
9985
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
|
|
9448
9986
|
}
|
|
9449
9987
|
function ignoredDevPath(path) {
|
|
9450
|
-
const parts =
|
|
9988
|
+
const parts = resolve4(path).split(sep2);
|
|
9451
9989
|
const name = parts.at(-1) ?? "";
|
|
9452
9990
|
return parts.some((part) => GENERATED_DIRS.has(part) || part === "fixtures" || part.endsWith(".fixtures")) || /\.(?:test|spec)\.[^.]+$/.test(name) || name.includes(".golden.");
|
|
9453
9991
|
}
|
|
9454
9992
|
function classifyDevChange(path, roots = defaultDevWatchRoots()) {
|
|
9455
|
-
const absolute =
|
|
9456
|
-
const root = roots.find((candidate) => isWithin(absolute,
|
|
9993
|
+
const absolute = resolve4(path);
|
|
9994
|
+
const root = roots.find((candidate) => isWithin(absolute, resolve4(candidate)));
|
|
9457
9995
|
if (!root) return null;
|
|
9458
9996
|
if (ignoredDevPath(absolute)) return null;
|
|
9459
|
-
const name =
|
|
9460
|
-
const relToWorkspace = relative(
|
|
9997
|
+
const name = basename6(absolute);
|
|
9998
|
+
const relToWorkspace = relative(resolve4(root), absolute);
|
|
9461
9999
|
const parts = relToWorkspace.split(sep2);
|
|
9462
10000
|
const packageName = parts[0] === "packages" ? parts[1] : void 0;
|
|
9463
10001
|
if (name === "package.json" && parts.length === 3 && packageName && CHILD_PACKAGE_METADATA.has(packageName)) {
|
|
@@ -9478,12 +10016,12 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
|
|
|
9478
10016
|
return null;
|
|
9479
10017
|
}
|
|
9480
10018
|
function devConfigSyntaxError(path) {
|
|
9481
|
-
const name =
|
|
10019
|
+
const name = basename6(path);
|
|
9482
10020
|
const isTsconfig = /^tsconfig(?:\.[^.]+)?\.json$/.test(name);
|
|
9483
10021
|
if (name !== "package.json" && !isTsconfig) return null;
|
|
9484
10022
|
let text;
|
|
9485
10023
|
try {
|
|
9486
|
-
text =
|
|
10024
|
+
text = readFileSync9(path, "utf8");
|
|
9487
10025
|
} catch (err) {
|
|
9488
10026
|
return `${name}: ${err instanceof Error ? err.message : err}`;
|
|
9489
10027
|
}
|
|
@@ -9598,10 +10136,10 @@ var Supervisor = class {
|
|
|
9598
10136
|
if (launchOwner.pid !== callerGeneration.pid || launchOwner.processStart !== callerGeneration.processStart) throw new Error("dev supervisor caller is not the exact project launch owner");
|
|
9599
10137
|
this.port = opts.port;
|
|
9600
10138
|
this.launchTarget = opts.launchTarget;
|
|
9601
|
-
this.cwd =
|
|
10139
|
+
this.cwd = resolve4(opts.cwd ?? opts.launchTarget.projectDir);
|
|
9602
10140
|
if (this.cwd !== opts.launchTarget.projectDir) throw new Error("dev supervisor cwd does not match its owned project");
|
|
9603
10141
|
this.parentEnv = projectLaunchEnvironment(opts.env ?? process.env, opts.launchTarget, opts.launchOwnerToken);
|
|
9604
|
-
this.roots = (opts.watchRoots ?? defaultDevWatchRoots()).map((root) =>
|
|
10142
|
+
this.roots = (opts.watchRoots ?? defaultDevWatchRoots()).map((root) => resolve4(root));
|
|
9605
10143
|
this.watchEnabled = opts.watch !== false;
|
|
9606
10144
|
this.childEnvironment = opts.childEnvironment ?? (() => ({}));
|
|
9607
10145
|
this.debounceMs = opts.debounceMs ?? DEV_RESTART_DEBOUNCE_MS;
|
|
@@ -9611,10 +10149,10 @@ var Supervisor = class {
|
|
|
9611
10149
|
this.childArgs = opts.childArgs ?? [];
|
|
9612
10150
|
this.watchSubscribe = opts.watchSubscribe ?? ((root, callback, options2) => watcher.subscribe(root, callback, options2));
|
|
9613
10151
|
this.reexec = opts.reexec ?? (typeof process.execve === "function" ? (request) => process.execve(request.executable, request.argv, request.env) : void 0);
|
|
9614
|
-
if (opts.stateDir &&
|
|
10152
|
+
if (opts.stateDir && resolve4(opts.stateDir) !== opts.launchTarget.stateDir) {
|
|
9615
10153
|
throw new Error("dev supervisor state directory does not match its owned project");
|
|
9616
10154
|
}
|
|
9617
|
-
this.supervisorLock =
|
|
10155
|
+
this.supervisorLock = resolve4(opts.launchTarget.stateDir, "dev-supervisor.lock");
|
|
9618
10156
|
this.ownerToken = opts.launchOwnerToken;
|
|
9619
10157
|
this.logLine = opts.log ?? ((line) => log.info("supervisor", stripPrefix(line)));
|
|
9620
10158
|
this.errorLine = opts.error ?? ((line) => log.error("supervisor", stripPrefix(line)));
|
|
@@ -9626,6 +10164,9 @@ var Supervisor = class {
|
|
|
9626
10164
|
host: opts.host,
|
|
9627
10165
|
allowedHosts: opts.allowedHosts,
|
|
9628
10166
|
publicOrigin: opts.publicOrigin,
|
|
10167
|
+
publicToken: opts.publicToken,
|
|
10168
|
+
onCodeConsumed: opts.onCodeConsumed,
|
|
10169
|
+
sessionKey: opts.sessionKey,
|
|
9629
10170
|
childPort: () => this.childPort,
|
|
9630
10171
|
restart: () => this.restartFromBrowser(),
|
|
9631
10172
|
updateRestart: this.updateRestart ? () => this.updateFromBrowser() : void 0,
|
|
@@ -9648,9 +10189,16 @@ var Supervisor = class {
|
|
|
9648
10189
|
currentBoot() {
|
|
9649
10190
|
return this.boot;
|
|
9650
10191
|
}
|
|
10192
|
+
/** Mint a single-use access link for the public origin, or null when none is declared. */
|
|
10193
|
+
issueAccessLink() {
|
|
10194
|
+
const code = this.publicProxy.issueAccessCode();
|
|
10195
|
+
if (!code) return null;
|
|
10196
|
+
const url = this.publicProxy.accessUrl(code.code);
|
|
10197
|
+
return url ? { code: code.code, url, expiresAt: code.expiresAt } : null;
|
|
10198
|
+
}
|
|
9651
10199
|
async start() {
|
|
9652
10200
|
if (this.supervisorLock) {
|
|
9653
|
-
|
|
10201
|
+
mkdirSync8(dirname7(this.supervisorLock), { recursive: true });
|
|
9654
10202
|
this.writeStatus("starting", "watcher initializing");
|
|
9655
10203
|
}
|
|
9656
10204
|
try {
|
|
@@ -10145,8 +10693,8 @@ var npmRegistryReleaseAdapter = {
|
|
|
10145
10693
|
// src/preflight.ts
|
|
10146
10694
|
import { spawnSync } from "node:child_process";
|
|
10147
10695
|
import { chmodSync, statSync as statSync5 } from "node:fs";
|
|
10148
|
-
import { createRequire
|
|
10149
|
-
import { dirname as
|
|
10696
|
+
import { createRequire } from "node:module";
|
|
10697
|
+
import { dirname as dirname8, join as join10 } from "node:path";
|
|
10150
10698
|
var SUPPORTED_NODE_LINES = [
|
|
10151
10699
|
{ major: 22, minor: 13 },
|
|
10152
10700
|
{ major: 23, minor: 4 }
|
|
@@ -10195,10 +10743,10 @@ function ensureNativeHelperPermissions(options2 = {}) {
|
|
|
10195
10743
|
if (platform === "win32") return;
|
|
10196
10744
|
const stat = options2.stat ?? ((path) => statSync5(path));
|
|
10197
10745
|
const chmod = options2.chmod ?? ((path, mode) => chmodSync(path, mode));
|
|
10198
|
-
const resolvePty = options2.resolvePty ?? (() =>
|
|
10746
|
+
const resolvePty = options2.resolvePty ?? (() => createRequire(import.meta.url).resolve("node-pty/package.json"));
|
|
10199
10747
|
try {
|
|
10200
10748
|
const helper = join10(
|
|
10201
|
-
|
|
10749
|
+
dirname8(resolvePty()),
|
|
10202
10750
|
"prebuilds",
|
|
10203
10751
|
`${platform}-${options2.arch ?? process.arch}`,
|
|
10204
10752
|
"spawn-helper"
|
|
@@ -10211,10 +10759,13 @@ function ensureNativeHelperPermissions(options2 = {}) {
|
|
|
10211
10759
|
}
|
|
10212
10760
|
|
|
10213
10761
|
// src/production.ts
|
|
10762
|
+
init_src();
|
|
10763
|
+
init_project_registry();
|
|
10764
|
+
init_project_identity();
|
|
10214
10765
|
var PACKAGE_NAME = process.env.FRIZZ_REGISTRY_PACKAGE ?? "frizz";
|
|
10215
10766
|
function resolvePackageVersion() {
|
|
10216
10767
|
try {
|
|
10217
|
-
const manifest = JSON.parse(
|
|
10768
|
+
const manifest = JSON.parse(readFileSync10(join11(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
10218
10769
|
if (typeof manifest.version === "string" && manifest.version.trim()) return manifest.version.trim();
|
|
10219
10770
|
} catch {
|
|
10220
10771
|
}
|
|
@@ -10251,6 +10802,7 @@ Options:
|
|
|
10251
10802
|
--host [address] serve on a network address instead of loopback (bare --host means 0.0.0.0)
|
|
10252
10803
|
--allowed-host <name> with --host, also accept this DNS name as the board's address (repeatable)
|
|
10253
10804
|
--public-origin <url> serve behind a proxy/tunnel reachable at this exact origin
|
|
10805
|
+
--link print a fresh single-use access link for the running board
|
|
10254
10806
|
--debug stream the full event feed to the terminal instead of the compact readout
|
|
10255
10807
|
-h, --help show this help
|
|
10256
10808
|
|
|
@@ -10258,14 +10810,16 @@ Environment:
|
|
|
10258
10810
|
FRIZZ_HOST same as --host
|
|
10259
10811
|
FRIZZ_ALLOWED_HOSTS same as --allowed-host, comma separated
|
|
10260
10812
|
FRIZZ_PUBLIC_ORIGIN same as --public-origin
|
|
10813
|
+
FRIZZ_PUBLIC_TOKEN standing secret for HEADLESS boxes that cannot show a QR
|
|
10261
10814
|
|
|
10262
10815
|
--host puts a board that can run shell commands as you on the network, and Frizz has no login: anyone
|
|
10263
10816
|
who reaches the port controls it. Only do this on a network you trust. An IP address works as-is; to
|
|
10264
10817
|
reach the board by DNS name you must list that name with --allowed-host ("*" allows any).
|
|
10265
10818
|
|
|
10266
10819
|
--public-origin serves the board through a tunnel or reverse proxy without putting it on the LAN
|
|
10267
|
-
at all \u2014 Frizz stays on loopback and the tunnel dials it.
|
|
10268
|
-
|
|
10820
|
+
at all \u2014 Frizz stays on loopback and the tunnel dials it. It prints a SINGLE-USE access link, and
|
|
10821
|
+
shows it as a QR so you can scan it from a phone; press L for a fresh one at any time. Scanning it
|
|
10822
|
+
trades the code for a session cookie, so the link itself stops working the moment it is used.`
|
|
10269
10823
|
);
|
|
10270
10824
|
process.exit(0);
|
|
10271
10825
|
}
|
|
@@ -10277,6 +10831,7 @@ var bind = (() => {
|
|
|
10277
10831
|
return fail(error);
|
|
10278
10832
|
}
|
|
10279
10833
|
})();
|
|
10834
|
+
var launchIntent;
|
|
10280
10835
|
var workspace = (() => {
|
|
10281
10836
|
try {
|
|
10282
10837
|
if (!reexec) {
|
|
@@ -10284,11 +10839,17 @@ var workspace = (() => {
|
|
|
10284
10839
|
else assertLaunchPrerequisites();
|
|
10285
10840
|
}
|
|
10286
10841
|
const pinned = projectLaunchTargetFromEnvironment(process.env);
|
|
10287
|
-
if (reexec) {
|
|
10842
|
+
if (reexec || process.env.FRIZZ_PRODUCTION_SUPERVISOR === "1") {
|
|
10288
10843
|
if (!pinned) throw new Error("registry successor is missing its pinned project identity");
|
|
10289
10844
|
return workspaceFromLaunchTarget(pinned);
|
|
10290
10845
|
}
|
|
10291
|
-
|
|
10846
|
+
const intent = resolveLaunchIntent(options.repoPath);
|
|
10847
|
+
if (intent.kind === "empty")
|
|
10848
|
+
throw new Error(
|
|
10849
|
+
intent.reason === "home" ? "frizz cannot open your home directory as a project, and there is no other project to show yet. cd into a repository and run frizz there." : `${intent.directory} is not a Frizz project yet, and there is no other project to show. Run frizz inside a repository, or add this one from the projects page once a board is open.`
|
|
10850
|
+
);
|
|
10851
|
+
launchIntent = intent;
|
|
10852
|
+
return intent.workspace;
|
|
10292
10853
|
} catch (error) {
|
|
10293
10854
|
return fail(error);
|
|
10294
10855
|
}
|
|
@@ -10313,8 +10874,39 @@ async function existingPort() {
|
|
|
10313
10874
|
for (const port of new Set(ports)) if (await probeFrizz(port, expected)) return port;
|
|
10314
10875
|
return void 0;
|
|
10315
10876
|
}
|
|
10316
|
-
|
|
10317
|
-
|
|
10877
|
+
function ownSlug() {
|
|
10878
|
+
try {
|
|
10879
|
+
return registerProject(
|
|
10880
|
+
{ dir: workspace.root, id: target.projectId, remoteOwner: resolveProjectLabel(workspace.root)?.split("/")[0] },
|
|
10881
|
+
homedir8()
|
|
10882
|
+
).entry?.slug;
|
|
10883
|
+
} catch {
|
|
10884
|
+
return void 0;
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
var cachedSlugPath;
|
|
10888
|
+
function slugPath() {
|
|
10889
|
+
if (cachedSlugPath === void 0) {
|
|
10890
|
+
if (launchIntent?.kind === "grid") cachedSlugPath = "/";
|
|
10891
|
+
else if (launchIntent?.kind === "offer") cachedSlugPath = `/?add=${encodeURIComponent(launchIntent.directory)}`;
|
|
10892
|
+
else {
|
|
10893
|
+
const slug = ownSlug();
|
|
10894
|
+
cachedSlugPath = slug ? `/project/${slug}` : "";
|
|
10895
|
+
}
|
|
10896
|
+
}
|
|
10897
|
+
return cachedSlugPath;
|
|
10898
|
+
}
|
|
10899
|
+
async function joinRunningFrizz() {
|
|
10900
|
+
const slug = ownSlug();
|
|
10901
|
+
if (!slug) return void 0;
|
|
10902
|
+
for (const port of /* @__PURE__ */ new Set([DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])) {
|
|
10903
|
+
if (await probeFrizz(port, { projectId: target.projectId, projectDir: target.projectDir, slug }))
|
|
10904
|
+
return { port, slug };
|
|
10905
|
+
}
|
|
10906
|
+
return void 0;
|
|
10907
|
+
}
|
|
10908
|
+
async function openOrPrint(port, reused, path = "") {
|
|
10909
|
+
const url = `http://127.0.0.1:${port}${path}`;
|
|
10318
10910
|
logger.info("launcher", `${reused ? "reusing" : "started"} Frizz at ${url}`);
|
|
10319
10911
|
readout?.settle("server", "done", reused ? `already running on port ${port}` : `port ${port}`);
|
|
10320
10912
|
let browser;
|
|
@@ -10339,24 +10931,26 @@ async function openOrPrint(port, reused) {
|
|
|
10339
10931
|
}
|
|
10340
10932
|
const network = reused ? [] : networkUrls(port, bind.host);
|
|
10341
10933
|
const publicOrigin = reused ? void 0 : bind.publicOrigin;
|
|
10934
|
+
const networkFlagsIgnored = reused && (bind.publicOrigin !== void 0 || bindHostIsExposed(bind.host));
|
|
10342
10935
|
const warnings = [
|
|
10343
10936
|
...network.length > 0 ? [EXPOSED_WARNING] : [],
|
|
10344
|
-
...publicOrigin ? [PUBLIC_ORIGIN_WARNING] : []
|
|
10937
|
+
...publicOrigin ? [PUBLIC_ORIGIN_WARNING] : [],
|
|
10938
|
+
...networkFlagsIgnored ? [REUSED_NETWORK_FLAGS_WARNING] : []
|
|
10345
10939
|
];
|
|
10346
10940
|
if (!readout) {
|
|
10347
10941
|
console.log(`${reused ? "reusing" : "started"} Frizz ${PACKAGE_VERSION} for ${workspace.root}`);
|
|
10348
10942
|
console.log(url);
|
|
10349
10943
|
for (const address of network) console.log(address);
|
|
10350
|
-
if (publicOrigin) console.log(publicOrigin);
|
|
10944
|
+
if (publicOrigin) console.log(`${publicOrigin}/${bind.publicToken ? `?frizz_token=${bind.publicToken}` : ""}`);
|
|
10351
10945
|
for (const warning of warnings) console.log(warning);
|
|
10352
10946
|
return;
|
|
10353
10947
|
}
|
|
10354
10948
|
const home = homedir8();
|
|
10355
10949
|
readout.ready(
|
|
10356
10950
|
[
|
|
10357
|
-
{ label: "Local", value:
|
|
10951
|
+
{ label: "Local", value: boardAddress(url), accent: true },
|
|
10358
10952
|
...network.map((address) => ({ label: "Network", value: `${address}/`, accent: true })),
|
|
10359
|
-
...publicOrigin ? [{ label: "Public", value: `${publicOrigin}
|
|
10953
|
+
...publicOrigin ? [{ label: "Public", value: `${publicOrigin}/${bind.publicToken ? `?frizz_token=${bind.publicToken}` : ""}`, accent: true }] : [],
|
|
10360
10954
|
{ label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
|
|
10361
10955
|
...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
|
|
10362
10956
|
],
|
|
@@ -10405,6 +10999,7 @@ async function runSupervisor(port, token) {
|
|
|
10405
10999
|
host: bind.host,
|
|
10406
11000
|
allowedHosts: bind.allowedHosts,
|
|
10407
11001
|
...bind.publicOrigin ? { publicOrigin: bind.publicOrigin } : {},
|
|
11002
|
+
...bind.publicToken ? { publicToken: bind.publicToken } : {},
|
|
10408
11003
|
cwd: workspace.root,
|
|
10409
11004
|
stateDir: workspace.stateDir,
|
|
10410
11005
|
launchTarget: target,
|
|
@@ -10444,7 +11039,7 @@ async function runSupervisor(port, token) {
|
|
|
10444
11039
|
log: ${tildePath(logger.file, homedir8())}` : "";
|
|
10445
11040
|
process.stdout.write(
|
|
10446
11041
|
code === 0 ? `
|
|
10447
|
-
Frizz stopped.
|
|
11042
|
+
Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
|
|
10448
11043
|
|
|
10449
11044
|
` : `
|
|
10450
11045
|
Frizz stopped with errors (exit ${code}).${suffix}
|
|
@@ -10457,6 +11052,7 @@ async function runSupervisor(port, token) {
|
|
|
10457
11052
|
});
|
|
10458
11053
|
process.on("SIGINT", stop);
|
|
10459
11054
|
process.on("SIGTERM", stop);
|
|
11055
|
+
process.on("SIGHUP", stop);
|
|
10460
11056
|
void supervisor.stopRequested.then(stop);
|
|
10461
11057
|
await supervisor.firstBoot;
|
|
10462
11058
|
return await new Promise(() => {
|
|
@@ -10471,7 +11067,12 @@ try {
|
|
|
10471
11067
|
}
|
|
10472
11068
|
const existing = await existingPort();
|
|
10473
11069
|
if (existing) {
|
|
10474
|
-
await openOrPrint(existing, true);
|
|
11070
|
+
await openOrPrint(existing, true, slugPath());
|
|
11071
|
+
process.exit(0);
|
|
11072
|
+
}
|
|
11073
|
+
const joined = await joinRunningFrizz();
|
|
11074
|
+
if (joined) {
|
|
11075
|
+
await openOrPrint(joined.port, true, slugPath());
|
|
10475
11076
|
process.exit(0);
|
|
10476
11077
|
}
|
|
10477
11078
|
const claim = tryAcquireProjectLaunchOwner(target, "launcher");
|
|
@@ -10496,7 +11097,7 @@ try {
|
|
|
10496
11097
|
]);
|
|
10497
11098
|
portReservation();
|
|
10498
11099
|
portReservation = void 0;
|
|
10499
|
-
await openOrPrint(port, false);
|
|
11100
|
+
await openOrPrint(port, false, slugPath());
|
|
10500
11101
|
await running;
|
|
10501
11102
|
} finally {
|
|
10502
11103
|
release?.();
|