frizz 0.13.8 → 0.13.10

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.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/dist/frizz.js +536 -402
  3. package/package.json +2 -2
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,295 @@ 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 isLegacyStateRoot(root, exists) {
866
+ if (!exists(root)) return false;
867
+ if (!exists(join3(root, ".id")) && !exists(join3(root, ".gitignore"))) return true;
868
+ return exists(join3(root, "projects")) || exists(join3(root, "registry.json"));
869
+ }
870
+ function xdg(env, name) {
871
+ const value = env[name];
872
+ return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
873
+ }
874
+ function insideHome(child, home) {
875
+ const norm = (value) => value.replace(/[\\/]+$/u, "").replaceAll("\\", "/").toLowerCase();
876
+ const parent = norm(home);
877
+ const inner = norm(child);
878
+ return inner === parent || inner.startsWith(`${parent}/`);
879
+ }
880
+ function windowsRoots(env, home) {
881
+ const local = env.LOCALAPPDATA && insideHome(env.LOCALAPPDATA, home) ? env.LOCALAPPDATA : join3(home, "AppData", "Local");
882
+ const base = join3(local, "Frizz");
883
+ return {
884
+ data: join3(base, "Data"),
885
+ state: join3(base, "State"),
886
+ cache: join3(base, "Cache")
887
+ };
888
+ }
889
+ function darwinRoots(home) {
890
+ const support = join3(home, "Library", "Application Support", "Frizz");
891
+ return {
892
+ data: support,
893
+ // macOS has no state directory concept; Application Support is where this belongs, and the log
894
+ // files themselves already live under each project's own directory.
895
+ state: support,
896
+ cache: join3(home, "Library", "Caches", "Frizz")
897
+ };
898
+ }
899
+ function xdgRoots(env, home) {
900
+ return {
901
+ data: join3(xdg(env, "XDG_DATA_HOME") ?? join3(home, ".local", "share"), "frizz"),
902
+ state: join3(xdg(env, "XDG_STATE_HOME") ?? join3(home, ".local", "state"), "frizz"),
903
+ cache: join3(xdg(env, "XDG_CACHE_HOME") ?? join3(home, ".cache"), "frizz")
904
+ };
905
+ }
906
+ function frizzPaths(options2 = {}) {
907
+ const env = options2.env ?? process.env;
908
+ const platform = options2.platform ?? process.platform;
909
+ const home = options2.home ?? homedir2();
910
+ const exists = options2.exists ?? existsSync2;
911
+ const legacyRoot = legacyFrizzRoot(home);
912
+ if (isLegacyStateRoot(legacyRoot, exists)) {
913
+ return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
914
+ }
915
+ const explicit = {
916
+ data: xdg(env, "XDG_DATA_HOME"),
917
+ state: xdg(env, "XDG_STATE_HOME"),
918
+ cache: xdg(env, "XDG_CACHE_HOME")
919
+ };
920
+ const platformRoots = platform === "win32" ? windowsRoots(env, home) : platform === "darwin" ? darwinRoots(home) : xdgRoots(env, home);
921
+ return {
922
+ data: explicit.data ? join3(explicit.data, "frizz") : platformRoots.data,
923
+ state: explicit.state ? join3(explicit.state, "frizz") : platformRoots.state,
924
+ cache: explicit.cache ? join3(explicit.cache, "frizz") : platformRoots.cache,
925
+ legacy: false
926
+ };
927
+ }
928
+ function frizzRoots() {
929
+ return cached ??= frizzPaths();
930
+ }
931
+ function projectStateDir(projectId, home) {
932
+ return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "projects", projectId);
933
+ }
934
+ var LEGACY_DIR_NAME, cached;
935
+ var init_frizz_paths = __esm({
936
+ "packages/server/src/frizz-paths.ts"() {
937
+ "use strict";
938
+ LEGACY_DIR_NAME = ".frizz";
939
+ }
940
+ });
941
+
942
+ // packages/server/src/logging.ts
943
+ import {
944
+ appendFileSync,
945
+ mkdirSync as mkdirSync2,
946
+ openSync,
947
+ readdirSync,
948
+ rmSync,
949
+ statSync as statSync2,
950
+ symlinkSync,
951
+ writeFileSync as writeFileSync2,
952
+ writeSync
953
+ } from "node:fs";
954
+ import { homedir as homedir3 } from "node:os";
955
+ import { join as join4 } from "node:path";
956
+ function defaultLogRoot(stateDir, home = homedir3()) {
957
+ return stateDir ? join4(stateDir, "logs") : join4(frizzPaths({ home }).state, "logs");
958
+ }
959
+ function runStamp(at) {
960
+ const pad = (value) => String(value).padStart(2, "0");
961
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
962
+ }
963
+ function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir3(), env = process.env) {
964
+ const name = `frizz-${runStamp(at)}-${pid}.log`;
965
+ const override = env[LOG_PATH_ENV]?.trim();
966
+ if (override) return override.endsWith(".log") ? override : join4(override, name);
967
+ return join4(defaultLogRoot(stateDir, home), name);
968
+ }
969
+ function latestLogPath(dir) {
970
+ return join4(dir, "latest.log");
971
+ }
972
+ function linkLatest(dir, target2) {
973
+ const link = latestLogPath(dir);
974
+ try {
975
+ rmSync(link, { force: true });
976
+ symlinkSync(target2, link);
977
+ } catch {
978
+ try {
979
+ writeFileSync2(link, `${target2}
980
+ `, { mode: 384 });
981
+ } catch {
982
+ }
983
+ }
984
+ }
985
+ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
986
+ let entries;
987
+ try {
988
+ entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
989
+ } catch {
990
+ return;
991
+ }
992
+ const dated = entries.map((name) => {
993
+ try {
994
+ return { name, at: statSync2(join4(dir, name)).mtimeMs };
995
+ } catch {
996
+ return { name, at: 0 };
997
+ }
998
+ }).sort((a, b) => b.at - a.at);
999
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
1000
+ const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
1001
+ for (const entry of stale) {
1002
+ try {
1003
+ rmSync(join4(dir, entry.name), { force: true });
1004
+ } catch {
1005
+ }
1006
+ }
1007
+ }
1008
+ function formatDiskLine(record2) {
1009
+ const at = new Date(record2.at);
1010
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
1011
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
1012
+ return `${clock} ${record2.level.toUpperCase().padEnd(5)} ${record2.scope.padEnd(12)} ${record2.message}
1013
+ `;
1014
+ }
1015
+ function openLogFile(path) {
1016
+ try {
1017
+ mkdirSync2(join4(path, ".."), { recursive: true, mode: 448 });
1018
+ return openSync(path, "a", 384);
1019
+ } catch {
1020
+ return null;
1021
+ }
1022
+ }
1023
+ function createLogger(options2 = {}) {
1024
+ const path = options2.file === null ? null : options2.file ?? runLogPath(options2.stateDir);
1025
+ const level = options2.level ?? "debug";
1026
+ const now = options2.now ?? Date.now;
1027
+ const threshold = LEVEL_ORDER[level];
1028
+ const maxBytes = options2.maxBytes ?? MAX_LOG_BYTES;
1029
+ let fd = path === null ? null : openLogFile(path);
1030
+ if (fd !== null && path !== null && options2.owner !== false) {
1031
+ const dir = join4(path, "..");
1032
+ pruneRunLogs(dir);
1033
+ linkLatest(dir, path);
1034
+ }
1035
+ let written = 0;
1036
+ const listeners = /* @__PURE__ */ new Set();
1037
+ const emit = (recordLevel, scope, message) => {
1038
+ if (LEVEL_ORDER[recordLevel] < threshold) return;
1039
+ const record2 = { at: now(), level: recordLevel, scope, message };
1040
+ if (fd !== null) {
1041
+ try {
1042
+ const line = formatDiskLine(record2);
1043
+ if (written + line.length > maxBytes) {
1044
+ writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
1045
+ `);
1046
+ fd = null;
1047
+ } else {
1048
+ writeSync(fd, line);
1049
+ written += line.length;
1050
+ }
1051
+ } catch {
1052
+ fd = null;
1053
+ }
1054
+ }
1055
+ for (const listener of listeners) {
1056
+ try {
1057
+ listener(record2);
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ };
1062
+ return {
1063
+ debug: (scope, message) => emit("debug", scope, message),
1064
+ info: (scope, message) => emit("info", scope, message),
1065
+ warn: (scope, message) => emit("warn", scope, message),
1066
+ error: (scope, message) => emit("error", scope, message),
1067
+ get file() {
1068
+ return fd === null ? null : path;
1069
+ },
1070
+ onRecord(listener) {
1071
+ listeners.add(listener);
1072
+ return () => listeners.delete(listener);
1073
+ },
1074
+ close() {
1075
+ listeners.clear();
1076
+ fd = null;
1077
+ }
1078
+ };
1079
+ }
1080
+ function setAmbientLogger(logger2) {
1081
+ ambient = logger2;
1082
+ return logger2;
1083
+ }
1084
+ function formatFeedLine(record2) {
1085
+ return formatDiskLine(record2).trimEnd();
1086
+ }
1087
+ function attachTerminalMirror(logger2, enabled = process.env.FRIZZ_DEBUG === "1") {
1088
+ if (!enabled) return () => {
1089
+ };
1090
+ return logger2.onRecord((record2) => process.stderr.write(`${formatFeedLine(record2)}
1091
+ `));
1092
+ }
1093
+ function ambientLogger() {
1094
+ if (ambient) return ambient;
1095
+ const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
1096
+ const level = process.env[LOG_LEVEL_ENV];
1097
+ const logger2 = createLogger({
1098
+ ...inherited ? { file: inherited, owner: false } : { file: null },
1099
+ ...level && level in LEVEL_ORDER ? { level } : {}
1100
+ });
1101
+ attachTerminalMirror(logger2);
1102
+ ambient = logger2;
1103
+ return ambient;
1104
+ }
1105
+ function logEnvironment(logger2, level = "debug") {
1106
+ return logger2.file ? { [LOG_FILE_ENV]: logger2.file, [LOG_LEVEL_ENV]: level } : {};
1107
+ }
1108
+ var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
1109
+ var init_logging = __esm({
1110
+ "packages/server/src/logging.ts"() {
1111
+ "use strict";
1112
+ init_frizz_paths();
1113
+ LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
1114
+ LOG_FILE_ENV = "FRIZZ_LOG_FILE";
1115
+ LOG_LEVEL_ENV = "FRIZZ_LOG_LEVEL";
1116
+ LOG_PATH_ENV = "FRIZZ_LOG_PATH";
1117
+ RETAINED_RUNS = 20;
1118
+ RETAINED_DAYS = 14;
1119
+ MAX_LOG_BYTES = 32 * 1024 * 1024;
1120
+ ambient = null;
1121
+ log = {
1122
+ debug: (scope, message) => ambientLogger().debug(scope, message),
1123
+ info: (scope, message) => ambientLogger().info(scope, message),
1124
+ warn: (scope, message) => ambientLogger().warn(scope, message),
1125
+ error: (scope, message) => ambientLogger().error(scope, message)
1126
+ };
1127
+ }
1128
+ });
1129
+
1130
+ // packages/server/src/audit.ts
1131
+ import { AsyncLocalStorage } from "node:async_hooks";
1132
+ function encodeCallerStamp(stamp) {
1133
+ return Buffer.from(JSON.stringify(stamp)).toString("base64url");
1134
+ }
1135
+ var CALLER_HEADER, requestOrigin;
1136
+ var init_audit = __esm({
1137
+ "packages/server/src/audit.ts"() {
1138
+ "use strict";
1139
+ init_access_codes();
1140
+ init_frizz_paths();
1141
+ init_logging();
1142
+ CALLER_HEADER = "x-frizz-caller";
1143
+ requestOrigin = new AsyncLocalStorage();
1144
+ }
1145
+ });
1146
+
849
1147
  // node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
850
1148
  var require_identity = __commonJS({
851
1149
  "node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -12982,31 +13280,6 @@ var init_drainable_worker = __esm({
12982
13280
  }
12983
13281
  });
12984
13282
 
12985
- // packages/shared/src/frizz-md.ts
12986
- var FrizzMdLanding, FrizzMdAutonomy, FRIZZ_MD_NOTES_MAX, FrizzMdAnswers, FrizzMdStatus;
12987
- var init_frizz_md = __esm({
12988
- "packages/shared/src/frizz-md.ts"() {
12989
- "use strict";
12990
- init_zod();
12991
- FrizzMdLanding = external_exports.enum(["pull-request", "main", "main-push", "branch"]);
12992
- FrizzMdAutonomy = external_exports.enum(["decide", "balanced", "check-in"]);
12993
- FRIZZ_MD_NOTES_MAX = 8e3;
12994
- FrizzMdAnswers = external_exports.object({
12995
- landing: FrizzMdLanding,
12996
- autonomy: FrizzMdAutonomy,
12997
- notes: external_exports.string().max(FRIZZ_MD_NOTES_MAX)
12998
- }).strict();
12999
- FrizzMdStatus = external_exports.object({
13000
- /** A FRIZZ.md is already at the project root: the project has its rules, so nothing is asked. */
13001
- exists: external_exports.boolean(),
13002
- /** The operator skipped the questionnaire for this project: it is not asked again. */
13003
- skipped: external_exports.boolean(),
13004
- /** The branch the landing rules name — the remote's default, else the checked-out one, else `main`. */
13005
- defaultBranch: external_exports.string()
13006
- });
13007
- }
13008
- });
13009
-
13010
13283
  // packages/shared/src/receipt-bus.ts
13011
13284
  var init_receipt_bus = __esm({
13012
13285
  "packages/shared/src/receipt-bus.ts"() {
@@ -13254,7 +13527,7 @@ function requireEffortOutsideAcp(profile, ctx) {
13254
13527
  function fallbackPort(base) {
13255
13528
  return base + 1e4;
13256
13529
  }
13257
- var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
13530
+ var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, SteerThreadInput, SteerThreadResult, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
13258
13531
  var init_src = __esm({
13259
13532
  "packages/shared/src/index.ts"() {
13260
13533
  "use strict";
@@ -13267,7 +13540,6 @@ var init_src = __esm({
13267
13540
  init_code_fences();
13268
13541
  init_delta();
13269
13542
  init_drainable_worker();
13270
- init_frizz_md();
13271
13543
  init_interactions();
13272
13544
  init_receipt_bus();
13273
13545
  init_relay_protocol();
@@ -14566,6 +14838,19 @@ var init_src = __esm({
14566
14838
  // ambiguous, and only the operator decides to risk a second copy.
14567
14839
  supersedes: external_exports.string().min(1).max(200).optional()
14568
14840
  });
14841
+ SteerThreadInput = external_exports.object({
14842
+ from: ThreadSlug,
14843
+ to: ThreadSlug,
14844
+ message: external_exports.string().min(1).max(1e5)
14845
+ }).strict();
14846
+ SteerThreadResult = external_exports.object({
14847
+ slug: ThreadSlug,
14848
+ title: external_exports.string(),
14849
+ // What the target was doing when the message arrived: `running` ⇒ it reads it at its next tool
14850
+ // boundary; `resting` ⇒ the message started its next turn; `reopened` ⇒ it was marked done and this
14851
+ // message put it back on the board.
14852
+ delivery: external_exports.enum(["running", "resting", "reopened"])
14853
+ }).strict();
14569
14854
  DismissFailedFollowUpInput = external_exports.object({
14570
14855
  slug: ThreadSlug,
14571
14856
  deliveryId: external_exports.string().min(1).max(200)
@@ -15275,6 +15560,20 @@ ${DISPATCH_TASK_BANNER}
15275
15560
  // pairing, and `projectTranscriptPeerNames` uses this id to ask it. Never a drawer key on its own —
15276
15561
  // that is `peerDispatchId`, which the same pass can also supply once this resolves.
15277
15562
  peerSenderTaskId: external_exports.string().optional(),
15563
+ // ANOTHER TOP-LEVEL SESSION wrote this user turn — a thread messaging this one through Claude Code's
15564
+ // cross-session channel (see parseCrossSessionMessage), most often the coordinator thread that spawned
15565
+ // it. Not the human, and not a sub-agent either: the chat draws it as an expandable hairline naming
15566
+ // the sending thread, with the body one click away. `displayText` carries the unwrapped body.
15567
+ //
15568
+ // `name` is the sender's Claude Code session name, the fallback label. `thread` is the Frizz thread
15569
+ // behind it, filled in by the server from the sender's socket (session-peer.ts) — absent when the
15570
+ // sender is not a Frizz thread or is gone without having been resolved, and then the line names the
15571
+ // session without a link. `project` is the URL slug of the sender's project.
15572
+ sessionPeer: external_exports.object({
15573
+ name: external_exports.string().optional(),
15574
+ socket: external_exports.string().optional(),
15575
+ thread: external_exports.object({ project: external_exports.string().optional(), slug: external_exports.string(), title: external_exports.string() }).optional()
15576
+ }).optional(),
15278
15577
  // FRIZZ REFUSED the ```awaiting fence this message ends in — it named something that is not running,
15279
15578
  // or named nothing at all, or used a retired line kind (see `isParkCorrection`). The fence is not a
15280
15579
  // park, so the chat draws nothing for it: an hourglass card with a park button asserts a wait that
@@ -15384,8 +15683,9 @@ function responseJson(res, status, value) {
15384
15683
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
15385
15684
  res.end(JSON.stringify(value));
15386
15685
  }
15387
- function proxyHeaders(req, childPort, vouchSameOrigin = false) {
15686
+ function proxyHeaders(req, childPort, callerStamp, vouchSameOrigin = false) {
15388
15687
  const headers = { ...req.headers };
15688
+ headers[CALLER_HEADER] = callerStamp;
15389
15689
  headers.host = `127.0.0.1:${childPort}`;
15390
15690
  if (typeof headers.origin === "string") headers.origin = `http://127.0.0.1:${childPort}`;
15391
15691
  for (const name of FORWARDED_HEADERS) delete headers[name];
@@ -15402,6 +15702,15 @@ function readCookie(header, name) {
15402
15702
  }
15403
15703
  return void 0;
15404
15704
  }
15705
+ function firstHeader(value) {
15706
+ const first = Array.isArray(value) ? value[0] : value;
15707
+ return first?.trim() || void 0;
15708
+ }
15709
+ function isLoopbackAddress(address) {
15710
+ if (!address) return false;
15711
+ const v4 = address.startsWith("::ffff:") ? address.slice("::ffff:".length) : address;
15712
+ return v4.startsWith("127.") || address === "::1";
15713
+ }
15405
15714
  function isControlRequest(req) {
15406
15715
  const url = new URL(req.url ?? "/", "http://frizz.invalid");
15407
15716
  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 +15744,7 @@ var init_restart_supervisor = __esm({
15435
15744
  init_local_origin();
15436
15745
  init_local_image();
15437
15746
  init_supervisor_pages();
15747
+ init_audit();
15438
15748
  init_src();
15439
15749
  SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
15440
15750
  SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
@@ -15799,6 +16109,28 @@ var init_restart_supervisor = __esm({
15799
16109
  if (!this.publicOrigin) return true;
15800
16110
  return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
15801
16111
  }
16112
+ /**
16113
+ * What this proxy knows about who sent a request, for the application server's audit trail
16114
+ * (audit.ts). The session is named by its record id, never its value, and only once it verifies.
16115
+ *
16116
+ * `clientIp` is only ever taken from a request that arrived as the public origin, and it is that
16117
+ * origin's CLAIM: a relay or tunnel reaches this port from loopback, so the real visitor address
16118
+ * exists only in what the relay chose to forward (`cf-connecting-ip` from Cloudflare).
16119
+ */
16120
+ callerStamp(req) {
16121
+ const remote2 = req.socket.remoteAddress ?? null;
16122
+ const via = this.arrivedPublicly(req) ? "public" : isLoopbackAddress(remote2) ? "loopback" : "network";
16123
+ const host = via === "loopback" ? void 0 : firstHeader(req.headers.host);
16124
+ const forwarded = firstHeader(req.headers["x-forwarded-for"])?.split(",")[0]?.trim();
16125
+ const clientIp = via === "public" ? firstHeader(req.headers["cf-connecting-ip"]) ?? firstHeader(req.headers["x-real-ip"]) ?? forwarded : void 0;
16126
+ return encodeCallerStamp({
16127
+ via,
16128
+ remote: remote2,
16129
+ ...host ? { host: host.slice(0, 200) } : {},
16130
+ ...clientIp ? { clientIp: clientIp.slice(0, 200) } : {},
16131
+ session: this.access.sessionIdOf(readCookie(req.headers.cookie, SESSION_COOKIE))
16132
+ });
16133
+ }
15802
16134
  /** Did this request arrive through the public origin carrying a live session? Loopback never does. */
15803
16135
  carriesRemoteSession(req) {
15804
16136
  return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
@@ -15898,134 +16230,55 @@ var init_restart_supervisor = __esm({
15898
16230
  port: childPort,
15899
16231
  method: req.method,
15900
16232
  path: req.url,
15901
- headers: proxyHeaders(req, childPort, this.vouchesSameOrigin(req))
16233
+ headers: proxyHeaders(req, childPort, this.callerStamp(req), this.vouchesSameOrigin(req))
15902
16234
  }, (upstreamResponse) => {
15903
16235
  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";
16236
+ upstreamResponse.pipe(res);
16237
+ });
16238
+ upstream.once("error", () => {
16239
+ if (res.headersSent) return res.destroy();
16240
+ res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
16241
+ res.end(recoveryPage(req.url ?? "/", "unreachable"));
16242
+ });
16243
+ req.pipe(upstream);
16244
+ }
16245
+ handleUpgrade(req, socket, head) {
16246
+ if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
16247
+ rejectWebSocketUpgrade(socket, 401, "Unauthorized");
16248
+ return;
16249
+ }
16250
+ if (!this.authorityAccepted(req, false)) {
16251
+ socket.destroy();
16252
+ return;
16253
+ }
16254
+ const childPort = this.options.childPort();
16255
+ if (!childPort) {
16256
+ socket.destroy();
16257
+ return;
16258
+ }
16259
+ const upstream = connect(childPort, "127.0.0.1");
16260
+ for (const half of [socket, upstream]) {
16261
+ this.upgradedSockets.add(half);
16262
+ half.once("close", () => this.upgradedSockets.delete(half));
16263
+ }
16264
+ upstream.once("connect", () => {
16265
+ const headers = proxyHeaders(req, childPort, this.callerStamp(req));
16266
+ headers.connection = req.headers.connection ?? "Upgrade";
16267
+ const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
16268
+ for (const [name, value] of Object.entries(headers)) {
16269
+ if (value === void 0) continue;
16270
+ for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
16271
+ }
16272
+ upstream.write(`${lines.join("\r\n")}\r
16273
+ \r
16274
+ `);
16275
+ if (head.length > 0) upstream.write(head);
16276
+ socket.pipe(upstream).pipe(socket);
16277
+ });
16278
+ upstream.once("error", () => socket.destroy());
16279
+ socket.once("error", () => upstream.destroy());
16280
+ }
16281
+ };
16029
16282
  }
16030
16283
  });
16031
16284
 
@@ -19723,194 +19976,6 @@ var init_wrapper = __esm({
19723
19976
  }
19724
19977
  });
19725
19978
 
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
19979
  // packages/server/src/process-generation.ts
19915
19980
  import { execFileSync } from "node:child_process";
19916
19981
  import { randomUUID } from "node:crypto";
@@ -20946,7 +21011,7 @@ var init_boot_progress = __esm({
20946
21011
 
20947
21012
  // packages/server/src/project-root.ts
20948
21013
  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";
21014
+ 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
21015
  import { homedir as homedir8 } from "node:os";
20951
21016
  import { dirname as dirname8, join as join11, parse, resolve as resolve3 } from "node:path";
20952
21017
  function projectIdPath(root) {
@@ -21040,6 +21105,24 @@ function isHomeDirectory(dir, home = homedir8()) {
21040
21105
  return false;
21041
21106
  }
21042
21107
  }
21108
+ function isStateRootHome(dir, home = homedir8()) {
21109
+ return isHomeDirectory(dir, home) && frizzPaths({ home }).legacy;
21110
+ }
21111
+ function refuseStateRootHome(dir, home = homedir8()) {
21112
+ if (isStateRootHome(dir, home))
21113
+ throw new Error("Frizz keeps its own files in ~/.frizz on this machine, so the home folder cannot be a project here \u2014 choose a folder inside it.");
21114
+ }
21115
+ function isWorldWritable(dir, platform = process.platform) {
21116
+ if (platform === "win32") return false;
21117
+ try {
21118
+ return (statSync5(dir).mode & 2) !== 0;
21119
+ } catch {
21120
+ return false;
21121
+ }
21122
+ }
21123
+ function refuseWorldWritableProject(dir) {
21124
+ if (isWorldWritable(dir)) throw new Error(`A folder every account can write to cannot be a project: ${dir}`);
21125
+ }
21043
21126
  function existingProjectId(dir) {
21044
21127
  const fromFile = readProjectIdFile(dir);
21045
21128
  if (fromFile) return fromFile;
@@ -21065,7 +21148,7 @@ function discoverProjectRoot(cwd = process.cwd(), home = homedir8()) {
21065
21148
  const stop = resolve3(home);
21066
21149
  const filesystemRoot = parse(dir).root;
21067
21150
  for (let at = dir; ; at = dirname8(at)) {
21068
- if (at === stop || at === filesystemRoot) break;
21151
+ if (at === stop || at === filesystemRoot || isHomeDirectory(at, home)) break;
21069
21152
  if (existsSync4(projectIdPath(at))) return at;
21070
21153
  if (hasAny(at, REPO_MARKERS)) return at;
21071
21154
  if (hasAny(at, PROJECT_MARKERS)) return at;
@@ -21077,6 +21160,7 @@ var FRIZZ_DIR, ID_FILE, SELF_IGNORE, REPO_MARKERS, PROJECT_MARKERS;
21077
21160
  var init_project_root = __esm({
21078
21161
  "packages/server/src/project-root.ts"() {
21079
21162
  "use strict";
21163
+ init_frizz_paths();
21080
21164
  init_project_identity();
21081
21165
  FRIZZ_DIR = ".frizz";
21082
21166
  ID_FILE = ".id";
@@ -21115,11 +21199,13 @@ function readRegistry(home = homedir9()) {
21115
21199
  try {
21116
21200
  const parsed = JSON.parse(raw);
21117
21201
  if (parsed?.version !== 1 || !Array.isArray(parsed.projects)) return { ...EMPTY, projects: [] };
21202
+ const forgotten = Array.isArray(parsed.forgotten) ? parsed.forgotten.filter((id) => typeof id === "string") : [];
21118
21203
  return {
21119
21204
  version: 1,
21120
21205
  projects: parsed.projects.filter(
21121
21206
  (p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string"
21122
- )
21207
+ ),
21208
+ ...forgotten.length > 0 ? { forgotten } : {}
21123
21209
  };
21124
21210
  } catch {
21125
21211
  return { ...EMPTY, projects: [] };
@@ -21184,6 +21270,10 @@ function canonicalPath(dir) {
21184
21270
  function registerProject(input, home = homedir9()) {
21185
21271
  input = { ...input, dir: canonicalPath(input.dir) };
21186
21272
  const registry = readRegistry(home);
21273
+ if (registry.forgotten) {
21274
+ registry.forgotten = registry.forgotten.filter((id) => id !== input.id);
21275
+ if (registry.forgotten.length === 0) delete registry.forgotten;
21276
+ }
21187
21277
  const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
21188
21278
  const byId = registry.projects.find((p) => p.id === input.id);
21189
21279
  if (byId) {
@@ -23699,27 +23789,27 @@ function createAccessPane(options2) {
23699
23789
  }
23700
23790
 
23701
23791
  // src/remote-controller.ts
23702
- import { homedir as homedir5 } from "node:os";
23792
+ import { homedir as homedir6 } from "node:os";
23703
23793
 
23704
23794
  // src/cloud.ts
23705
23795
  init_frizz_paths();
23706
23796
  init_src();
23707
23797
  import { spawn as spawn2 } from "node:child_process";
23708
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, rmSync } from "node:fs";
23798
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "node:fs";
23709
23799
  import { createInterface } from "node:readline/promises";
23710
- import { homedir as homedir4 } from "node:os";
23711
- import { dirname as dirname4, join as join5 } from "node:path";
23800
+ import { homedir as homedir5 } from "node:os";
23801
+ import { dirname as dirname4, join as join6 } from "node:path";
23712
23802
 
23713
23803
  // src/identity.ts
23714
23804
  init_frizz_paths();
23715
23805
  init_src();
23716
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
23717
- import { homedir as homedir3 } from "node:os";
23718
- import { dirname as dirname3, join as join4 } from "node:path";
23719
- function claimIdentityPath(home = homedir3(), env) {
23720
- return join4(frizzPaths({ home, env }).state, "identity.key");
23806
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
23807
+ import { homedir as homedir4 } from "node:os";
23808
+ import { dirname as dirname3, join as join5 } from "node:path";
23809
+ function claimIdentityPath(home = homedir4(), env) {
23810
+ return join5(frizzPaths({ home, env }).state, "identity.key");
23721
23811
  }
23722
- async function loadOrCreateClaimIdentity(home = homedir3()) {
23812
+ async function loadOrCreateClaimIdentity(home = homedir4()) {
23723
23813
  const path = claimIdentityPath(home);
23724
23814
  let stored;
23725
23815
  try {
@@ -23735,8 +23825,8 @@ async function loadOrCreateClaimIdentity(home = homedir3()) {
23735
23825
  );
23736
23826
  }
23737
23827
  const identity = await generateClaimIdentity();
23738
- mkdirSync2(dirname3(path), { recursive: true });
23739
- writeFileSync2(path, await exportClaimPrivateKey(identity.privateKey), { mode: 384 });
23828
+ mkdirSync3(dirname3(path), { recursive: true });
23829
+ writeFileSync3(path, await exportClaimPrivateKey(identity.privateKey), { mode: 384 });
23740
23830
  return identity;
23741
23831
  }
23742
23832
 
@@ -24332,6 +24422,11 @@ function describeCloudConfig(config) {
24332
24422
  return "Cloudflare Tunnel";
24333
24423
  }
24334
24424
  var REGISTRAR_IS_LIVE = true;
24425
+ var CUSTOM_NAMES_OFFERED = false;
24426
+ function customNamesOffered(env = process.env) {
24427
+ return CUSTOM_NAMES_OFFERED || env.FRIZZ_CUSTOM_NAMES === "1";
24428
+ }
24429
+ 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
24430
  function isClaimedConfig(config) {
24336
24431
  return typeof config.claim === "string" && config.claim.length > 0;
24337
24432
  }
@@ -24345,19 +24440,24 @@ function zoneClaimLabel(hostname) {
24345
24440
  const label = hostname.slice(0, -suffix.length);
24346
24441
  return label.length > 0 && !label.includes(".") ? label : null;
24347
24442
  }
24348
- async function reconcileCloudConfig(config, port, home = homedir4(), onNotice, origin, github = githubDeviceFlow(onNotice ? { onPrompt: (prompt) => onNotice(deviceCodeMessage(prompt)) } : {})) {
24443
+ async function reconcileCloudConfig(config, port, home = homedir5(), onNotice, origin, github = githubDeviceFlow(onNotice ? { onPrompt: (prompt) => onNotice(deviceCodeMessage(prompt)) } : {})) {
24349
24444
  if (isRelayConfig(config)) return config;
24350
24445
  const label = config.claim ?? zoneClaimLabel(config.hostname);
24351
24446
  if (!label) return config;
24447
+ if (!isAnonymousClaimName(label) && !customNamesOffered()) {
24448
+ throw new Error(
24449
+ `${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`
24450
+ );
24451
+ }
24352
24452
  onNotice?.(
24353
24453
  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
24454
  );
24355
24455
  return establishCloudConfig(label, port, home, origin, github);
24356
24456
  }
24357
- function cloudConfigPath(home = homedir4()) {
24358
- return join5(frizzPaths({ home }).data, "cloud.json");
24457
+ function cloudConfigPath(home = homedir5()) {
24458
+ return join6(frizzPaths({ home }).data, "cloud.json");
24359
24459
  }
24360
- function readCloudConfig(home = homedir4()) {
24460
+ function readCloudConfig(home = homedir5()) {
24361
24461
  try {
24362
24462
  const parsed = JSON.parse(readFileSync3(cloudConfigPath(home), "utf8"));
24363
24463
  if (typeof parsed.hostname !== "string" || !parsed.hostname) return null;
@@ -24377,10 +24477,10 @@ function readCloudConfig(home = homedir4()) {
24377
24477
  return null;
24378
24478
  }
24379
24479
  }
24380
- function tunnelTokenPath(home = homedir4()) {
24381
- return join5(frizzPaths({ home }).state, "tunnel-token");
24480
+ function tunnelTokenPath(home = homedir5()) {
24481
+ return join6(frizzPaths({ home }).state, "tunnel-token");
24382
24482
  }
24383
- function readTunnelToken(home = homedir4()) {
24483
+ function readTunnelToken(home = homedir5()) {
24384
24484
  try {
24385
24485
  const token = readFileSync3(tunnelTokenPath(home), "utf8").trim();
24386
24486
  return token.length > 0 ? token : null;
@@ -24388,19 +24488,19 @@ function readTunnelToken(home = homedir4()) {
24388
24488
  return null;
24389
24489
  }
24390
24490
  }
24391
- function writeTunnelToken(token, home = homedir4()) {
24491
+ function writeTunnelToken(token, home = homedir5()) {
24392
24492
  const path = tunnelTokenPath(home);
24393
- mkdirSync3(dirname4(path), { recursive: true });
24394
- writeFileSync3(path, `${token}
24493
+ mkdirSync4(dirname4(path), { recursive: true });
24494
+ writeFileSync4(path, `${token}
24395
24495
  `, { mode: 384 });
24396
24496
  }
24397
- function deleteCloudConfig(home = homedir4()) {
24398
- rmSync(cloudConfigPath(home), { force: true });
24497
+ function deleteCloudConfig(home = homedir5()) {
24498
+ rmSync2(cloudConfigPath(home), { force: true });
24399
24499
  }
24400
- function writeCloudConfig(config, home = homedir4()) {
24500
+ function writeCloudConfig(config, home = homedir5()) {
24401
24501
  const path = cloudConfigPath(home);
24402
- mkdirSync3(dirname4(path), { recursive: true });
24403
- writeFileSync3(path, `${JSON.stringify(config, null, 2)}
24502
+ mkdirSync4(dirname4(path), { recursive: true });
24503
+ writeFileSync4(path, `${JSON.stringify(config, null, 2)}
24404
24504
  `);
24405
24505
  }
24406
24506
  function normalizeHostname(raw) {
@@ -24420,7 +24520,7 @@ async function promptForTunnelName(hostname, fallback) {
24420
24520
  rl.close();
24421
24521
  }
24422
24522
  }
24423
- async function establishCloudConfig(answer, port, home = homedir4(), origin, github = githubDeviceFlow()) {
24523
+ async function establishCloudConfig(answer, port, home = homedir5(), origin, github = githubDeviceFlow()) {
24424
24524
  if (answer.includes(".")) {
24425
24525
  const hostname = normalizeHostname(answer);
24426
24526
  const tunnel = await promptForTunnelName(hostname, hostname.split(".")[0]);
@@ -24432,6 +24532,7 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
24432
24532
  );
24433
24533
  }
24434
24534
  const anonymous = answer === "" || isAnonymousClaimName(answer);
24535
+ if (!anonymous && !customNamesOffered()) throw new Error(CUSTOM_NAMES_PAUSED);
24435
24536
  if (!anonymous && !claimNameIsValid(answer)) {
24436
24537
  normalizeClaimName(answer);
24437
24538
  }
@@ -24464,7 +24565,7 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
24464
24565
  }
24465
24566
  return { hostname: result.hostname, claim: name, serve: "relay" };
24466
24567
  }
24467
- async function resolveRunToken(config, port, home = homedir4(), onWarning, origin) {
24568
+ async function resolveRunToken(config, port, home = homedir5(), onWarning, origin) {
24468
24569
  if (!isClaimedConfig(config)) return null;
24469
24570
  const cached2 = readTunnelToken(home);
24470
24571
  try {
@@ -24481,12 +24582,12 @@ async function resolveRunToken(config, port, home = homedir4(), onWarning, origi
24481
24582
  throw error;
24482
24583
  }
24483
24584
  }
24484
- function resolveTunnelConfigPath(config, home = homedir4()) {
24585
+ function resolveTunnelConfigPath(config, home = homedir5()) {
24485
24586
  if (config.config) return config.config;
24486
- const candidate = join5(home, ".cloudflared", "frizz.yml");
24587
+ const candidate = join6(home, ".cloudflared", "frizz.yml");
24487
24588
  return existsSync3(candidate) ? candidate : null;
24488
24589
  }
24489
- async function startRelay(config, port, home = homedir4(), onStatus) {
24590
+ async function startRelay(config, port, home = homedir5(), onStatus) {
24490
24591
  const identity = await loadOrCreateClaimIdentity(home);
24491
24592
  const origin = `https://${config.hostname}`;
24492
24593
  return connectRelay({
@@ -24504,7 +24605,7 @@ async function startRelay(config, port, home = homedir4(), onStatus) {
24504
24605
  } : {}
24505
24606
  });
24506
24607
  }
24507
- function startTunnel(config, onExit, onError, home = homedir4(), runToken) {
24608
+ function startTunnel(config, onExit, onError, home = homedir5(), runToken) {
24508
24609
  const configPath = runToken ? null : resolveTunnelConfigPath(config, home);
24509
24610
  const args2 = runToken ? ["tunnel", "--no-autoupdate", "run", "--token", runToken] : [
24510
24611
  "tunnel",
@@ -24535,7 +24636,7 @@ function startTunnel(config, onExit, onError, home = homedir4(), runToken) {
24535
24636
 
24536
24637
  // src/remote-controller.ts
24537
24638
  function createRemoteController(options2) {
24538
- const home = options2.home ?? homedir5();
24639
+ const home = options2.home ?? homedir6();
24539
24640
  let config = null;
24540
24641
  let transport = null;
24541
24642
  const stopTransport = () => {
@@ -24650,6 +24751,15 @@ var CHOICES = [
24650
24751
  { kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
24651
24752
  { kind: "off", title: "Off", blurb: "loopback only" }
24652
24753
  ];
24754
+ function choicesFor(current) {
24755
+ if (customNamesOffered()) return CHOICES;
24756
+ if (kindOf(current) === "frizz") {
24757
+ return CHOICES.map(
24758
+ (choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom names are paused` } : choice
24759
+ );
24760
+ }
24761
+ return CHOICES.filter((choice) => choice.kind !== "frizz");
24762
+ }
24653
24763
  var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
24654
24764
  var BACKSPACE = /* @__PURE__ */ new Set(["\x7F", "\b"]);
24655
24765
  var ESC = "\x1B";
@@ -24690,6 +24800,8 @@ function createRemotePane(options2) {
24690
24800
  output.write(fitScreen([{ text: "", drop: 0 }, ...indented], output.rows, output.columns));
24691
24801
  };
24692
24802
  const check = (ok, text) => ok ? `${text} \u2713` : text;
24803
+ const choices = () => choicesFor(options2.current());
24804
+ const rowOf = (kind) => Math.max(0, choices().findIndex((choice) => choice.kind === kind));
24693
24805
  const paint = () => {
24694
24806
  if (!open) return;
24695
24807
  const s = screen;
@@ -24702,7 +24814,7 @@ function createRemotePane(options2) {
24702
24814
  "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
24815
  ),
24704
24816
  "",
24705
- ...CHOICES.map((choice, index) => {
24817
+ ...choices().map((choice, index) => {
24706
24818
  const marker = index === s.index ? "\u276F" : " ";
24707
24819
  const title = choice.title.padEnd(18);
24708
24820
  const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
@@ -24911,6 +25023,21 @@ function createRemotePane(options2) {
24911
25023
  }
24912
25024
  paint();
24913
25025
  };
25026
+ const serveCurrent = async () => {
25027
+ const current = options2.current();
25028
+ const back = { name: "menu", index: rowOf("frizz") };
25029
+ if (!current) return;
25030
+ screen = { name: "busy", message: `serving ${current.hostname}\u2026` };
25031
+ paint();
25032
+ try {
25033
+ await options2.apply(current);
25034
+ options2.onChanged?.(current);
25035
+ screen = { name: "done", message: `Serving https://${current.hostname} (${describeCloudConfig(current)}).`, link: options2.issueLink(), config: current };
25036
+ } catch (error) {
25037
+ screen = { name: "error", message: error instanceof Error ? error.message : String(error), back };
25038
+ }
25039
+ paint();
25040
+ };
24914
25041
  const turnOff = async () => {
24915
25042
  screen = { name: "busy", message: "back to loopback only\u2026" };
24916
25043
  paint();
@@ -24919,15 +25046,14 @@ function createRemotePane(options2) {
24919
25046
  options2.onChanged?.(null);
24920
25047
  screen = { name: "done", message: "Loopback only. This board is reachable from this machine alone.", link: null, config: null };
24921
25048
  } catch (error) {
24922
- screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: CHOICES.length - 1 } };
25049
+ screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: rowOf("off") } };
24923
25050
  }
24924
25051
  paint();
24925
25052
  };
24926
25053
  return {
24927
25054
  open() {
24928
25055
  open = true;
24929
- const current = kindOf(options2.current());
24930
- screen = { name: "menu", index: Math.max(0, CHOICES.findIndex((choice) => choice.kind === current)) };
25056
+ screen = { name: "menu", index: rowOf(kindOf(options2.current())) };
24931
25057
  output.write(ALT_SCREEN_ON);
24932
25058
  output.write(HIDE_CURSOR);
24933
25059
  paint();
@@ -24950,13 +25076,15 @@ function createRemotePane(options2) {
24950
25076
  }
24951
25077
  if (s.name === "menu") {
24952
25078
  if (key === ESC || key === "q") return "close";
24953
- if (key === UP || key === "k") s.index = (s.index + CHOICES.length - 1) % CHOICES.length;
24954
- else if (key === DOWN || key === "j") s.index = (s.index + 1) % CHOICES.length;
24955
- else if (/^[1-6]$/.test(key)) s.index = Number(key) - 1;
25079
+ const rows = choices();
25080
+ if (key === UP || key === "k") s.index = (s.index + rows.length - 1) % rows.length;
25081
+ else if (key === DOWN || key === "j") s.index = (s.index + 1) % rows.length;
25082
+ else if (/^[1-9]$/.test(key) && Number(key) <= rows.length) s.index = Number(key) - 1;
24956
25083
  else if (ENTER.has(key)) {
24957
- const choice = CHOICES[s.index];
25084
+ const choice = rows[Math.min(s.index, rows.length - 1)];
24958
25085
  if (choice.kind === "off") void turnOff();
24959
25086
  else if (choice.kind === "private") void claimPrivate();
25087
+ else if (choice.kind === "frizz" && !customNamesOffered()) void serveCurrent();
24960
25088
  else openForm(choice.kind);
24961
25089
  return "keep";
24962
25090
  }
@@ -24964,7 +25092,7 @@ function createRemotePane(options2) {
24964
25092
  return "keep";
24965
25093
  }
24966
25094
  if (key === ESC) {
24967
- screen = { name: "menu", index: CHOICES.findIndex((choice) => choice.kind === s.kind) };
25095
+ screen = { name: "menu", index: rowOf(s.kind) };
24968
25096
  paint();
24969
25097
  return "keep";
24970
25098
  }
@@ -25352,7 +25480,7 @@ var LAUNCH_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
25352
25480
  var LAUNCH_HARD_TIMEOUT_MS = BOOT_HARD_TIMEOUT_MS;
25353
25481
  var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
25354
25482
  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 or custom frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
25483
+ 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
25484
  }
25357
25485
  function parseCliArgs(argv) {
25358
25486
  const args2 = new Set(argv);
@@ -25518,19 +25646,26 @@ function cleanupSandbox(home) {
25518
25646
  } catch {
25519
25647
  }
25520
25648
  }
25649
+ function emptyLaunchMessage(intent) {
25650
+ return intent.reason === "home" ? "Frizz keeps its own files in ~/.frizz on this machine, so it cannot open your home directory as a project, and there is no other project to show yet. cd into another folder and run frizz there." : `A folder every account can write to cannot be a project: ${intent.directory}. Run frizz in a folder only you can write to.`;
25651
+ }
25521
25652
  function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
25522
25653
  const candidate = realpathSync5(discoverProjectRoot(cwd, home));
25523
25654
  const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
25524
- if ((known || hasProjectMarker(candidate)) && !isHomeDirectory(candidate, home))
25655
+ const isHome = isHomeDirectory(candidate, home);
25656
+ const refused = isWorldWritable(candidate) ? "world-writable" : isStateRootHome(candidate, home) ? "home" : void 0;
25657
+ if (!refused && (known || hasProjectMarker(candidate) && !isHome))
25525
25658
  return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25526
25659
  const host = mostRecentProject(home, env);
25527
- const reason = isHomeDirectory(candidate, home) ? "home" : "unadopted";
25528
- if (!host) return { kind: "empty", directory: candidate, reason };
25529
- return reason === "home" ? { kind: "grid", workspace: host } : { kind: "offer", workspace: host, directory: candidate };
25660
+ if (!host) {
25661
+ if (refused) return { kind: "empty", directory: candidate, reason: refused };
25662
+ return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25663
+ }
25664
+ return refused || isHome ? { kind: "grid", workspace: host } : { kind: "offer", workspace: host, directory: candidate };
25530
25665
  }
25531
25666
  function mostRecentProject(home, env) {
25532
25667
  for (const entry of listProjects(home)) {
25533
- if (entry.stale || isHomeDirectory(entry.path, home)) continue;
25668
+ if (entry.stale || isStateRootHome(entry.path, home) || isWorldWritable(entry.path)) continue;
25534
25669
  try {
25535
25670
  return resolveWorkspace(entry.path, home, env);
25536
25671
  } catch {
@@ -25553,6 +25688,8 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process
25553
25688
  gitRoot = void 0;
25554
25689
  }
25555
25690
  const root0 = realpathSync5(gitRoot ?? discoverProjectRoot(cwd, home));
25691
+ refuseWorldWritableProject(root0);
25692
+ refuseStateRootHome(root0, home);
25556
25693
  const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
25557
25694
  const root = identity?.root ?? root0;
25558
25695
  const id = ensureProjectIdFile(root, home, identity?.id);
@@ -27094,7 +27231,7 @@ import {
27094
27231
  realpathSync as realpathSync6,
27095
27232
  renameSync as renameSync6,
27096
27233
  rmSync as rmSync9,
27097
- statSync as statSync5,
27234
+ statSync as statSync6,
27098
27235
  writeFileSync as writeFileSync11
27099
27236
  } from "node:fs";
27100
27237
  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 +27275,7 @@ function containedFile(root, name, directory = false) {
27138
27275
  const rel = relative3(realpathSync6(root), path);
27139
27276
  if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute6(rel))
27140
27277
  throw new Error(`Frizz server artifact escapes its package: ${name}`);
27141
- const stat = statSync5(path);
27278
+ const stat = statSync6(path);
27142
27279
  if (directory ? !stat.isDirectory() : !stat.isFile()) throw new Error(`invalid Frizz server artifact: ${name}`);
27143
27280
  return path;
27144
27281
  }
@@ -27181,7 +27318,7 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
27181
27318
  } catch {
27182
27319
  }
27183
27320
  }
27184
- const found = candidates.find((path) => existsSync9(path) && statSync5(path).isFile());
27321
+ const found = candidates.find((path) => existsSync9(path) && statSync6(path).isFile());
27185
27322
  if (!found) throw new Error("npm was not found beside Node or on PATH; install Node with npm to install Frizz server releases");
27186
27323
  return resolve6(found);
27187
27324
  }
@@ -27563,7 +27700,7 @@ Options:
27563
27700
 
27564
27701
 
27565
27702
  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 custom one, a Cloudflare
27703
+ walkthrough sets up a private frizz.sh name (no account needed), a Cloudflare
27567
27704
  Tunnel, Tailscale, or a proxy of your own, and
27568
27705
  remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
27569
27706
  shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.`
@@ -27596,10 +27733,7 @@ var workspace = (() => {
27596
27733
  return workspaceFromLaunchTarget(pinned);
27597
27734
  }
27598
27735
  const intent = resolveLaunchIntent();
27599
- if (intent.kind === "empty")
27600
- 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.`
27602
- );
27736
+ if (intent.kind === "empty") throw new Error(emptyLaunchMessage(intent));
27603
27737
  launchIntent = intent;
27604
27738
  return intent.workspace;
27605
27739
  } catch (error) {