frizz 0.13.8 → 0.13.9
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 +3 -3
- package/dist/frizz.js +504 -380
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ Options:
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
To reach the board from a phone or another machine, press R in the terminal running it: a short
|
|
154
|
-
walkthrough sets up a private frizz.sh name (no account needed), a
|
|
154
|
+
walkthrough sets up a private frizz.sh name (no account needed), a Cloudflare
|
|
155
155
|
Tunnel, Tailscale, or a proxy of your own, and
|
|
156
156
|
remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
|
|
157
157
|
shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.
|
|
@@ -217,9 +217,9 @@ shows a single-use sign-in link as a QR; press L for a fresh one, or run --link
|
|
|
217
217
|
<details>
|
|
218
218
|
<summary><b>Can I reach it from anywhere, not just my LAN?</b></summary>
|
|
219
219
|
|
|
220
|
-
> Same answer: press **R** and pick a private frizz.sh name (unguessable, no account), a
|
|
220
|
+
> Same answer: press **R** and pick a private frizz.sh name (unguessable, no account), a Cloudflare Tunnel, or Tailscale. (Custom frizz.sh names are paused for now; a board that already holds one keeps it.) Each is reachable from anywhere the transport is — a frizz.sh name and a Cloudflare Tunnel from the open internet, Tailscale from your own devices.
|
|
221
221
|
>
|
|
222
|
-
> Frizz has no accounts, so the single-use sign-in link **is** the door: a phone that scans it gets a session; nobody else gets in. Sessions are per device and can be listed and revoked with `npx frizz --sessions` and `npx frizz --sign-out`.
|
|
222
|
+
> Frizz has no accounts, so the single-use sign-in link **is** the door: a phone that scans it gets a session; nobody else gets in. Sessions are per device and can be listed and revoked with `npx frizz --sessions` and `npx frizz --sign-out`. Every project added or removed, thread started, message sent and question answered is recorded in `logs/audit.jsonl` under Frizz's data folder, with the session id of the device that did it and whether it came through that public address.
|
|
223
223
|
|
|
224
224
|
</details>
|
|
225
225
|
|
package/dist/frizz.js
CHANGED
|
@@ -450,6 +450,15 @@ var init_access_codes = __esm({
|
|
|
450
450
|
const id = _AccessStore.idOf(payload);
|
|
451
451
|
return !(id && this.sessions.isRevoked(id));
|
|
452
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Which device this session belongs to, for the audit trail: its record id (what `frizz --sessions`
|
|
455
|
+
* lists and `--sign-out` takes), `"legacy"` for a session minted before ids existed, or null when it
|
|
456
|
+
* does not verify. Never the session itself — the id is the one part of it that is not a credential.
|
|
457
|
+
*/
|
|
458
|
+
sessionIdOf(session) {
|
|
459
|
+
if (!session || !this.verifySession(session)) return null;
|
|
460
|
+
return _AccessStore.idOf(session.slice(0, session.lastIndexOf("."))) ?? "legacy";
|
|
461
|
+
}
|
|
453
462
|
/**
|
|
454
463
|
* Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
|
|
455
464
|
*
|
|
@@ -846,6 +855,290 @@ code{font-family:var(--mono);font-size:.92em;padding:.1em .35em;border-radius:4p
|
|
|
846
855
|
}
|
|
847
856
|
});
|
|
848
857
|
|
|
858
|
+
// packages/server/src/frizz-paths.ts
|
|
859
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
860
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
861
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve, sep as sep2 } from "node:path";
|
|
862
|
+
function legacyFrizzRoot(home = homedir2()) {
|
|
863
|
+
return join3(home, LEGACY_DIR_NAME);
|
|
864
|
+
}
|
|
865
|
+
function xdg(env, name) {
|
|
866
|
+
const value = env[name];
|
|
867
|
+
return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
|
|
868
|
+
}
|
|
869
|
+
function insideHome(child, home) {
|
|
870
|
+
const norm = (value) => value.replace(/[\\/]+$/u, "").replaceAll("\\", "/").toLowerCase();
|
|
871
|
+
const parent = norm(home);
|
|
872
|
+
const inner = norm(child);
|
|
873
|
+
return inner === parent || inner.startsWith(`${parent}/`);
|
|
874
|
+
}
|
|
875
|
+
function windowsRoots(env, home) {
|
|
876
|
+
const local = env.LOCALAPPDATA && insideHome(env.LOCALAPPDATA, home) ? env.LOCALAPPDATA : join3(home, "AppData", "Local");
|
|
877
|
+
const base = join3(local, "Frizz");
|
|
878
|
+
return {
|
|
879
|
+
data: join3(base, "Data"),
|
|
880
|
+
state: join3(base, "State"),
|
|
881
|
+
cache: join3(base, "Cache")
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function darwinRoots(home) {
|
|
885
|
+
const support = join3(home, "Library", "Application Support", "Frizz");
|
|
886
|
+
return {
|
|
887
|
+
data: support,
|
|
888
|
+
// macOS has no state directory concept; Application Support is where this belongs, and the log
|
|
889
|
+
// files themselves already live under each project's own directory.
|
|
890
|
+
state: support,
|
|
891
|
+
cache: join3(home, "Library", "Caches", "Frizz")
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function xdgRoots(env, home) {
|
|
895
|
+
return {
|
|
896
|
+
data: join3(xdg(env, "XDG_DATA_HOME") ?? join3(home, ".local", "share"), "frizz"),
|
|
897
|
+
state: join3(xdg(env, "XDG_STATE_HOME") ?? join3(home, ".local", "state"), "frizz"),
|
|
898
|
+
cache: join3(xdg(env, "XDG_CACHE_HOME") ?? join3(home, ".cache"), "frizz")
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function frizzPaths(options2 = {}) {
|
|
902
|
+
const env = options2.env ?? process.env;
|
|
903
|
+
const platform = options2.platform ?? process.platform;
|
|
904
|
+
const home = options2.home ?? homedir2();
|
|
905
|
+
const exists = options2.exists ?? existsSync2;
|
|
906
|
+
const legacyRoot = legacyFrizzRoot(home);
|
|
907
|
+
if (exists(legacyRoot)) {
|
|
908
|
+
return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
|
|
909
|
+
}
|
|
910
|
+
const explicit = {
|
|
911
|
+
data: xdg(env, "XDG_DATA_HOME"),
|
|
912
|
+
state: xdg(env, "XDG_STATE_HOME"),
|
|
913
|
+
cache: xdg(env, "XDG_CACHE_HOME")
|
|
914
|
+
};
|
|
915
|
+
const platformRoots = platform === "win32" ? windowsRoots(env, home) : platform === "darwin" ? darwinRoots(home) : xdgRoots(env, home);
|
|
916
|
+
return {
|
|
917
|
+
data: explicit.data ? join3(explicit.data, "frizz") : platformRoots.data,
|
|
918
|
+
state: explicit.state ? join3(explicit.state, "frizz") : platformRoots.state,
|
|
919
|
+
cache: explicit.cache ? join3(explicit.cache, "frizz") : platformRoots.cache,
|
|
920
|
+
legacy: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function frizzRoots() {
|
|
924
|
+
return cached ??= frizzPaths();
|
|
925
|
+
}
|
|
926
|
+
function projectStateDir(projectId, home) {
|
|
927
|
+
return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "projects", projectId);
|
|
928
|
+
}
|
|
929
|
+
var LEGACY_DIR_NAME, cached;
|
|
930
|
+
var init_frizz_paths = __esm({
|
|
931
|
+
"packages/server/src/frizz-paths.ts"() {
|
|
932
|
+
"use strict";
|
|
933
|
+
LEGACY_DIR_NAME = ".frizz";
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// packages/server/src/logging.ts
|
|
938
|
+
import {
|
|
939
|
+
appendFileSync,
|
|
940
|
+
mkdirSync as mkdirSync2,
|
|
941
|
+
openSync,
|
|
942
|
+
readdirSync,
|
|
943
|
+
rmSync,
|
|
944
|
+
statSync as statSync2,
|
|
945
|
+
symlinkSync,
|
|
946
|
+
writeFileSync as writeFileSync2,
|
|
947
|
+
writeSync
|
|
948
|
+
} from "node:fs";
|
|
949
|
+
import { homedir as homedir3 } from "node:os";
|
|
950
|
+
import { join as join4 } from "node:path";
|
|
951
|
+
function defaultLogRoot(stateDir, home = homedir3()) {
|
|
952
|
+
return stateDir ? join4(stateDir, "logs") : join4(frizzPaths({ home }).state, "logs");
|
|
953
|
+
}
|
|
954
|
+
function runStamp(at) {
|
|
955
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
956
|
+
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
|
|
957
|
+
}
|
|
958
|
+
function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir3(), env = process.env) {
|
|
959
|
+
const name = `frizz-${runStamp(at)}-${pid}.log`;
|
|
960
|
+
const override = env[LOG_PATH_ENV]?.trim();
|
|
961
|
+
if (override) return override.endsWith(".log") ? override : join4(override, name);
|
|
962
|
+
return join4(defaultLogRoot(stateDir, home), name);
|
|
963
|
+
}
|
|
964
|
+
function latestLogPath(dir) {
|
|
965
|
+
return join4(dir, "latest.log");
|
|
966
|
+
}
|
|
967
|
+
function linkLatest(dir, target2) {
|
|
968
|
+
const link = latestLogPath(dir);
|
|
969
|
+
try {
|
|
970
|
+
rmSync(link, { force: true });
|
|
971
|
+
symlinkSync(target2, link);
|
|
972
|
+
} catch {
|
|
973
|
+
try {
|
|
974
|
+
writeFileSync2(link, `${target2}
|
|
975
|
+
`, { mode: 384 });
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
|
|
981
|
+
let entries;
|
|
982
|
+
try {
|
|
983
|
+
entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
|
|
984
|
+
} catch {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const dated = entries.map((name) => {
|
|
988
|
+
try {
|
|
989
|
+
return { name, at: statSync2(join4(dir, name)).mtimeMs };
|
|
990
|
+
} catch {
|
|
991
|
+
return { name, at: 0 };
|
|
992
|
+
}
|
|
993
|
+
}).sort((a, b) => b.at - a.at);
|
|
994
|
+
const cutoff = now - days * 24 * 60 * 60 * 1e3;
|
|
995
|
+
const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
|
|
996
|
+
for (const entry of stale) {
|
|
997
|
+
try {
|
|
998
|
+
rmSync(join4(dir, entry.name), { force: true });
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function formatDiskLine(record2) {
|
|
1004
|
+
const at = new Date(record2.at);
|
|
1005
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
1006
|
+
const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
|
|
1007
|
+
return `${clock} ${record2.level.toUpperCase().padEnd(5)} ${record2.scope.padEnd(12)} ${record2.message}
|
|
1008
|
+
`;
|
|
1009
|
+
}
|
|
1010
|
+
function openLogFile(path) {
|
|
1011
|
+
try {
|
|
1012
|
+
mkdirSync2(join4(path, ".."), { recursive: true, mode: 448 });
|
|
1013
|
+
return openSync(path, "a", 384);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function createLogger(options2 = {}) {
|
|
1019
|
+
const path = options2.file === null ? null : options2.file ?? runLogPath(options2.stateDir);
|
|
1020
|
+
const level = options2.level ?? "debug";
|
|
1021
|
+
const now = options2.now ?? Date.now;
|
|
1022
|
+
const threshold = LEVEL_ORDER[level];
|
|
1023
|
+
const maxBytes = options2.maxBytes ?? MAX_LOG_BYTES;
|
|
1024
|
+
let fd = path === null ? null : openLogFile(path);
|
|
1025
|
+
if (fd !== null && path !== null && options2.owner !== false) {
|
|
1026
|
+
const dir = join4(path, "..");
|
|
1027
|
+
pruneRunLogs(dir);
|
|
1028
|
+
linkLatest(dir, path);
|
|
1029
|
+
}
|
|
1030
|
+
let written = 0;
|
|
1031
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1032
|
+
const emit = (recordLevel, scope, message) => {
|
|
1033
|
+
if (LEVEL_ORDER[recordLevel] < threshold) return;
|
|
1034
|
+
const record2 = { at: now(), level: recordLevel, scope, message };
|
|
1035
|
+
if (fd !== null) {
|
|
1036
|
+
try {
|
|
1037
|
+
const line = formatDiskLine(record2);
|
|
1038
|
+
if (written + line.length > maxBytes) {
|
|
1039
|
+
writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
|
|
1040
|
+
`);
|
|
1041
|
+
fd = null;
|
|
1042
|
+
} else {
|
|
1043
|
+
writeSync(fd, line);
|
|
1044
|
+
written += line.length;
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
fd = null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const listener of listeners) {
|
|
1051
|
+
try {
|
|
1052
|
+
listener(record2);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
return {
|
|
1058
|
+
debug: (scope, message) => emit("debug", scope, message),
|
|
1059
|
+
info: (scope, message) => emit("info", scope, message),
|
|
1060
|
+
warn: (scope, message) => emit("warn", scope, message),
|
|
1061
|
+
error: (scope, message) => emit("error", scope, message),
|
|
1062
|
+
get file() {
|
|
1063
|
+
return fd === null ? null : path;
|
|
1064
|
+
},
|
|
1065
|
+
onRecord(listener) {
|
|
1066
|
+
listeners.add(listener);
|
|
1067
|
+
return () => listeners.delete(listener);
|
|
1068
|
+
},
|
|
1069
|
+
close() {
|
|
1070
|
+
listeners.clear();
|
|
1071
|
+
fd = null;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function setAmbientLogger(logger2) {
|
|
1076
|
+
ambient = logger2;
|
|
1077
|
+
return logger2;
|
|
1078
|
+
}
|
|
1079
|
+
function formatFeedLine(record2) {
|
|
1080
|
+
return formatDiskLine(record2).trimEnd();
|
|
1081
|
+
}
|
|
1082
|
+
function attachTerminalMirror(logger2, enabled = process.env.FRIZZ_DEBUG === "1") {
|
|
1083
|
+
if (!enabled) return () => {
|
|
1084
|
+
};
|
|
1085
|
+
return logger2.onRecord((record2) => process.stderr.write(`${formatFeedLine(record2)}
|
|
1086
|
+
`));
|
|
1087
|
+
}
|
|
1088
|
+
function ambientLogger() {
|
|
1089
|
+
if (ambient) return ambient;
|
|
1090
|
+
const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
|
|
1091
|
+
const level = process.env[LOG_LEVEL_ENV];
|
|
1092
|
+
const logger2 = createLogger({
|
|
1093
|
+
...inherited ? { file: inherited, owner: false } : { file: null },
|
|
1094
|
+
...level && level in LEVEL_ORDER ? { level } : {}
|
|
1095
|
+
});
|
|
1096
|
+
attachTerminalMirror(logger2);
|
|
1097
|
+
ambient = logger2;
|
|
1098
|
+
return ambient;
|
|
1099
|
+
}
|
|
1100
|
+
function logEnvironment(logger2, level = "debug") {
|
|
1101
|
+
return logger2.file ? { [LOG_FILE_ENV]: logger2.file, [LOG_LEVEL_ENV]: level } : {};
|
|
1102
|
+
}
|
|
1103
|
+
var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
|
|
1104
|
+
var init_logging = __esm({
|
|
1105
|
+
"packages/server/src/logging.ts"() {
|
|
1106
|
+
"use strict";
|
|
1107
|
+
init_frizz_paths();
|
|
1108
|
+
LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
1109
|
+
LOG_FILE_ENV = "FRIZZ_LOG_FILE";
|
|
1110
|
+
LOG_LEVEL_ENV = "FRIZZ_LOG_LEVEL";
|
|
1111
|
+
LOG_PATH_ENV = "FRIZZ_LOG_PATH";
|
|
1112
|
+
RETAINED_RUNS = 20;
|
|
1113
|
+
RETAINED_DAYS = 14;
|
|
1114
|
+
MAX_LOG_BYTES = 32 * 1024 * 1024;
|
|
1115
|
+
ambient = null;
|
|
1116
|
+
log = {
|
|
1117
|
+
debug: (scope, message) => ambientLogger().debug(scope, message),
|
|
1118
|
+
info: (scope, message) => ambientLogger().info(scope, message),
|
|
1119
|
+
warn: (scope, message) => ambientLogger().warn(scope, message),
|
|
1120
|
+
error: (scope, message) => ambientLogger().error(scope, message)
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// packages/server/src/audit.ts
|
|
1126
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1127
|
+
function encodeCallerStamp(stamp) {
|
|
1128
|
+
return Buffer.from(JSON.stringify(stamp)).toString("base64url");
|
|
1129
|
+
}
|
|
1130
|
+
var CALLER_HEADER, requestOrigin;
|
|
1131
|
+
var init_audit = __esm({
|
|
1132
|
+
"packages/server/src/audit.ts"() {
|
|
1133
|
+
"use strict";
|
|
1134
|
+
init_access_codes();
|
|
1135
|
+
init_frizz_paths();
|
|
1136
|
+
init_logging();
|
|
1137
|
+
CALLER_HEADER = "x-frizz-caller";
|
|
1138
|
+
requestOrigin = new AsyncLocalStorage();
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
849
1142
|
// node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
|
|
850
1143
|
var require_identity = __commonJS({
|
|
851
1144
|
"node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js"(exports) {
|
|
@@ -15275,6 +15568,20 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15275
15568
|
// pairing, and `projectTranscriptPeerNames` uses this id to ask it. Never a drawer key on its own —
|
|
15276
15569
|
// that is `peerDispatchId`, which the same pass can also supply once this resolves.
|
|
15277
15570
|
peerSenderTaskId: external_exports.string().optional(),
|
|
15571
|
+
// ANOTHER TOP-LEVEL SESSION wrote this user turn — a thread messaging this one through Claude Code's
|
|
15572
|
+
// cross-session channel (see parseCrossSessionMessage), most often the coordinator thread that spawned
|
|
15573
|
+
// it. Not the human, and not a sub-agent either: the chat draws it as an expandable hairline naming
|
|
15574
|
+
// the sending thread, with the body one click away. `displayText` carries the unwrapped body.
|
|
15575
|
+
//
|
|
15576
|
+
// `name` is the sender's Claude Code session name, the fallback label. `thread` is the Frizz thread
|
|
15577
|
+
// behind it, filled in by the server from the sender's socket (session-peer.ts) — absent when the
|
|
15578
|
+
// sender is not a Frizz thread or is gone without having been resolved, and then the line names the
|
|
15579
|
+
// session without a link. `project` is the URL slug of the sender's project.
|
|
15580
|
+
sessionPeer: external_exports.object({
|
|
15581
|
+
name: external_exports.string().optional(),
|
|
15582
|
+
socket: external_exports.string().optional(),
|
|
15583
|
+
thread: external_exports.object({ project: external_exports.string().optional(), slug: external_exports.string(), title: external_exports.string() }).optional()
|
|
15584
|
+
}).optional(),
|
|
15278
15585
|
// FRIZZ REFUSED the ```awaiting fence this message ends in — it named something that is not running,
|
|
15279
15586
|
// or named nothing at all, or used a retired line kind (see `isParkCorrection`). The fence is not a
|
|
15280
15587
|
// park, so the chat draws nothing for it: an hourglass card with a park button asserts a wait that
|
|
@@ -15384,8 +15691,9 @@ function responseJson(res, status, value) {
|
|
|
15384
15691
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
15385
15692
|
res.end(JSON.stringify(value));
|
|
15386
15693
|
}
|
|
15387
|
-
function proxyHeaders(req, childPort, vouchSameOrigin = false) {
|
|
15694
|
+
function proxyHeaders(req, childPort, callerStamp, vouchSameOrigin = false) {
|
|
15388
15695
|
const headers = { ...req.headers };
|
|
15696
|
+
headers[CALLER_HEADER] = callerStamp;
|
|
15389
15697
|
headers.host = `127.0.0.1:${childPort}`;
|
|
15390
15698
|
if (typeof headers.origin === "string") headers.origin = `http://127.0.0.1:${childPort}`;
|
|
15391
15699
|
for (const name of FORWARDED_HEADERS) delete headers[name];
|
|
@@ -15402,6 +15710,15 @@ function readCookie(header, name) {
|
|
|
15402
15710
|
}
|
|
15403
15711
|
return void 0;
|
|
15404
15712
|
}
|
|
15713
|
+
function firstHeader(value) {
|
|
15714
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
15715
|
+
return first?.trim() || void 0;
|
|
15716
|
+
}
|
|
15717
|
+
function isLoopbackAddress(address) {
|
|
15718
|
+
if (!address) return false;
|
|
15719
|
+
const v4 = address.startsWith("::ffff:") ? address.slice("::ffff:".length) : address;
|
|
15720
|
+
return v4.startsWith("127.") || address === "::1";
|
|
15721
|
+
}
|
|
15405
15722
|
function isControlRequest(req) {
|
|
15406
15723
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
15407
15724
|
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 || url.pathname === SUPERVISOR_SIGN_OUT_PATH;
|
|
@@ -15435,6 +15752,7 @@ var init_restart_supervisor = __esm({
|
|
|
15435
15752
|
init_local_origin();
|
|
15436
15753
|
init_local_image();
|
|
15437
15754
|
init_supervisor_pages();
|
|
15755
|
+
init_audit();
|
|
15438
15756
|
init_src();
|
|
15439
15757
|
SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
|
|
15440
15758
|
SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
@@ -15799,6 +16117,28 @@ var init_restart_supervisor = __esm({
|
|
|
15799
16117
|
if (!this.publicOrigin) return true;
|
|
15800
16118
|
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
15801
16119
|
}
|
|
16120
|
+
/**
|
|
16121
|
+
* What this proxy knows about who sent a request, for the application server's audit trail
|
|
16122
|
+
* (audit.ts). The session is named by its record id, never its value, and only once it verifies.
|
|
16123
|
+
*
|
|
16124
|
+
* `clientIp` is only ever taken from a request that arrived as the public origin, and it is that
|
|
16125
|
+
* origin's CLAIM: a relay or tunnel reaches this port from loopback, so the real visitor address
|
|
16126
|
+
* exists only in what the relay chose to forward (`cf-connecting-ip` from Cloudflare).
|
|
16127
|
+
*/
|
|
16128
|
+
callerStamp(req) {
|
|
16129
|
+
const remote2 = req.socket.remoteAddress ?? null;
|
|
16130
|
+
const via = this.arrivedPublicly(req) ? "public" : isLoopbackAddress(remote2) ? "loopback" : "network";
|
|
16131
|
+
const host = via === "loopback" ? void 0 : firstHeader(req.headers.host);
|
|
16132
|
+
const forwarded = firstHeader(req.headers["x-forwarded-for"])?.split(",")[0]?.trim();
|
|
16133
|
+
const clientIp = via === "public" ? firstHeader(req.headers["cf-connecting-ip"]) ?? firstHeader(req.headers["x-real-ip"]) ?? forwarded : void 0;
|
|
16134
|
+
return encodeCallerStamp({
|
|
16135
|
+
via,
|
|
16136
|
+
remote: remote2,
|
|
16137
|
+
...host ? { host: host.slice(0, 200) } : {},
|
|
16138
|
+
...clientIp ? { clientIp: clientIp.slice(0, 200) } : {},
|
|
16139
|
+
session: this.access.sessionIdOf(readCookie(req.headers.cookie, SESSION_COOKIE))
|
|
16140
|
+
});
|
|
16141
|
+
}
|
|
15802
16142
|
/** Did this request arrive through the public origin carrying a live session? Loopback never does. */
|
|
15803
16143
|
carriesRemoteSession(req) {
|
|
15804
16144
|
return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
@@ -15889,143 +16229,64 @@ var init_restart_supervisor = __esm({
|
|
|
15889
16229
|
}
|
|
15890
16230
|
const childPort = this.options.childPort();
|
|
15891
16231
|
if (!childPort) {
|
|
15892
|
-
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "retry-after": "2" });
|
|
15893
|
-
res.end(recoveryPage(req.url ?? "/"));
|
|
15894
|
-
return;
|
|
15895
|
-
}
|
|
15896
|
-
const upstream = requestHttp({
|
|
15897
|
-
host: "127.0.0.1",
|
|
15898
|
-
port: childPort,
|
|
15899
|
-
method: req.method,
|
|
15900
|
-
path: req.url,
|
|
15901
|
-
headers: proxyHeaders(req, childPort, this.vouchesSameOrigin(req))
|
|
15902
|
-
}, (upstreamResponse) => {
|
|
15903
|
-
res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
|
15904
|
-
upstreamResponse.pipe(res);
|
|
15905
|
-
});
|
|
15906
|
-
upstream.once("error", () => {
|
|
15907
|
-
if (res.headersSent) return res.destroy();
|
|
15908
|
-
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
15909
|
-
res.end(recoveryPage(req.url ?? "/", "unreachable"));
|
|
15910
|
-
});
|
|
15911
|
-
req.pipe(upstream);
|
|
15912
|
-
}
|
|
15913
|
-
handleUpgrade(req, socket, head) {
|
|
15914
|
-
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
15915
|
-
rejectWebSocketUpgrade(socket, 401, "Unauthorized");
|
|
15916
|
-
return;
|
|
15917
|
-
}
|
|
15918
|
-
if (!this.authorityAccepted(req, false)) {
|
|
15919
|
-
socket.destroy();
|
|
15920
|
-
return;
|
|
15921
|
-
}
|
|
15922
|
-
const childPort = this.options.childPort();
|
|
15923
|
-
if (!childPort) {
|
|
15924
|
-
socket.destroy();
|
|
15925
|
-
return;
|
|
15926
|
-
}
|
|
15927
|
-
const upstream = connect(childPort, "127.0.0.1");
|
|
15928
|
-
for (const half of [socket, upstream]) {
|
|
15929
|
-
this.upgradedSockets.add(half);
|
|
15930
|
-
half.once("close", () => this.upgradedSockets.delete(half));
|
|
15931
|
-
}
|
|
15932
|
-
upstream.once("connect", () => {
|
|
15933
|
-
const headers = proxyHeaders(req, childPort);
|
|
15934
|
-
headers.connection = req.headers.connection ?? "Upgrade";
|
|
15935
|
-
const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
|
|
15936
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
15937
|
-
if (value === void 0) continue;
|
|
15938
|
-
for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
|
|
15939
|
-
}
|
|
15940
|
-
upstream.write(`${lines.join("\r\n")}\r
|
|
15941
|
-
\r
|
|
15942
|
-
`);
|
|
15943
|
-
if (head.length > 0) upstream.write(head);
|
|
15944
|
-
socket.pipe(upstream).pipe(socket);
|
|
15945
|
-
});
|
|
15946
|
-
upstream.once("error", () => socket.destroy());
|
|
15947
|
-
socket.once("error", () => upstream.destroy());
|
|
15948
|
-
}
|
|
15949
|
-
};
|
|
15950
|
-
}
|
|
15951
|
-
});
|
|
15952
|
-
|
|
15953
|
-
// packages/server/src/frizz-paths.ts
|
|
15954
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
15955
|
-
import { homedir as homedir2, tmpdir } from "node:os";
|
|
15956
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve, sep as sep2 } from "node:path";
|
|
15957
|
-
function legacyFrizzRoot(home = homedir2()) {
|
|
15958
|
-
return join3(home, LEGACY_DIR_NAME);
|
|
15959
|
-
}
|
|
15960
|
-
function xdg(env, name) {
|
|
15961
|
-
const value = env[name];
|
|
15962
|
-
return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
|
|
15963
|
-
}
|
|
15964
|
-
function insideHome(child, home) {
|
|
15965
|
-
const norm = (value) => value.replace(/[\\/]+$/u, "").replaceAll("\\", "/").toLowerCase();
|
|
15966
|
-
const parent = norm(home);
|
|
15967
|
-
const inner = norm(child);
|
|
15968
|
-
return inner === parent || inner.startsWith(`${parent}/`);
|
|
15969
|
-
}
|
|
15970
|
-
function windowsRoots(env, home) {
|
|
15971
|
-
const local = env.LOCALAPPDATA && insideHome(env.LOCALAPPDATA, home) ? env.LOCALAPPDATA : join3(home, "AppData", "Local");
|
|
15972
|
-
const base = join3(local, "Frizz");
|
|
15973
|
-
return {
|
|
15974
|
-
data: join3(base, "Data"),
|
|
15975
|
-
state: join3(base, "State"),
|
|
15976
|
-
cache: join3(base, "Cache")
|
|
15977
|
-
};
|
|
15978
|
-
}
|
|
15979
|
-
function darwinRoots(home) {
|
|
15980
|
-
const support = join3(home, "Library", "Application Support", "Frizz");
|
|
15981
|
-
return {
|
|
15982
|
-
data: support,
|
|
15983
|
-
// macOS has no state directory concept; Application Support is where this belongs, and the log
|
|
15984
|
-
// files themselves already live under each project's own directory.
|
|
15985
|
-
state: support,
|
|
15986
|
-
cache: join3(home, "Library", "Caches", "Frizz")
|
|
15987
|
-
};
|
|
15988
|
-
}
|
|
15989
|
-
function xdgRoots(env, home) {
|
|
15990
|
-
return {
|
|
15991
|
-
data: join3(xdg(env, "XDG_DATA_HOME") ?? join3(home, ".local", "share"), "frizz"),
|
|
15992
|
-
state: join3(xdg(env, "XDG_STATE_HOME") ?? join3(home, ".local", "state"), "frizz"),
|
|
15993
|
-
cache: join3(xdg(env, "XDG_CACHE_HOME") ?? join3(home, ".cache"), "frizz")
|
|
15994
|
-
};
|
|
15995
|
-
}
|
|
15996
|
-
function frizzPaths(options2 = {}) {
|
|
15997
|
-
const env = options2.env ?? process.env;
|
|
15998
|
-
const platform = options2.platform ?? process.platform;
|
|
15999
|
-
const home = options2.home ?? homedir2();
|
|
16000
|
-
const exists = options2.exists ?? existsSync2;
|
|
16001
|
-
const legacyRoot = legacyFrizzRoot(home);
|
|
16002
|
-
if (exists(legacyRoot)) {
|
|
16003
|
-
return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
|
|
16004
|
-
}
|
|
16005
|
-
const explicit = {
|
|
16006
|
-
data: xdg(env, "XDG_DATA_HOME"),
|
|
16007
|
-
state: xdg(env, "XDG_STATE_HOME"),
|
|
16008
|
-
cache: xdg(env, "XDG_CACHE_HOME")
|
|
16009
|
-
};
|
|
16010
|
-
const platformRoots = platform === "win32" ? windowsRoots(env, home) : platform === "darwin" ? darwinRoots(home) : xdgRoots(env, home);
|
|
16011
|
-
return {
|
|
16012
|
-
data: explicit.data ? join3(explicit.data, "frizz") : platformRoots.data,
|
|
16013
|
-
state: explicit.state ? join3(explicit.state, "frizz") : platformRoots.state,
|
|
16014
|
-
cache: explicit.cache ? join3(explicit.cache, "frizz") : platformRoots.cache,
|
|
16015
|
-
legacy: false
|
|
16016
|
-
};
|
|
16017
|
-
}
|
|
16018
|
-
function frizzRoots() {
|
|
16019
|
-
return cached ??= frizzPaths();
|
|
16020
|
-
}
|
|
16021
|
-
function projectStateDir(projectId, home) {
|
|
16022
|
-
return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "projects", projectId);
|
|
16023
|
-
}
|
|
16024
|
-
var LEGACY_DIR_NAME, cached;
|
|
16025
|
-
var init_frizz_paths = __esm({
|
|
16026
|
-
"packages/server/src/frizz-paths.ts"() {
|
|
16027
|
-
"use strict";
|
|
16028
|
-
LEGACY_DIR_NAME = ".frizz";
|
|
16232
|
+
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "retry-after": "2" });
|
|
16233
|
+
res.end(recoveryPage(req.url ?? "/"));
|
|
16234
|
+
return;
|
|
16235
|
+
}
|
|
16236
|
+
const upstream = requestHttp({
|
|
16237
|
+
host: "127.0.0.1",
|
|
16238
|
+
port: childPort,
|
|
16239
|
+
method: req.method,
|
|
16240
|
+
path: req.url,
|
|
16241
|
+
headers: proxyHeaders(req, childPort, this.callerStamp(req), this.vouchesSameOrigin(req))
|
|
16242
|
+
}, (upstreamResponse) => {
|
|
16243
|
+
res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
|
16244
|
+
upstreamResponse.pipe(res);
|
|
16245
|
+
});
|
|
16246
|
+
upstream.once("error", () => {
|
|
16247
|
+
if (res.headersSent) return res.destroy();
|
|
16248
|
+
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
16249
|
+
res.end(recoveryPage(req.url ?? "/", "unreachable"));
|
|
16250
|
+
});
|
|
16251
|
+
req.pipe(upstream);
|
|
16252
|
+
}
|
|
16253
|
+
handleUpgrade(req, socket, head) {
|
|
16254
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
16255
|
+
rejectWebSocketUpgrade(socket, 401, "Unauthorized");
|
|
16256
|
+
return;
|
|
16257
|
+
}
|
|
16258
|
+
if (!this.authorityAccepted(req, false)) {
|
|
16259
|
+
socket.destroy();
|
|
16260
|
+
return;
|
|
16261
|
+
}
|
|
16262
|
+
const childPort = this.options.childPort();
|
|
16263
|
+
if (!childPort) {
|
|
16264
|
+
socket.destroy();
|
|
16265
|
+
return;
|
|
16266
|
+
}
|
|
16267
|
+
const upstream = connect(childPort, "127.0.0.1");
|
|
16268
|
+
for (const half of [socket, upstream]) {
|
|
16269
|
+
this.upgradedSockets.add(half);
|
|
16270
|
+
half.once("close", () => this.upgradedSockets.delete(half));
|
|
16271
|
+
}
|
|
16272
|
+
upstream.once("connect", () => {
|
|
16273
|
+
const headers = proxyHeaders(req, childPort, this.callerStamp(req));
|
|
16274
|
+
headers.connection = req.headers.connection ?? "Upgrade";
|
|
16275
|
+
const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
|
|
16276
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
16277
|
+
if (value === void 0) continue;
|
|
16278
|
+
for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
|
|
16279
|
+
}
|
|
16280
|
+
upstream.write(`${lines.join("\r\n")}\r
|
|
16281
|
+
\r
|
|
16282
|
+
`);
|
|
16283
|
+
if (head.length > 0) upstream.write(head);
|
|
16284
|
+
socket.pipe(upstream).pipe(socket);
|
|
16285
|
+
});
|
|
16286
|
+
upstream.once("error", () => socket.destroy());
|
|
16287
|
+
socket.once("error", () => upstream.destroy());
|
|
16288
|
+
}
|
|
16289
|
+
};
|
|
16029
16290
|
}
|
|
16030
16291
|
});
|
|
16031
16292
|
|
|
@@ -19723,194 +19984,6 @@ var init_wrapper = __esm({
|
|
|
19723
19984
|
}
|
|
19724
19985
|
});
|
|
19725
19986
|
|
|
19726
|
-
// packages/server/src/logging.ts
|
|
19727
|
-
import {
|
|
19728
|
-
appendFileSync,
|
|
19729
|
-
mkdirSync as mkdirSync4,
|
|
19730
|
-
openSync,
|
|
19731
|
-
readdirSync,
|
|
19732
|
-
rmSync as rmSync2,
|
|
19733
|
-
statSync as statSync2,
|
|
19734
|
-
symlinkSync,
|
|
19735
|
-
writeFileSync as writeFileSync4,
|
|
19736
|
-
writeSync
|
|
19737
|
-
} from "node:fs";
|
|
19738
|
-
import { homedir as homedir6 } from "node:os";
|
|
19739
|
-
import { join as join6 } from "node:path";
|
|
19740
|
-
function defaultLogRoot(stateDir, home = homedir6()) {
|
|
19741
|
-
return stateDir ? join6(stateDir, "logs") : join6(frizzPaths({ home }).state, "logs");
|
|
19742
|
-
}
|
|
19743
|
-
function runStamp(at) {
|
|
19744
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
19745
|
-
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
|
|
19746
|
-
}
|
|
19747
|
-
function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir6(), env = process.env) {
|
|
19748
|
-
const name = `frizz-${runStamp(at)}-${pid}.log`;
|
|
19749
|
-
const override = env[LOG_PATH_ENV]?.trim();
|
|
19750
|
-
if (override) return override.endsWith(".log") ? override : join6(override, name);
|
|
19751
|
-
return join6(defaultLogRoot(stateDir, home), name);
|
|
19752
|
-
}
|
|
19753
|
-
function latestLogPath(dir) {
|
|
19754
|
-
return join6(dir, "latest.log");
|
|
19755
|
-
}
|
|
19756
|
-
function linkLatest(dir, target2) {
|
|
19757
|
-
const link = latestLogPath(dir);
|
|
19758
|
-
try {
|
|
19759
|
-
rmSync2(link, { force: true });
|
|
19760
|
-
symlinkSync(target2, link);
|
|
19761
|
-
} catch {
|
|
19762
|
-
try {
|
|
19763
|
-
writeFileSync4(link, `${target2}
|
|
19764
|
-
`, { mode: 384 });
|
|
19765
|
-
} catch {
|
|
19766
|
-
}
|
|
19767
|
-
}
|
|
19768
|
-
}
|
|
19769
|
-
function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
|
|
19770
|
-
let entries;
|
|
19771
|
-
try {
|
|
19772
|
-
entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
|
|
19773
|
-
} catch {
|
|
19774
|
-
return;
|
|
19775
|
-
}
|
|
19776
|
-
const dated = entries.map((name) => {
|
|
19777
|
-
try {
|
|
19778
|
-
return { name, at: statSync2(join6(dir, name)).mtimeMs };
|
|
19779
|
-
} catch {
|
|
19780
|
-
return { name, at: 0 };
|
|
19781
|
-
}
|
|
19782
|
-
}).sort((a, b) => b.at - a.at);
|
|
19783
|
-
const cutoff = now - days * 24 * 60 * 60 * 1e3;
|
|
19784
|
-
const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
|
|
19785
|
-
for (const entry of stale) {
|
|
19786
|
-
try {
|
|
19787
|
-
rmSync2(join6(dir, entry.name), { force: true });
|
|
19788
|
-
} catch {
|
|
19789
|
-
}
|
|
19790
|
-
}
|
|
19791
|
-
}
|
|
19792
|
-
function formatDiskLine(record2) {
|
|
19793
|
-
const at = new Date(record2.at);
|
|
19794
|
-
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
19795
|
-
const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
|
|
19796
|
-
return `${clock} ${record2.level.toUpperCase().padEnd(5)} ${record2.scope.padEnd(12)} ${record2.message}
|
|
19797
|
-
`;
|
|
19798
|
-
}
|
|
19799
|
-
function openLogFile(path) {
|
|
19800
|
-
try {
|
|
19801
|
-
mkdirSync4(join6(path, ".."), { recursive: true, mode: 448 });
|
|
19802
|
-
return openSync(path, "a", 384);
|
|
19803
|
-
} catch {
|
|
19804
|
-
return null;
|
|
19805
|
-
}
|
|
19806
|
-
}
|
|
19807
|
-
function createLogger(options2 = {}) {
|
|
19808
|
-
const path = options2.file === null ? null : options2.file ?? runLogPath(options2.stateDir);
|
|
19809
|
-
const level = options2.level ?? "debug";
|
|
19810
|
-
const now = options2.now ?? Date.now;
|
|
19811
|
-
const threshold = LEVEL_ORDER[level];
|
|
19812
|
-
const maxBytes = options2.maxBytes ?? MAX_LOG_BYTES;
|
|
19813
|
-
let fd = path === null ? null : openLogFile(path);
|
|
19814
|
-
if (fd !== null && path !== null && options2.owner !== false) {
|
|
19815
|
-
const dir = join6(path, "..");
|
|
19816
|
-
pruneRunLogs(dir);
|
|
19817
|
-
linkLatest(dir, path);
|
|
19818
|
-
}
|
|
19819
|
-
let written = 0;
|
|
19820
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
19821
|
-
const emit = (recordLevel, scope, message) => {
|
|
19822
|
-
if (LEVEL_ORDER[recordLevel] < threshold) return;
|
|
19823
|
-
const record2 = { at: now(), level: recordLevel, scope, message };
|
|
19824
|
-
if (fd !== null) {
|
|
19825
|
-
try {
|
|
19826
|
-
const line = formatDiskLine(record2);
|
|
19827
|
-
if (written + line.length > maxBytes) {
|
|
19828
|
-
writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
|
|
19829
|
-
`);
|
|
19830
|
-
fd = null;
|
|
19831
|
-
} else {
|
|
19832
|
-
writeSync(fd, line);
|
|
19833
|
-
written += line.length;
|
|
19834
|
-
}
|
|
19835
|
-
} catch {
|
|
19836
|
-
fd = null;
|
|
19837
|
-
}
|
|
19838
|
-
}
|
|
19839
|
-
for (const listener of listeners) {
|
|
19840
|
-
try {
|
|
19841
|
-
listener(record2);
|
|
19842
|
-
} catch {
|
|
19843
|
-
}
|
|
19844
|
-
}
|
|
19845
|
-
};
|
|
19846
|
-
return {
|
|
19847
|
-
debug: (scope, message) => emit("debug", scope, message),
|
|
19848
|
-
info: (scope, message) => emit("info", scope, message),
|
|
19849
|
-
warn: (scope, message) => emit("warn", scope, message),
|
|
19850
|
-
error: (scope, message) => emit("error", scope, message),
|
|
19851
|
-
get file() {
|
|
19852
|
-
return fd === null ? null : path;
|
|
19853
|
-
},
|
|
19854
|
-
onRecord(listener) {
|
|
19855
|
-
listeners.add(listener);
|
|
19856
|
-
return () => listeners.delete(listener);
|
|
19857
|
-
},
|
|
19858
|
-
close() {
|
|
19859
|
-
listeners.clear();
|
|
19860
|
-
fd = null;
|
|
19861
|
-
}
|
|
19862
|
-
};
|
|
19863
|
-
}
|
|
19864
|
-
function setAmbientLogger(logger2) {
|
|
19865
|
-
ambient = logger2;
|
|
19866
|
-
return logger2;
|
|
19867
|
-
}
|
|
19868
|
-
function formatFeedLine(record2) {
|
|
19869
|
-
return formatDiskLine(record2).trimEnd();
|
|
19870
|
-
}
|
|
19871
|
-
function attachTerminalMirror(logger2, enabled = process.env.FRIZZ_DEBUG === "1") {
|
|
19872
|
-
if (!enabled) return () => {
|
|
19873
|
-
};
|
|
19874
|
-
return logger2.onRecord((record2) => process.stderr.write(`${formatFeedLine(record2)}
|
|
19875
|
-
`));
|
|
19876
|
-
}
|
|
19877
|
-
function ambientLogger() {
|
|
19878
|
-
if (ambient) return ambient;
|
|
19879
|
-
const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
|
|
19880
|
-
const level = process.env[LOG_LEVEL_ENV];
|
|
19881
|
-
const logger2 = createLogger({
|
|
19882
|
-
...inherited ? { file: inherited, owner: false } : { file: null },
|
|
19883
|
-
...level && level in LEVEL_ORDER ? { level } : {}
|
|
19884
|
-
});
|
|
19885
|
-
attachTerminalMirror(logger2);
|
|
19886
|
-
ambient = logger2;
|
|
19887
|
-
return ambient;
|
|
19888
|
-
}
|
|
19889
|
-
function logEnvironment(logger2, level = "debug") {
|
|
19890
|
-
return logger2.file ? { [LOG_FILE_ENV]: logger2.file, [LOG_LEVEL_ENV]: level } : {};
|
|
19891
|
-
}
|
|
19892
|
-
var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
|
|
19893
|
-
var init_logging = __esm({
|
|
19894
|
-
"packages/server/src/logging.ts"() {
|
|
19895
|
-
"use strict";
|
|
19896
|
-
init_frizz_paths();
|
|
19897
|
-
LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
19898
|
-
LOG_FILE_ENV = "FRIZZ_LOG_FILE";
|
|
19899
|
-
LOG_LEVEL_ENV = "FRIZZ_LOG_LEVEL";
|
|
19900
|
-
LOG_PATH_ENV = "FRIZZ_LOG_PATH";
|
|
19901
|
-
RETAINED_RUNS = 20;
|
|
19902
|
-
RETAINED_DAYS = 14;
|
|
19903
|
-
MAX_LOG_BYTES = 32 * 1024 * 1024;
|
|
19904
|
-
ambient = null;
|
|
19905
|
-
log = {
|
|
19906
|
-
debug: (scope, message) => ambientLogger().debug(scope, message),
|
|
19907
|
-
info: (scope, message) => ambientLogger().info(scope, message),
|
|
19908
|
-
warn: (scope, message) => ambientLogger().warn(scope, message),
|
|
19909
|
-
error: (scope, message) => ambientLogger().error(scope, message)
|
|
19910
|
-
};
|
|
19911
|
-
}
|
|
19912
|
-
});
|
|
19913
|
-
|
|
19914
19987
|
// packages/server/src/process-generation.ts
|
|
19915
19988
|
import { execFileSync } from "node:child_process";
|
|
19916
19989
|
import { randomUUID } from "node:crypto";
|
|
@@ -20946,7 +21019,7 @@ var init_boot_progress = __esm({
|
|
|
20946
21019
|
|
|
20947
21020
|
// packages/server/src/project-root.ts
|
|
20948
21021
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
20949
|
-
import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
21022
|
+
import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20950
21023
|
import { homedir as homedir8 } from "node:os";
|
|
20951
21024
|
import { dirname as dirname8, join as join11, parse, resolve as resolve3 } from "node:path";
|
|
20952
21025
|
function projectIdPath(root) {
|
|
@@ -21040,6 +21113,17 @@ function isHomeDirectory(dir, home = homedir8()) {
|
|
|
21040
21113
|
return false;
|
|
21041
21114
|
}
|
|
21042
21115
|
}
|
|
21116
|
+
function isWorldWritable(dir, platform = process.platform) {
|
|
21117
|
+
if (platform === "win32") return false;
|
|
21118
|
+
try {
|
|
21119
|
+
return (statSync5(dir).mode & 2) !== 0;
|
|
21120
|
+
} catch {
|
|
21121
|
+
return false;
|
|
21122
|
+
}
|
|
21123
|
+
}
|
|
21124
|
+
function refuseWorldWritableProject(dir) {
|
|
21125
|
+
if (isWorldWritable(dir)) throw new Error(`A folder every account can write to cannot be a project: ${dir}`);
|
|
21126
|
+
}
|
|
21043
21127
|
function existingProjectId(dir) {
|
|
21044
21128
|
const fromFile = readProjectIdFile(dir);
|
|
21045
21129
|
if (fromFile) return fromFile;
|
|
@@ -23699,27 +23783,27 @@ function createAccessPane(options2) {
|
|
|
23699
23783
|
}
|
|
23700
23784
|
|
|
23701
23785
|
// src/remote-controller.ts
|
|
23702
|
-
import { homedir as
|
|
23786
|
+
import { homedir as homedir6 } from "node:os";
|
|
23703
23787
|
|
|
23704
23788
|
// src/cloud.ts
|
|
23705
23789
|
init_frizz_paths();
|
|
23706
23790
|
init_src();
|
|
23707
23791
|
import { spawn as spawn2 } from "node:child_process";
|
|
23708
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
23792
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "node:fs";
|
|
23709
23793
|
import { createInterface } from "node:readline/promises";
|
|
23710
|
-
import { homedir as
|
|
23711
|
-
import { dirname as dirname4, join as
|
|
23794
|
+
import { homedir as homedir5 } from "node:os";
|
|
23795
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
23712
23796
|
|
|
23713
23797
|
// src/identity.ts
|
|
23714
23798
|
init_frizz_paths();
|
|
23715
23799
|
init_src();
|
|
23716
|
-
import { mkdirSync as
|
|
23717
|
-
import { homedir as
|
|
23718
|
-
import { dirname as dirname3, join as
|
|
23719
|
-
function claimIdentityPath(home =
|
|
23720
|
-
return
|
|
23800
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
23801
|
+
import { homedir as homedir4 } from "node:os";
|
|
23802
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
23803
|
+
function claimIdentityPath(home = homedir4(), env) {
|
|
23804
|
+
return join5(frizzPaths({ home, env }).state, "identity.key");
|
|
23721
23805
|
}
|
|
23722
|
-
async function loadOrCreateClaimIdentity(home =
|
|
23806
|
+
async function loadOrCreateClaimIdentity(home = homedir4()) {
|
|
23723
23807
|
const path = claimIdentityPath(home);
|
|
23724
23808
|
let stored;
|
|
23725
23809
|
try {
|
|
@@ -23735,8 +23819,8 @@ async function loadOrCreateClaimIdentity(home = homedir3()) {
|
|
|
23735
23819
|
);
|
|
23736
23820
|
}
|
|
23737
23821
|
const identity = await generateClaimIdentity();
|
|
23738
|
-
|
|
23739
|
-
|
|
23822
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
23823
|
+
writeFileSync3(path, await exportClaimPrivateKey(identity.privateKey), { mode: 384 });
|
|
23740
23824
|
return identity;
|
|
23741
23825
|
}
|
|
23742
23826
|
|
|
@@ -24332,6 +24416,11 @@ function describeCloudConfig(config) {
|
|
|
24332
24416
|
return "Cloudflare Tunnel";
|
|
24333
24417
|
}
|
|
24334
24418
|
var REGISTRAR_IS_LIVE = true;
|
|
24419
|
+
var CUSTOM_NAMES_OFFERED = false;
|
|
24420
|
+
function customNamesOffered(env = process.env) {
|
|
24421
|
+
return CUSTOM_NAMES_OFFERED || env.FRIZZ_CUSTOM_NAMES === "1";
|
|
24422
|
+
}
|
|
24423
|
+
var CUSTOM_NAMES_PAUSED = "custom frizz.sh names are paused for now \u2014 press enter for a private name (no account needed), or answer with the hostname of a tunnel you run";
|
|
24335
24424
|
function isClaimedConfig(config) {
|
|
24336
24425
|
return typeof config.claim === "string" && config.claim.length > 0;
|
|
24337
24426
|
}
|
|
@@ -24345,19 +24434,24 @@ function zoneClaimLabel(hostname) {
|
|
|
24345
24434
|
const label = hostname.slice(0, -suffix.length);
|
|
24346
24435
|
return label.length > 0 && !label.includes(".") ? label : null;
|
|
24347
24436
|
}
|
|
24348
|
-
async function reconcileCloudConfig(config, port, home =
|
|
24437
|
+
async function reconcileCloudConfig(config, port, home = homedir5(), onNotice, origin, github = githubDeviceFlow(onNotice ? { onPrompt: (prompt) => onNotice(deviceCodeMessage(prompt)) } : {})) {
|
|
24349
24438
|
if (isRelayConfig(config)) return config;
|
|
24350
24439
|
const label = config.claim ?? zoneClaimLabel(config.hostname);
|
|
24351
24440
|
if (!label) return config;
|
|
24441
|
+
if (!isAnonymousClaimName(label) && !customNamesOffered()) {
|
|
24442
|
+
throw new Error(
|
|
24443
|
+
`${config.hostname} needs moving onto the Frizz relay, which means claiming it again, and custom frizz.sh names are paused for now \u2014 press R to pick a private name or another setup`
|
|
24444
|
+
);
|
|
24445
|
+
}
|
|
24352
24446
|
onNotice?.(
|
|
24353
24447
|
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}`
|
|
24354
24448
|
);
|
|
24355
24449
|
return establishCloudConfig(label, port, home, origin, github);
|
|
24356
24450
|
}
|
|
24357
|
-
function cloudConfigPath(home =
|
|
24358
|
-
return
|
|
24451
|
+
function cloudConfigPath(home = homedir5()) {
|
|
24452
|
+
return join6(frizzPaths({ home }).data, "cloud.json");
|
|
24359
24453
|
}
|
|
24360
|
-
function readCloudConfig(home =
|
|
24454
|
+
function readCloudConfig(home = homedir5()) {
|
|
24361
24455
|
try {
|
|
24362
24456
|
const parsed = JSON.parse(readFileSync3(cloudConfigPath(home), "utf8"));
|
|
24363
24457
|
if (typeof parsed.hostname !== "string" || !parsed.hostname) return null;
|
|
@@ -24377,10 +24471,10 @@ function readCloudConfig(home = homedir4()) {
|
|
|
24377
24471
|
return null;
|
|
24378
24472
|
}
|
|
24379
24473
|
}
|
|
24380
|
-
function tunnelTokenPath(home =
|
|
24381
|
-
return
|
|
24474
|
+
function tunnelTokenPath(home = homedir5()) {
|
|
24475
|
+
return join6(frizzPaths({ home }).state, "tunnel-token");
|
|
24382
24476
|
}
|
|
24383
|
-
function readTunnelToken(home =
|
|
24477
|
+
function readTunnelToken(home = homedir5()) {
|
|
24384
24478
|
try {
|
|
24385
24479
|
const token = readFileSync3(tunnelTokenPath(home), "utf8").trim();
|
|
24386
24480
|
return token.length > 0 ? token : null;
|
|
@@ -24388,19 +24482,19 @@ function readTunnelToken(home = homedir4()) {
|
|
|
24388
24482
|
return null;
|
|
24389
24483
|
}
|
|
24390
24484
|
}
|
|
24391
|
-
function writeTunnelToken(token, home =
|
|
24485
|
+
function writeTunnelToken(token, home = homedir5()) {
|
|
24392
24486
|
const path = tunnelTokenPath(home);
|
|
24393
|
-
|
|
24394
|
-
|
|
24487
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
24488
|
+
writeFileSync4(path, `${token}
|
|
24395
24489
|
`, { mode: 384 });
|
|
24396
24490
|
}
|
|
24397
|
-
function deleteCloudConfig(home =
|
|
24398
|
-
|
|
24491
|
+
function deleteCloudConfig(home = homedir5()) {
|
|
24492
|
+
rmSync2(cloudConfigPath(home), { force: true });
|
|
24399
24493
|
}
|
|
24400
|
-
function writeCloudConfig(config, home =
|
|
24494
|
+
function writeCloudConfig(config, home = homedir5()) {
|
|
24401
24495
|
const path = cloudConfigPath(home);
|
|
24402
|
-
|
|
24403
|
-
|
|
24496
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
24497
|
+
writeFileSync4(path, `${JSON.stringify(config, null, 2)}
|
|
24404
24498
|
`);
|
|
24405
24499
|
}
|
|
24406
24500
|
function normalizeHostname(raw) {
|
|
@@ -24420,7 +24514,7 @@ async function promptForTunnelName(hostname, fallback) {
|
|
|
24420
24514
|
rl.close();
|
|
24421
24515
|
}
|
|
24422
24516
|
}
|
|
24423
|
-
async function establishCloudConfig(answer, port, home =
|
|
24517
|
+
async function establishCloudConfig(answer, port, home = homedir5(), origin, github = githubDeviceFlow()) {
|
|
24424
24518
|
if (answer.includes(".")) {
|
|
24425
24519
|
const hostname = normalizeHostname(answer);
|
|
24426
24520
|
const tunnel = await promptForTunnelName(hostname, hostname.split(".")[0]);
|
|
@@ -24432,6 +24526,7 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
|
|
|
24432
24526
|
);
|
|
24433
24527
|
}
|
|
24434
24528
|
const anonymous = answer === "" || isAnonymousClaimName(answer);
|
|
24529
|
+
if (!anonymous && !customNamesOffered()) throw new Error(CUSTOM_NAMES_PAUSED);
|
|
24435
24530
|
if (!anonymous && !claimNameIsValid(answer)) {
|
|
24436
24531
|
normalizeClaimName(answer);
|
|
24437
24532
|
}
|
|
@@ -24464,7 +24559,7 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
|
|
|
24464
24559
|
}
|
|
24465
24560
|
return { hostname: result.hostname, claim: name, serve: "relay" };
|
|
24466
24561
|
}
|
|
24467
|
-
async function resolveRunToken(config, port, home =
|
|
24562
|
+
async function resolveRunToken(config, port, home = homedir5(), onWarning, origin) {
|
|
24468
24563
|
if (!isClaimedConfig(config)) return null;
|
|
24469
24564
|
const cached2 = readTunnelToken(home);
|
|
24470
24565
|
try {
|
|
@@ -24481,12 +24576,12 @@ async function resolveRunToken(config, port, home = homedir4(), onWarning, origi
|
|
|
24481
24576
|
throw error;
|
|
24482
24577
|
}
|
|
24483
24578
|
}
|
|
24484
|
-
function resolveTunnelConfigPath(config, home =
|
|
24579
|
+
function resolveTunnelConfigPath(config, home = homedir5()) {
|
|
24485
24580
|
if (config.config) return config.config;
|
|
24486
|
-
const candidate =
|
|
24581
|
+
const candidate = join6(home, ".cloudflared", "frizz.yml");
|
|
24487
24582
|
return existsSync3(candidate) ? candidate : null;
|
|
24488
24583
|
}
|
|
24489
|
-
async function startRelay(config, port, home =
|
|
24584
|
+
async function startRelay(config, port, home = homedir5(), onStatus) {
|
|
24490
24585
|
const identity = await loadOrCreateClaimIdentity(home);
|
|
24491
24586
|
const origin = `https://${config.hostname}`;
|
|
24492
24587
|
return connectRelay({
|
|
@@ -24504,7 +24599,7 @@ async function startRelay(config, port, home = homedir4(), onStatus) {
|
|
|
24504
24599
|
} : {}
|
|
24505
24600
|
});
|
|
24506
24601
|
}
|
|
24507
|
-
function startTunnel(config, onExit, onError, home =
|
|
24602
|
+
function startTunnel(config, onExit, onError, home = homedir5(), runToken) {
|
|
24508
24603
|
const configPath = runToken ? null : resolveTunnelConfigPath(config, home);
|
|
24509
24604
|
const args2 = runToken ? ["tunnel", "--no-autoupdate", "run", "--token", runToken] : [
|
|
24510
24605
|
"tunnel",
|
|
@@ -24535,7 +24630,7 @@ function startTunnel(config, onExit, onError, home = homedir4(), runToken) {
|
|
|
24535
24630
|
|
|
24536
24631
|
// src/remote-controller.ts
|
|
24537
24632
|
function createRemoteController(options2) {
|
|
24538
|
-
const home = options2.home ??
|
|
24633
|
+
const home = options2.home ?? homedir6();
|
|
24539
24634
|
let config = null;
|
|
24540
24635
|
let transport = null;
|
|
24541
24636
|
const stopTransport = () => {
|
|
@@ -24650,6 +24745,15 @@ var CHOICES = [
|
|
|
24650
24745
|
{ kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
|
|
24651
24746
|
{ kind: "off", title: "Off", blurb: "loopback only" }
|
|
24652
24747
|
];
|
|
24748
|
+
function choicesFor(current) {
|
|
24749
|
+
if (customNamesOffered()) return CHOICES;
|
|
24750
|
+
if (kindOf(current) === "frizz") {
|
|
24751
|
+
return CHOICES.map(
|
|
24752
|
+
(choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom names are paused` } : choice
|
|
24753
|
+
);
|
|
24754
|
+
}
|
|
24755
|
+
return CHOICES.filter((choice) => choice.kind !== "frizz");
|
|
24756
|
+
}
|
|
24653
24757
|
var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
|
|
24654
24758
|
var BACKSPACE = /* @__PURE__ */ new Set(["\x7F", "\b"]);
|
|
24655
24759
|
var ESC = "\x1B";
|
|
@@ -24690,6 +24794,8 @@ function createRemotePane(options2) {
|
|
|
24690
24794
|
output.write(fitScreen([{ text: "", drop: 0 }, ...indented], output.rows, output.columns));
|
|
24691
24795
|
};
|
|
24692
24796
|
const check = (ok, text) => ok ? `${text} \u2713` : text;
|
|
24797
|
+
const choices = () => choicesFor(options2.current());
|
|
24798
|
+
const rowOf = (kind) => Math.max(0, choices().findIndex((choice) => choice.kind === kind));
|
|
24693
24799
|
const paint = () => {
|
|
24694
24800
|
if (!open) return;
|
|
24695
24801
|
const s = screen;
|
|
@@ -24702,7 +24808,7 @@ function createRemotePane(options2) {
|
|
|
24702
24808
|
"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."
|
|
24703
24809
|
),
|
|
24704
24810
|
"",
|
|
24705
|
-
...
|
|
24811
|
+
...choices().map((choice, index) => {
|
|
24706
24812
|
const marker = index === s.index ? "\u276F" : " ";
|
|
24707
24813
|
const title = choice.title.padEnd(18);
|
|
24708
24814
|
const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
|
|
@@ -24911,6 +25017,21 @@ function createRemotePane(options2) {
|
|
|
24911
25017
|
}
|
|
24912
25018
|
paint();
|
|
24913
25019
|
};
|
|
25020
|
+
const serveCurrent = async () => {
|
|
25021
|
+
const current = options2.current();
|
|
25022
|
+
const back = { name: "menu", index: rowOf("frizz") };
|
|
25023
|
+
if (!current) return;
|
|
25024
|
+
screen = { name: "busy", message: `serving ${current.hostname}\u2026` };
|
|
25025
|
+
paint();
|
|
25026
|
+
try {
|
|
25027
|
+
await options2.apply(current);
|
|
25028
|
+
options2.onChanged?.(current);
|
|
25029
|
+
screen = { name: "done", message: `Serving https://${current.hostname} (${describeCloudConfig(current)}).`, link: options2.issueLink(), config: current };
|
|
25030
|
+
} catch (error) {
|
|
25031
|
+
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back };
|
|
25032
|
+
}
|
|
25033
|
+
paint();
|
|
25034
|
+
};
|
|
24914
25035
|
const turnOff = async () => {
|
|
24915
25036
|
screen = { name: "busy", message: "back to loopback only\u2026" };
|
|
24916
25037
|
paint();
|
|
@@ -24919,15 +25040,14 @@ function createRemotePane(options2) {
|
|
|
24919
25040
|
options2.onChanged?.(null);
|
|
24920
25041
|
screen = { name: "done", message: "Loopback only. This board is reachable from this machine alone.", link: null, config: null };
|
|
24921
25042
|
} catch (error) {
|
|
24922
|
-
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index:
|
|
25043
|
+
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: rowOf("off") } };
|
|
24923
25044
|
}
|
|
24924
25045
|
paint();
|
|
24925
25046
|
};
|
|
24926
25047
|
return {
|
|
24927
25048
|
open() {
|
|
24928
25049
|
open = true;
|
|
24929
|
-
|
|
24930
|
-
screen = { name: "menu", index: Math.max(0, CHOICES.findIndex((choice) => choice.kind === current)) };
|
|
25050
|
+
screen = { name: "menu", index: rowOf(kindOf(options2.current())) };
|
|
24931
25051
|
output.write(ALT_SCREEN_ON);
|
|
24932
25052
|
output.write(HIDE_CURSOR);
|
|
24933
25053
|
paint();
|
|
@@ -24950,13 +25070,15 @@ function createRemotePane(options2) {
|
|
|
24950
25070
|
}
|
|
24951
25071
|
if (s.name === "menu") {
|
|
24952
25072
|
if (key === ESC || key === "q") return "close";
|
|
24953
|
-
|
|
24954
|
-
|
|
24955
|
-
else if (
|
|
25073
|
+
const rows = choices();
|
|
25074
|
+
if (key === UP || key === "k") s.index = (s.index + rows.length - 1) % rows.length;
|
|
25075
|
+
else if (key === DOWN || key === "j") s.index = (s.index + 1) % rows.length;
|
|
25076
|
+
else if (/^[1-9]$/.test(key) && Number(key) <= rows.length) s.index = Number(key) - 1;
|
|
24956
25077
|
else if (ENTER.has(key)) {
|
|
24957
|
-
const choice =
|
|
25078
|
+
const choice = rows[Math.min(s.index, rows.length - 1)];
|
|
24958
25079
|
if (choice.kind === "off") void turnOff();
|
|
24959
25080
|
else if (choice.kind === "private") void claimPrivate();
|
|
25081
|
+
else if (choice.kind === "frizz" && !customNamesOffered()) void serveCurrent();
|
|
24960
25082
|
else openForm(choice.kind);
|
|
24961
25083
|
return "keep";
|
|
24962
25084
|
}
|
|
@@ -24964,7 +25086,7 @@ function createRemotePane(options2) {
|
|
|
24964
25086
|
return "keep";
|
|
24965
25087
|
}
|
|
24966
25088
|
if (key === ESC) {
|
|
24967
|
-
screen = { name: "menu", index:
|
|
25089
|
+
screen = { name: "menu", index: rowOf(s.kind) };
|
|
24968
25090
|
paint();
|
|
24969
25091
|
return "keep";
|
|
24970
25092
|
}
|
|
@@ -25352,7 +25474,7 @@ var LAUNCH_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
|
|
|
25352
25474
|
var LAUNCH_HARD_TIMEOUT_MS = BOOT_HARD_TIMEOUT_MS;
|
|
25353
25475
|
var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
|
|
25354
25476
|
function retiredFlagMessage(flag) {
|
|
25355
|
-
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 private
|
|
25477
|
+
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 private frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
|
|
25356
25478
|
}
|
|
25357
25479
|
function parseCliArgs(argv) {
|
|
25358
25480
|
const args2 = new Set(argv);
|
|
@@ -25521,16 +25643,17 @@ function cleanupSandbox(home) {
|
|
|
25521
25643
|
function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
|
|
25522
25644
|
const candidate = realpathSync5(discoverProjectRoot(cwd, home));
|
|
25523
25645
|
const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
|
|
25524
|
-
|
|
25646
|
+
const refused = isHomeDirectory(candidate, home) ? "home" : isWorldWritable(candidate) ? "world-writable" : void 0;
|
|
25647
|
+
if ((known || hasProjectMarker(candidate)) && !refused)
|
|
25525
25648
|
return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
|
|
25526
25649
|
const host = mostRecentProject(home, env);
|
|
25527
|
-
const reason =
|
|
25650
|
+
const reason = refused ?? "unadopted";
|
|
25528
25651
|
if (!host) return { kind: "empty", directory: candidate, reason };
|
|
25529
|
-
return reason === "
|
|
25652
|
+
return reason === "unadopted" ? { kind: "offer", workspace: host, directory: candidate } : { kind: "grid", workspace: host };
|
|
25530
25653
|
}
|
|
25531
25654
|
function mostRecentProject(home, env) {
|
|
25532
25655
|
for (const entry of listProjects(home)) {
|
|
25533
|
-
if (entry.stale || isHomeDirectory(entry.path, home)) continue;
|
|
25656
|
+
if (entry.stale || isHomeDirectory(entry.path, home) || isWorldWritable(entry.path)) continue;
|
|
25534
25657
|
try {
|
|
25535
25658
|
return resolveWorkspace(entry.path, home, env);
|
|
25536
25659
|
} catch {
|
|
@@ -25553,6 +25676,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process
|
|
|
25553
25676
|
gitRoot = void 0;
|
|
25554
25677
|
}
|
|
25555
25678
|
const root0 = realpathSync5(gitRoot ?? discoverProjectRoot(cwd, home));
|
|
25679
|
+
refuseWorldWritableProject(root0);
|
|
25556
25680
|
const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
|
|
25557
25681
|
const root = identity?.root ?? root0;
|
|
25558
25682
|
const id = ensureProjectIdFile(root, home, identity?.id);
|
|
@@ -27094,7 +27218,7 @@ import {
|
|
|
27094
27218
|
realpathSync as realpathSync6,
|
|
27095
27219
|
renameSync as renameSync6,
|
|
27096
27220
|
rmSync as rmSync9,
|
|
27097
|
-
statSync as
|
|
27221
|
+
statSync as statSync6,
|
|
27098
27222
|
writeFileSync as writeFileSync11
|
|
27099
27223
|
} from "node:fs";
|
|
27100
27224
|
import { delimiter as delimiter2, dirname as dirname13, isAbsolute as isAbsolute6, join as join15, relative as relative3, resolve as resolve6, sep as sep4 } from "node:path";
|
|
@@ -27138,7 +27262,7 @@ function containedFile(root, name, directory = false) {
|
|
|
27138
27262
|
const rel = relative3(realpathSync6(root), path);
|
|
27139
27263
|
if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute6(rel))
|
|
27140
27264
|
throw new Error(`Frizz server artifact escapes its package: ${name}`);
|
|
27141
|
-
const stat =
|
|
27265
|
+
const stat = statSync6(path);
|
|
27142
27266
|
if (directory ? !stat.isDirectory() : !stat.isFile()) throw new Error(`invalid Frizz server artifact: ${name}`);
|
|
27143
27267
|
return path;
|
|
27144
27268
|
}
|
|
@@ -27181,7 +27305,7 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
|
|
|
27181
27305
|
} catch {
|
|
27182
27306
|
}
|
|
27183
27307
|
}
|
|
27184
|
-
const found = candidates.find((path) => existsSync9(path) &&
|
|
27308
|
+
const found = candidates.find((path) => existsSync9(path) && statSync6(path).isFile());
|
|
27185
27309
|
if (!found) throw new Error("npm was not found beside Node or on PATH; install Node with npm to install Frizz server releases");
|
|
27186
27310
|
return resolve6(found);
|
|
27187
27311
|
}
|
|
@@ -27563,7 +27687,7 @@ Options:
|
|
|
27563
27687
|
|
|
27564
27688
|
|
|
27565
27689
|
To reach the board from a phone or another machine, press R in the terminal running it: a short
|
|
27566
|
-
walkthrough sets up a private frizz.sh name (no account needed), a
|
|
27690
|
+
walkthrough sets up a private frizz.sh name (no account needed), a Cloudflare
|
|
27567
27691
|
Tunnel, Tailscale, or a proxy of your own, and
|
|
27568
27692
|
remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
|
|
27569
27693
|
shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.`
|
|
@@ -27598,7 +27722,7 @@ var workspace = (() => {
|
|
|
27598
27722
|
const intent = resolveLaunchIntent();
|
|
27599
27723
|
if (intent.kind === "empty")
|
|
27600
27724
|
throw new Error(
|
|
27601
|
-
intent.reason === "home" ? "frizz cannot open your home directory as a project, and there is no other project to show yet. cd into a repository and run frizz there." : `${intent.directory} is not a Frizz project yet, and there is no other project to show. Run frizz inside a repository, or add this one from the projects page once a board is open.`
|
|
27725
|
+
intent.reason === "home" ? "frizz cannot open your home directory as a project, and there is no other project to show yet. cd into a repository and run frizz there." : intent.reason === "world-writable" ? `A folder every account can write to cannot be a project: ${intent.directory}. Run frizz in a folder only you can write to.` : `${intent.directory} is not a Frizz project yet, and there is no other project to show. Run frizz inside a repository, or add this one from the projects page once a board is open.`
|
|
27602
27726
|
);
|
|
27603
27727
|
launchIntent = intent;
|
|
27604
27728
|
return intent.workspace;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frizz",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A local web UI for running many coding agents at once",
|
|
6
6
|
"keywords": [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"frizzServer": {
|
|
68
68
|
"package": "frizz-server",
|
|
69
|
-
"version": "0.15.
|
|
69
|
+
"version": "0.15.18",
|
|
70
70
|
"protocol": 1,
|
|
71
71
|
"dataEpoch": 1
|
|
72
72
|
}
|