frizz 0.7.7 → 0.8.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 +15 -45
- package/dist/claude-agent-broker.js +7 -4
- package/dist/dev-child.js +422 -273
- package/dist/frizz.js +1003 -398
- package/package.json +1 -1
- package/runtime/cc-worker/.claude-plugin/plugin.json +1 -1
- package/runtime/cc-worker/DECISIONS.md +31 -2
- package/runtime/cc-worker/agents/high.md +7 -0
- package/runtime/cc-worker/agents/low.md +7 -0
- package/runtime/cc-worker/agents/max.md +7 -0
- package/runtime/cc-worker/agents/medium.md +7 -0
- package/runtime/cc-worker/agents/xhigh.md +7 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +3 -2
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +7 -6
- package/runtime/cc-worker/hooks/hooks.json +0 -2
- package/runtime/cc-worker/hooks/scratchpad.mjs +5 -3
- package/runtime/cc-worker/hooks/session-seed.mjs +17 -17
- package/runtime/cc-worker/skills/gh/SKILL.md +5 -19
- package/web-dist/assets/{TerminalPane-Cb8jOFP9.js → TerminalPane-BIBHC61j.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-Dtbg2eZ0.js → abnfDiagram-VRR7QNED-CcFQGFPf.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-BZEZt7jZ.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-DzQjWW7u.js → architectureDiagram-ZJ3FMSHR-DErIQOIs.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BJnpkLMt.js → blockDiagram-677ZJIJ3-CpsUqur1.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-COdatsYu.js → c4Diagram-LMCZKHZV-D2dHmCEJ.js} +1 -1
- package/web-dist/assets/channel-DdFRPviT.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-CORMw4Vm.js → chunk-32BRIVSS-aOCEfxRW.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-C7lEadTg.js → chunk-52WLFC77-2iw4nJPC.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-DZy4F97l.js → chunk-C7G6YPKG-BPD3iHVE.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-HynlfV9p.js → chunk-EX3LRPZG-DrefHift.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-BbwKNhKm.js → chunk-FWX5IMBZ-By940EHT.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-lIhOZSmL.js → chunk-HOUHSVGY-B1Cqvz7e.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-WGxaMO8c.js → chunk-ICXQ74PX-1LkVpWuI.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-B-cx0f3u.js → chunk-MOJQB5TN-Ip1cy64r.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-DJceVwZH.js → chunk-OGEWGWER-CNjVyu8F.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-CGwHZns8.js → chunk-PUDLZKDR-9HZR6DAU.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-PTSWtmk1.js → chunk-Q4XR5HBZ-vtWwOCuQ.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-VNxHZPcf.js → chunk-V7JOEXUC-DC6YaHG0.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-ZXYCz_Aw.js → chunk-VAUOI2AC-CP-xe7SD.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-9WCtmFJp.js → chunk-VR4S4FIN-wbxDFptH.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-V4EBjpmt.js → chunk-WYO6CB5R-dibXmwpK.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CPW0Z0ae.js → chunk-ZGVPDNZ5-_y_itJHG.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-Bf3duLb8.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-Bf3duLb8.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-E7PklfJD.js → cynefin-VYW2F7L2-D1RYokZP.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-JIXAPjDf.js → cynefinDiagram-TSTJHNR4-qofqoy3T.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-DjXrdcNx.js → dagre-VKFMJZFB-CgHLoGFk.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY--i9i0qhm.js → diagram-FQU43EPY-JEAs4LOb.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-DxgcENZ_.js → diagram-G47NLZAW--tLd4Lf7.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-CUy9Nr90.js → diagram-NH7WQ7WH-DPbw9CJU.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-DbTbUaCb.js → diagram-OA4YK3LP-CDrb7nWs.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-2gk6xhOq.js → diagram-WEI45ONY-DJswZe3f.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-uE2iDETg.js → ebnfDiagram-CCIWWBDH-BP8-PZMF.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-dztd4MUw.js → erDiagram-Q63AITRT-BQXuebf5.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-DOLxRQQ5.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-DoK1yb0H.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-DAds6Aht.js → ganttDiagram-NO4QXBWP-DotMb935.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-Ci0XVvIa.js → gitGraph-TEB2WS4Q-B1vbYyKQ.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-C6FULyt8.js → gitGraphDiagram-IHSO6WYX-BjDXKk4m.js} +1 -1
- package/web-dist/assets/index-CmrDpsD8.css +1 -0
- package/web-dist/assets/{index-CNy5uOpI.js → index-Cs0TDGMu.js} +124 -124
- package/web-dist/assets/{info-DKCQHKI2-9N77dOiw.js → info-DKCQHKI2-xLTLqQju.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BymP3PgG.js → infoDiagram-FWYZ7A6U-BFfYUsrC.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-CU_kQBz1.js → ishikawaDiagram-FXEZZL3T-45a7VMwm.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-DZN15HSd.js → journeyDiagram-5HDEW3XC-CEBUpu59.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-J4wR5g_3.js → kanban-definition-HUTT4EX6-RwKtDFzG.js} +1 -1
- package/web-dist/assets/{line-SRzGpiDr.js → line-DStcMIry.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-CGBscNGo.js → mermaid-parser.core-B0ekgQMA.js} +3 -3
- package/web-dist/assets/{mermaid.core-xNdwXb1r.js → mermaid.core-w1hcQaPy.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-gR2dbzrj.js → mindmap-definition-LN4V7U3C-D_n38Imb.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-DrR4W6I3.js → packet-7NZHBO7P-CstGeBOX.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-DN7R-_t4.js → pegDiagram-2B236MQR-DvowVyTO.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-C8VjQAFZ.js → pie-RZYD4A2V-BOP2j_HH.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-5w64zdD1.js → pieDiagram-ENE6RG2P-CKRqu_2f.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-B4rbezq_.js → quadrantDiagram-ABIIQ3AL-zSArSS8R.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-CTbbPB8m.js → radar-I7S5WNFK-BqRdNIBw.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-CY1er15b.js → railroad-3IZDKUUU-BaKcl_LU.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-itqDLg1p.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-ze7rLVCi.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-DKsYkEuG.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-Bx2HJ3yT.js → railroadDiagram-RFXS5EU6-BrXTQge9.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-BLK-Y2rx.js → requirementDiagram-TGXJPOKE-DBBNi_SS.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-Dk-8Miv-.js → sankeyDiagram-HTMAVEWB-BwFi7xFy.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-DL2alfR-.js → sequenceDiagram-DBY2YBRQ-CeivqMeT.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-Bka44wf1.js → stateDiagram-2N3HPSRC-C6NZsnFm.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-B3YRnI4W.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-e0GbMwIr.js → swimlanes-5IMT3BWC-C0MubXTG.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-CXva4Ic9.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-CT3GFq89.js → timeline-definition-FHXFAJF6-CtxwSvFu.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-BJauJ0bm.js → treeView-QDETBFTQ-CXcwSjey.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-DCX7A-nx.js → treemap-6X3UGDF4-Uspgr-d1.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-D4dm2iTR.js → vennDiagram-L72KCM5P-tZamQQAQ.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-DklTDSId.js → wardley-OPB4EBWU-0_79c19P.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-CikP5hBd.js → wardleyDiagram-EHGQE667-uYmUsP66.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-FXuOAFpz.js → xychartDiagram-FW5EYKEG-BsOU7Wf7.js} +1 -1
- package/web-dist/index.html +2 -2
- package/runtime/cc-worker/agents/fable-high.md +0 -8
- package/runtime/cc-worker/agents/fable-low.md +0 -8
- package/runtime/cc-worker/agents/fable-max.md +0 -8
- package/runtime/cc-worker/agents/fable-medium.md +0 -8
- package/runtime/cc-worker/agents/fable-xhigh.md +0 -8
- package/runtime/cc-worker/agents/haiku.md +0 -7
- package/runtime/cc-worker/agents/opus-high.md +0 -8
- package/runtime/cc-worker/agents/opus-low.md +0 -8
- package/runtime/cc-worker/agents/opus-max.md +0 -8
- package/runtime/cc-worker/agents/opus-medium.md +0 -8
- package/runtime/cc-worker/agents/opus-xhigh.md +0 -8
- package/runtime/cc-worker/agents/sonnet-high.md +0 -8
- package/runtime/cc-worker/agents/sonnet-low.md +0 -8
- package/runtime/cc-worker/agents/sonnet-max.md +0 -8
- package/runtime/cc-worker/agents/sonnet-medium.md +0 -8
- package/runtime/cc-worker/agents/sonnet-xhigh.md +0 -8
- package/runtime/cc-worker/bin/browser-mcp-tools.json +0 -1192
- package/runtime/cc-worker/bin/browser-mcp.mjs +0 -730
- package/runtime/cc-worker/hooks/precompact-instructions.mjs +0 -133
- package/runtime/cc-worker/skills/waits/SKILL.md +0 -84
- package/web-dist/assets/architecture-TIHT7OUA-CTxVpl9_.js +0 -1
- package/web-dist/assets/channel-COS0Ygbp.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-Df5TLnrN.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-Df5TLnrN.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-DPVPOEaJ.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-BMIMgVYd.js +0 -1
- package/web-dist/assets/index-D6LsViUY.css +0 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-DfEpkrtJ.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-Csunejgu.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-BgRslbM_.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-6KcScVPX.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-jaJhlDbE.js +0 -8
package/dist/frizz.js
CHANGED
|
@@ -140,52 +140,9 @@ function isTrustedLocalHttpRequest(headers, expectedPort, allowMissingOrigin = f
|
|
|
140
140
|
function allowedLocalCorsOrigin(origin, expectedPort, policy) {
|
|
141
141
|
return parseLocalHttpOrigin(origin, expectedPort, policy) ? origin : void 0;
|
|
142
142
|
}
|
|
143
|
-
function normalizeBindHost(value) {
|
|
144
|
-
const trimmed = value.trim();
|
|
145
|
-
if (!trimmed) throw new Error("--host requires an address");
|
|
146
|
-
const bare = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
147
|
-
const lower = bare.toLowerCase();
|
|
148
|
-
if (lower === "localhost") return LOOPBACK_BIND_HOST;
|
|
149
|
-
if (isIP(bare) === 0) {
|
|
150
|
-
throw new Error(`invalid --host address: ${value} (use an IP address such as 0.0.0.0, :: or 127.0.0.1)`);
|
|
151
|
-
}
|
|
152
|
-
return bare;
|
|
153
|
-
}
|
|
154
143
|
function bindHostIsExposed(host) {
|
|
155
144
|
return !LOOPBACK_BIND_HOSTS.has(host.trim().toLowerCase());
|
|
156
145
|
}
|
|
157
|
-
function normalizePublicOrigin(value) {
|
|
158
|
-
const trimmed = value.trim();
|
|
159
|
-
const invalid = (why) => new Error(`invalid --public-origin: ${value} (${why})`);
|
|
160
|
-
if (!trimmed) throw new Error("--public-origin requires a URL");
|
|
161
|
-
let url;
|
|
162
|
-
try {
|
|
163
|
-
url = new URL(trimmed);
|
|
164
|
-
} catch {
|
|
165
|
-
throw invalid("use a full URL such as https://frizz.example.com");
|
|
166
|
-
}
|
|
167
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw invalid("only http and https are supported");
|
|
168
|
-
if (url.username || url.password) throw invalid("an origin carries no credentials");
|
|
169
|
-
if (url.pathname !== "/" || url.search || url.hash) throw invalid("an origin is a scheme and host only, with no path");
|
|
170
|
-
if (!url.hostname) throw invalid("no hostname");
|
|
171
|
-
return url.origin;
|
|
172
|
-
}
|
|
173
|
-
function machineHostNames(hostname) {
|
|
174
|
-
const full = hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
175
|
-
if (!full || full === "localhost" || isIP(full) !== 0) return [];
|
|
176
|
-
const short = full.split(".")[0];
|
|
177
|
-
return [.../* @__PURE__ */ new Set([full, short, `${short}.local`])].filter((name) => name !== "localhost" && name !== "localhost.local");
|
|
178
|
-
}
|
|
179
|
-
function normalizeAllowedHosts(values) {
|
|
180
|
-
const seen = /* @__PURE__ */ new Set();
|
|
181
|
-
for (const value of values) {
|
|
182
|
-
for (const entry of value.split(",")) {
|
|
183
|
-
const host = entry.trim().toLowerCase();
|
|
184
|
-
if (host) seen.add(host);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return [...seen];
|
|
188
|
-
}
|
|
189
146
|
function rejectWebSocketUpgrade(socket, status = 403, reason = "Forbidden") {
|
|
190
147
|
const body = `${reason}
|
|
191
148
|
`;
|
|
@@ -206,7 +163,7 @@ function rejectWebSocketUpgrade(socket, status = 403, reason = "Forbidden") {
|
|
|
206
163
|
}
|
|
207
164
|
}
|
|
208
165
|
}
|
|
209
|
-
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST,
|
|
166
|
+
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST, LOOPBACK_BIND_HOSTS;
|
|
210
167
|
var init_local_origin = __esm({
|
|
211
168
|
"packages/server/src/local-origin.ts"() {
|
|
212
169
|
"use strict";
|
|
@@ -219,7 +176,6 @@ var init_local_origin = __esm({
|
|
|
219
176
|
"x-forwarded-proto"
|
|
220
177
|
];
|
|
221
178
|
LOOPBACK_BIND_HOST = "127.0.0.1";
|
|
222
|
-
ALL_INTERFACES_BIND_HOST = "0.0.0.0";
|
|
223
179
|
LOOPBACK_BIND_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "[::1]"]);
|
|
224
180
|
}
|
|
225
181
|
});
|
|
@@ -12747,7 +12703,7 @@ var init_src = __esm({
|
|
|
12747
12703
|
questions: external_exports.array(AskQuestion)
|
|
12748
12704
|
});
|
|
12749
12705
|
AwaitingHint = external_exports.object({
|
|
12750
|
-
kind: external_exports.enum(["shell", "agent", "timer", "pr", "for"]),
|
|
12706
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "for", "title"]),
|
|
12751
12707
|
value: external_exports.string()
|
|
12752
12708
|
});
|
|
12753
12709
|
RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
|
|
@@ -13324,6 +13280,14 @@ var init_src = __esm({
|
|
|
13324
13280
|
// Default action for a vetted non-image local path in agent markdown. Image clicks always use the
|
|
13325
13281
|
// OS default viewer so screenshots retain their expected behavior.
|
|
13326
13282
|
localFileOpener: LocalFileOpener.optional(),
|
|
13283
|
+
// The token count at which a NEW Claude worker auto-compacts its conversation. Frizz requests the
|
|
13284
|
+
// 1M context window on every dispatch (resolveClaudeLaunchModel), so without this a worker grows
|
|
13285
|
+
// toward 1M before it ever compacts — and every turn past 200K re-sends up to 5x the conversation a
|
|
13286
|
+
// 200K TUI session would, which is the single largest reason a Frizz thread spends quota faster than
|
|
13287
|
+
// the TUI (measured 2026-08-26). Reaches the worker as CLAUDE_CODE_AUTO_COMPACT_WINDOW, which Claude
|
|
13288
|
+
// Code caps to the model's real window. Optional so an old blob parses; defaultSettings pins 500_000
|
|
13289
|
+
// (maintainer 2026-08-26: "a default compaction window of 500k by default").
|
|
13290
|
+
autoCompactWindow: external_exports.number().int().positive().optional(),
|
|
13327
13291
|
// The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
|
|
13328
13292
|
// unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
|
|
13329
13293
|
// the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
|
|
@@ -17842,9 +17806,9 @@ var init_wrapper = __esm({
|
|
|
17842
17806
|
|
|
17843
17807
|
// packages/server/src/frizz-paths.ts
|
|
17844
17808
|
import { existsSync as existsSync2 } from "node:fs";
|
|
17845
|
-
import { homedir as
|
|
17809
|
+
import { homedir as homedir5, tmpdir } from "node:os";
|
|
17846
17810
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
17847
|
-
function legacyFrizzRoot(home =
|
|
17811
|
+
function legacyFrizzRoot(home = homedir5()) {
|
|
17848
17812
|
return join5(home, LEGACY_DIR_NAME);
|
|
17849
17813
|
}
|
|
17850
17814
|
function xdg(env, name) {
|
|
@@ -17886,7 +17850,7 @@ function xdgRoots(env, home) {
|
|
|
17886
17850
|
function frizzPaths(options2 = {}) {
|
|
17887
17851
|
const env = options2.env ?? process.env;
|
|
17888
17852
|
const platform = options2.platform ?? process.platform;
|
|
17889
|
-
const home = options2.home ??
|
|
17853
|
+
const home = options2.home ?? homedir5();
|
|
17890
17854
|
const exists = options2.exists ?? existsSync2;
|
|
17891
17855
|
const legacyRoot = legacyFrizzRoot(home);
|
|
17892
17856
|
if (exists(legacyRoot)) {
|
|
@@ -17925,22 +17889,22 @@ import {
|
|
|
17925
17889
|
mkdirSync as mkdirSync4,
|
|
17926
17890
|
openSync,
|
|
17927
17891
|
readdirSync,
|
|
17928
|
-
rmSync,
|
|
17892
|
+
rmSync as rmSync2,
|
|
17929
17893
|
statSync as statSync2,
|
|
17930
17894
|
symlinkSync,
|
|
17931
17895
|
writeFileSync as writeFileSync4,
|
|
17932
17896
|
writeSync
|
|
17933
17897
|
} from "node:fs";
|
|
17934
|
-
import { homedir as
|
|
17898
|
+
import { homedir as homedir6 } from "node:os";
|
|
17935
17899
|
import { join as join6 } from "node:path";
|
|
17936
|
-
function defaultLogRoot(stateDir, home =
|
|
17900
|
+
function defaultLogRoot(stateDir, home = homedir6()) {
|
|
17937
17901
|
return stateDir ? join6(stateDir, "logs") : join6(frizzPaths({ home }).state, "logs");
|
|
17938
17902
|
}
|
|
17939
17903
|
function runStamp(at) {
|
|
17940
17904
|
const pad = (value) => String(value).padStart(2, "0");
|
|
17941
17905
|
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
|
|
17942
17906
|
}
|
|
17943
|
-
function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home =
|
|
17907
|
+
function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir6(), env = process.env) {
|
|
17944
17908
|
const name = `frizz-${runStamp(at)}-${pid}.log`;
|
|
17945
17909
|
const override = env[LOG_PATH_ENV]?.trim();
|
|
17946
17910
|
if (override) return override.endsWith(".log") ? override : join6(override, name);
|
|
@@ -17952,7 +17916,7 @@ function latestLogPath(dir) {
|
|
|
17952
17916
|
function linkLatest(dir, target2) {
|
|
17953
17917
|
const link = latestLogPath(dir);
|
|
17954
17918
|
try {
|
|
17955
|
-
|
|
17919
|
+
rmSync2(link, { force: true });
|
|
17956
17920
|
symlinkSync(target2, link);
|
|
17957
17921
|
} catch {
|
|
17958
17922
|
try {
|
|
@@ -17980,7 +17944,7 @@ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Dat
|
|
|
17980
17944
|
const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
|
|
17981
17945
|
for (const entry of stale) {
|
|
17982
17946
|
try {
|
|
17983
|
-
|
|
17947
|
+
rmSync2(join6(dir, entry.name), { force: true });
|
|
17984
17948
|
} catch {
|
|
17985
17949
|
}
|
|
17986
17950
|
}
|
|
@@ -18252,11 +18216,11 @@ import {
|
|
|
18252
18216
|
readFileSync as readFileSync6,
|
|
18253
18217
|
realpathSync as realpathSync2,
|
|
18254
18218
|
renameSync,
|
|
18255
|
-
rmSync as
|
|
18219
|
+
rmSync as rmSync3,
|
|
18256
18220
|
statSync as statSync3,
|
|
18257
18221
|
writeFileSync as writeFileSync5
|
|
18258
18222
|
} from "node:fs";
|
|
18259
|
-
import { homedir as
|
|
18223
|
+
import { homedir as homedir7 } from "node:os";
|
|
18260
18224
|
import { dirname as dirname5, join as join8, resolve } from "node:path";
|
|
18261
18225
|
import { setTimeout as delay } from "node:timers/promises";
|
|
18262
18226
|
function errorCode2(error) {
|
|
@@ -18272,7 +18236,7 @@ function canonicalHome(home) {
|
|
|
18272
18236
|
return resolve(home);
|
|
18273
18237
|
}
|
|
18274
18238
|
}
|
|
18275
|
-
function globalLaunchLockPath(home =
|
|
18239
|
+
function globalLaunchLockPath(home = homedir7()) {
|
|
18276
18240
|
return join8(frizzPaths({ home: canonicalHome(home) }).state, GLOBAL_LOCK_NAME);
|
|
18277
18241
|
}
|
|
18278
18242
|
function namedLaunchLockPath(home, name) {
|
|
@@ -18356,7 +18320,7 @@ function quarantineLock(lockPath) {
|
|
|
18356
18320
|
return false;
|
|
18357
18321
|
}
|
|
18358
18322
|
try {
|
|
18359
|
-
|
|
18323
|
+
rmSync3(quarantine2, { recursive: true, force: true });
|
|
18360
18324
|
} catch {
|
|
18361
18325
|
}
|
|
18362
18326
|
return true;
|
|
@@ -18402,7 +18366,7 @@ function tryAcquireLock(lockPath, adapter) {
|
|
|
18402
18366
|
const quarantine2 = `${lockPath}.release-${process.pid}-${owner.token}`;
|
|
18403
18367
|
try {
|
|
18404
18368
|
renameSync(lockPath, quarantine2);
|
|
18405
|
-
|
|
18369
|
+
rmSync3(quarantine2, { recursive: true, force: true });
|
|
18406
18370
|
} catch {
|
|
18407
18371
|
}
|
|
18408
18372
|
};
|
|
@@ -18410,10 +18374,10 @@ function tryAcquireLock(lockPath, adapter) {
|
|
|
18410
18374
|
function tryAcquireGlobalLaunchLock(home, adapter) {
|
|
18411
18375
|
return tryAcquireLock(globalLaunchLockPath(home), adapter);
|
|
18412
18376
|
}
|
|
18413
|
-
function portReservationPath(port, home =
|
|
18377
|
+
function portReservationPath(port, home = homedir7()) {
|
|
18414
18378
|
return join8(frizzPaths({ home: canonicalHome(home) }).state, PORT_LOCK_DIR, `${port}.lock`);
|
|
18415
18379
|
}
|
|
18416
|
-
function tryReservePort(port, home =
|
|
18380
|
+
function tryReservePort(port, home = homedir7(), adapter = defaultProcessPlatformAdapter) {
|
|
18417
18381
|
const lockPath = portReservationPath(port, home);
|
|
18418
18382
|
const release = tryAcquireLock(lockPath, adapter);
|
|
18419
18383
|
if (release) return release;
|
|
@@ -18426,7 +18390,7 @@ function lockTimeoutError(observation) {
|
|
|
18426
18390
|
observation.ownerPid === void 0 ? "another Frizz launch still owns the global launch lock" : `another Frizz launch (pid ${observation.ownerPid}) still owns the global launch lock`
|
|
18427
18391
|
);
|
|
18428
18392
|
}
|
|
18429
|
-
async function acquireGlobalLaunchLock(home =
|
|
18393
|
+
async function acquireGlobalLaunchLock(home = homedir7(), timeoutMs = 1e4, adapter = defaultProcessPlatformAdapter) {
|
|
18430
18394
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
18431
18395
|
let observation = { stale: false };
|
|
18432
18396
|
for (; ; ) {
|
|
@@ -18594,7 +18558,7 @@ function resolvedIdentity(worktree) {
|
|
|
18594
18558
|
}
|
|
18595
18559
|
return { ...worktree, id: worktreeId };
|
|
18596
18560
|
}
|
|
18597
|
-
function resolveGitProjectIdentity(dir, home =
|
|
18561
|
+
function resolveGitProjectIdentity(dir, home = homedir7()) {
|
|
18598
18562
|
let worktree = resolveGitWorktree(dir);
|
|
18599
18563
|
const existing = resolvedIdentity(worktree);
|
|
18600
18564
|
if (existing) return existing;
|
|
@@ -18668,7 +18632,7 @@ import {
|
|
|
18668
18632
|
openSync as openSync3,
|
|
18669
18633
|
readFileSync as readFileSync7,
|
|
18670
18634
|
renameSync as renameSync2,
|
|
18671
|
-
rmSync as
|
|
18635
|
+
rmSync as rmSync4,
|
|
18672
18636
|
statSync as statSync4,
|
|
18673
18637
|
writeFileSync as writeFileSync6
|
|
18674
18638
|
} from "node:fs";
|
|
@@ -18715,7 +18679,7 @@ function atomicJson(path, value) {
|
|
|
18715
18679
|
}
|
|
18716
18680
|
}
|
|
18717
18681
|
try {
|
|
18718
|
-
|
|
18682
|
+
rmSync4(temp, { force: true });
|
|
18719
18683
|
} catch {
|
|
18720
18684
|
}
|
|
18721
18685
|
throw error;
|
|
@@ -18729,7 +18693,7 @@ function quarantine(path, suffix) {
|
|
|
18729
18693
|
return false;
|
|
18730
18694
|
}
|
|
18731
18695
|
try {
|
|
18732
|
-
|
|
18696
|
+
rmSync4(moved, { recursive: true, force: true });
|
|
18733
18697
|
} catch {
|
|
18734
18698
|
}
|
|
18735
18699
|
return true;
|
|
@@ -19116,7 +19080,7 @@ var init_project_launch = __esm({
|
|
|
19116
19080
|
});
|
|
19117
19081
|
|
|
19118
19082
|
// packages/server/src/boot-progress.ts
|
|
19119
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as
|
|
19083
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
19120
19084
|
import { basename as basename3, dirname as dirname7, join as join10 } from "node:path";
|
|
19121
19085
|
function bootProgressPath(stateDir) {
|
|
19122
19086
|
return join10(stateDir, BOOT_PROGRESS_NAME);
|
|
@@ -19140,8 +19104,8 @@ var init_boot_progress = __esm({
|
|
|
19140
19104
|
|
|
19141
19105
|
// packages/server/src/project-root.ts
|
|
19142
19106
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
19143
|
-
import { closeSync as closeSync3, existsSync as existsSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as
|
|
19144
|
-
import { homedir as
|
|
19107
|
+
import { closeSync as closeSync3, existsSync as existsSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
19108
|
+
import { homedir as homedir8 } from "node:os";
|
|
19145
19109
|
import { dirname as dirname8, join as join11, parse, resolve as resolve2 } from "node:path";
|
|
19146
19110
|
function projectIdPath(root) {
|
|
19147
19111
|
return join11(root, FRIZZ_DIR, ID_FILE);
|
|
@@ -19188,7 +19152,7 @@ function writeProjectIdFile(root, id) {
|
|
|
19188
19152
|
}
|
|
19189
19153
|
}
|
|
19190
19154
|
try {
|
|
19191
|
-
|
|
19155
|
+
rmSync6(temp, { force: true });
|
|
19192
19156
|
} catch {
|
|
19193
19157
|
}
|
|
19194
19158
|
throw error;
|
|
@@ -19198,7 +19162,7 @@ function writeProjectIdFile(root, id) {
|
|
|
19198
19162
|
function projectRootLockName(root) {
|
|
19199
19163
|
return `identity-path-${createHash3("sha256").update(root).digest("hex")}.lock`;
|
|
19200
19164
|
}
|
|
19201
|
-
function ensureProjectIdFile(root, home =
|
|
19165
|
+
function ensureProjectIdFile(root, home = homedir8(), seed) {
|
|
19202
19166
|
const existing = readProjectIdFile(root);
|
|
19203
19167
|
if (existing) return existing;
|
|
19204
19168
|
const release = acquireNamedLaunchLockSync(home, projectRootLockName(root));
|
|
@@ -19220,7 +19184,7 @@ function isNotAGitWorktree(error) {
|
|
|
19220
19184
|
function hasAny(dir, names) {
|
|
19221
19185
|
return names.some((name) => existsSync3(join11(dir, name)));
|
|
19222
19186
|
}
|
|
19223
|
-
function isHomeDirectory(dir, home =
|
|
19187
|
+
function isHomeDirectory(dir, home = homedir8()) {
|
|
19224
19188
|
const canonical = (value) => {
|
|
19225
19189
|
try {
|
|
19226
19190
|
return realpathSync3(resolve2(value));
|
|
@@ -19249,7 +19213,7 @@ function isExistingProjectRoot(dir) {
|
|
|
19249
19213
|
function hasProjectMarker(dir) {
|
|
19250
19214
|
return hasAny(dir, REPO_MARKERS) || hasAny(dir, PROJECT_MARKERS);
|
|
19251
19215
|
}
|
|
19252
|
-
function discoverProjectRoot(cwd = process.cwd(), home =
|
|
19216
|
+
function discoverProjectRoot(cwd = process.cwd(), home = homedir8()) {
|
|
19253
19217
|
let dir;
|
|
19254
19218
|
try {
|
|
19255
19219
|
dir = resolve2(cwd);
|
|
@@ -19293,13 +19257,13 @@ var init_project_root = __esm({
|
|
|
19293
19257
|
});
|
|
19294
19258
|
|
|
19295
19259
|
// packages/server/src/project-registry.ts
|
|
19296
|
-
import { closeSync as closeSync4, existsSync as existsSync4, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync10, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as
|
|
19297
|
-
import { homedir as
|
|
19260
|
+
import { closeSync as closeSync4, existsSync as existsSync4, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync10, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
19261
|
+
import { homedir as homedir9 } from "node:os";
|
|
19298
19262
|
import { basename as basename4, dirname as dirname9, join as join12 } from "node:path";
|
|
19299
|
-
function registryPath(home =
|
|
19263
|
+
function registryPath(home = homedir9()) {
|
|
19300
19264
|
return join12(frizzPaths({ home }).data, "registry.json");
|
|
19301
19265
|
}
|
|
19302
|
-
function readRegistry(home =
|
|
19266
|
+
function readRegistry(home = homedir9()) {
|
|
19303
19267
|
let raw;
|
|
19304
19268
|
try {
|
|
19305
19269
|
raw = readFileSync10(registryPath(home), "utf8");
|
|
@@ -19319,7 +19283,7 @@ function readRegistry(home = homedir8()) {
|
|
|
19319
19283
|
return { ...EMPTY, projects: [] };
|
|
19320
19284
|
}
|
|
19321
19285
|
}
|
|
19322
|
-
function writeRegistry(registry, home =
|
|
19286
|
+
function writeRegistry(registry, home = homedir9()) {
|
|
19323
19287
|
const path = registryPath(home);
|
|
19324
19288
|
mkdirSync9(dirname9(path), { recursive: true });
|
|
19325
19289
|
const temp = `${path}.${process.pid}.tmp`;
|
|
@@ -19340,7 +19304,7 @@ function writeRegistry(registry, home = homedir8()) {
|
|
|
19340
19304
|
}
|
|
19341
19305
|
}
|
|
19342
19306
|
try {
|
|
19343
|
-
|
|
19307
|
+
rmSync7(temp, { force: true });
|
|
19344
19308
|
} catch {
|
|
19345
19309
|
}
|
|
19346
19310
|
throw error;
|
|
@@ -19375,7 +19339,7 @@ function canonicalPath(dir) {
|
|
|
19375
19339
|
return dir;
|
|
19376
19340
|
}
|
|
19377
19341
|
}
|
|
19378
|
-
function registerProject(input, home =
|
|
19342
|
+
function registerProject(input, home = homedir9()) {
|
|
19379
19343
|
input = { ...input, dir: canonicalPath(input.dir) };
|
|
19380
19344
|
const registry = readRegistry(home);
|
|
19381
19345
|
const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
@@ -19410,7 +19374,7 @@ function registerProject(input, home = homedir8()) {
|
|
|
19410
19374
|
writeRegistry(registry, home);
|
|
19411
19375
|
return { entry, action: "created" };
|
|
19412
19376
|
}
|
|
19413
|
-
function listProjects(home =
|
|
19377
|
+
function listProjects(home = homedir9()) {
|
|
19414
19378
|
const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
|
|
19415
19379
|
return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync4(p.path) })).sort((a, b) => {
|
|
19416
19380
|
if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
|
|
@@ -19419,7 +19383,7 @@ function listProjects(home = homedir8()) {
|
|
|
19419
19383
|
return a.order - b.order;
|
|
19420
19384
|
});
|
|
19421
19385
|
}
|
|
19422
|
-
function findByPath(path, home =
|
|
19386
|
+
function findByPath(path, home = homedir9()) {
|
|
19423
19387
|
const canonical = canonicalPath(path);
|
|
19424
19388
|
return readRegistry(home).projects.find((p) => p.path === canonical || p.path === path);
|
|
19425
19389
|
}
|
|
@@ -19475,7 +19439,7 @@ var init_project_registry = __esm({
|
|
|
19475
19439
|
// src/production.ts
|
|
19476
19440
|
init_local_origin();
|
|
19477
19441
|
import { readFileSync as readFileSync13 } from "node:fs";
|
|
19478
|
-
import { homedir as
|
|
19442
|
+
import { homedir as homedir11 } from "node:os";
|
|
19479
19443
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19480
19444
|
import { join as join15 } from "node:path";
|
|
19481
19445
|
|
|
@@ -19723,7 +19687,7 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
19723
19687
|
cmdPipe.on("error", failAll);
|
|
19724
19688
|
resPipe.on("error", failAll);
|
|
19725
19689
|
child.on("exit", () => failAll(new Error("chrome exited before the CDP handshake completed")));
|
|
19726
|
-
const
|
|
19690
|
+
const call2 = (method, params = {}, timeoutMs = 2e4) => new Promise((resolve5, reject) => {
|
|
19727
19691
|
const id = ++seq;
|
|
19728
19692
|
pending.set(id, { resolve: resolve5, reject });
|
|
19729
19693
|
cmdPipe.write(JSON.stringify({ id, method, params }) + "\0");
|
|
@@ -19735,7 +19699,7 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
19735
19699
|
}, timeoutMs).unref();
|
|
19736
19700
|
});
|
|
19737
19701
|
const close = async () => {
|
|
19738
|
-
await
|
|
19702
|
+
await call2("Browser.close").catch(() => {
|
|
19739
19703
|
});
|
|
19740
19704
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
19741
19705
|
await new Promise((resolve5) => {
|
|
@@ -19750,7 +19714,7 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
19750
19714
|
});
|
|
19751
19715
|
});
|
|
19752
19716
|
};
|
|
19753
|
-
return { call, close, kill: () => child.kill() };
|
|
19717
|
+
return { call: call2, close, kill: () => child.kill() };
|
|
19754
19718
|
}
|
|
19755
19719
|
async function cdpInstallPwa(browserPath, url, dataPath) {
|
|
19756
19720
|
const manifestId = manifestIdFor(url);
|
|
@@ -19935,8 +19899,41 @@ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
19935
19899
|
import { dirname, join as join2 } from "node:path";
|
|
19936
19900
|
var CODE_BYTES = 16;
|
|
19937
19901
|
var SESSION_BYTES = 32;
|
|
19902
|
+
var SESSION_ID_BYTES = 6;
|
|
19938
19903
|
var DEFAULT_CODE_TTL_MS = 5 * 6e4;
|
|
19939
19904
|
var DEFAULT_SESSION_TTL_MS = 365 * 24 * 60 * 6e4;
|
|
19905
|
+
function memorySessionDirectory(seed = []) {
|
|
19906
|
+
const records = new Map(seed.map((r) => [r.id, r]));
|
|
19907
|
+
return {
|
|
19908
|
+
record: (r) => void records.set(r.id, r),
|
|
19909
|
+
isRevoked: (id) => records.get(id)?.revokedAt !== void 0,
|
|
19910
|
+
// Reverse FIRST, then sort. Array.sort is stable, so two devices that redeemed in the same
|
|
19911
|
+
// millisecond — which two taps on one page reliably are — keep newest-inserted-first instead of
|
|
19912
|
+
// falling back to insertion order and reading as oldest first.
|
|
19913
|
+
list: () => [...records.values()].reverse().sort((a, b) => b.createdAt - a.createdAt),
|
|
19914
|
+
revoke(id) {
|
|
19915
|
+
const found = records.get(id);
|
|
19916
|
+
if (!found || found.revokedAt !== void 0) return false;
|
|
19917
|
+
found.revokedAt = Date.now();
|
|
19918
|
+
return true;
|
|
19919
|
+
},
|
|
19920
|
+
revokeAll() {
|
|
19921
|
+
let n = 0;
|
|
19922
|
+
for (const r of records.values()) if (r.revokedAt === void 0) {
|
|
19923
|
+
r.revokedAt = Date.now();
|
|
19924
|
+
n++;
|
|
19925
|
+
}
|
|
19926
|
+
return n;
|
|
19927
|
+
}
|
|
19928
|
+
};
|
|
19929
|
+
}
|
|
19930
|
+
function describeDevice(userAgent) {
|
|
19931
|
+
const ua = userAgent ?? "";
|
|
19932
|
+
if (!ua.trim()) return "unknown device";
|
|
19933
|
+
const os = /iPhone/i.test(ua) ? "iPhone" : /iPad/i.test(ua) ? "iPad" : /Android/i.test(ua) ? "Android" : /Mac OS X|Macintosh/i.test(ua) ? "macOS" : /Windows/i.test(ua) ? "Windows" : /Linux/i.test(ua) ? "Linux" : "unknown device";
|
|
19934
|
+
const browser = /Edg\//i.test(ua) ? "Edge" : /OPR\/|Opera/i.test(ua) ? "Opera" : /Firefox\//i.test(ua) ? "Firefox" : /Chrome\//i.test(ua) ? "Chrome" : /Safari\//i.test(ua) ? "Safari" : "";
|
|
19935
|
+
return browser ? `${browser} on ${os}` : os;
|
|
19936
|
+
}
|
|
19940
19937
|
function defaultRandomToken(bytes) {
|
|
19941
19938
|
return randomBytes(bytes).toString("base64url");
|
|
19942
19939
|
}
|
|
@@ -19945,13 +19942,15 @@ function secretsMatch(a, b) {
|
|
|
19945
19942
|
const right = Buffer.from(b);
|
|
19946
19943
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
19947
19944
|
}
|
|
19948
|
-
var AccessStore = class {
|
|
19945
|
+
var AccessStore = class _AccessStore {
|
|
19949
19946
|
codes = /* @__PURE__ */ new Map();
|
|
19950
19947
|
options;
|
|
19948
|
+
sessions;
|
|
19951
19949
|
/** Signing key for sessions. Persisted by the caller; a fresh one signs every device out. */
|
|
19952
19950
|
signingKey;
|
|
19953
19951
|
constructor(options2 = {}) {
|
|
19954
19952
|
this.signingKey = options2.signingKey ?? randomBytes(32);
|
|
19953
|
+
this.sessions = options2.sessions ?? memorySessionDirectory();
|
|
19955
19954
|
this.options = {
|
|
19956
19955
|
codeTtlMs: options2.codeTtlMs ?? DEFAULT_CODE_TTL_MS,
|
|
19957
19956
|
sessionTtlMs: options2.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,
|
|
@@ -19980,7 +19979,7 @@ var AccessStore = class {
|
|
|
19980
19979
|
* atomicity is what makes "single-use" true rather than aspirational — it is the first thing the
|
|
19981
19980
|
* tests attack.
|
|
19982
19981
|
*/
|
|
19983
|
-
redeem(code) {
|
|
19982
|
+
redeem(code, device) {
|
|
19984
19983
|
if (!code) return { ok: false, reason: "unknown" };
|
|
19985
19984
|
const stored = this.codes.get(code);
|
|
19986
19985
|
if (!stored || !secretsMatch(stored.code, code)) return { ok: false, reason: "unknown" };
|
|
@@ -19993,18 +19992,32 @@ var AccessStore = class {
|
|
|
19993
19992
|
stored.consumedAt = now;
|
|
19994
19993
|
this.sweep();
|
|
19995
19994
|
this.options.onConsumed?.(code);
|
|
19996
|
-
|
|
19995
|
+
const minted = this.mintSession(now);
|
|
19996
|
+
this.sessions.record({ id: minted.id, label: device ?? "unknown device", createdAt: now });
|
|
19997
|
+
return { ok: true, session: minted.session, expiresAt: now + this.options.sessionTtlMs, id: minted.id };
|
|
19997
19998
|
}
|
|
19998
19999
|
/**
|
|
19999
|
-
* A session is `<expiry>.<hmac>` —
|
|
20000
|
-
*
|
|
20001
|
-
*
|
|
20000
|
+
* A session is `<expiry>.<id>.<nonce>.<hmac>` — signed with a key the caller persists so it survives
|
|
20001
|
+
* a restart, and self-describing enough that a stale cookie is rejected without any lookup.
|
|
20002
|
+
*
|
|
20003
|
+
* The `id` is what makes ONE device signable-out. It is short because an operator types it into
|
|
20004
|
+
* `frizz --sign-out <id>`, and it is inside the signed payload so it cannot be swapped for another
|
|
20005
|
+
* device's. Rotating the key still revokes everything at once; this is the scalpel beside that axe.
|
|
20006
|
+
*
|
|
20007
|
+
* A session minted before ids existed has a two-part payload and still verifies — it simply has no id
|
|
20008
|
+
* to revoke individually. Upgrading must not sign every device out.
|
|
20002
20009
|
*/
|
|
20003
20010
|
mintSession(now) {
|
|
20004
20011
|
const expiresAt = now + this.options.sessionTtlMs;
|
|
20012
|
+
const id = this.options.randomToken(SESSION_ID_BYTES);
|
|
20005
20013
|
const nonce = this.options.randomToken(SESSION_BYTES);
|
|
20006
|
-
const payload = `${expiresAt}.${nonce}`;
|
|
20007
|
-
return `${payload}.${this.sign(payload)}
|
|
20014
|
+
const payload = `${expiresAt}.${id}.${nonce}`;
|
|
20015
|
+
return { session: `${payload}.${this.sign(payload)}`, id };
|
|
20016
|
+
}
|
|
20017
|
+
/** The id inside a session payload, or null for a legacy two-part one. */
|
|
20018
|
+
static idOf(payload) {
|
|
20019
|
+
const parts = payload.split(".");
|
|
20020
|
+
return parts.length >= 3 ? parts[1] ?? null : null;
|
|
20008
20021
|
}
|
|
20009
20022
|
sign(payload) {
|
|
20010
20023
|
return createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
@@ -20018,7 +20031,9 @@ var AccessStore = class {
|
|
|
20018
20031
|
const signature = session.slice(cut + 1);
|
|
20019
20032
|
if (!secretsMatch(signature, this.sign(payload))) return false;
|
|
20020
20033
|
const expiresAt = Number(payload.slice(0, payload.indexOf(".")));
|
|
20021
|
-
|
|
20034
|
+
if (!Number.isFinite(expiresAt) || this.options.now() >= expiresAt) return false;
|
|
20035
|
+
const id = _AccessStore.idOf(payload);
|
|
20036
|
+
return !(id && this.sessions.isRevoked(id));
|
|
20022
20037
|
}
|
|
20023
20038
|
/**
|
|
20024
20039
|
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
@@ -20040,6 +20055,46 @@ var AccessStore = class {
|
|
|
20040
20055
|
return [...this.codes.values()].filter((stored) => stored.consumedAt === void 0).map(({ code, createdAt, expiresAt }) => ({ code, createdAt, expiresAt }));
|
|
20041
20056
|
}
|
|
20042
20057
|
};
|
|
20058
|
+
function fileSessionDirectory(stateDir, now = Date.now) {
|
|
20059
|
+
const path = join2(stateDir, "sessions.json");
|
|
20060
|
+
let records = [];
|
|
20061
|
+
try {
|
|
20062
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
20063
|
+
if (Array.isArray(parsed)) {
|
|
20064
|
+
records = parsed.filter(
|
|
20065
|
+
(r) => !!r && typeof r.id === "string" && typeof r.createdAt === "number"
|
|
20066
|
+
);
|
|
20067
|
+
}
|
|
20068
|
+
} catch {
|
|
20069
|
+
}
|
|
20070
|
+
const inner = memorySessionDirectory(records);
|
|
20071
|
+
const flush = () => {
|
|
20072
|
+
try {
|
|
20073
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
20074
|
+
writeFileSync(path, JSON.stringify(inner.list()), { mode: 384 });
|
|
20075
|
+
} catch (error) {
|
|
20076
|
+
if (process.env.FRIZZ_DEBUG_SESSIONS) console.error("[sessions] flush failed:", error);
|
|
20077
|
+
}
|
|
20078
|
+
};
|
|
20079
|
+
return {
|
|
20080
|
+
record(r) {
|
|
20081
|
+
inner.record(r);
|
|
20082
|
+
flush();
|
|
20083
|
+
},
|
|
20084
|
+
isRevoked: (id) => inner.isRevoked(id),
|
|
20085
|
+
list: () => inner.list(),
|
|
20086
|
+
revoke(id) {
|
|
20087
|
+
const ok = inner.revoke(id);
|
|
20088
|
+
if (ok) flush();
|
|
20089
|
+
return ok;
|
|
20090
|
+
},
|
|
20091
|
+
revokeAll() {
|
|
20092
|
+
const n = inner.revokeAll();
|
|
20093
|
+
if (n > 0) flush();
|
|
20094
|
+
return n;
|
|
20095
|
+
}
|
|
20096
|
+
};
|
|
20097
|
+
}
|
|
20043
20098
|
function loadOrCreateSessionKey(stateDir) {
|
|
20044
20099
|
const path = join2(stateDir, "session-key");
|
|
20045
20100
|
try {
|
|
@@ -21746,6 +21801,7 @@ var SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
|
21746
21801
|
var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restart`;
|
|
21747
21802
|
var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
21748
21803
|
var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
21804
|
+
var SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
|
|
21749
21805
|
var SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
21750
21806
|
function responseJson(res, status, value) {
|
|
21751
21807
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -21786,7 +21842,7 @@ function unlistedHostPage(name) {
|
|
|
21786
21842
|
}
|
|
21787
21843
|
function isControlRequest(req) {
|
|
21788
21844
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
21789
|
-
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH;
|
|
21845
|
+
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH;
|
|
21790
21846
|
}
|
|
21791
21847
|
function isLocalImageRequest(req) {
|
|
21792
21848
|
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
@@ -21794,6 +21850,22 @@ function isLocalImageRequest(req) {
|
|
|
21794
21850
|
return LOCAL_IMAGE_PATH.test(pathname);
|
|
21795
21851
|
}
|
|
21796
21852
|
var LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
|
|
21853
|
+
async function readJsonBody(req, limit = 64 * 1024) {
|
|
21854
|
+
const chunks = [];
|
|
21855
|
+
let size = 0;
|
|
21856
|
+
for await (const chunk of req) {
|
|
21857
|
+
size += chunk.byteLength;
|
|
21858
|
+
if (size > limit) return null;
|
|
21859
|
+
chunks.push(chunk);
|
|
21860
|
+
}
|
|
21861
|
+
if (chunks.length === 0) return {};
|
|
21862
|
+
try {
|
|
21863
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
21864
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
21865
|
+
} catch {
|
|
21866
|
+
return null;
|
|
21867
|
+
}
|
|
21868
|
+
}
|
|
21797
21869
|
var RestartSupervisorProxy = class {
|
|
21798
21870
|
server = null;
|
|
21799
21871
|
restartInFlight = null;
|
|
@@ -21808,6 +21880,8 @@ var RestartSupervisorProxy = class {
|
|
|
21808
21880
|
* judged here first.
|
|
21809
21881
|
*/
|
|
21810
21882
|
policy;
|
|
21883
|
+
/** The origin a tunnel or proxy serves this board at; undefined while loopback-only. Live: see setPublicOrigin. */
|
|
21884
|
+
publicOrigin;
|
|
21811
21885
|
/**
|
|
21812
21886
|
* Codes and sessions for the public origin, or null when no public origin is declared (loopback-only
|
|
21813
21887
|
* boards are never gated, so there is nothing to store).
|
|
@@ -21818,14 +21892,28 @@ var RestartSupervisorProxy = class {
|
|
|
21818
21892
|
constructor(options2) {
|
|
21819
21893
|
this.options = options2;
|
|
21820
21894
|
this.host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
21821
|
-
this.
|
|
21895
|
+
this.publicOrigin = options2.publicOrigin;
|
|
21896
|
+
this.access = new AccessStore({
|
|
21822
21897
|
onConsumed: () => options2.onCodeConsumed?.(),
|
|
21823
|
-
...options2.sessionKey ? { signingKey: options2.sessionKey } : {}
|
|
21824
|
-
|
|
21825
|
-
|
|
21898
|
+
...options2.sessionKey ? { signingKey: options2.sessionKey } : {},
|
|
21899
|
+
...options2.sessionDirectory ? { sessions: options2.sessionDirectory } : {}
|
|
21900
|
+
});
|
|
21901
|
+
this.policy = this.buildPolicy();
|
|
21902
|
+
}
|
|
21903
|
+
/**
|
|
21904
|
+
* Change the public origin on a running board — what the R pane does when the operator picks or
|
|
21905
|
+
* clears a remote setup. Requests already in flight finish under the policy they started with; the
|
|
21906
|
+
* next one is judged by the new origin. Sessions survive the change.
|
|
21907
|
+
*/
|
|
21908
|
+
setPublicOrigin(origin) {
|
|
21909
|
+
this.publicOrigin = origin;
|
|
21910
|
+
this.policy = this.buildPolicy();
|
|
21911
|
+
}
|
|
21912
|
+
buildPolicy() {
|
|
21913
|
+
return {
|
|
21826
21914
|
exposed: bindHostIsExposed(this.host),
|
|
21827
|
-
allowedHosts:
|
|
21828
|
-
...
|
|
21915
|
+
allowedHosts: this.options.allowedHosts ?? [],
|
|
21916
|
+
...this.publicOrigin ? { publicOrigin: this.publicOrigin } : {}
|
|
21829
21917
|
};
|
|
21830
21918
|
}
|
|
21831
21919
|
/**
|
|
@@ -21835,11 +21923,11 @@ var RestartSupervisorProxy = class {
|
|
|
21835
21923
|
* asking at once, must not silently break the first QR somebody is still walking towards.
|
|
21836
21924
|
*/
|
|
21837
21925
|
issueAccessCode() {
|
|
21838
|
-
return this.access
|
|
21926
|
+
return this.publicOrigin ? this.access.issue() : null;
|
|
21839
21927
|
}
|
|
21840
21928
|
/** The full URL to show as a link or a QR. */
|
|
21841
21929
|
accessUrl(code) {
|
|
21842
|
-
const origin = this.
|
|
21930
|
+
const origin = this.publicOrigin;
|
|
21843
21931
|
return origin ? `${origin}/?${ACCESS_CODE_PARAM}=${encodeURIComponent(code)}` : null;
|
|
21844
21932
|
}
|
|
21845
21933
|
async listen() {
|
|
@@ -21980,6 +22068,42 @@ var RestartSupervisorProxy = class {
|
|
|
21980
22068
|
responseJson(res, 200, { url, expiresAt: link.expiresAt });
|
|
21981
22069
|
return;
|
|
21982
22070
|
}
|
|
22071
|
+
if (pathname === SUPERVISOR_SESSIONS_PATH) {
|
|
22072
|
+
if (this.arrivedPublicly(req)) {
|
|
22073
|
+
res.writeHead(403);
|
|
22074
|
+
res.end("Forbidden");
|
|
22075
|
+
return;
|
|
22076
|
+
}
|
|
22077
|
+
if (!this.publicOrigin) {
|
|
22078
|
+
responseJson(res, 409, { error: "this board has no public origin, so nothing is signed in" });
|
|
22079
|
+
return;
|
|
22080
|
+
}
|
|
22081
|
+
if (req.method === "GET") {
|
|
22082
|
+
responseJson(res, 200, { sessions: this.access.sessions.list() });
|
|
22083
|
+
return;
|
|
22084
|
+
}
|
|
22085
|
+
if (req.method !== "POST") {
|
|
22086
|
+
res.writeHead(405, { allow: "GET, POST" });
|
|
22087
|
+
res.end();
|
|
22088
|
+
return;
|
|
22089
|
+
}
|
|
22090
|
+
const body = await readJsonBody(req);
|
|
22091
|
+
const id = typeof body?.id === "string" ? body.id : void 0;
|
|
22092
|
+
if (body?.all === true) {
|
|
22093
|
+
responseJson(res, 200, { signedOut: this.access.sessions.revokeAll() });
|
|
22094
|
+
return;
|
|
22095
|
+
}
|
|
22096
|
+
if (!id) {
|
|
22097
|
+
responseJson(res, 400, { error: "name a device id, or pass all: true" });
|
|
22098
|
+
return;
|
|
22099
|
+
}
|
|
22100
|
+
if (!this.access.sessions.revoke(id)) {
|
|
22101
|
+
responseJson(res, 404, { error: `no signed-in device has the id ${id}` });
|
|
22102
|
+
return;
|
|
22103
|
+
}
|
|
22104
|
+
responseJson(res, 200, { signedOut: 1 });
|
|
22105
|
+
return;
|
|
22106
|
+
}
|
|
21983
22107
|
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
21984
22108
|
responseJson(res, 200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...this.status() });
|
|
21985
22109
|
return;
|
|
@@ -22036,7 +22160,7 @@ var RestartSupervisorProxy = class {
|
|
|
22036
22160
|
* a tunnelled board stays bound to 127.0.0.1 while being reachable from the internet by name.
|
|
22037
22161
|
*/
|
|
22038
22162
|
arrivedPublicly(req) {
|
|
22039
|
-
const declared = this.
|
|
22163
|
+
const declared = this.publicOrigin;
|
|
22040
22164
|
if (!declared) return false;
|
|
22041
22165
|
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
22042
22166
|
return !!host && !LOOPBACK_HOSTNAMES.has(host.hostname);
|
|
@@ -22056,7 +22180,7 @@ var RestartSupervisorProxy = class {
|
|
|
22056
22180
|
* staying in somebody's URL bar.
|
|
22057
22181
|
*/
|
|
22058
22182
|
sessionAccepted(req) {
|
|
22059
|
-
if (!this.
|
|
22183
|
+
if (!this.publicOrigin) return true;
|
|
22060
22184
|
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
22061
22185
|
}
|
|
22062
22186
|
/**
|
|
@@ -22067,7 +22191,7 @@ var RestartSupervisorProxy = class {
|
|
|
22067
22191
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
22068
22192
|
const code = url.searchParams.get(ACCESS_CODE_PARAM);
|
|
22069
22193
|
if (code === null) return false;
|
|
22070
|
-
const redeemed = this.access
|
|
22194
|
+
const redeemed = this.access.redeem(code, describeDevice(req.headers["user-agent"]));
|
|
22071
22195
|
if (!redeemed?.ok) {
|
|
22072
22196
|
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
22073
22197
|
res.end(unauthorizedPage(redeemed?.reason));
|
|
@@ -22171,66 +22295,78 @@ var RestartSupervisorProxy = class {
|
|
|
22171
22295
|
}
|
|
22172
22296
|
};
|
|
22173
22297
|
|
|
22174
|
-
// src/
|
|
22175
|
-
var
|
|
22176
|
-
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
function
|
|
22185
|
-
|
|
22298
|
+
// src/sessions-cli.ts
|
|
22299
|
+
var ago = (at, now = Date.now()) => {
|
|
22300
|
+
const seconds = Math.max(0, Math.round((now - at) / 1e3));
|
|
22301
|
+
if (seconds < 90) return "just now";
|
|
22302
|
+
const minutes = Math.round(seconds / 60);
|
|
22303
|
+
if (minutes < 90) return `${minutes}m ago`;
|
|
22304
|
+
const hours = Math.round(minutes / 60);
|
|
22305
|
+
if (hours < 36) return `${hours}h ago`;
|
|
22306
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
22307
|
+
};
|
|
22308
|
+
async function call(port, init) {
|
|
22309
|
+
const response = await fetch(`http://127.0.0.1:${port}${SUPERVISOR_SESSIONS_PATH}`, {
|
|
22310
|
+
...init,
|
|
22311
|
+
headers: { origin: `http://127.0.0.1:${port}`, ...init?.body ? { "content-type": "application/json" } : {}, ...init?.headers }
|
|
22312
|
+
}).catch(() => void 0);
|
|
22313
|
+
if (!response) {
|
|
22314
|
+
console.error("frizz: the running board did not answer");
|
|
22315
|
+
process.exit(1);
|
|
22316
|
+
}
|
|
22317
|
+
const body = await response.json().catch(() => void 0);
|
|
22318
|
+
if (!response.ok) {
|
|
22319
|
+
console.error(`frizz: ${body?.error ?? `the board answered ${response.status}`}`);
|
|
22320
|
+
process.exit(1);
|
|
22321
|
+
}
|
|
22322
|
+
return body;
|
|
22186
22323
|
}
|
|
22187
|
-
function
|
|
22324
|
+
async function listSessions(port) {
|
|
22325
|
+
const body = await call(port);
|
|
22326
|
+
const sessions = body?.sessions ?? [];
|
|
22327
|
+
const live = sessions.filter((s) => s.revokedAt === void 0);
|
|
22328
|
+
if (live.length === 0) {
|
|
22329
|
+
console.log("No device has redeemed an access link.");
|
|
22330
|
+
process.exit(0);
|
|
22331
|
+
}
|
|
22332
|
+
const width = Math.max(...live.map((s) => s.id.length));
|
|
22333
|
+
for (const session of live) {
|
|
22334
|
+
console.log(` ${session.id.padEnd(width)} ${session.label} ${ago(session.createdAt)}`);
|
|
22335
|
+
}
|
|
22336
|
+
console.log(`
|
|
22337
|
+
frizz --sign-out <id> sign one out
|
|
22338
|
+
frizz --sign-out all sign every device out`);
|
|
22339
|
+
process.exit(0);
|
|
22340
|
+
}
|
|
22341
|
+
async function signOutSession(port, target2) {
|
|
22342
|
+
const all = target2 === "all";
|
|
22343
|
+
const body = await call(port, {
|
|
22344
|
+
method: "POST",
|
|
22345
|
+
body: JSON.stringify(all ? { all: true } : { id: target2 })
|
|
22346
|
+
});
|
|
22347
|
+
const count = Number(body?.signedOut ?? 0);
|
|
22348
|
+
if (all) {
|
|
22349
|
+
console.log(count === 0 ? "No device was signed in." : `Signed out ${count} device${count === 1 ? "" : "s"}.`);
|
|
22350
|
+
} else {
|
|
22351
|
+
console.log(`Signed out ${target2}.`);
|
|
22352
|
+
}
|
|
22353
|
+
process.exit(0);
|
|
22354
|
+
}
|
|
22355
|
+
|
|
22356
|
+
// src/pane-host.ts
|
|
22357
|
+
var CTRL_C = "";
|
|
22358
|
+
function installPaneHost(options2) {
|
|
22188
22359
|
const input = options2.input ?? process.stdin;
|
|
22189
22360
|
const output = options2.output ?? process.stdout;
|
|
22190
|
-
const now = options2.now ?? Date.now;
|
|
22191
22361
|
const onInterrupt = options2.onInterrupt ?? (() => process.kill(process.pid, "SIGINT"));
|
|
22192
22362
|
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return null;
|
|
22193
|
-
let
|
|
22363
|
+
let active = null;
|
|
22194
22364
|
let disposed = false;
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
const status = consumed ? "This link has been used. Press L for another." : remaining === 0 ? "This link has expired. Press L for another." : `Single use, expires in ${remaining}s.`;
|
|
22201
|
-
output.write(CLEAR);
|
|
22202
|
-
output.write("\n");
|
|
22203
|
-
for (const row of renderQrLines(shown.url)) output.write(` ${row}
|
|
22204
|
-
`);
|
|
22205
|
-
output.write(`
|
|
22206
|
-
${shown.url}
|
|
22207
|
-
`);
|
|
22208
|
-
output.write(`
|
|
22209
|
-
${DIM}${status} Press any other key to close.${RESET2}
|
|
22210
|
-
`);
|
|
22211
|
-
};
|
|
22212
|
-
let ticker;
|
|
22213
|
-
const close = () => {
|
|
22214
|
-
if (!open) return;
|
|
22215
|
-
open = false;
|
|
22216
|
-
shown = null;
|
|
22217
|
-
consumed = false;
|
|
22218
|
-
if (ticker) clearInterval(ticker);
|
|
22219
|
-
ticker = void 0;
|
|
22220
|
-
output.write(ALT_SCREEN_OFF);
|
|
22221
|
-
output.write(SHOW_CURSOR);
|
|
22222
|
-
};
|
|
22223
|
-
const openPane = () => {
|
|
22224
|
-
const link = options2.issue();
|
|
22225
|
-
if (!link) return;
|
|
22226
|
-
shown = link;
|
|
22227
|
-
consumed = false;
|
|
22228
|
-
open = true;
|
|
22229
|
-
output.write(ALT_SCREEN_ON);
|
|
22230
|
-
output.write(HIDE_CURSOR);
|
|
22231
|
-
paint();
|
|
22232
|
-
ticker = setInterval(paint, 1e3);
|
|
22233
|
-
ticker.unref?.();
|
|
22365
|
+
const closeActive = () => {
|
|
22366
|
+
if (!active) return;
|
|
22367
|
+
const pane = active;
|
|
22368
|
+
active = null;
|
|
22369
|
+
pane.close();
|
|
22234
22370
|
};
|
|
22235
22371
|
const onKey = (chunk) => {
|
|
22236
22372
|
const key = chunk.toString();
|
|
@@ -22239,16 +22375,17 @@ function installAccessPane(options2) {
|
|
|
22239
22375
|
onInterrupt();
|
|
22240
22376
|
return;
|
|
22241
22377
|
}
|
|
22242
|
-
if (
|
|
22243
|
-
close();
|
|
22378
|
+
if (active) {
|
|
22379
|
+
if (active.key(key) === "close") closeActive();
|
|
22244
22380
|
return;
|
|
22245
22381
|
}
|
|
22246
|
-
|
|
22382
|
+
const pane = options2.bindings[key];
|
|
22383
|
+
if (pane && pane.open()) active = pane;
|
|
22247
22384
|
};
|
|
22248
22385
|
const restore = () => {
|
|
22249
22386
|
if (disposed) return;
|
|
22250
22387
|
disposed = true;
|
|
22251
|
-
|
|
22388
|
+
closeActive();
|
|
22252
22389
|
input.off("data", onKey);
|
|
22253
22390
|
try {
|
|
22254
22391
|
input.setRawMode?.(false);
|
|
@@ -22262,9 +22399,70 @@ function installAccessPane(options2) {
|
|
|
22262
22399
|
input.setEncoding("utf8");
|
|
22263
22400
|
input.on("data", onKey);
|
|
22264
22401
|
process.on("exit", restore);
|
|
22402
|
+
return { dispose: restore, isOpen: () => active !== null };
|
|
22403
|
+
}
|
|
22404
|
+
|
|
22405
|
+
// src/access-pane.ts
|
|
22406
|
+
var ALT_SCREEN_ON = "\x1B[?1049h";
|
|
22407
|
+
var ALT_SCREEN_OFF = "\x1B[?1049l";
|
|
22408
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
22409
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
22410
|
+
var CLEAR = "\x1B[2J\x1B[H";
|
|
22411
|
+
var DIM = "\x1B[2m";
|
|
22412
|
+
var RESET2 = "\x1B[0m";
|
|
22413
|
+
function secondsUntil(expiresAt, now) {
|
|
22414
|
+
return Math.max(0, Math.ceil((expiresAt - now) / 1e3));
|
|
22415
|
+
}
|
|
22416
|
+
function createAccessPane(options2) {
|
|
22417
|
+
const output = options2.output ?? process.stdout;
|
|
22418
|
+
const now = options2.now ?? Date.now;
|
|
22419
|
+
let open = false;
|
|
22420
|
+
let shown = null;
|
|
22421
|
+
let consumed = false;
|
|
22422
|
+
let ticker;
|
|
22423
|
+
const paint = () => {
|
|
22424
|
+
if (!shown) return;
|
|
22425
|
+
const remaining = secondsUntil(shown.expiresAt, now());
|
|
22426
|
+
const status = consumed ? "This link has been used. Press L for another." : remaining === 0 ? "This link has expired. Press L for another." : `Single use, expires in ${remaining}s.`;
|
|
22427
|
+
output.write(CLEAR);
|
|
22428
|
+
output.write("\n");
|
|
22429
|
+
for (const row of renderQrLines(shown.url)) output.write(` ${row}
|
|
22430
|
+
`);
|
|
22431
|
+
output.write(`
|
|
22432
|
+
${shown.url}
|
|
22433
|
+
`);
|
|
22434
|
+
output.write(`
|
|
22435
|
+
${DIM}${status} Press any other key to close.${RESET2}
|
|
22436
|
+
`);
|
|
22437
|
+
};
|
|
22265
22438
|
return {
|
|
22266
|
-
|
|
22267
|
-
|
|
22439
|
+
open() {
|
|
22440
|
+
const link = options2.issue();
|
|
22441
|
+
if (!link) return false;
|
|
22442
|
+
shown = link;
|
|
22443
|
+
consumed = false;
|
|
22444
|
+
open = true;
|
|
22445
|
+
output.write(ALT_SCREEN_ON);
|
|
22446
|
+
output.write(HIDE_CURSOR);
|
|
22447
|
+
paint();
|
|
22448
|
+
ticker = setInterval(paint, 1e3);
|
|
22449
|
+
ticker.unref?.();
|
|
22450
|
+
return true;
|
|
22451
|
+
},
|
|
22452
|
+
key() {
|
|
22453
|
+
return "close";
|
|
22454
|
+
},
|
|
22455
|
+
close() {
|
|
22456
|
+
if (!open) return;
|
|
22457
|
+
open = false;
|
|
22458
|
+
shown = null;
|
|
22459
|
+
consumed = false;
|
|
22460
|
+
if (ticker) clearInterval(ticker);
|
|
22461
|
+
ticker = void 0;
|
|
22462
|
+
output.write(ALT_SCREEN_OFF);
|
|
22463
|
+
output.write(SHOW_CURSOR);
|
|
22464
|
+
},
|
|
22465
|
+
markConsumed() {
|
|
22268
22466
|
if (!open) return;
|
|
22269
22467
|
consumed = true;
|
|
22270
22468
|
paint();
|
|
@@ -22272,10 +22470,13 @@ function installAccessPane(options2) {
|
|
|
22272
22470
|
};
|
|
22273
22471
|
}
|
|
22274
22472
|
|
|
22473
|
+
// src/remote-controller.ts
|
|
22474
|
+
import { homedir as homedir4 } from "node:os";
|
|
22475
|
+
|
|
22275
22476
|
// src/cloud.ts
|
|
22276
22477
|
init_src();
|
|
22277
22478
|
import { spawn as spawn2 } from "node:child_process";
|
|
22278
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
22479
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "node:fs";
|
|
22279
22480
|
import { createInterface } from "node:readline/promises";
|
|
22280
22481
|
import { homedir as homedir3 } from "node:os";
|
|
22281
22482
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
@@ -22604,6 +22805,7 @@ var b64url = (bytes) => {
|
|
|
22604
22805
|
for (const byte of view) binary += String.fromCharCode(byte);
|
|
22605
22806
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
22606
22807
|
};
|
|
22808
|
+
var SETTLED_MS = 1e4;
|
|
22607
22809
|
function defaultBackoff(attempt, random = Math.random) {
|
|
22608
22810
|
const base = Math.min(3e4, 500 * 2 ** Math.min(attempt, 6));
|
|
22609
22811
|
return Math.round(base / 2 + random() * (base / 2));
|
|
@@ -22653,8 +22855,11 @@ function connectRelay(options2) {
|
|
|
22653
22855
|
const url = `${options2.relayOrigin.replace(/^http/, "ws").replace(/\/$/, "")}/_relay/connect?h=${encoded}`;
|
|
22654
22856
|
const next = makeSocket(url);
|
|
22655
22857
|
socket = next;
|
|
22858
|
+
let settled;
|
|
22656
22859
|
next.addEventListener("open", () => {
|
|
22657
|
-
|
|
22860
|
+
settled = setTimer(() => {
|
|
22861
|
+
attempt = 0;
|
|
22862
|
+
}, SETTLED_MS);
|
|
22658
22863
|
options2.onStatus?.("connected");
|
|
22659
22864
|
});
|
|
22660
22865
|
next.addEventListener("message", (event) => {
|
|
@@ -22695,6 +22900,10 @@ function connectRelay(options2) {
|
|
|
22695
22900
|
});
|
|
22696
22901
|
const dropped = () => {
|
|
22697
22902
|
if (socket !== next) return;
|
|
22903
|
+
if (settled !== void 0) {
|
|
22904
|
+
clearTimer(settled);
|
|
22905
|
+
settled = void 0;
|
|
22906
|
+
}
|
|
22698
22907
|
socket = null;
|
|
22699
22908
|
for (const session of nested.values()) session.close();
|
|
22700
22909
|
nested.clear();
|
|
@@ -22724,6 +22933,14 @@ function connectRelay(options2) {
|
|
|
22724
22933
|
}
|
|
22725
22934
|
|
|
22726
22935
|
// src/cloud.ts
|
|
22936
|
+
function isExternalConfig(config) {
|
|
22937
|
+
return config.serve === "external";
|
|
22938
|
+
}
|
|
22939
|
+
function describeCloudConfig(config) {
|
|
22940
|
+
if (isClaimedConfig(config)) return "frizz.sh";
|
|
22941
|
+
if (isExternalConfig(config)) return config.provider === "tailscale" ? "Tailscale" : "your proxy";
|
|
22942
|
+
return "Cloudflare Tunnel";
|
|
22943
|
+
}
|
|
22727
22944
|
var REGISTRAR_IS_LIVE = true;
|
|
22728
22945
|
function isClaimedConfig(config) {
|
|
22729
22946
|
return typeof config.claim === "string" && config.claim.length > 0;
|
|
@@ -22731,6 +22948,22 @@ function isClaimedConfig(config) {
|
|
|
22731
22948
|
function isRelayConfig(config) {
|
|
22732
22949
|
return isClaimedConfig(config) && config.serve === "relay";
|
|
22733
22950
|
}
|
|
22951
|
+
var CLAIM_ZONE = "frizz.sh";
|
|
22952
|
+
function zoneClaimLabel(hostname) {
|
|
22953
|
+
const suffix = `.${CLAIM_ZONE}`;
|
|
22954
|
+
if (!hostname.endsWith(suffix)) return null;
|
|
22955
|
+
const label = hostname.slice(0, -suffix.length);
|
|
22956
|
+
return label.length > 0 && !label.includes(".") ? label : null;
|
|
22957
|
+
}
|
|
22958
|
+
async function reconcileCloudConfig(config, port, home = homedir3(), onNotice, origin, github = githubCli) {
|
|
22959
|
+
if (isRelayConfig(config)) return config;
|
|
22960
|
+
const label = config.claim ?? zoneClaimLabel(config.hostname);
|
|
22961
|
+
if (!label) return config;
|
|
22962
|
+
onNotice?.(
|
|
22963
|
+
isClaimedConfig(config) ? `${config.hostname} is served by the Frizz relay now; moving this board off its tunnel` : `${config.hostname} is inside ${CLAIM_ZONE}, where names are claimed, not tunnelled; claiming ${label}`
|
|
22964
|
+
);
|
|
22965
|
+
return establishCloudConfig(label, port, home, origin, github);
|
|
22966
|
+
}
|
|
22734
22967
|
function cloudConfigPath(home = homedir3()) {
|
|
22735
22968
|
return join4(home, ".frizz", "cloud.json");
|
|
22736
22969
|
}
|
|
@@ -22740,12 +22973,14 @@ function readCloudConfig(home = homedir3()) {
|
|
|
22740
22973
|
if (typeof parsed.hostname !== "string" || !parsed.hostname) return null;
|
|
22741
22974
|
const claimed = typeof parsed.claim === "string" && parsed.claim.length > 0;
|
|
22742
22975
|
const named = typeof parsed.tunnel === "string" && parsed.tunnel.length > 0;
|
|
22743
|
-
|
|
22976
|
+
const external = parsed.serve === "external";
|
|
22977
|
+
if (!claimed && !named && !external) return null;
|
|
22744
22978
|
return {
|
|
22745
22979
|
hostname: parsed.hostname,
|
|
22746
22980
|
...named ? { tunnel: parsed.tunnel } : {},
|
|
22747
22981
|
...claimed ? { claim: parsed.claim } : {},
|
|
22748
|
-
...parsed.serve === "relay" || parsed.serve === "tunnel" ? { serve: parsed.serve } : {},
|
|
22982
|
+
...parsed.serve === "relay" || parsed.serve === "tunnel" || external ? { serve: parsed.serve } : {},
|
|
22983
|
+
...external && (parsed.provider === "tailscale" || parsed.provider === "other") ? { provider: parsed.provider } : {},
|
|
22749
22984
|
...parsed.config ? { config: parsed.config } : {}
|
|
22750
22985
|
};
|
|
22751
22986
|
} catch {
|
|
@@ -22769,6 +23004,9 @@ function writeTunnelToken(token, home = homedir3()) {
|
|
|
22769
23004
|
writeFileSync3(path, `${token}
|
|
22770
23005
|
`, { mode: 384 });
|
|
22771
23006
|
}
|
|
23007
|
+
function deleteCloudConfig(home = homedir3()) {
|
|
23008
|
+
rmSync(cloudConfigPath(home), { force: true });
|
|
23009
|
+
}
|
|
22772
23010
|
function writeCloudConfig(config, home = homedir3()) {
|
|
22773
23011
|
const path = cloudConfigPath(home);
|
|
22774
23012
|
mkdirSync3(dirname3(path), { recursive: true });
|
|
@@ -22782,19 +23020,6 @@ function normalizeHostname(raw) {
|
|
|
22782
23020
|
}
|
|
22783
23021
|
return trimmed.toLowerCase();
|
|
22784
23022
|
}
|
|
22785
|
-
async function promptForCloudName() {
|
|
22786
|
-
if (!process.stdin.isTTY) {
|
|
22787
|
-
throw new Error(
|
|
22788
|
-
"--cloud needs a saved name when there is no terminal to ask \u2014 run it once interactively first"
|
|
22789
|
-
);
|
|
22790
|
-
}
|
|
22791
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22792
|
-
try {
|
|
22793
|
-
return (await rl.question("Name for this board \u2014 a word claims <name>.frizz.sh, or paste a hostname you already run a tunnel for: ")).trim();
|
|
22794
|
-
} finally {
|
|
22795
|
-
rl.close();
|
|
22796
|
-
}
|
|
22797
|
-
}
|
|
22798
23023
|
async function promptForTunnelName(hostname, fallback) {
|
|
22799
23024
|
if (!process.stdin.isTTY) return fallback;
|
|
22800
23025
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -22808,8 +23033,8 @@ async function promptForTunnelName(hostname, fallback) {
|
|
|
22808
23033
|
async function establishCloudConfig(answer, port, home = homedir3(), origin, github = githubCli) {
|
|
22809
23034
|
if (answer.includes(".")) {
|
|
22810
23035
|
const hostname = normalizeHostname(answer);
|
|
22811
|
-
const
|
|
22812
|
-
return { hostname, tunnel
|
|
23036
|
+
const tunnel = await promptForTunnelName(hostname, hostname.split(".")[0]);
|
|
23037
|
+
return { hostname, tunnel };
|
|
22813
23038
|
}
|
|
22814
23039
|
if (!REGISTRAR_IS_LIVE && !origin && !process.env.FRIZZ_REGISTRAR) {
|
|
22815
23040
|
throw new Error(
|
|
@@ -22912,6 +23137,421 @@ function startTunnel(config, onExit, onError, home = homedir3(), runToken) {
|
|
|
22912
23137
|
};
|
|
22913
23138
|
}
|
|
22914
23139
|
|
|
23140
|
+
// src/remote-controller.ts
|
|
23141
|
+
function createRemoteController(options2) {
|
|
23142
|
+
const home = options2.home ?? homedir4();
|
|
23143
|
+
let config = null;
|
|
23144
|
+
let transport = null;
|
|
23145
|
+
const stopTransport = () => {
|
|
23146
|
+
transport?.stop();
|
|
23147
|
+
transport = null;
|
|
23148
|
+
};
|
|
23149
|
+
const startTransport = async (next, justClaimed) => {
|
|
23150
|
+
if (isExternalConfig(next)) return;
|
|
23151
|
+
if (isRelayConfig(next)) {
|
|
23152
|
+
if (!justClaimed) {
|
|
23153
|
+
await resolveRunToken(next, options2.port, home, (message) => {
|
|
23154
|
+
options2.log.warn("relay", message);
|
|
23155
|
+
options2.say(message);
|
|
23156
|
+
});
|
|
23157
|
+
}
|
|
23158
|
+
transport = await startRelay(next, options2.port, home, (message) => options2.log.info("relay", message));
|
|
23159
|
+
options2.log.info("relay", `serving ${next.hostname} through the Frizz relay`);
|
|
23160
|
+
return;
|
|
23161
|
+
}
|
|
23162
|
+
const runToken = isClaimedConfig(next) ? justClaimed ? readTunnelToken(home) : await resolveRunToken(next, options2.port, home, (message) => {
|
|
23163
|
+
options2.log.warn("tunnel", message);
|
|
23164
|
+
options2.say(message);
|
|
23165
|
+
}) : null;
|
|
23166
|
+
transport = startTunnel(
|
|
23167
|
+
next,
|
|
23168
|
+
(code) => {
|
|
23169
|
+
if (code === 0 || code === null) return;
|
|
23170
|
+
options2.log.error("tunnel", `cloudflared exited with code ${code}; the public hostname is now unreachable`);
|
|
23171
|
+
},
|
|
23172
|
+
(message) => {
|
|
23173
|
+
options2.log.error("tunnel", message);
|
|
23174
|
+
options2.say(message);
|
|
23175
|
+
},
|
|
23176
|
+
home,
|
|
23177
|
+
runToken ?? void 0
|
|
23178
|
+
);
|
|
23179
|
+
options2.log.info("tunnel", `running cloudflared for ${next.hostname}`);
|
|
23180
|
+
};
|
|
23181
|
+
const originOf = (next) => next ? `https://${next.hostname}` : void 0;
|
|
23182
|
+
return {
|
|
23183
|
+
current: () => config,
|
|
23184
|
+
origin: () => originOf(config),
|
|
23185
|
+
async serveSaved() {
|
|
23186
|
+
const saved = readCloudConfig(home);
|
|
23187
|
+
if (!saved) return;
|
|
23188
|
+
const reconciled = await reconcileCloudConfig(saved, options2.port, home, (message) => {
|
|
23189
|
+
options2.log.warn("cloud", message);
|
|
23190
|
+
options2.say(message);
|
|
23191
|
+
});
|
|
23192
|
+
const justClaimed = reconciled !== saved;
|
|
23193
|
+
if (justClaimed) writeCloudConfig(reconciled, home);
|
|
23194
|
+
await startTransport(reconciled, justClaimed);
|
|
23195
|
+
config = reconciled;
|
|
23196
|
+
options2.host.setPublicOrigin(originOf(reconciled));
|
|
23197
|
+
},
|
|
23198
|
+
async apply(next, applyOptions = {}) {
|
|
23199
|
+
stopTransport();
|
|
23200
|
+
options2.host.setPublicOrigin(void 0);
|
|
23201
|
+
if (!next) {
|
|
23202
|
+
deleteCloudConfig(home);
|
|
23203
|
+
config = null;
|
|
23204
|
+
return;
|
|
23205
|
+
}
|
|
23206
|
+
writeCloudConfig(next, home);
|
|
23207
|
+
try {
|
|
23208
|
+
await startTransport(next, applyOptions.justClaimed ?? false);
|
|
23209
|
+
} catch (error) {
|
|
23210
|
+
config = null;
|
|
23211
|
+
throw error;
|
|
23212
|
+
}
|
|
23213
|
+
config = next;
|
|
23214
|
+
options2.host.setPublicOrigin(originOf(next));
|
|
23215
|
+
},
|
|
23216
|
+
stop: stopTransport
|
|
23217
|
+
};
|
|
23218
|
+
}
|
|
23219
|
+
|
|
23220
|
+
// src/remote-detect.ts
|
|
23221
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
23222
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
23223
|
+
function run2(command, args2) {
|
|
23224
|
+
return new Promise((resolve5) => {
|
|
23225
|
+
execFile3(command, args2, { encoding: "utf8", timeout: PROBE_TIMEOUT_MS }, (error, stdout) => {
|
|
23226
|
+
resolve5(error ? null : stdout);
|
|
23227
|
+
});
|
|
23228
|
+
});
|
|
23229
|
+
}
|
|
23230
|
+
async function probeGithub() {
|
|
23231
|
+
const version = await run2("gh", ["--version"]);
|
|
23232
|
+
if (version === null) return { login: null, installed: false };
|
|
23233
|
+
const login = await run2("gh", ["api", "user", "--jq", ".login"]);
|
|
23234
|
+
return { login: login?.trim() || null, installed: true };
|
|
23235
|
+
}
|
|
23236
|
+
async function probeCloudflared() {
|
|
23237
|
+
const out = await run2("cloudflared", ["--version"]);
|
|
23238
|
+
const version = out?.match(/\d+\.\d+\.\d+/)?.[0] ?? (out ? out.trim() : null);
|
|
23239
|
+
return { version };
|
|
23240
|
+
}
|
|
23241
|
+
async function probeTailscale() {
|
|
23242
|
+
const out = await run2("tailscale", ["status", "--json"]);
|
|
23243
|
+
if (out === null) return { installed: await run2("tailscale", ["--version"]) !== null, dnsName: null };
|
|
23244
|
+
try {
|
|
23245
|
+
const status = JSON.parse(out);
|
|
23246
|
+
const dnsName = status.Self?.DNSName?.replace(/\.$/, "") || null;
|
|
23247
|
+
return { installed: true, dnsName };
|
|
23248
|
+
} catch {
|
|
23249
|
+
return { installed: true, dnsName: null };
|
|
23250
|
+
}
|
|
23251
|
+
}
|
|
23252
|
+
|
|
23253
|
+
// src/remote-pane.ts
|
|
23254
|
+
var CHOICES = [
|
|
23255
|
+
{ kind: "frizz", title: "frizz.sh name", blurb: "<name>.frizz.sh \u2014 nothing to install; needs the GitHub CLI" },
|
|
23256
|
+
{ kind: "cloudflare", title: "Cloudflare Tunnel", blurb: "a domain you own on Cloudflare; cloudflared on this machine" },
|
|
23257
|
+
{ kind: "tailscale", title: "Tailscale", blurb: "your tailnet; tailscale serve does the TLS" },
|
|
23258
|
+
{ kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
|
|
23259
|
+
{ kind: "off", title: "Off", blurb: "loopback only" }
|
|
23260
|
+
];
|
|
23261
|
+
var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
|
|
23262
|
+
var BACKSPACE = /* @__PURE__ */ new Set(["\x7F", "\b"]);
|
|
23263
|
+
var ESC = "\x1B";
|
|
23264
|
+
var UP = "\x1B[A";
|
|
23265
|
+
var DOWN = "\x1B[B";
|
|
23266
|
+
var TAB = " ";
|
|
23267
|
+
var SHIFT_TAB = "\x1B[Z";
|
|
23268
|
+
function kindOf(config) {
|
|
23269
|
+
if (!config) return "off";
|
|
23270
|
+
if (isClaimedConfig(config)) return "frizz";
|
|
23271
|
+
if (isExternalConfig(config)) return config.provider === "tailscale" ? "tailscale" : "other";
|
|
23272
|
+
return "cloudflare";
|
|
23273
|
+
}
|
|
23274
|
+
function wrap(text, width = 74) {
|
|
23275
|
+
const lines = [];
|
|
23276
|
+
for (const paragraph of text.split("\n")) {
|
|
23277
|
+
let line = "";
|
|
23278
|
+
for (const word of paragraph.split(" ")) {
|
|
23279
|
+
if (line && line.length + 1 + word.length > width) {
|
|
23280
|
+
lines.push(line);
|
|
23281
|
+
line = word;
|
|
23282
|
+
} else line = line ? `${line} ${word}` : word;
|
|
23283
|
+
}
|
|
23284
|
+
lines.push(line);
|
|
23285
|
+
}
|
|
23286
|
+
return lines;
|
|
23287
|
+
}
|
|
23288
|
+
function createRemotePane(options2) {
|
|
23289
|
+
const output = options2.output ?? process.stdout;
|
|
23290
|
+
let open = false;
|
|
23291
|
+
let screen = { name: "menu", index: 0 };
|
|
23292
|
+
let github = null;
|
|
23293
|
+
let cloudflared = null;
|
|
23294
|
+
let tailscale = null;
|
|
23295
|
+
const write = (lines) => {
|
|
23296
|
+
if (!open) return;
|
|
23297
|
+
output.write(CLEAR);
|
|
23298
|
+
output.write("\n");
|
|
23299
|
+
for (const line of lines) output.write(` ${line}
|
|
23300
|
+
`);
|
|
23301
|
+
};
|
|
23302
|
+
const check = (ok, text) => ok ? `${text} \u2713` : text;
|
|
23303
|
+
const paint = () => {
|
|
23304
|
+
if (!open) return;
|
|
23305
|
+
const s = screen;
|
|
23306
|
+
if (s.name === "menu") {
|
|
23307
|
+
const current = kindOf(options2.current());
|
|
23308
|
+
write([
|
|
23309
|
+
"Reach this board from anywhere",
|
|
23310
|
+
"",
|
|
23311
|
+
...wrap(
|
|
23312
|
+
"Frizz binds 127.0.0.1 and has no login. Reaching it from another device means something in front of it does the authenticating. Whatever you pick here is remembered; from then on a plain `npx frizz` serves it."
|
|
23313
|
+
),
|
|
23314
|
+
"",
|
|
23315
|
+
...CHOICES.map((choice, index) => {
|
|
23316
|
+
const marker = index === s.index ? "\u276F" : " ";
|
|
23317
|
+
const title = choice.title.padEnd(18);
|
|
23318
|
+
const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
|
|
23319
|
+
return `${marker} ${title}${DIM}${choice.blurb}${RESET2}${now}`;
|
|
23320
|
+
}),
|
|
23321
|
+
"",
|
|
23322
|
+
`${DIM}\u2191\u2193 move \xB7 enter choose \xB7 esc back${RESET2}`
|
|
23323
|
+
]);
|
|
23324
|
+
return;
|
|
23325
|
+
}
|
|
23326
|
+
if (s.name === "form") {
|
|
23327
|
+
const head = formHead(s.kind);
|
|
23328
|
+
const fields = s.fields.map((field, index) => {
|
|
23329
|
+
const focused = index === s.focus;
|
|
23330
|
+
const shown = field.value || (field.placeholder ? `${DIM}${field.placeholder}${RESET2}` : "");
|
|
23331
|
+
const cursor = focused ? "\u2588" : "";
|
|
23332
|
+
return `${field.label.padEnd(10)} ${shown}${cursor}`;
|
|
23333
|
+
});
|
|
23334
|
+
write([...head, "", ...fields, ...s.note ? ["", s.note] : [], "", `${DIM}enter ${s.kind === "frizz" ? "claim" : "save"} \xB7 tab next field \xB7 esc back${RESET2}`]);
|
|
23335
|
+
return;
|
|
23336
|
+
}
|
|
23337
|
+
if (s.name === "busy") {
|
|
23338
|
+
write([s.message, "", `${DIM}working\u2026${RESET2}`]);
|
|
23339
|
+
return;
|
|
23340
|
+
}
|
|
23341
|
+
if (s.name === "done") {
|
|
23342
|
+
const lines = [s.message, ""];
|
|
23343
|
+
if (s.link) {
|
|
23344
|
+
for (const row of renderQrLines(s.link.url)) lines.push(row);
|
|
23345
|
+
lines.push("", s.link.url, "", `${DIM}Scan to sign in on a phone. Single use, expires in 5 minutes. Press L later for another.${RESET2}`);
|
|
23346
|
+
}
|
|
23347
|
+
lines.push("", `${DIM}press any key to return${RESET2}`);
|
|
23348
|
+
write(lines);
|
|
23349
|
+
return;
|
|
23350
|
+
}
|
|
23351
|
+
write([`Could not apply that: ${s.message}`, "", `${DIM}press any key to go back${RESET2}`]);
|
|
23352
|
+
};
|
|
23353
|
+
const formHead = (kind) => {
|
|
23354
|
+
if (kind === "frizz") {
|
|
23355
|
+
const gh = github === "pending" || github === null ? `${DIM}GitHub CLI checking\u2026${RESET2}` : !github.installed ? "GitHub CLI not installed \u2014 see https://cli.github.com, then `gh auth login`" : github.login ? check(true, `GitHub CLI signed in as ${github.login}`) : "GitHub CLI not signed in \u2014 run `gh auth login` in another terminal";
|
|
23356
|
+
return [
|
|
23357
|
+
"frizz.sh name",
|
|
23358
|
+
"",
|
|
23359
|
+
...wrap(
|
|
23360
|
+
"Claims <name>.frizz.sh for your GitHub account. The board dials out to frizz.sh \u2014 no port, no DNS record, no tunnel binary. One name per account; a name nobody runs for 30 days is released."
|
|
23361
|
+
),
|
|
23362
|
+
...options2.sandbox ? ["", ...wrap("This is a sandbox, but a claim is real: it binds this machine's one name to your account, and your real board keeps it. Only the setup saved here is thrown away.")] : [],
|
|
23363
|
+
"",
|
|
23364
|
+
gh
|
|
23365
|
+
];
|
|
23366
|
+
}
|
|
23367
|
+
if (kind === "cloudflare") {
|
|
23368
|
+
const cf = cloudflared === "pending" || cloudflared === null ? `${DIM}cloudflared checking\u2026${RESET2}` : cloudflared.version ? check(true, `cloudflared found, ${cloudflared.version}`) : "cloudflared not found \u2014 install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/";
|
|
23369
|
+
return [
|
|
23370
|
+
"Cloudflare Tunnel",
|
|
23371
|
+
"",
|
|
23372
|
+
"Needs a domain on Cloudflare and cloudflared on this machine.",
|
|
23373
|
+
cf,
|
|
23374
|
+
"",
|
|
23375
|
+
"Create the tunnel and its DNS record once, in another terminal:",
|
|
23376
|
+
"",
|
|
23377
|
+
" cloudflared tunnel login",
|
|
23378
|
+
" cloudflared tunnel create my-board",
|
|
23379
|
+
" cloudflared tunnel route dns my-board board.example.com",
|
|
23380
|
+
"",
|
|
23381
|
+
...wrap(
|
|
23382
|
+
`Then name them here. Frizz writes ~/.cloudflared/frizz.yml (ingress to http://127.0.0.1:${options2.port}) and runs the tunnel beside the board, so the two share a lifetime.`
|
|
23383
|
+
)
|
|
23384
|
+
];
|
|
23385
|
+
}
|
|
23386
|
+
if (kind === "tailscale") {
|
|
23387
|
+
const ts = tailscale === "pending" || tailscale === null ? `${DIM}tailscale checking\u2026${RESET2}` : !tailscale.installed ? "tailscale not found \u2014 install it from https://tailscale.com/download" : tailscale.dnsName ? check(true, `tailscale found, this machine is ${tailscale.dnsName}`) : "tailscale found, but the daemon did not answer \u2014 is it signed in?";
|
|
23388
|
+
return [
|
|
23389
|
+
"Tailscale",
|
|
23390
|
+
"",
|
|
23391
|
+
...wrap("Serves the board on your tailnet. Tailscale terminates TLS and only devices signed into the tailnet can reach the name."),
|
|
23392
|
+
ts,
|
|
23393
|
+
"",
|
|
23394
|
+
"Run once, in another terminal:",
|
|
23395
|
+
"",
|
|
23396
|
+
` tailscale serve --bg ${options2.port}`,
|
|
23397
|
+
"",
|
|
23398
|
+
"Tailscale answers at the address below \u2014 that is the origin Frizz accepts."
|
|
23399
|
+
];
|
|
23400
|
+
}
|
|
23401
|
+
return [
|
|
23402
|
+
"Something else",
|
|
23403
|
+
"",
|
|
23404
|
+
...wrap(
|
|
23405
|
+
`Terminate TLS wherever you like, proxy to http://127.0.0.1:${options2.port}, and give Frizz the exact origin a browser will show \u2014 scheme and host, no path. Frizz answers only to that Host and prints a single-use sign-in link.`
|
|
23406
|
+
)
|
|
23407
|
+
];
|
|
23408
|
+
};
|
|
23409
|
+
const openForm = (kind) => {
|
|
23410
|
+
const current = options2.current();
|
|
23411
|
+
const same = kindOf(current) === kind ? current : null;
|
|
23412
|
+
let fields;
|
|
23413
|
+
if (kind === "frizz") {
|
|
23414
|
+
github = "pending";
|
|
23415
|
+
void options2.probes.github().then((result) => {
|
|
23416
|
+
github = result;
|
|
23417
|
+
if (screen.name === "form" && screen.kind === "frizz" && !screen.fields[0].value && result.login) {
|
|
23418
|
+
screen.fields[0].placeholder = result.login;
|
|
23419
|
+
}
|
|
23420
|
+
paint();
|
|
23421
|
+
});
|
|
23422
|
+
fields = [{ label: "Name", value: same?.claim ?? "" }];
|
|
23423
|
+
} else if (kind === "cloudflare") {
|
|
23424
|
+
cloudflared = "pending";
|
|
23425
|
+
void options2.probes.cloudflared().then((result) => {
|
|
23426
|
+
cloudflared = result;
|
|
23427
|
+
paint();
|
|
23428
|
+
});
|
|
23429
|
+
fields = [
|
|
23430
|
+
{ label: "Hostname", value: same?.hostname ?? "", placeholder: "board.example.com" },
|
|
23431
|
+
{ label: "Tunnel", value: same?.tunnel ?? "", placeholder: "my-board" }
|
|
23432
|
+
];
|
|
23433
|
+
} else if (kind === "tailscale") {
|
|
23434
|
+
tailscale = "pending";
|
|
23435
|
+
void options2.probes.tailscale().then((result) => {
|
|
23436
|
+
tailscale = result;
|
|
23437
|
+
if (screen.name === "form" && screen.kind === "tailscale" && !screen.fields[0].value && result.dnsName) {
|
|
23438
|
+
screen.fields[0].placeholder = `https://${result.dnsName}`;
|
|
23439
|
+
}
|
|
23440
|
+
paint();
|
|
23441
|
+
});
|
|
23442
|
+
fields = [{ label: "Origin", value: same ? `https://${same.hostname}` : "", placeholder: "https://mac-mini.your-tailnet.ts.net" }];
|
|
23443
|
+
} else {
|
|
23444
|
+
fields = [{ label: "Origin", value: same ? `https://${same.hostname}` : "", placeholder: "https://board.example.com" }];
|
|
23445
|
+
}
|
|
23446
|
+
screen = { name: "form", kind, fields, focus: 0 };
|
|
23447
|
+
paint();
|
|
23448
|
+
};
|
|
23449
|
+
const valueOf = (field) => field.value || field.placeholder || "";
|
|
23450
|
+
const submit = async (form) => {
|
|
23451
|
+
const back = form;
|
|
23452
|
+
try {
|
|
23453
|
+
let next;
|
|
23454
|
+
let justClaimed = false;
|
|
23455
|
+
if (form.kind === "frizz") {
|
|
23456
|
+
const name = valueOf(form.fields[0]).trim();
|
|
23457
|
+
if (!name) throw new Error("a name is needed");
|
|
23458
|
+
screen = { name: "busy", message: `claiming ${name}.frizz.sh\u2026` };
|
|
23459
|
+
paint();
|
|
23460
|
+
next = await options2.claim(name);
|
|
23461
|
+
justClaimed = true;
|
|
23462
|
+
} else if (form.kind === "cloudflare") {
|
|
23463
|
+
const hostname = normalizeHostname(valueOf(form.fields[0]));
|
|
23464
|
+
const tunnel = valueOf(form.fields[1]).trim();
|
|
23465
|
+
if (!tunnel) throw new Error("the tunnel's name is needed");
|
|
23466
|
+
next = { hostname, tunnel };
|
|
23467
|
+
} else {
|
|
23468
|
+
const hostname = normalizeHostname(valueOf(form.fields[0]));
|
|
23469
|
+
next = { hostname, serve: "external", provider: form.kind === "tailscale" ? "tailscale" : "other" };
|
|
23470
|
+
}
|
|
23471
|
+
screen = { name: "busy", message: `serving ${next.hostname}\u2026` };
|
|
23472
|
+
paint();
|
|
23473
|
+
await options2.apply(next, { justClaimed });
|
|
23474
|
+
options2.onChanged?.(next);
|
|
23475
|
+
screen = { name: "done", message: `Serving https://${next.hostname} (${describeCloudConfig(next)}).`, link: options2.issueLink(), config: next };
|
|
23476
|
+
} catch (error) {
|
|
23477
|
+
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back };
|
|
23478
|
+
}
|
|
23479
|
+
paint();
|
|
23480
|
+
};
|
|
23481
|
+
const turnOff = async () => {
|
|
23482
|
+
screen = { name: "busy", message: "back to loopback only\u2026" };
|
|
23483
|
+
paint();
|
|
23484
|
+
try {
|
|
23485
|
+
await options2.apply(null);
|
|
23486
|
+
options2.onChanged?.(null);
|
|
23487
|
+
screen = { name: "done", message: "Loopback only. This board is reachable from this machine alone.", link: null, config: null };
|
|
23488
|
+
} catch (error) {
|
|
23489
|
+
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: CHOICES.length - 1 } };
|
|
23490
|
+
}
|
|
23491
|
+
paint();
|
|
23492
|
+
};
|
|
23493
|
+
return {
|
|
23494
|
+
open() {
|
|
23495
|
+
open = true;
|
|
23496
|
+
const current = kindOf(options2.current());
|
|
23497
|
+
screen = { name: "menu", index: Math.max(0, CHOICES.findIndex((choice) => choice.kind === current)) };
|
|
23498
|
+
output.write(ALT_SCREEN_ON);
|
|
23499
|
+
output.write(HIDE_CURSOR);
|
|
23500
|
+
paint();
|
|
23501
|
+
return true;
|
|
23502
|
+
},
|
|
23503
|
+
key(key) {
|
|
23504
|
+
const s = screen;
|
|
23505
|
+
if (s.name === "busy") return "keep";
|
|
23506
|
+
if (s.name === "done") return "close";
|
|
23507
|
+
if (s.name === "error") {
|
|
23508
|
+
screen = s.back;
|
|
23509
|
+
paint();
|
|
23510
|
+
return "keep";
|
|
23511
|
+
}
|
|
23512
|
+
if (s.name === "menu") {
|
|
23513
|
+
if (key === ESC || key === "q") return "close";
|
|
23514
|
+
if (key === UP || key === "k") s.index = (s.index + CHOICES.length - 1) % CHOICES.length;
|
|
23515
|
+
else if (key === DOWN || key === "j") s.index = (s.index + 1) % CHOICES.length;
|
|
23516
|
+
else if (/^[1-5]$/.test(key)) s.index = Number(key) - 1;
|
|
23517
|
+
else if (ENTER.has(key)) {
|
|
23518
|
+
const choice = CHOICES[s.index];
|
|
23519
|
+
if (choice.kind === "off") void turnOff();
|
|
23520
|
+
else openForm(choice.kind);
|
|
23521
|
+
return "keep";
|
|
23522
|
+
}
|
|
23523
|
+
paint();
|
|
23524
|
+
return "keep";
|
|
23525
|
+
}
|
|
23526
|
+
if (key === ESC) {
|
|
23527
|
+
screen = { name: "menu", index: CHOICES.findIndex((choice) => choice.kind === s.kind) };
|
|
23528
|
+
paint();
|
|
23529
|
+
return "keep";
|
|
23530
|
+
}
|
|
23531
|
+
const field = s.fields[s.focus];
|
|
23532
|
+
if (ENTER.has(key)) {
|
|
23533
|
+
if (s.focus < s.fields.length - 1 && valueOf(field)) s.focus += 1;
|
|
23534
|
+
else void submit(s);
|
|
23535
|
+
} else if (key === TAB || key === DOWN) s.focus = (s.focus + 1) % s.fields.length;
|
|
23536
|
+
else if (key === SHIFT_TAB || key === UP) s.focus = (s.focus + s.fields.length - 1) % s.fields.length;
|
|
23537
|
+
else if (BACKSPACE.has(key)) field.value = field.value.slice(0, -1);
|
|
23538
|
+
else if (key.length === 1 && key >= " ") field.value += key;
|
|
23539
|
+
else if (!key.startsWith(ESC)) field.value += key.replace(/[\r\n\t]/g, "");
|
|
23540
|
+
paint();
|
|
23541
|
+
return "keep";
|
|
23542
|
+
},
|
|
23543
|
+
close() {
|
|
23544
|
+
if (!open) return;
|
|
23545
|
+
open = false;
|
|
23546
|
+
output.write(ALT_SCREEN_OFF);
|
|
23547
|
+
output.write(SHOW_CURSOR);
|
|
23548
|
+
}
|
|
23549
|
+
};
|
|
23550
|
+
}
|
|
23551
|
+
|
|
23552
|
+
// src/production.ts
|
|
23553
|
+
init_local_origin();
|
|
23554
|
+
|
|
22915
23555
|
// src/readout.ts
|
|
22916
23556
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
22917
23557
|
var SYNC_BEGIN = "\x1B[?2026h";
|
|
@@ -23258,33 +23898,27 @@ init_src();
|
|
|
23258
23898
|
init_project_registry();
|
|
23259
23899
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
23260
23900
|
import { createServer as createServer2 } from "node:net";
|
|
23261
|
-
import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, realpathSync as realpathSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
23262
|
-
import { homedir as
|
|
23901
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync11, realpathSync as realpathSync5, rmSync as rmSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
23902
|
+
import { homedir as homedir10, tmpdir as tmpdir2 } from "node:os";
|
|
23263
23903
|
import { basename as basename5, join as join13, resolve as resolve3 } from "node:path";
|
|
23264
23904
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
23265
23905
|
var PORT_SCAN_COUNT = 100;
|
|
23266
23906
|
var LAUNCH_TIMEOUT_MS = 3e4;
|
|
23267
23907
|
var LAUNCH_HARD_TIMEOUT_MS = 10 * 6e4;
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
normalizeBindHost(value);
|
|
23272
|
-
return true;
|
|
23273
|
-
} catch {
|
|
23274
|
-
return false;
|
|
23275
|
-
}
|
|
23908
|
+
var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
|
|
23909
|
+
function retiredFlagMessage(flag) {
|
|
23910
|
+
return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
|
|
23276
23911
|
}
|
|
23277
23912
|
function parseCliArgs(argv) {
|
|
23278
23913
|
const args2 = new Set(argv);
|
|
23279
|
-
let upCommand = false;
|
|
23280
23914
|
let rawPort;
|
|
23281
|
-
let
|
|
23282
|
-
let rawPublicOrigin;
|
|
23283
|
-
const rawAllowedHosts = [];
|
|
23915
|
+
let rawSignOut;
|
|
23284
23916
|
const consumed = /* @__PURE__ */ new Set();
|
|
23285
23917
|
for (let index = 0; index < argv.length; index++) {
|
|
23286
23918
|
const arg = argv[index];
|
|
23287
23919
|
if (consumed.has(index)) continue;
|
|
23920
|
+
const retired = arg === "up" ? "up" : RETIRED_NETWORK_FLAGS.find((flag) => arg === flag || arg.startsWith(`${flag}=`));
|
|
23921
|
+
if (retired) throw new Error(retiredFlagMessage(retired));
|
|
23288
23922
|
if (arg === "--port") {
|
|
23289
23923
|
rawPort = argv[++index];
|
|
23290
23924
|
if (rawPort === void 0 || rawPort.startsWith("-"))
|
|
@@ -23296,49 +23930,21 @@ function parseCliArgs(argv) {
|
|
|
23296
23930
|
rawPort = arg.slice("--port=".length);
|
|
23297
23931
|
continue;
|
|
23298
23932
|
}
|
|
23299
|
-
if (arg === "--
|
|
23300
|
-
|
|
23301
|
-
rawHost = argv[++index];
|
|
23302
|
-
consumed.add(index);
|
|
23303
|
-
} else {
|
|
23304
|
-
rawHost = ALL_INTERFACES_BIND_HOST;
|
|
23305
|
-
}
|
|
23306
|
-
continue;
|
|
23307
|
-
}
|
|
23308
|
-
if (arg.startsWith("--host=")) {
|
|
23309
|
-
rawHost = arg.slice("--host=".length);
|
|
23310
|
-
continue;
|
|
23311
|
-
}
|
|
23312
|
-
if (arg === "--allowed-host") {
|
|
23313
|
-
const value = argv[++index];
|
|
23314
|
-
if (value === void 0 || value.startsWith("-"))
|
|
23315
|
-
throw new Error("--allowed-host requires a value");
|
|
23316
|
-
consumed.add(index);
|
|
23317
|
-
rawAllowedHosts.push(value);
|
|
23318
|
-
continue;
|
|
23319
|
-
}
|
|
23320
|
-
if (arg.startsWith("--allowed-host=")) {
|
|
23321
|
-
rawAllowedHosts.push(arg.slice("--allowed-host=".length));
|
|
23322
|
-
continue;
|
|
23323
|
-
}
|
|
23324
|
-
if (arg === "--public-origin") {
|
|
23325
|
-
const value = argv[++index];
|
|
23933
|
+
if (arg === "--sign-out") {
|
|
23934
|
+
const value = argv[index + 1];
|
|
23326
23935
|
if (value === void 0 || value.startsWith("-"))
|
|
23327
|
-
throw new Error("--
|
|
23936
|
+
throw new Error("--sign-out requires a device id, or `all` to sign out every device");
|
|
23937
|
+
index++;
|
|
23328
23938
|
consumed.add(index);
|
|
23329
|
-
|
|
23939
|
+
rawSignOut = value;
|
|
23330
23940
|
continue;
|
|
23331
23941
|
}
|
|
23332
|
-
if (arg.startsWith("--
|
|
23333
|
-
|
|
23942
|
+
if (arg.startsWith("--sign-out=")) {
|
|
23943
|
+
rawSignOut = arg.slice("--sign-out=".length);
|
|
23944
|
+
if (!rawSignOut) throw new Error("--sign-out requires a device id, or `all` to sign out every device");
|
|
23334
23945
|
continue;
|
|
23335
23946
|
}
|
|
23336
23947
|
if (arg.startsWith("-")) continue;
|
|
23337
|
-
if (arg === "up") {
|
|
23338
|
-
upCommand = true;
|
|
23339
|
-
consumed.add(index);
|
|
23340
|
-
continue;
|
|
23341
|
-
}
|
|
23342
23948
|
throw new Error(
|
|
23343
23949
|
`unexpected argument: ${arg} \u2014 Frizz takes no repository path, because one server serves every project on this machine. cd into the directory you want and run it with no arguments.`
|
|
23344
23950
|
);
|
|
@@ -23349,9 +23955,6 @@ function parseCliArgs(argv) {
|
|
|
23349
23955
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
23350
23956
|
throw new Error(`invalid --port value: ${rawPort}`);
|
|
23351
23957
|
}
|
|
23352
|
-
if ((upCommand || args2.has("--cloud")) && rawPublicOrigin !== void 0)
|
|
23353
|
-
throw new Error("choose either up (the saved public hostname) or --public-origin, not both");
|
|
23354
|
-
const host = rawHost === void 0 ? void 0 : normalizeBindHost(rawHost);
|
|
23355
23958
|
const known = /* @__PURE__ */ new Set([
|
|
23356
23959
|
"--app",
|
|
23357
23960
|
"--no-app",
|
|
@@ -23360,15 +23963,14 @@ function parseCliArgs(argv) {
|
|
|
23360
23963
|
"--stop",
|
|
23361
23964
|
"--status",
|
|
23362
23965
|
"--link",
|
|
23363
|
-
"--
|
|
23966
|
+
"--sandbox",
|
|
23967
|
+
"--sessions",
|
|
23968
|
+
"--sign-out",
|
|
23364
23969
|
"--help",
|
|
23365
23970
|
"-h",
|
|
23366
23971
|
"--dev",
|
|
23367
23972
|
"--prod",
|
|
23368
23973
|
"--port",
|
|
23369
|
-
"--host",
|
|
23370
|
-
"--allowed-host",
|
|
23371
|
-
"--public-origin",
|
|
23372
23974
|
"--debug"
|
|
23373
23975
|
]);
|
|
23374
23976
|
for (let index = 0; index < argv.length; index++) {
|
|
@@ -23378,13 +23980,23 @@ function parseCliArgs(argv) {
|
|
|
23378
23980
|
index++;
|
|
23379
23981
|
continue;
|
|
23380
23982
|
}
|
|
23381
|
-
if (arg
|
|
23983
|
+
if (arg === "--sign-out") {
|
|
23984
|
+
index++;
|
|
23985
|
+
continue;
|
|
23986
|
+
}
|
|
23987
|
+
if (arg.startsWith("--port=") || arg.startsWith("--sign-out=")) continue;
|
|
23382
23988
|
if (!known.has(arg)) throw new Error(`unknown option: ${arg}`);
|
|
23383
23989
|
}
|
|
23384
23990
|
if (args2.has("--detach"))
|
|
23385
23991
|
throw new Error("--detach is no longer available; frizz-dev always runs in the foreground");
|
|
23386
23992
|
if (args2.has("--app") && args2.has("--no-app"))
|
|
23387
23993
|
throw new Error("choose either --app or --no-app");
|
|
23994
|
+
if (args2.has("--sandbox")) {
|
|
23995
|
+
for (const query of ["--stop", "--status", "--link", "--sessions", "--sign-out"]) {
|
|
23996
|
+
if (args2.has(query) || argv.some((arg) => arg.startsWith(`${query}=`)))
|
|
23997
|
+
throw new Error(`${query} asks the running board, and --sandbox starts a fresh disposable one \u2014 run ${query} without --sandbox`);
|
|
23998
|
+
}
|
|
23999
|
+
}
|
|
23388
24000
|
return {
|
|
23389
24001
|
noApp: args2.has("--no-app"),
|
|
23390
24002
|
appMode: args2.has("--app"),
|
|
@@ -23392,65 +24004,71 @@ function parseCliArgs(argv) {
|
|
|
23392
24004
|
foreground: true,
|
|
23393
24005
|
stop: args2.has("--stop"),
|
|
23394
24006
|
link: args2.has("--link"),
|
|
23395
|
-
|
|
24007
|
+
sandbox: args2.has("--sandbox"),
|
|
24008
|
+
sessions: args2.has("--sessions"),
|
|
24009
|
+
...rawSignOut !== void 0 ? { signOut: rawSignOut } : {},
|
|
23396
24010
|
status: args2.has("--status"),
|
|
23397
24011
|
help: args2.has("--help") || args2.has("-h"),
|
|
23398
24012
|
dev: args2.has("--dev"),
|
|
23399
24013
|
debug: args2.has("--debug"),
|
|
23400
|
-
port
|
|
23401
|
-
host,
|
|
23402
|
-
allowedHosts: normalizeAllowedHosts(rawAllowedHosts),
|
|
23403
|
-
...rawPublicOrigin === void 0 ? {} : { publicOrigin: normalizePublicOrigin(rawPublicOrigin) }
|
|
23404
|
-
};
|
|
23405
|
-
}
|
|
23406
|
-
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.";
|
|
23407
|
-
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.";
|
|
23408
|
-
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.";
|
|
23409
|
-
function resolveBindSelection(options2, env = process.env, machineName = osHostname()) {
|
|
23410
|
-
const fromEnv = env.FRIZZ_HOST?.trim();
|
|
23411
|
-
const host = options2.host ?? (fromEnv ? normalizeBindHost(fromEnv) : LOOPBACK_BIND_HOST);
|
|
23412
|
-
const exposed = bindHostIsExposed(host);
|
|
23413
|
-
const ownNames = exposed ? machineHostNames(machineName) : [];
|
|
23414
|
-
const allowedHosts = normalizeAllowedHosts([
|
|
23415
|
-
...options2.allowedHosts,
|
|
23416
|
-
...env.FRIZZ_ALLOWED_HOSTS ? [env.FRIZZ_ALLOWED_HOSTS] : [],
|
|
23417
|
-
...ownNames
|
|
23418
|
-
]);
|
|
23419
|
-
const publicOriginRaw = options2.publicOrigin ?? env.FRIZZ_PUBLIC_ORIGIN?.trim();
|
|
23420
|
-
const publicOrigin = publicOriginRaw ? normalizePublicOrigin(publicOriginRaw) : void 0;
|
|
23421
|
-
return {
|
|
23422
|
-
host,
|
|
23423
|
-
exposed,
|
|
23424
|
-
allowedHosts,
|
|
23425
|
-
...ownNames.length > 0 ? { hostname: ownNames[0] } : {},
|
|
23426
|
-
...publicOrigin ? { publicOrigin } : {}
|
|
24014
|
+
port
|
|
23427
24015
|
};
|
|
23428
24016
|
}
|
|
23429
24017
|
function boardAddress(url) {
|
|
23430
24018
|
return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
|
|
23431
24019
|
}
|
|
23432
|
-
function
|
|
23433
|
-
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
24020
|
+
function prepareSandbox(env = process.env, realHome = homedir10()) {
|
|
24021
|
+
const home = mkdtempSync(join13(tmpdir2(), "frizz-sandbox-"));
|
|
24022
|
+
shareCredentials(realHome, home);
|
|
24023
|
+
env.HOME = home;
|
|
24024
|
+
env.USERPROFILE = home;
|
|
24025
|
+
const project = join13(home, "sandbox");
|
|
24026
|
+
mkdirSync10(project, { recursive: true });
|
|
24027
|
+
writeFileSync10(join13(project, "README.md"), "# frizz sandbox\n\nA throwaway project; everything here is deleted when the sandbox exits.\n");
|
|
24028
|
+
try {
|
|
24029
|
+
execFileSync3("git", ["init", "-q"], { cwd: project, stdio: "ignore" });
|
|
24030
|
+
} catch {
|
|
24031
|
+
throw new Error("--sandbox mints a throwaway git repository and git is not available \u2014 install git, or play in a repo of your own with a temp HOME");
|
|
24032
|
+
}
|
|
24033
|
+
process.chdir(project);
|
|
24034
|
+
return { home, project };
|
|
24035
|
+
}
|
|
24036
|
+
function shareCredentials(realHome, home) {
|
|
24037
|
+
const link = (from, to) => {
|
|
24038
|
+
try {
|
|
24039
|
+
symlinkSync2(from, to);
|
|
24040
|
+
} catch {
|
|
24041
|
+
}
|
|
23439
24042
|
};
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
24043
|
+
mkdirSync10(join13(realHome, ".frizz"), { recursive: true });
|
|
24044
|
+
mkdirSync10(join13(home, ".frizz"), { recursive: true });
|
|
24045
|
+
link(join13(realHome, ".frizz", "identity.key"), join13(home, ".frizz", "identity.key"));
|
|
24046
|
+
if (existsSync5(join13(realHome, ".config", "gh"))) {
|
|
24047
|
+
mkdirSync10(join13(home, ".config"), { recursive: true });
|
|
24048
|
+
link(join13(realHome, ".config", "gh"), join13(home, ".config", "gh"));
|
|
23443
24049
|
}
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
24050
|
+
const cloudflared = join13(realHome, ".cloudflared");
|
|
24051
|
+
if (existsSync5(cloudflared)) {
|
|
24052
|
+
mkdirSync10(join13(home, ".cloudflared"), { recursive: true });
|
|
24053
|
+
for (const entry of readdirSync3(cloudflared)) {
|
|
24054
|
+
if (entry === "cert.pem" || entry.endsWith(".json")) link(join13(cloudflared, entry), join13(home, ".cloudflared", entry));
|
|
23449
24055
|
}
|
|
23450
24056
|
}
|
|
23451
|
-
|
|
24057
|
+
for (const entry of [".claude", ".claude.json", ".codex"]) {
|
|
24058
|
+
if (existsSync5(join13(realHome, entry))) link(join13(realHome, entry), join13(home, entry));
|
|
24059
|
+
}
|
|
24060
|
+
if (process.platform === "darwin" && existsSync5(join13(realHome, "Library", "Keychains"))) {
|
|
24061
|
+
mkdirSync10(join13(home, "Library"), { recursive: true });
|
|
24062
|
+
link(join13(realHome, "Library", "Keychains"), join13(home, "Library", "Keychains"));
|
|
24063
|
+
}
|
|
24064
|
+
}
|
|
24065
|
+
function cleanupSandbox(home) {
|
|
24066
|
+
try {
|
|
24067
|
+
rmSync8(home, { recursive: true, force: true });
|
|
24068
|
+
} catch {
|
|
24069
|
+
}
|
|
23452
24070
|
}
|
|
23453
|
-
function resolveLaunchIntent(cwd = process.cwd(), home =
|
|
24071
|
+
function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
|
|
23454
24072
|
const candidate = realpathSync5(discoverProjectRoot(cwd, home));
|
|
23455
24073
|
const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
|
|
23456
24074
|
if ((known || hasProjectMarker(candidate)) && !isHomeDirectory(candidate, home))
|
|
@@ -23470,7 +24088,7 @@ function mostRecentProject(home, env) {
|
|
|
23470
24088
|
}
|
|
23471
24089
|
return void 0;
|
|
23472
24090
|
}
|
|
23473
|
-
function resolveWorkspace(cwd = process.cwd(), home =
|
|
24091
|
+
function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process.env) {
|
|
23474
24092
|
let gitRoot;
|
|
23475
24093
|
try {
|
|
23476
24094
|
gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -23704,7 +24322,7 @@ init_project_launch();
|
|
|
23704
24322
|
init_project_launch();
|
|
23705
24323
|
import { fork } from "node:child_process";
|
|
23706
24324
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
23707
|
-
import { existsSync as
|
|
24325
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync11, readFileSync as readFileSync12 } from "node:fs";
|
|
23708
24326
|
import { createServer as createNetServer } from "node:net";
|
|
23709
24327
|
import { basename as basename6, dirname as dirname10, extname as extname2, isAbsolute as isAbsolute3, relative, resolve as resolve4, sep as sep2 } from "node:path";
|
|
23710
24328
|
import { fileURLToPath } from "node:url";
|
|
@@ -23745,12 +24363,15 @@ function devCrashRetryDelay(attempt) {
|
|
|
23745
24363
|
return Math.min(DEV_CRASH_RETRY_BASE_MS * 2 ** exponent, DEV_CRASH_RETRY_MAX_MS);
|
|
23746
24364
|
}
|
|
23747
24365
|
var SUPERVISOR_ESCALATE_GRACE_MS = 500;
|
|
24366
|
+
var SUPERVISOR_DRAIN_DEADLINE_MS = CHILD_STOP_TIMEOUT_MS + 5e3;
|
|
23748
24367
|
function stripPrefix(line) {
|
|
23749
24368
|
return line.startsWith("[frizz] ") ? line.slice("[frizz] ".length) : line;
|
|
23750
24369
|
}
|
|
23751
24370
|
function createSupervisorShutdownHandler(options2) {
|
|
23752
24371
|
const now = options2.now ?? (() => Date.now());
|
|
23753
24372
|
const escalateAfterMs = options2.escalateAfterMs ?? SUPERVISOR_ESCALATE_GRACE_MS;
|
|
24373
|
+
const drainDeadlineMs = options2.drainDeadlineMs ?? SUPERVISOR_DRAIN_DEADLINE_MS;
|
|
24374
|
+
const scheduleForce = options2.scheduleForce ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
23754
24375
|
let startedAt = 0;
|
|
23755
24376
|
let stopping = false;
|
|
23756
24377
|
let decided = false;
|
|
@@ -23764,20 +24385,27 @@ function createSupervisorShutdownHandler(options2) {
|
|
|
23764
24385
|
}
|
|
23765
24386
|
options2.exit(code);
|
|
23766
24387
|
};
|
|
24388
|
+
const abandon = (reason) => {
|
|
24389
|
+
options2.error?.(`[frizz] ${reason} \u2014 abandoning the graceful drain`);
|
|
24390
|
+
try {
|
|
24391
|
+
options2.force?.();
|
|
24392
|
+
} catch (error) {
|
|
24393
|
+
options2.error?.(`[frizz] forced supervisor stop failed: ${error instanceof Error ? error.message : error}`);
|
|
24394
|
+
}
|
|
24395
|
+
decide(1);
|
|
24396
|
+
};
|
|
23767
24397
|
return () => {
|
|
23768
24398
|
if (stopping) {
|
|
23769
24399
|
if (decided || now() - startedAt < escalateAfterMs) return;
|
|
23770
|
-
|
|
23771
|
-
try {
|
|
23772
|
-
options2.force?.();
|
|
23773
|
-
} catch (error) {
|
|
23774
|
-
options2.error?.(`[frizz] forced supervisor stop failed: ${error instanceof Error ? error.message : error}`);
|
|
23775
|
-
}
|
|
23776
|
-
decide(1);
|
|
24400
|
+
abandon("second stop signal");
|
|
23777
24401
|
return;
|
|
23778
24402
|
}
|
|
23779
24403
|
stopping = true;
|
|
23780
24404
|
startedAt = now();
|
|
24405
|
+
options2.onStop?.();
|
|
24406
|
+
scheduleForce(() => {
|
|
24407
|
+
if (!decided) abandon(`graceful stop did not finish in ${drainDeadlineMs}ms`);
|
|
24408
|
+
}, drainDeadlineMs).unref?.();
|
|
23781
24409
|
void options2.close().then(
|
|
23782
24410
|
() => decide(0),
|
|
23783
24411
|
(error) => {
|
|
@@ -23987,6 +24615,7 @@ var Supervisor = class {
|
|
|
23987
24615
|
publicOrigin: opts.publicOrigin,
|
|
23988
24616
|
onCodeConsumed: opts.onCodeConsumed,
|
|
23989
24617
|
sessionKey: opts.sessionKey,
|
|
24618
|
+
sessionDirectory: opts.sessionDirectory,
|
|
23990
24619
|
childPort: () => this.childPort,
|
|
23991
24620
|
restart: () => this.restartFromBrowser(),
|
|
23992
24621
|
updateRestart: this.updateRestart ? () => this.updateFromBrowser() : void 0,
|
|
@@ -24012,6 +24641,9 @@ var Supervisor = class {
|
|
|
24012
24641
|
return this.boot;
|
|
24013
24642
|
}
|
|
24014
24643
|
/** Mint a single-use access link for the public origin, or null when none is declared. */
|
|
24644
|
+
setPublicOrigin(origin) {
|
|
24645
|
+
this.publicProxy.setPublicOrigin(origin);
|
|
24646
|
+
}
|
|
24015
24647
|
issueAccessLink() {
|
|
24016
24648
|
const code = this.publicProxy.issueAccessCode();
|
|
24017
24649
|
if (!code) return null;
|
|
@@ -24027,7 +24659,7 @@ var Supervisor = class {
|
|
|
24027
24659
|
await this.publicProxy.listen();
|
|
24028
24660
|
if (this.watchEnabled) {
|
|
24029
24661
|
const settled = await Promise.allSettled(
|
|
24030
|
-
this.roots.filter(
|
|
24662
|
+
this.roots.filter(existsSync6).map((root) => this.watchSubscribe(root, (err, events) => this.onWatch(err, events), {
|
|
24031
24663
|
ignore: DEV_WATCH_IGNORE
|
|
24032
24664
|
}))
|
|
24033
24665
|
);
|
|
@@ -24082,7 +24714,7 @@ var Supervisor = class {
|
|
|
24082
24714
|
if (this.closed) return;
|
|
24083
24715
|
this.reloadLauncher ||= reloadLauncher;
|
|
24084
24716
|
if (this.debounce) clearTimeout(this.debounce);
|
|
24085
|
-
const
|
|
24717
|
+
const run3 = () => {
|
|
24086
24718
|
this.debounce = null;
|
|
24087
24719
|
const scope = this.reloadLauncher ? "control plane + launcher" : "control plane";
|
|
24088
24720
|
this.logLine(
|
|
@@ -24091,8 +24723,8 @@ var Supervisor = class {
|
|
|
24091
24723
|
this.writeStatus("restarting", reason);
|
|
24092
24724
|
void this.restart();
|
|
24093
24725
|
};
|
|
24094
|
-
if (immediate)
|
|
24095
|
-
else this.debounce = setTimeout(
|
|
24726
|
+
if (immediate) run3();
|
|
24727
|
+
else this.debounce = setTimeout(run3, delayMs);
|
|
24096
24728
|
}
|
|
24097
24729
|
async restart() {
|
|
24098
24730
|
if (this.restartRunning) {
|
|
@@ -24479,7 +25111,7 @@ async function startDevSupervisor(opts) {
|
|
|
24479
25111
|
}
|
|
24480
25112
|
|
|
24481
25113
|
// src/production-update.ts
|
|
24482
|
-
import { execFile as
|
|
25114
|
+
import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
|
|
24483
25115
|
var PRODUCTION_REEXEC_FLAG = "--_frizz-production-reexec";
|
|
24484
25116
|
function compareReleaseVersions(a, b) {
|
|
24485
25117
|
const parse2 = (value) => {
|
|
@@ -24528,7 +25160,7 @@ function handoffToRegistrySuccessor(plan, request, adapter) {
|
|
|
24528
25160
|
var npmRegistryReleaseAdapter = {
|
|
24529
25161
|
latestVersion(packageName) {
|
|
24530
25162
|
return new Promise((resolveVersion, reject) => {
|
|
24531
|
-
|
|
25163
|
+
execFile4("npm", ["view", `${packageName}@latest`, "version", "--json"], { encoding: "utf8" }, (error, stdout) => {
|
|
24532
25164
|
if (error) return reject(new Error(`could not check npm for ${packageName}: ${error.message}`));
|
|
24533
25165
|
try {
|
|
24534
25166
|
const parsed = JSON.parse(stdout);
|
|
@@ -24649,11 +25281,7 @@ if (options.help) {
|
|
|
24649
25281
|
console.log(
|
|
24650
25282
|
`Frizz production launcher
|
|
24651
25283
|
|
|
24652
|
-
Usage: npx ${PACKAGE_NAME} [
|
|
24653
|
-
|
|
24654
|
-
Commands:
|
|
24655
|
-
up serve at the saved public hostname and run its tunnel; asked once,
|
|
24656
|
-
then remembered
|
|
25284
|
+
Usage: npx ${PACKAGE_NAME} [options]
|
|
24657
25285
|
|
|
24658
25286
|
Run it in the directory you want to work in. One server serves EVERY project on this machine,
|
|
24659
25287
|
each at its own /project/<name> URL, so a second run joins the one already going. Runs the
|
|
@@ -24663,45 +25291,30 @@ for a source checkout.
|
|
|
24663
25291
|
Options:
|
|
24664
25292
|
--no-app print the URL without opening a browser
|
|
24665
25293
|
--port <port> request a fixed port for a new workspace server
|
|
24666
|
-
--
|
|
24667
|
-
|
|
24668
|
-
|
|
24669
|
-
--cloud alias of up, for saved commands
|
|
25294
|
+
--sandbox a disposable Frizz to try things in: throwaway home and project, its
|
|
25295
|
+
own port, deleted when this terminal closes; credentials (gh,
|
|
25296
|
+
cloudflared, Claude, Codex, the machine's frizz.sh key) are shared
|
|
24670
25297
|
--link print a fresh single-use access link for the running board
|
|
25298
|
+
--sessions list the devices signed in to the running board
|
|
25299
|
+
--sign-out <id|all> sign one device out, or every one of them
|
|
24671
25300
|
--debug stream the full event feed to the terminal instead of the compact readout
|
|
24672
25301
|
-h, --help show this help
|
|
24673
25302
|
|
|
24674
|
-
Environment:
|
|
24675
|
-
FRIZZ_HOST same as --host
|
|
24676
|
-
FRIZZ_ALLOWED_HOSTS same as --allowed-host, comma separated
|
|
24677
|
-
FRIZZ_PUBLIC_ORIGIN same as --public-origin
|
|
24678
|
-
|
|
24679
|
-
--host puts a board that can run shell commands as you on the network, and Frizz has no login: anyone
|
|
24680
|
-
who reaches the port controls it. Only do this on a network you trust. An IP address and this
|
|
24681
|
-
machine's own hostname work as-is; any other DNS name must be listed with --allowed-host ("*"
|
|
24682
|
-
allows any).
|
|
24683
25303
|
|
|
24684
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
24687
|
-
|
|
25304
|
+
To reach the board from a phone or another machine, press R in the terminal running it: a short
|
|
25305
|
+
walkthrough sets up a frizz.sh name, a Cloudflare Tunnel, Tailscale, or a proxy of your own, and
|
|
25306
|
+
remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
|
|
25307
|
+
shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.`
|
|
24688
25308
|
);
|
|
24689
25309
|
process.exit(0);
|
|
24690
25310
|
}
|
|
24691
25311
|
if (options.dev || rawArgs.includes("--prod")) fail("--dev and --prod are not available from the registry launcher");
|
|
24692
|
-
var
|
|
24693
|
-
if (
|
|
24694
|
-
var
|
|
25312
|
+
var sandbox = options.sandbox ? prepareSandbox() : null;
|
|
25313
|
+
if (sandbox) process.on("exit", () => cleanupSandbox(sandbox.home));
|
|
25314
|
+
var remote = null;
|
|
25315
|
+
var paneHost = null;
|
|
24695
25316
|
var accessPane = null;
|
|
24696
25317
|
var activeAccessLink = null;
|
|
24697
|
-
var justClaimed = false;
|
|
24698
|
-
var bind = (() => {
|
|
24699
|
-
try {
|
|
24700
|
-
return resolveBindSelection(options, process.env);
|
|
24701
|
-
} catch (error) {
|
|
24702
|
-
return fail(error);
|
|
24703
|
-
}
|
|
24704
|
-
})();
|
|
24705
25318
|
var launchIntent;
|
|
24706
25319
|
var workspace = (() => {
|
|
24707
25320
|
try {
|
|
@@ -24726,6 +25339,15 @@ var workspace = (() => {
|
|
|
24726
25339
|
}
|
|
24727
25340
|
})();
|
|
24728
25341
|
process.chdir(workspace.root);
|
|
25342
|
+
if (options.sessions || options.signOut !== void 0) {
|
|
25343
|
+
const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
|
|
25344
|
+
if (!running?.port) {
|
|
25345
|
+
console.error("frizz: no board is running for this workspace");
|
|
25346
|
+
process.exit(1);
|
|
25347
|
+
}
|
|
25348
|
+
if (options.signOut !== void 0) await signOutSession(running.port, options.signOut);
|
|
25349
|
+
await listSessions(running.port);
|
|
25350
|
+
}
|
|
24729
25351
|
if (options.link) {
|
|
24730
25352
|
const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
|
|
24731
25353
|
if (!running?.port) {
|
|
@@ -24770,7 +25392,7 @@ function ownSlug() {
|
|
|
24770
25392
|
try {
|
|
24771
25393
|
return registerProject(
|
|
24772
25394
|
{ dir: workspace.root, id: target.projectId, remoteOwner: resolveProjectLabel(workspace.root)?.split("/")[0] },
|
|
24773
|
-
|
|
25395
|
+
homedir11()
|
|
24774
25396
|
).entry?.slug;
|
|
24775
25397
|
} catch {
|
|
24776
25398
|
return void 0;
|
|
@@ -24821,27 +25443,20 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
24821
25443
|
} else {
|
|
24822
25444
|
readout?.settle("browser", "done", url);
|
|
24823
25445
|
}
|
|
24824
|
-
const
|
|
24825
|
-
const
|
|
24826
|
-
|
|
24827
|
-
const warnings = [
|
|
24828
|
-
...network.length > 0 ? [EXPOSED_WARNING] : [],
|
|
24829
|
-
...publicOrigin ? [PUBLIC_ORIGIN_WARNING] : [],
|
|
24830
|
-
...networkFlagsIgnored ? [REUSED_NETWORK_FLAGS_WARNING] : []
|
|
24831
|
-
];
|
|
25446
|
+
const publicOrigin = reused ? void 0 : remote?.origin();
|
|
25447
|
+
const warnings = [];
|
|
25448
|
+
if (sandbox) warnings.push(`Sandbox: everything here is throwaway (${sandbox.home}) and is deleted when this terminal closes.`);
|
|
24832
25449
|
if (!readout) {
|
|
24833
25450
|
console.log(`${reused ? "reusing" : "started"} Frizz ${PACKAGE_VERSION} for ${workspace.root}`);
|
|
24834
25451
|
console.log(url);
|
|
24835
|
-
for (const address of network) console.log(address);
|
|
24836
25452
|
if (publicOrigin) console.log(activeAccessLink?.url ?? `${publicOrigin}/`);
|
|
24837
25453
|
for (const warning of warnings) console.log(warning);
|
|
24838
25454
|
return;
|
|
24839
25455
|
}
|
|
24840
|
-
const home =
|
|
25456
|
+
const home = homedir11();
|
|
24841
25457
|
readout.ready(
|
|
24842
25458
|
[
|
|
24843
25459
|
{ label: "Local", value: boardAddress(url), accent: true },
|
|
24844
|
-
...network.map((address) => ({ label: "Network", value: `${address}/`, accent: true })),
|
|
24845
25460
|
...publicOrigin ? [{ label: "Public", value: activeAccessLink?.url ?? `${publicOrigin}/`, accent: true }] : [],
|
|
24846
25461
|
{ label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
|
|
24847
25462
|
...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
|
|
@@ -24849,10 +25464,11 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
24849
25464
|
reused ? (
|
|
24850
25465
|
// This launch owns nothing and exits immediately, so ctrl-c would not stop what it reopened.
|
|
24851
25466
|
"reopened the server already running for this project \xB7 stop it from the terminal that started it"
|
|
24852
|
-
) : options.debug ? void 0 : "press ctrl-c to stop
|
|
25467
|
+
) : options.debug ? void 0 : publicOrigin ? "press L for a fresh sign-in link \xB7 R to change how this board is reached \xB7 ctrl-c to stop" : "press R to reach this board from a phone or another machine \xB7 ctrl-c to stop",
|
|
24853
25468
|
{
|
|
24854
25469
|
...reused ? { status: `already running on port ${port}` } : {},
|
|
24855
|
-
...warnings.length > 0 ? { warning: warnings.join(" ") } : {}
|
|
25470
|
+
...warnings.length > 0 ? { warning: warnings.join(" ") } : {},
|
|
25471
|
+
...activeAccessLink && !reused ? { qr: renderQrLines(activeAccessLink.url) } : {}
|
|
24856
25472
|
}
|
|
24857
25473
|
);
|
|
24858
25474
|
}
|
|
@@ -24889,21 +25505,17 @@ async function runSupervisor(port, token) {
|
|
|
24889
25505
|
void refreshUpdateAvailable();
|
|
24890
25506
|
const updateAvailablePoll = setInterval(() => void refreshUpdateAvailable(), 30 * 60 * 1e3);
|
|
24891
25507
|
updateAvailablePoll.unref();
|
|
24892
|
-
if (options.cloud && !cloudConfig) {
|
|
24893
|
-
cloudConfig = await establishCloudConfig(await promptForCloudName(), port);
|
|
24894
|
-
writeCloudConfig(cloudConfig);
|
|
24895
|
-
bind.publicOrigin = `https://${cloudConfig.hostname}`;
|
|
24896
|
-
justClaimed = true;
|
|
24897
|
-
}
|
|
24898
25508
|
const supervisor = await startDevSupervisor({
|
|
24899
25509
|
port,
|
|
24900
|
-
host:
|
|
24901
|
-
allowedHosts:
|
|
24902
|
-
...bind.publicOrigin ? { publicOrigin: bind.publicOrigin } : {},
|
|
25510
|
+
host: LOOPBACK_BIND_HOST,
|
|
25511
|
+
allowedHosts: [],
|
|
24903
25512
|
// A spent code repaints the open QR pane as stale, so nobody photographs a dead link.
|
|
24904
25513
|
onCodeConsumed: () => accessPane?.markConsumed(),
|
|
24905
25514
|
// Persisted beside the project's other state, so a restart does not sign every device out.
|
|
24906
|
-
|
|
25515
|
+
// Always, not only when public: the origin can be switched on later (press R), and a phone signed
|
|
25516
|
+
// in through one name stays signed in through the next.
|
|
25517
|
+
sessionKey: loadOrCreateSessionKey(workspace.stateDir),
|
|
25518
|
+
sessionDirectory: fileSessionDirectory(workspace.stateDir),
|
|
24907
25519
|
cwd: workspace.root,
|
|
24908
25520
|
stateDir: workspace.stateDir,
|
|
24909
25521
|
launchTarget: target,
|
|
@@ -24945,49 +25557,42 @@ async function runSupervisor(port, token) {
|
|
|
24945
25557
|
process.exit(0);
|
|
24946
25558
|
}
|
|
24947
25559
|
});
|
|
24948
|
-
|
|
24949
|
-
|
|
24950
|
-
|
|
24951
|
-
|
|
24952
|
-
|
|
24953
|
-
|
|
24954
|
-
|
|
24955
|
-
});
|
|
24956
|
-
}
|
|
24957
|
-
tunnel = await startRelay(cloudConfig, port, homedir10(), (message) => logger.info("relay", message));
|
|
24958
|
-
logger.info("relay", `serving ${cloudConfig.hostname} through the Frizz relay`);
|
|
24959
|
-
} else {
|
|
24960
|
-
const runToken = justClaimed ? readTunnelToken() : await resolveRunToken(cloudConfig, port, homedir10(), (message) => {
|
|
24961
|
-
logger.warn("tunnel", message);
|
|
24962
|
-
console.error(`frizz: ${message}`);
|
|
24963
|
-
});
|
|
24964
|
-
tunnel = startTunnel(
|
|
24965
|
-
cloudConfig,
|
|
24966
|
-
(code) => {
|
|
24967
|
-
if (code === 0 || code === null) return;
|
|
24968
|
-
logger.error("tunnel", `cloudflared exited with code ${code}; the public hostname is now unreachable`);
|
|
24969
|
-
},
|
|
24970
|
-
(message) => {
|
|
24971
|
-
logger.error("tunnel", message);
|
|
24972
|
-
console.error(`frizz: ${message}`);
|
|
24973
|
-
},
|
|
24974
|
-
homedir10(),
|
|
24975
|
-
runToken ?? void 0
|
|
24976
|
-
);
|
|
24977
|
-
logger.info("tunnel", `running cloudflared for ${cloudConfig.hostname}`);
|
|
24978
|
-
}
|
|
25560
|
+
remote = createRemoteController({ host: supervisor, port, log: logger, say: (message) => console.error(`frizz: ${message}`) });
|
|
25561
|
+
try {
|
|
25562
|
+
await remote.serveSaved();
|
|
25563
|
+
} catch (error) {
|
|
25564
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25565
|
+
logger.error("remote", message);
|
|
25566
|
+
console.error(`frizz: the saved remote setup could not start: ${message}`);
|
|
24979
25567
|
}
|
|
24980
|
-
|
|
25568
|
+
activeAccessLink = remote.origin() ? supervisor.issueAccessLink() : null;
|
|
25569
|
+
accessPane = createAccessPane({ issue: () => supervisor.issueAccessLink() });
|
|
25570
|
+
const remotePane = createRemotePane({
|
|
25571
|
+
port,
|
|
25572
|
+
current: () => remote?.current() ?? null,
|
|
25573
|
+
apply: (next, applyOptions) => remote.apply(next, applyOptions),
|
|
25574
|
+
claim: (name) => establishCloudConfig(name, port),
|
|
25575
|
+
issueLink: () => supervisor.issueAccessLink(),
|
|
25576
|
+
probes: { github: probeGithub, cloudflared: probeCloudflared, tailscale: probeTailscale },
|
|
25577
|
+
onChanged: (config) => logger.info("remote", config ? `reached at https://${config.hostname}` : "loopback only"),
|
|
25578
|
+
sandbox: sandbox !== null
|
|
25579
|
+
});
|
|
25580
|
+
paneHost = installPaneHost({ bindings: { l: accessPane, L: accessPane, r: remotePane, R: remotePane } });
|
|
24981
25581
|
const stop = createSupervisorShutdownHandler({
|
|
24982
25582
|
close: () => supervisor.close(),
|
|
24983
25583
|
force: () => supervisor.forceStop(),
|
|
24984
25584
|
release: () => owner.release(),
|
|
25585
|
+
// Acknowledge the first signal on the spot; the drain that follows is bounded but not instant.
|
|
25586
|
+
onStop: () => {
|
|
25587
|
+
logger.info("launcher", "stop signal received; draining the control plane");
|
|
25588
|
+
readout?.notice("progress", "Stopping", "draining the control plane \u2014 press ctrl-c again to force");
|
|
25589
|
+
},
|
|
24985
25590
|
exit: (code) => {
|
|
24986
25591
|
logger.info("launcher", `stopped with code ${code}`);
|
|
24987
|
-
|
|
24988
|
-
|
|
25592
|
+
paneHost?.dispose();
|
|
25593
|
+
remote?.stop();
|
|
24989
25594
|
const suffix = logger.file ? `
|
|
24990
|
-
log: ${tildePath(logger.file,
|
|
25595
|
+
log: ${tildePath(logger.file, homedir11())}` : "";
|
|
24991
25596
|
process.stdout.write(
|
|
24992
25597
|
code === 0 ? `
|
|
24993
25598
|
Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
|
|
@@ -25031,7 +25636,7 @@ try {
|
|
|
25031
25636
|
let release = await acquireGlobalLaunchLock();
|
|
25032
25637
|
let portReservation;
|
|
25033
25638
|
try {
|
|
25034
|
-
const allocation = await allocatePort(options.port, readPreferredPort(workspace.stateDir), { host:
|
|
25639
|
+
const allocation = await allocatePort(options.port, readPreferredPort(workspace.stateDir), { host: LOOPBACK_BIND_HOST });
|
|
25035
25640
|
const port = allocation.port;
|
|
25036
25641
|
portReservation = allocation.release;
|
|
25037
25642
|
const running = runSupervisor(port, claim.lease.token);
|