frizz 0.13.12 → 0.13.14
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 +21 -13
- package/dist/frizz.js +888 -233
- package/package.json +3 -2
package/dist/frizz.js
CHANGED
|
@@ -182,7 +182,7 @@ var init_local_origin = __esm({
|
|
|
182
182
|
|
|
183
183
|
// packages/server/src/access-codes.ts
|
|
184
184
|
import { randomBytes, timingSafeEqual, createHmac } from "node:crypto";
|
|
185
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
185
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
186
186
|
import { dirname, join as join2 } from "node:path";
|
|
187
187
|
function memorySessionDirectory(seed = []) {
|
|
188
188
|
const records = new Map(seed.map((r) => [r.id, r]));
|
|
@@ -227,7 +227,7 @@ function secretsMatch(a, b) {
|
|
|
227
227
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
228
228
|
}
|
|
229
229
|
function fileSessionDirectory(stateDir, now = Date.now) {
|
|
230
|
-
const path = join2(stateDir,
|
|
230
|
+
const path = join2(stateDir, SESSION_DIRECTORY_FILE);
|
|
231
231
|
let records = [];
|
|
232
232
|
try {
|
|
233
233
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -283,6 +283,39 @@ function loadOrCreateSessionKey(stateDir) {
|
|
|
283
283
|
}
|
|
284
284
|
return rotateSessionKey(stateDir);
|
|
285
285
|
}
|
|
286
|
+
function adoptSessionState(stateDir, others) {
|
|
287
|
+
if (existsSync(join2(stateDir, SESSION_KEY_FILE))) return void 0;
|
|
288
|
+
const usedAt = (dir) => {
|
|
289
|
+
try {
|
|
290
|
+
const key = statSync(join2(dir, SESSION_KEY_FILE));
|
|
291
|
+
if (!key.isFile() || key.size < 32) return void 0;
|
|
292
|
+
try {
|
|
293
|
+
return Math.max(key.mtimeMs, statSync(join2(dir, SESSION_DIRECTORY_FILE)).mtimeMs);
|
|
294
|
+
} catch {
|
|
295
|
+
return key.mtimeMs;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
let newest;
|
|
302
|
+
for (const dir of others) {
|
|
303
|
+
if (dir === stateDir) continue;
|
|
304
|
+
const at = usedAt(dir);
|
|
305
|
+
if (at !== void 0 && (!newest || at > newest.at)) newest = { dir, at };
|
|
306
|
+
}
|
|
307
|
+
if (!newest) return void 0;
|
|
308
|
+
mkdirSync(stateDir, { recursive: true });
|
|
309
|
+
for (const name of [SESSION_EPOCH_FILE, SESSION_DIRECTORY_FILE, SESSION_KEY_FILE]) {
|
|
310
|
+
try {
|
|
311
|
+
copyFileSync(join2(newest.dir, name), join2(stateDir, name));
|
|
312
|
+
chmodSync(join2(stateDir, name), 384);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (name === SESSION_KEY_FILE) throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return newest.dir;
|
|
318
|
+
}
|
|
286
319
|
function rotateSessionKey(stateDir) {
|
|
287
320
|
const key = randomBytes(32);
|
|
288
321
|
writeSessionKey(join2(stateDir, SESSION_KEY_FILE), key);
|
|
@@ -327,7 +360,7 @@ function loadSessionState(stateDir, epoch = SESSION_EPOCH) {
|
|
|
327
360
|
const signedOut = advance.advanced ? directory.revokeAll() : 0;
|
|
328
361
|
return { key, directory, rotateKey: () => rotateSessionKey(stateDir), advance, signedOut };
|
|
329
362
|
}
|
|
330
|
-
var CODE_BYTES, SESSION_BYTES, SESSION_ID_BYTES, DEFAULT_CODE_TTL_MS, DEFAULT_SESSION_TTL_MS, SESSION_EPOCH, AccessStore, SESSION_KEY_FILE, SESSION_EPOCH_FILE;
|
|
363
|
+
var CODE_BYTES, SESSION_BYTES, SESSION_ID_BYTES, DEFAULT_CODE_TTL_MS, DEFAULT_SESSION_TTL_MS, SESSION_EPOCH, AccessStore, SESSION_KEY_FILE, SESSION_EPOCH_FILE, SESSION_DIRECTORY_FILE;
|
|
331
364
|
var init_access_codes = __esm({
|
|
332
365
|
"packages/server/src/access-codes.ts"() {
|
|
333
366
|
"use strict";
|
|
@@ -504,6 +537,7 @@ var init_access_codes = __esm({
|
|
|
504
537
|
};
|
|
505
538
|
SESSION_KEY_FILE = "session-key";
|
|
506
539
|
SESSION_EPOCH_FILE = "session-epoch";
|
|
540
|
+
SESSION_DIRECTORY_FILE = "sessions.json";
|
|
507
541
|
}
|
|
508
542
|
});
|
|
509
543
|
|
|
@@ -518,7 +552,7 @@ var init_local_path = __esm({
|
|
|
518
552
|
});
|
|
519
553
|
|
|
520
554
|
// packages/server/src/local-image.ts
|
|
521
|
-
import { createReadStream, realpathSync, statSync } from "node:fs";
|
|
555
|
+
import { createReadStream, realpathSync, statSync as statSync2 } from "node:fs";
|
|
522
556
|
import { extname, isAbsolute } from "node:path";
|
|
523
557
|
function resolveLocalImage(rawPath, range) {
|
|
524
558
|
if (rawPath) rawPath = normalizeLocalPath(rawPath);
|
|
@@ -533,7 +567,7 @@ function resolveLocalImage(rawPath, range) {
|
|
|
533
567
|
}
|
|
534
568
|
let size;
|
|
535
569
|
try {
|
|
536
|
-
const stat =
|
|
570
|
+
const stat = statSync2(real);
|
|
537
571
|
if (!stat.isFile()) return { status: 404 };
|
|
538
572
|
size = stat.size;
|
|
539
573
|
} catch {
|
|
@@ -944,7 +978,7 @@ import {
|
|
|
944
978
|
openSync,
|
|
945
979
|
readdirSync,
|
|
946
980
|
rmSync,
|
|
947
|
-
statSync as
|
|
981
|
+
statSync as statSync3,
|
|
948
982
|
symlinkSync,
|
|
949
983
|
writeFileSync as writeFileSync2,
|
|
950
984
|
writeSync
|
|
@@ -989,7 +1023,7 @@ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Dat
|
|
|
989
1023
|
}
|
|
990
1024
|
const dated = entries.map((name) => {
|
|
991
1025
|
try {
|
|
992
|
-
return { name, at:
|
|
1026
|
+
return { name, at: statSync3(join4(dir, name)).mtimeMs };
|
|
993
1027
|
} catch {
|
|
994
1028
|
return { name, at: 0 };
|
|
995
1029
|
}
|
|
@@ -13111,6 +13145,15 @@ var init_interactions = __esm({
|
|
|
13111
13145
|
}
|
|
13112
13146
|
});
|
|
13113
13147
|
|
|
13148
|
+
// packages/shared/src/launch-prefix.ts
|
|
13149
|
+
var LAUNCH_PREFIX_MAX_LENGTH;
|
|
13150
|
+
var init_launch_prefix = __esm({
|
|
13151
|
+
"packages/shared/src/launch-prefix.ts"() {
|
|
13152
|
+
"use strict";
|
|
13153
|
+
LAUNCH_PREFIX_MAX_LENGTH = 2048;
|
|
13154
|
+
}
|
|
13155
|
+
});
|
|
13156
|
+
|
|
13114
13157
|
// packages/shared/src/claim.ts
|
|
13115
13158
|
function isAnonymousClaimName(name) {
|
|
13116
13159
|
return ANONYMOUS_CLAIM_PATTERN.test(name);
|
|
@@ -13264,6 +13307,17 @@ var init_code_fences = __esm({
|
|
|
13264
13307
|
}
|
|
13265
13308
|
});
|
|
13266
13309
|
|
|
13310
|
+
// packages/shared/src/question-fence.ts
|
|
13311
|
+
var import_yaml;
|
|
13312
|
+
var init_question_fence = __esm({
|
|
13313
|
+
"packages/shared/src/question-fence.ts"() {
|
|
13314
|
+
"use strict";
|
|
13315
|
+
import_yaml = __toESM(require_dist(), 1);
|
|
13316
|
+
init_code_fences();
|
|
13317
|
+
init_src();
|
|
13318
|
+
}
|
|
13319
|
+
});
|
|
13320
|
+
|
|
13267
13321
|
// packages/shared/src/delta.ts
|
|
13268
13322
|
var init_delta = __esm({
|
|
13269
13323
|
"packages/shared/src/delta.ts"() {
|
|
@@ -13468,16 +13522,18 @@ function signoffNudgeText(needsInput) {
|
|
|
13468
13522
|
"the write-up.",
|
|
13469
13523
|
"",
|
|
13470
13524
|
"**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
|
|
13471
|
-
"right now:
|
|
13525
|
+
"right now: ask that one in a ```question fence. Every other open choice INSIDE the task \u2014 a name, a",
|
|
13472
13526
|
"default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
|
|
13473
13527
|
"what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
|
|
13474
13528
|
"unanswered question is not permission to go build the answer.",
|
|
13475
13529
|
"",
|
|
13476
|
-
"Otherwise, sign off \u2014 a
|
|
13530
|
+
"Otherwise, sign off \u2014 a question in your prose, or a fence at the END of your next message:",
|
|
13477
13531
|
"",
|
|
13478
|
-
"-
|
|
13479
|
-
"
|
|
13480
|
-
"
|
|
13532
|
+
"- `` ```question `` \u2014 you need the human's decision. Write each fence right under the paragraph it",
|
|
13533
|
+
" decides, never stacked at the bottom: `id:` (a slug of your choosing), `kind:` (`choice`, `multi`,",
|
|
13534
|
+
" `text` or `secret`), a one-line `question:`, and `options:` with one-line trade-offs, the recommended",
|
|
13535
|
+
" one first. Frizz registers it when the message lands, and a question asked at this rest is its",
|
|
13536
|
+
" sign-off: rest normally.",
|
|
13481
13537
|
"- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
|
|
13482
13538
|
" if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
|
|
13483
13539
|
" **bolded verb phrase**.",
|
|
@@ -13485,24 +13541,24 @@ function signoffNudgeText(needsInput) {
|
|
|
13485
13541
|
"",
|
|
13486
13542
|
" Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
|
|
13487
13543
|
" A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
|
|
13488
|
-
" awaiting, you are done. Register a PR with `
|
|
13489
|
-
" `
|
|
13544
|
+
" awaiting, you are done. Register a PR with `frizzctl watch-pr add` and a timer with",
|
|
13545
|
+
" `frizzctl timer set`; `frizzctl activity` reads back everything you have running, with its id.",
|
|
13490
13546
|
"- `` ```awaiting `` with `steps:` \u2014 the human must PERFORM something you cannot: sign in, approve,",
|
|
13491
13547
|
" merge, press a button you may not. List each step as a `- ` line under `steps:`, written to be",
|
|
13492
13548
|
" followed cold: a real Markdown link to every page and file it names, every command in a code span,",
|
|
13493
13549
|
" every value the act needs written in. Frizz reads them verbatim. Steps name the HUMAN as the wait,",
|
|
13494
13550
|
" so the fence needs no other name and no `for:`, and the thread goes into their queue. Their Done",
|
|
13495
13551
|
" comes back to you as their reply; anything else they need to say comes as a message of their own.",
|
|
13496
|
-
"-
|
|
13497
|
-
"
|
|
13498
|
-
"
|
|
13499
|
-
"
|
|
13552
|
+
"- A question you asked at an EARLIER rest, whose answer you still need: write a ```question fence",
|
|
13553
|
+
" carrying its `id:` alone. That draws its card in this message and keeps the thread in the human's",
|
|
13554
|
+
" queue. One you no longer need, withdraw with `frizzctl unask <id>`; one you leave unmentioned stays",
|
|
13555
|
+
" answerable where you asked it.",
|
|
13500
13556
|
"",
|
|
13501
13557
|
"**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
|
|
13502
13558
|
"draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
|
|
13503
|
-
"the part that is someone else's to do. Each ends on a question with your recommendation as option
|
|
13559
|
+
"the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
|
|
13504
13560
|
"or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
|
|
13505
|
-
"`
|
|
13561
|
+
"`frizzctl spawn-thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
|
|
13506
13562
|
"None of them is a `done`. And what is not",
|
|
13507
13563
|
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
13508
13564
|
"in the handoff.",
|
|
@@ -13522,23 +13578,37 @@ function requireEffortOutsideAcp(profile, ctx) {
|
|
|
13522
13578
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["effort"], message: `effort is required for a ${profile.backend} profile` });
|
|
13523
13579
|
}
|
|
13524
13580
|
}
|
|
13581
|
+
function formatElapsed(ms) {
|
|
13582
|
+
if (!Number.isFinite(ms) || ms < 0) return "an unknown time";
|
|
13583
|
+
const s = Math.floor(ms / 1e3);
|
|
13584
|
+
if (s < 60) return `${s}s`;
|
|
13585
|
+
const m = Math.floor(s / 60);
|
|
13586
|
+
if (m < 60) return `${m}m`;
|
|
13587
|
+
const h = Math.floor(m / 60);
|
|
13588
|
+
if (h < 24) return m % 60 === 0 ? `${h}h` : `${h}h ${m % 60}m`;
|
|
13589
|
+
const d = Math.floor(h / 24);
|
|
13590
|
+
return h % 24 === 0 ? `${d}d` : `${d}d ${h % 24}h`;
|
|
13591
|
+
}
|
|
13525
13592
|
function fallbackPort(base) {
|
|
13526
13593
|
return base + 1e4;
|
|
13527
13594
|
}
|
|
13528
|
-
var
|
|
13595
|
+
var import_yaml2, 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, CommandWatchRun, MonitorStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, COMMAND_WATCH_EVERY_MAX_MS, COMMAND_WATCH_FOR_MAX_MS, COMMAND_WATCH_COMMAND_MAX, COMMAND_WATCH_RUN_TIMEOUT_MS, CommandWatchState, CommandWatchView, AddOwnCommandWatchInput, AddOwnCommandWatchResult, DropOwnCommandWatchInput, DropOwnCommandWatchResult, ListOwnCommandWatchesInput, OwnCommandWatchesResult, MONITOR_FLOOD_MAX_EVENTS, MONITOR_FLOOD_WINDOW_MS, MONITOR_DESCRIPTION_MAX, AddOwnMonitorInput, AddOwnMonitorResult, MONITOR_FLOOD_REASON, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, QuestionFenceFaultView, ClosedQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, ANSWER_ROW_ID_PATTERN, ANSWER_ROW_ID, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, BoardState, 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, SpawnThreadInput, SpawnThreadResult, SpawnChildInput, SpawnChildResult, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, ReorderPinnedThreadsInput, SetThreadConsolePinnedInput, ReorderConsolePinnedThreadsInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, ThreadBand, ThreadListing, ListThreadsInput, ListThreadsResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, McpTarget, McpServerState, McpServerReading, McpStatusResult, McpSignInInput, McpSignInState, McpSignInResult, McpSignInAttemptInput, McpSignInPollResult, McpSignInFinishInput, 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, GithubAccount, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, BoardDeltaEvent, ServerEvent, BoardDeltasInput, BoardDeltasResult, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketBoardClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
|
|
13529
13596
|
var init_src = __esm({
|
|
13530
13597
|
"packages/shared/src/index.ts"() {
|
|
13531
13598
|
"use strict";
|
|
13532
|
-
|
|
13599
|
+
import_yaml2 = __toESM(require_dist(), 1);
|
|
13533
13600
|
init_zod();
|
|
13534
13601
|
init_interactions();
|
|
13602
|
+
init_launch_prefix();
|
|
13535
13603
|
init_thread_slug();
|
|
13536
13604
|
init_claim();
|
|
13537
13605
|
init_claude_editions();
|
|
13538
13606
|
init_code_fences();
|
|
13607
|
+
init_question_fence();
|
|
13539
13608
|
init_delta();
|
|
13540
13609
|
init_drainable_worker();
|
|
13541
13610
|
init_interactions();
|
|
13611
|
+
init_launch_prefix();
|
|
13542
13612
|
init_receipt_bus();
|
|
13543
13613
|
init_relay_protocol();
|
|
13544
13614
|
init_shell_writes();
|
|
@@ -13863,7 +13933,7 @@ var init_src = __esm({
|
|
|
13863
13933
|
questions: external_exports.array(AskQuestion)
|
|
13864
13934
|
});
|
|
13865
13935
|
AwaitingHint = external_exports.object({
|
|
13866
|
-
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title", "status", "needs_input", "step", "question"]),
|
|
13936
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "command", "thread", "for", "title", "status", "needs_input", "step", "question"]),
|
|
13867
13937
|
value: external_exports.string()
|
|
13868
13938
|
});
|
|
13869
13939
|
RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
|
|
@@ -13904,7 +13974,7 @@ var init_src = __esm({
|
|
|
13904
13974
|
SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
|
|
13905
13975
|
SIGNOFF_NUDGE_MESSAGE = signoffNudgeText(false);
|
|
13906
13976
|
SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT = signoffNudgeText(true);
|
|
13907
|
-
ThreadWatchKind = external_exports.enum(["shell", "github", "timer"]);
|
|
13977
|
+
ThreadWatchKind = external_exports.enum(["shell", "github", "timer", "command"]);
|
|
13908
13978
|
GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
|
|
13909
13979
|
GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
|
|
13910
13980
|
GithubWatchStatus = external_exports.object({
|
|
@@ -13942,7 +14012,16 @@ var init_src = __esm({
|
|
|
13942
14012
|
/** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
|
|
13943
14013
|
* same job, or a slower job going red after the first — both of which are a second failure the worker
|
|
13944
14014
|
* must hear about. Empty when nothing is failing. */
|
|
13945
|
-
failureSig: external_exports.string().optional()
|
|
14015
|
+
failureSig: external_exports.string().optional(),
|
|
14016
|
+
/** WHO THE PR IS WAITING ON, the facts behind "will anything move here unless we act?"
|
|
14017
|
+
* (awaiting.prWaitsOnNoOne). Whether GitHub will merge it by itself (`queued` in a merge queue, `auto`
|
|
14018
|
+
* with auto-merge on, `none`), whether the signed-in `gh` account opened it and may merge it, and
|
|
14019
|
+
* whether a review is still requested of someone. Absent means unknown: the `gh` fallback and a
|
|
14020
|
+
* reading taken before 2026-10-27 carry none, and an unknown never counts as "nobody". */
|
|
14021
|
+
mergePath: external_exports.enum(["none", "queued", "auto"]).optional(),
|
|
14022
|
+
viewerAuthored: external_exports.boolean().optional(),
|
|
14023
|
+
viewerCanMerge: external_exports.boolean().optional(),
|
|
14024
|
+
reviewRequested: external_exports.boolean().optional()
|
|
13946
14025
|
}).strict();
|
|
13947
14026
|
GithubIssueStatus = external_exports.object({
|
|
13948
14027
|
/** OPEN | CLOSED, lowercased. A closed issue ends the wait outright. */
|
|
@@ -13958,6 +14037,20 @@ var init_src = __esm({
|
|
|
13958
14037
|
/** When frizz last heard from GitHub, for the same reason `GithubWatchStatus.polledAt` carries it. */
|
|
13959
14038
|
polledAt: external_exports.string()
|
|
13960
14039
|
}).strict();
|
|
14040
|
+
CommandWatchRun = external_exports.object({
|
|
14041
|
+
at: external_exports.string(),
|
|
14042
|
+
outcome: external_exports.enum(["exit", "timeout", "signal", "error"]),
|
|
14043
|
+
exitCode: external_exports.number().int().nullable(),
|
|
14044
|
+
output: external_exports.string()
|
|
14045
|
+
}).strict();
|
|
14046
|
+
MonitorStatus = external_exports.object({
|
|
14047
|
+
/** The worker's own words for what it watches ("errors in deploy.log"), when it gave any. */
|
|
14048
|
+
description: external_exports.string().optional(),
|
|
14049
|
+
/** Events printed so far: each batch of lines that woke (or will wake) the thread is one. */
|
|
14050
|
+
events: external_exports.number().int().nonnegative(),
|
|
14051
|
+
/** The newest event's text, trimmed, and when it was printed. */
|
|
14052
|
+
lastEvent: external_exports.object({ at: external_exports.string(), text: external_exports.string() }).strict().optional()
|
|
14053
|
+
}).strict();
|
|
13961
14054
|
ThreadWatchView = external_exports.object({
|
|
13962
14055
|
id: external_exports.string(),
|
|
13963
14056
|
kind: ThreadWatchKind,
|
|
@@ -13979,7 +14072,22 @@ var init_src = __esm({
|
|
|
13979
14072
|
* worker's prompt is the row's NAME (a `tmr_…` id names nothing to a human) and the fire instant is
|
|
13980
14073
|
* its status. Unlike `github` there is no polled half to be absent: a timer that exists is fully
|
|
13981
14074
|
* known, so a timer row always carries this. */
|
|
13982
|
-
timer: external_exports.object({ fireAt: external_exports.string(), prompt: external_exports.string() }).strict().optional()
|
|
14075
|
+
timer: external_exports.object({ fireAt: external_exports.string(), prompt: external_exports.string() }).strict().optional(),
|
|
14076
|
+
/** `command` rows only: the armed command watch's registration and its last check — what the worker's
|
|
14077
|
+
* own read-back carries (CommandWatchView), minus the id and state the row already has. The command is
|
|
14078
|
+
* the row's NAME and the cadence its status. Until 2026-10-13 a command watch had no row at all, so a
|
|
14079
|
+
* thread parked on nothing else drew a card that said only "Waiting for an external update." */
|
|
14080
|
+
command: external_exports.object({
|
|
14081
|
+
command: external_exports.string(),
|
|
14082
|
+
/** The cadence in the house duration grammar (`2m`, `1h 30m`). */
|
|
14083
|
+
every: external_exports.string(),
|
|
14084
|
+
expiresAt: external_exports.string(),
|
|
14085
|
+
runs: external_exports.number().int().nonnegative(),
|
|
14086
|
+
lastRun: CommandWatchRun.optional(),
|
|
14087
|
+
/** A MONITOR's own reading (`frizzctl monitor`), and present on nothing else: the row is a running
|
|
14088
|
+
* script, so its status is how many events it has printed, not a cadence. */
|
|
14089
|
+
monitor: MonitorStatus.optional()
|
|
14090
|
+
}).strict().optional()
|
|
13983
14091
|
}).strict();
|
|
13984
14092
|
PR_WATCH_FOR_MAX_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
13985
14093
|
PR_WATCH_DEFAULT_FOR_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -14021,7 +14129,7 @@ var init_src = __esm({
|
|
|
14021
14129
|
* still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
|
|
14022
14130
|
* is better bounded than broken. */
|
|
14023
14131
|
for: external_exports.string().trim().min(1).max(16).optional(),
|
|
14024
|
-
/** What `target` names. `issue` (from `
|
|
14132
|
+
/** What `target` names. `issue` (from `frizzctl watch-issue`, 2026-09-14) registers a watcher on a
|
|
14025
14133
|
* GitHub issue — same registry, same poll cadence, same drop verb, but a different probe (`gh issue
|
|
14026
14134
|
* view`), a different GraphQL fragment and a report with no CI in it. ABSENT MEANS A PULL REQUEST,
|
|
14027
14135
|
* for the same reason `for` is optional on the wire: every `watch_pr` binary dispatched before this
|
|
@@ -14109,6 +14217,98 @@ var init_src = __esm({
|
|
|
14109
14217
|
dropped: external_exports.boolean(),
|
|
14110
14218
|
watches: external_exports.array(OwnWatchView)
|
|
14111
14219
|
}).strict();
|
|
14220
|
+
COMMAND_WATCH_EVERY_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
14221
|
+
COMMAND_WATCH_FOR_MAX_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14222
|
+
COMMAND_WATCH_COMMAND_MAX = 4e3;
|
|
14223
|
+
COMMAND_WATCH_RUN_TIMEOUT_MS = 2 * 6e4;
|
|
14224
|
+
CommandWatchState = external_exports.enum(["armed", "dropped", "passed", "expired", "cancelled"]);
|
|
14225
|
+
CommandWatchView = external_exports.object({
|
|
14226
|
+
id: external_exports.string(),
|
|
14227
|
+
command: external_exports.string(),
|
|
14228
|
+
/** The cadence in the house duration grammar (`5m`, `1h 30m`). */
|
|
14229
|
+
every: external_exports.string(),
|
|
14230
|
+
state: CommandWatchState,
|
|
14231
|
+
createdAt: external_exports.string(),
|
|
14232
|
+
expiresAt: external_exports.string(),
|
|
14233
|
+
/** How many checks have run since it was armed, the registration's own first check included. */
|
|
14234
|
+
runs: external_exports.number().int().nonnegative(),
|
|
14235
|
+
lastRun: CommandWatchRun.optional(),
|
|
14236
|
+
/** Present on a MONITOR (`frizzctl monitor`) and nothing else: a script the server keeps RUNNING, whose
|
|
14237
|
+
* every printed line is a wake. A monitor has no cadence and no checks, so `every` reads `0s` and
|
|
14238
|
+
* `runs` 0 on one; read this instead. */
|
|
14239
|
+
monitor: MonitorStatus.optional()
|
|
14240
|
+
}).strict();
|
|
14241
|
+
AddOwnCommandWatchInput = external_exports.object({
|
|
14242
|
+
slug: ThreadSlug,
|
|
14243
|
+
/** Run with `/bin/sh -c` in the project root, with the server's environment. Exit status 0 is "passed". */
|
|
14244
|
+
command: external_exports.string().trim().min(1).max(COMMAND_WATCH_COMMAND_MAX),
|
|
14245
|
+
/** How often to check, as a DURATION (`5m`; parseAwaitingDurationRaw). Clamped to [1m, 24h] and reported. */
|
|
14246
|
+
every: external_exports.string().trim().min(1).max(16),
|
|
14247
|
+
/** How long to keep checking, as a DURATION. Clamped to COMMAND_WATCH_FOR_MAX_MS and reported. */
|
|
14248
|
+
for: external_exports.string().trim().min(1).max(16)
|
|
14249
|
+
}).strict();
|
|
14250
|
+
AddOwnCommandWatchResult = external_exports.object({
|
|
14251
|
+
/** Present when the call was REFUSED — the thread may not run commands outside the CLI's permission
|
|
14252
|
+
* system, or the command cannot run here at all. A refusal is an answer, not a fault: the text says
|
|
14253
|
+
* why and what to use instead, and nothing was stored. */
|
|
14254
|
+
refused: external_exports.string().optional(),
|
|
14255
|
+
/** The armed row's id. Absent when nothing was armed: a refusal, or a first check that already passed. */
|
|
14256
|
+
id: external_exports.string().optional(),
|
|
14257
|
+
/** This exact command was already watched on this thread, so the call registered nothing new. */
|
|
14258
|
+
alreadyArmed: external_exports.boolean().default(false),
|
|
14259
|
+
/** The FIRST CHECK, run before anything was stored. Exit 0 there means the condition already holds:
|
|
14260
|
+
* nothing is armed, and the worker proceeds now rather than waiting a cadence to be told so. */
|
|
14261
|
+
firstRun: CommandWatchRun.optional(),
|
|
14262
|
+
passed: external_exports.boolean().default(false),
|
|
14263
|
+
expiresAt: external_exports.string().optional(),
|
|
14264
|
+
every: external_exports.string().optional(),
|
|
14265
|
+
/** The `every` / `for` the worker wrote, present ONLY when it was clamped — a clamp nobody is told about
|
|
14266
|
+
* is a worker resting on coverage it does not have. */
|
|
14267
|
+
clamped: external_exports.object({ every: external_exports.string().optional(), for: external_exports.string().optional() }).strict().optional(),
|
|
14268
|
+
watches: external_exports.array(CommandWatchView)
|
|
14269
|
+
}).strict();
|
|
14270
|
+
DropOwnCommandWatchInput = external_exports.object({
|
|
14271
|
+
slug: ThreadSlug,
|
|
14272
|
+
id: external_exports.string().min(1).max(64)
|
|
14273
|
+
}).strict();
|
|
14274
|
+
DropOwnCommandWatchResult = external_exports.object({
|
|
14275
|
+
dropped: external_exports.boolean(),
|
|
14276
|
+
watches: external_exports.array(CommandWatchView)
|
|
14277
|
+
}).strict();
|
|
14278
|
+
ListOwnCommandWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
14279
|
+
OwnCommandWatchesResult = external_exports.object({ watches: external_exports.array(CommandWatchView) }).strict();
|
|
14280
|
+
MONITOR_FLOOD_MAX_EVENTS = 30;
|
|
14281
|
+
MONITOR_FLOOD_WINDOW_MS = 10 * 6e4;
|
|
14282
|
+
MONITOR_DESCRIPTION_MAX = 120;
|
|
14283
|
+
AddOwnMonitorInput = external_exports.object({
|
|
14284
|
+
slug: ThreadSlug,
|
|
14285
|
+
/** Run with `/bin/sh -c` in the project root, with the server's environment. Each stdout line is an event. */
|
|
14286
|
+
command: external_exports.string().trim().min(1).max(COMMAND_WATCH_COMMAND_MAX),
|
|
14287
|
+
/** What it watches, in the worker's words — the name on its row and drawer, and the label on every wake.
|
|
14288
|
+
* Required (2026-11-03), as a sub-agent's description is: without one the row could only show the script. */
|
|
14289
|
+
description: external_exports.string().trim().min(1).max(MONITOR_DESCRIPTION_MAX),
|
|
14290
|
+
/** How long to keep it running, as a DURATION. Clamped to COMMAND_WATCH_FOR_MAX_MS and reported. */
|
|
14291
|
+
for: external_exports.string().trim().min(1).max(16)
|
|
14292
|
+
}).strict();
|
|
14293
|
+
AddOwnMonitorResult = external_exports.object({
|
|
14294
|
+
/** Present when the call was REFUSED: the gate, the caps, or a script that could not run here. Nothing
|
|
14295
|
+
* is armed, and the text says why and what to use instead. */
|
|
14296
|
+
refused: external_exports.string().optional(),
|
|
14297
|
+
id: external_exports.string().optional(),
|
|
14298
|
+
/** This exact command already runs as a monitor on this thread; nothing new was registered. */
|
|
14299
|
+
alreadyArmed: external_exports.boolean().default(false),
|
|
14300
|
+
/** Still running when registration stopped watching it (MONITOR_REGISTRATION_WINDOW_MS). */
|
|
14301
|
+
running: external_exports.boolean().default(false),
|
|
14302
|
+
/** The events it printed while registration watched — handed back here rather than woken. */
|
|
14303
|
+
events: external_exports.array(external_exports.string()).default([]),
|
|
14304
|
+
/** It EXITED while registration watched: how, and the end of what it printed. Nothing stays armed. */
|
|
14305
|
+
exited: CommandWatchRun.optional(),
|
|
14306
|
+
expiresAt: external_exports.string().optional(),
|
|
14307
|
+
/** The `for` the worker wrote, present ONLY when it was clamped. */
|
|
14308
|
+
clamped: external_exports.object({ for: external_exports.string() }).strict().optional(),
|
|
14309
|
+
watches: external_exports.array(CommandWatchView)
|
|
14310
|
+
}).strict();
|
|
14311
|
+
MONITOR_FLOOD_REASON = `it printed ${MONITOR_FLOOD_MAX_EVENTS} events inside ${formatElapsed(MONITOR_FLOOD_WINDOW_MS)}, and every event is a turn of yours. Register it again with a tighter filter: print only the lines you would act on (\`grep --line-buffered\`), never raw logs.`;
|
|
14112
14312
|
AskQuestionKind = external_exports.enum(["question", "multi"]);
|
|
14113
14313
|
AskedOptionSchema = external_exports.lazy(() => external_exports.object({
|
|
14114
14314
|
// 400, not the 120 it launched with (2026-09-03): a label is a chip line, but "Post the drafted
|
|
@@ -14127,32 +14327,55 @@ var init_src = __esm({
|
|
|
14127
14327
|
// 4000, not the 600 it launched with (2026-09-03): this field is the whole card context — everything
|
|
14128
14328
|
// the human needs to answer cold — and a question that carries its own evidence needs the room. The
|
|
14129
14329
|
// answer restates it, so QuestionAnswerSchema's cap matches.
|
|
14330
|
+
handle: external_exports.string().trim().min(1).max(64).optional(),
|
|
14130
14331
|
question: external_exports.string().trim().min(1).max(4e3),
|
|
14332
|
+
context: external_exports.string().trim().max(2e4).optional(),
|
|
14131
14333
|
header: external_exports.string().trim().max(24).optional(),
|
|
14132
14334
|
kind: AskQuestionKind,
|
|
14133
14335
|
danger: external_exports.boolean().optional(),
|
|
14134
14336
|
secret: external_exports.boolean().optional(),
|
|
14135
14337
|
// UNBOUNDED, deliberately. This carried `.max(8)` from launch until 2026-09-03, when the maintainer
|
|
14136
14338
|
// asked for the cap to go ("allow arbitrary numbers of options"): a `multi` over a long list — which
|
|
14137
|
-
// gates to run, which of twenty findings to act on — is a real shape, and the card
|
|
14138
|
-
// (
|
|
14339
|
+
// gates to run, which of twenty findings to act on — is a real shape, and the card letters past 26
|
|
14340
|
+
// (`AA.`) already. The count is the worker's to choose; the answer's `chosen` is unbounded to match.
|
|
14139
14341
|
options: external_exports.array(AskedOptionSchema).optional()
|
|
14140
14342
|
}).strict());
|
|
14141
14343
|
AskInput = external_exports.object({
|
|
14142
14344
|
slug: ThreadSlug,
|
|
14143
|
-
// No cap on the count (four until 2026-09-03): the
|
|
14144
|
-
//
|
|
14345
|
+
// No cap on the count (four until 2026-09-03): the command's own help says several at once is one
|
|
14346
|
+
// run — register them together — and a cap here told a worker with six to batch and then refused it.
|
|
14145
14347
|
questions: external_exports.array(AskedQuestionSchema).min(1)
|
|
14146
14348
|
}).strict();
|
|
14147
14349
|
RegisteredQuestionView = external_exports.object({
|
|
14148
14350
|
/** Minted by frizz. The worker never chose it, which is why an answer RESTATES the question text —
|
|
14149
14351
|
* an id alone cannot be correlated back to what was asked. */
|
|
14150
14352
|
id: external_exports.string(),
|
|
14353
|
+
/** The worker's own id for it, from its ```question fence — what `unask` and every correction name it
|
|
14354
|
+
* by. Absent on a question `ask` registered, which only ever had the minted id. */
|
|
14355
|
+
handle: external_exports.string().optional(),
|
|
14151
14356
|
spec: AskedQuestionSchema,
|
|
14152
14357
|
askedAt: external_exports.string(),
|
|
14153
14358
|
/** A `secret` question only: the path its value will be served at. Known at registration, so the
|
|
14154
14359
|
* worker can prepare the command that reads it before the human has answered. */
|
|
14155
|
-
secretPath: external_exports.string().optional()
|
|
14360
|
+
secretPath: external_exports.string().optional(),
|
|
14361
|
+
/** ON THE BOARD ONLY: does this question STAND at the thread's current rest — asked at it, or
|
|
14362
|
+
* referenced by it (server question-fences.ts standingQuestions)? `false` is a question that has
|
|
14363
|
+
* FADED: still open and answerable where it was asked, but not what this rest is waiting on, so it
|
|
14364
|
+
* neither queues the thread nor marks its row. Absent — the worker's own read-back, a snapshot from
|
|
14365
|
+
* before 2026-10-13 — reads as standing (hasStandingQuestion). */
|
|
14366
|
+
standing: external_exports.boolean().optional()
|
|
14367
|
+
}).strict();
|
|
14368
|
+
QuestionFenceFaultView = external_exports.object({
|
|
14369
|
+
at: external_exports.string(),
|
|
14370
|
+
id: external_exports.string().optional(),
|
|
14371
|
+
faults: external_exports.array(external_exports.string())
|
|
14372
|
+
}).strict();
|
|
14373
|
+
ClosedQuestionView = external_exports.object({
|
|
14374
|
+
id: external_exports.string(),
|
|
14375
|
+
/** The worker's own id for it — what its fence in the transcript carries. */
|
|
14376
|
+
handle: external_exports.string(),
|
|
14377
|
+
state: external_exports.enum(["dismissed", "withdrawn"]),
|
|
14378
|
+
askedAt: external_exports.string()
|
|
14156
14379
|
}).strict();
|
|
14157
14380
|
SECRET_TTL_MS = 15 * 6e4;
|
|
14158
14381
|
AskResult = external_exports.object({
|
|
@@ -14166,6 +14389,14 @@ var init_src = __esm({
|
|
|
14166
14389
|
}).strict();
|
|
14167
14390
|
UnaskResult = external_exports.object({
|
|
14168
14391
|
withdrawn: external_exports.boolean(),
|
|
14392
|
+
/** The question the id resolved to, by the name the WORKER knows it by (its fence's `id:`, or the
|
|
14393
|
+
* `qst_…` an `ask` minted) — absent when the id names nothing on this thread. It is the ROOT's: a
|
|
14394
|
+
* follow-up rides inside its parent's row and cannot be withdrawn alone, so `unask sqlite-path`
|
|
14395
|
+
* withdraws nothing and answers `settings-store` here — the id that takes the pair back. */
|
|
14396
|
+
id: external_exports.string().optional(),
|
|
14397
|
+
/** What that question was when the call arrived. `open` beside `withdrawn: true` is the ordinary
|
|
14398
|
+
* withdrawal; anything else is why nothing was withdrawn — and `answered` means a wake is on its way. */
|
|
14399
|
+
state: external_exports.enum(["open", "answered", "withdrawn", "dismissed"]).optional(),
|
|
14169
14400
|
open: external_exports.array(RegisteredQuestionView)
|
|
14170
14401
|
}).strict();
|
|
14171
14402
|
QuestionAnswerSchema = external_exports.lazy(() => external_exports.object({
|
|
@@ -14209,6 +14440,8 @@ var init_src = __esm({
|
|
|
14209
14440
|
ThreadSettledQuestionsResult = external_exports.object({
|
|
14210
14441
|
questions: external_exports.array(SettledQuestionView)
|
|
14211
14442
|
}).strict();
|
|
14443
|
+
ANSWER_ROW_ID_PATTERN = "[a-z0-9][a-z0-9._-]{0,63}";
|
|
14444
|
+
ANSWER_ROW_ID = new RegExp(`^${ANSWER_ROW_ID_PATTERN}$`);
|
|
14212
14445
|
MarkOwnDoneInput = external_exports.object({
|
|
14213
14446
|
slug: ThreadSlug,
|
|
14214
14447
|
body: external_exports.string().trim().min(1).max(2e4)
|
|
@@ -14235,7 +14468,7 @@ var init_src = __esm({
|
|
|
14235
14468
|
body: external_exports.string(),
|
|
14236
14469
|
// fence body minus hint lines, capped server-side; may be ""
|
|
14237
14470
|
hints: external_exports.array(AwaitingHint).default([]),
|
|
14238
|
-
// Present only on a completion the worker REGISTERED (`
|
|
14471
|
+
// Present only on a completion the worker REGISTERED (`frizzctl done`, board.registeredDoneFence)
|
|
14239
14472
|
// rather than wrote as a ```done fence in its final message. The two are one fence to every predicate,
|
|
14240
14473
|
// but the TRANSCRIPT draws a fence card from the message text it parses — so a registered done, which
|
|
14241
14474
|
// is in no message, needs the client to know it must draw the card itself at the bottom of the thread
|
|
@@ -14379,6 +14612,20 @@ var init_src = __esm({
|
|
|
14379
14612
|
// outlive the message that carried it. These rows can, and they carry the question itself rather than
|
|
14380
14613
|
// merely asserting one exists — the card renders from this instead of re-parsing prose.
|
|
14381
14614
|
questions: external_exports.array(RegisteredQuestionView).default([]),
|
|
14615
|
+
/** The ```question fences of this thread that could NOT register, from its latest sync — every one in
|
|
14616
|
+
* the server's ring of fenced messages, not only the current rest's. Present only when there is one.
|
|
14617
|
+
* A refused fence has no row, and so does a fence the registrar has not reached yet; this is what
|
|
14618
|
+
* tells them apart, so the transcript draws the first as the code block it is and only the second as
|
|
14619
|
+
* the question it is about to become (web lib/questionFaults). Its other job is the reused id: a fence
|
|
14620
|
+
* refused for naming a spent id must not move that id's card, which a name match alone would do. */
|
|
14621
|
+
questionFaults: external_exports.array(QuestionFenceFaultView).optional(),
|
|
14622
|
+
/** The questions this thread asked with a ```question fence that CLOSED WITHOUT AN ANSWER — dismissed
|
|
14623
|
+
* (the human's ×, or Frizz cancelling a question an autonomous thread wrote) or withdrawn by the
|
|
14624
|
+
* worker. Present only when there is one. Their fences are still in the transcript, and without this a
|
|
14625
|
+
* fence with no open and no answered row reads as a question about to become answerable; with it the
|
|
14626
|
+
* fence draws its question closed, saying which way (web ClosedQuestionCard). Compact on purpose — the
|
|
14627
|
+
* fence itself carries the question — because the board ships every thread on every keyframe. */
|
|
14628
|
+
closedQuestions: external_exports.array(ClosedQuestionView).optional(),
|
|
14382
14629
|
/** The answer the human has already SENT that the worker has not received yet, as the exact message
|
|
14383
14630
|
* the delivery will carry (board.answersInFlight → questionAnswerMessage). The chat parses it with
|
|
14384
14631
|
* the same reader it uses on the delivered turn, so the in-flight card and the landed one are the
|
|
@@ -14418,6 +14665,16 @@ var init_src = __esm({
|
|
|
14418
14665
|
foreign: external_exports.boolean().optional(),
|
|
14419
14666
|
// ui.db lifecycle for session threads (open|archived) — written ONLY by explicit Archive/Reopen.
|
|
14420
14667
|
state: external_exports.enum(["open", "archived"]).optional(),
|
|
14668
|
+
// The instant the thread was marked done (session.archived_at), present while it is archived. The
|
|
14669
|
+
// Done band's order: the one marked done most recently is at its top (maintainer 2026-10-23). Absent
|
|
14670
|
+
// on a thread archived before the column existed, which the band places by its rest time instead.
|
|
14671
|
+
archivedAt: external_exports.string().optional(),
|
|
14672
|
+
// The instant this thread last ENTERED the Running band (session.running_since), present while it is
|
|
14673
|
+
// in it: the board stamps it the first build that finds the thread there and clears it the first
|
|
14674
|
+
// build that does not. The Running band's order: the thread that most recently switched back to
|
|
14675
|
+
// running is at its top (maintainer 2026-10-23). A thread that stays in the band keeps its instant, so
|
|
14676
|
+
// a working rest, a sub-agent returning or a mid-turn message never moves it.
|
|
14677
|
+
runningSince: external_exports.string().optional(),
|
|
14421
14678
|
// Exact durable user snooze. While this instant is in the future, an otherwise-resting thread is
|
|
14422
14679
|
// suppressed from Queue and shown dimmed in Held. Hard interactive gates (question, permission,
|
|
14423
14680
|
// native approval, crash) deliberately break through it. Expired values are cleared server-side.
|
|
@@ -14431,6 +14688,23 @@ var init_src = __esm({
|
|
|
14431
14688
|
// this instant's order. Lifecycle metadata like the snooze: written only by the pin/unpin verb, and
|
|
14432
14689
|
// it outranks every derived state (a pinned thread that finishes stays pinned).
|
|
14433
14690
|
pinnedAt: external_exports.string().optional(),
|
|
14691
|
+
// The instant the human pinned this thread on the CONSOLE's "By status" view (absent = not pinned
|
|
14692
|
+
// there). A second shelf, independent of `pinnedAt`: pinning on the Console does not pin on the
|
|
14693
|
+
// project's board, nor the other way round. The instant is the Console's pinned band's order across
|
|
14694
|
+
// every project, and like the project pin it outranks every derived state.
|
|
14695
|
+
consolePinnedAt: external_exports.string().optional(),
|
|
14696
|
+
/** CHILD THREADS (Settings.childThreads): the thread that spawned this one with `frizzctl spawn-child`.
|
|
14697
|
+
* Present only while that thread still exists. A child is never a row of its own on the board — not in
|
|
14698
|
+
* any band, the queue, a count or a badge — and is drawn under its parent instead, unless it stands at a
|
|
14699
|
+
* gate only the human can settle (drawnUnderParent). */
|
|
14700
|
+
parentSlug: external_exports.string().optional(),
|
|
14701
|
+
/** This child's rest is one that would have put it in the human's queue, so its handoff goes to its
|
|
14702
|
+
* PARENT instead (board.ts deriveChildHandoff; the scheduler delivers it once per rest). A permission
|
|
14703
|
+
* prompt, a secret request and a usage-limit stop are not handoffs: they still queue for the human. */
|
|
14704
|
+
handoffToParent: external_exports.literal(true).optional(),
|
|
14705
|
+
/** The instant of the rest `handoffToParent` describes (the tailer's lastAssistantAt), so the scheduler
|
|
14706
|
+
* can tell a verdict about the rest it sees from one a cached board still carries about an older rest. */
|
|
14707
|
+
handoffRestAt: external_exports.string().optional(),
|
|
14434
14708
|
/** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
|
|
14435
14709
|
* until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
|
|
14436
14710
|
* thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
|
|
@@ -14439,8 +14713,20 @@ var init_src = __esm({
|
|
|
14439
14713
|
* the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
|
|
14440
14714
|
* reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
|
|
14441
14715
|
* explicitly parked THIS rest, showing them the same card with the same button one surface over is
|
|
14442
|
-
* not information, and they said so.
|
|
14716
|
+
* not information, and they said so.
|
|
14717
|
+
*
|
|
14718
|
+
* The header's "Until activity" (SnoozeMenu, 2026-10-19) arms the same snooze on any rested thread. */
|
|
14443
14719
|
bgSnoozed: external_exports.boolean().optional(),
|
|
14720
|
+
/** Something of the thread's own can wake it without a person — a running sub-agent or shell, an
|
|
14721
|
+
* armed watch or timer, a Goal that fires on rest or on a clock (board.wakesOnItsOwn). The snooze
|
|
14722
|
+
* menu greys out "Until activity" without it, and the server stops honouring that snooze once it
|
|
14723
|
+
* goes false. Absent on a dead worker and on an archived thread. */
|
|
14724
|
+
wakesOnItsOwn: external_exports.boolean().optional(),
|
|
14725
|
+
/** A QUIET PARK THE BOARD REFUSED because an earlier `needs_input` handoff still stands: the human has
|
|
14726
|
+
* neither typed to the thread nor snoozed a rest since it (board handoffStands). The surfaces keep that
|
|
14727
|
+
* handoff's fence drawn (web lib/fenceBlocks heldHandoffIndex), since it is what the thread is in the
|
|
14728
|
+
* queue for. Set only while it is what queued the thread. */
|
|
14729
|
+
heldHandoff: external_exports.boolean().optional(),
|
|
14444
14730
|
// Which Claude transport serves this thread. "broker" — a session-broker-owned Agent SDK session with
|
|
14445
14731
|
// a typed control channel — is the only one there is; ABSENT means a row dispatched before the broker
|
|
14446
14732
|
// became the sole transport, which frizz can no longer reach that way. Only the broker can be asked to
|
|
@@ -14594,6 +14880,11 @@ var init_src = __esm({
|
|
|
14594
14880
|
// falls back to rendering the plain `errors` strings.
|
|
14595
14881
|
errorItems: external_exports.array(BoardErrorItem).optional()
|
|
14596
14882
|
});
|
|
14883
|
+
BoardState = external_exports.object({
|
|
14884
|
+
board: BoardSnapshot,
|
|
14885
|
+
seq: external_exports.number().int().min(0),
|
|
14886
|
+
bootId: external_exports.string()
|
|
14887
|
+
});
|
|
14597
14888
|
QuotaWindow = external_exports.object({
|
|
14598
14889
|
key: external_exports.string(),
|
|
14599
14890
|
// stable id: "5h" | "weekly" (provider-neutral)
|
|
@@ -14609,8 +14900,20 @@ var init_src = __esm({
|
|
|
14609
14900
|
planType: external_exports.string().optional(),
|
|
14610
14901
|
// "pro" / "max" / etc. when the source reports it
|
|
14611
14902
|
windows: external_exports.array(QuotaWindow),
|
|
14612
|
-
detail: external_exports.string().optional()
|
|
14903
|
+
detail: external_exports.string().optional(),
|
|
14613
14904
|
// why unavailable, or an extra note
|
|
14905
|
+
// Recovery evidence from a LIVE Codex read only. Rollout quota has no account identity or freshness
|
|
14906
|
+
// guarantee and must never wake a failed thread. accountKey is a hash, not a credential/account id.
|
|
14907
|
+
codexRecovery: external_exports.object({
|
|
14908
|
+
observedAt: external_exports.number(),
|
|
14909
|
+
// request start, unix milliseconds (not the later cache/read time)
|
|
14910
|
+
accountKey: external_exports.string().optional(),
|
|
14911
|
+
creditsAvailable: external_exports.boolean().optional(),
|
|
14912
|
+
ordinaryUsageAllowed: external_exports.boolean().nullable().optional(),
|
|
14913
|
+
// null is explicitly unavailable
|
|
14914
|
+
blocked: external_exports.boolean().optional()
|
|
14915
|
+
// explicit workspace/spend-control denial beats apparent headroom
|
|
14916
|
+
}).optional()
|
|
14614
14917
|
});
|
|
14615
14918
|
QuotaSnapshot = external_exports.object({
|
|
14616
14919
|
claude: ProviderQuota,
|
|
@@ -14672,6 +14975,16 @@ var init_src = __esm({
|
|
|
14672
14975
|
* Machine-level: which chrome you want is a property of the person, not the repo.
|
|
14673
14976
|
*/
|
|
14674
14977
|
projectRail: external_exports.boolean(),
|
|
14978
|
+
/**
|
|
14979
|
+
* CHILD THREADS, an experiment (2026-11-04). On, a worker may run `frizzctl spawn-child`: a new thread
|
|
14980
|
+
* in this project, on either harness, linked to the thread that spawned it. Its rests that would
|
|
14981
|
+
* have queued for the human wake its parent instead, it can own its own PR watchers, and it sits
|
|
14982
|
+
* under its parent on the board rather than in the queue. Off (the default), the command refuses and
|
|
14983
|
+
* the worker contract never mentions it. A child spawned while it was on keeps its link either way.
|
|
14984
|
+
*
|
|
14985
|
+
* Machine-level, like the rail: whether to try an experiment is the person's call, not the repo's.
|
|
14986
|
+
*/
|
|
14987
|
+
childThreads: external_exports.boolean(),
|
|
14675
14988
|
// There is no `font` key any more. The interface rendered in one of two type families as a machine
|
|
14676
14989
|
// setting until 2026-09-19 (maintainer: "let's drop monospace as an option"); every surface is sans
|
|
14677
14990
|
// now, and index.html pins `data-font="sans"` on <html> directly. Settings is a non-strict object,
|
|
@@ -14711,6 +15024,19 @@ var init_src = __esm({
|
|
|
14711
15024
|
command: external_exports.string().trim().min(1).max(1024),
|
|
14712
15025
|
args: external_exports.array(external_exports.string().max(1024)).max(32).optional()
|
|
14713
15026
|
})).max(32).optional(),
|
|
15027
|
+
// A command each runtime's workers are started THROUGH, per runtime: `op run --env-file .env --`
|
|
15028
|
+
// makes a new Claude worker start as `op run --env-file .env -- /path/to/claude …`. Kept as the text
|
|
15029
|
+
// the operator typed and split into words by parseLaunchPrefix (launch-prefix.ts), the one parser the
|
|
15030
|
+
// settings field and the spawn share; it is never run in a shell. Claude applies it at every fork of a
|
|
15031
|
+
// thread's worker; Codex to the app-server the project's Codex threads share, which is replaced at
|
|
15032
|
+
// the next moment no Codex turn is running once the prefix changes. ACP agents need no key here:
|
|
15033
|
+
// their `command` above is already the operator's own. A string rather than a refined argv so a
|
|
15034
|
+
// stored value the parser later stops accepting fails ITS launch, not the whole settings parse
|
|
15035
|
+
// (getSettings falls back to every default on a parse miss). Optional so an old blob parses.
|
|
15036
|
+
launchPrefix: external_exports.object({
|
|
15037
|
+
claude: external_exports.string().max(LAUNCH_PREFIX_MAX_LENGTH).optional(),
|
|
15038
|
+
codex: external_exports.string().max(LAUNCH_PREFIX_MAX_LENGTH).optional()
|
|
15039
|
+
}).optional(),
|
|
14714
15040
|
// The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
|
|
14715
15041
|
// unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
|
|
14716
15042
|
// the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
|
|
@@ -14805,7 +15131,21 @@ var init_src = __esm({
|
|
|
14805
15131
|
// Binds the call to the session the tab is looking at, so a stale page cannot deliver a follow-up
|
|
14806
15132
|
// into a thread that has since been re-dispatched (merged from origin/main, 2026-07-21).
|
|
14807
15133
|
sessionId: external_exports.string().min(1),
|
|
14808
|
-
|
|
15134
|
+
// EMPTY only when `answers` ride: the staged answers alone are a message. The server refuses a send
|
|
15135
|
+
// that carries neither.
|
|
15136
|
+
message: external_exports.string(),
|
|
15137
|
+
// REGISTERED-QUESTION ANSWERS SENT WITH THIS MESSAGE — the prompt box's one Send. The server settles
|
|
15138
|
+
// each question exactly as `answerQuestions` does, then delivers ONE turn: the answers in their wire
|
|
15139
|
+
// form (questionAnswerMessage) and `message` under them as the note (withAnswersNote). They ride the
|
|
15140
|
+
// follow-up rather than the scheduler's answer wake so the pair arrives together, in the order the
|
|
15141
|
+
// human wrote it, and as fast as a steer does.
|
|
15142
|
+
answers: external_exports.array(QuestionAnswerSchema).optional(),
|
|
15143
|
+
// SEND TO THE QUEUE: while the agent is mid-turn, Frizz keeps this message and delivers it when the
|
|
15144
|
+
// thread comes to rest, so it opens the next turn instead of steering this one. The queue goes out ONE
|
|
15145
|
+
// message per turn (2026-10-20), so a message also waits, even at rest, behind one already held or one
|
|
15146
|
+
// whose turn has not ended. With nothing ahead of it, it is an ordinary send. See `held` and
|
|
15147
|
+
// nextHeldRelease in the server's delivery-ledger.ts.
|
|
15148
|
+
hold: external_exports.boolean().optional(),
|
|
14809
15149
|
// Generated once before the optimistic clear so a transport replay can be idempotent.
|
|
14810
15150
|
deliveryId: external_exports.string().min(1).max(200).optional(),
|
|
14811
15151
|
// Retire the worker's live process before delivering, so this message lands in a `claude` that has
|
|
@@ -14814,17 +15154,19 @@ var init_src = __esm({
|
|
|
14814
15154
|
// newer frizz build in place (hooks are read once, at startup). Everything else that needs a fresh
|
|
14815
15155
|
// process derives it server-side; see needsFreshProcessForLimit.
|
|
14816
15156
|
freshProcess: external_exports.boolean().optional(),
|
|
14817
|
-
//
|
|
14818
|
-
//
|
|
14819
|
-
//
|
|
14820
|
-
//
|
|
14821
|
-
//
|
|
15157
|
+
// SEND NOW: this message is read at once instead of when the operation the worker is running
|
|
15158
|
+
// finishes. The operator's forced send (⌘⏎), and opt-in because it can cost work in flight. Since
|
|
15159
|
+
// 2026-10-13 it usually does not: the runtime moves a running command or foreground sub-agent to the
|
|
15160
|
+
// background and takes the message into the same turn. Only when it has nothing to move — the model
|
|
15161
|
+
// is mid-answer, a wake Frizz wrote is waiting too — is the turn aborted, as it always was before,
|
|
15162
|
+
// and that costs the in-flight call its result. (An abort has not cost the worker's BACKGROUND
|
|
15163
|
+
// sub-agents since 2026-09-24: the broker's SDK query declares `perTaskStopAffordance`.)
|
|
14822
15164
|
//
|
|
14823
15165
|
// It exists because delivery is ALREADY as fast as queueing can be. Measured over 14 days of this
|
|
14824
15166
|
// project's own transcripts, Claude Code drains its queue at the first sampling boundary that
|
|
14825
15167
|
// exists; the wait an operator feels is the remaining time of whatever was in flight (a long `Bash`,
|
|
14826
15168
|
// or one 73–133s reasoning+answer generation), which put mid-turn operator prose at p50 13.8s,
|
|
14827
|
-
// p90 49s, p99 2.5m.
|
|
15169
|
+
// p90 49s, p99 2.5m. Getting that operation out of the way is the only lever left.
|
|
14828
15170
|
//
|
|
14829
15171
|
// Broker-backed Claude only — that is every Claude thread dispatched since the broker cutover. On
|
|
14830
15172
|
// any other runtime the message is delivered normally and this is ignored, never refused: a send
|
|
@@ -14839,16 +15181,38 @@ var init_src = __esm({
|
|
|
14839
15181
|
SteerThreadInput = external_exports.object({
|
|
14840
15182
|
from: ThreadSlug,
|
|
14841
15183
|
to: ThreadSlug,
|
|
15184
|
+
project: external_exports.string().min(1).max(200).optional(),
|
|
14842
15185
|
message: external_exports.string().min(1).max(1e5)
|
|
14843
15186
|
}).strict();
|
|
14844
15187
|
SteerThreadResult = external_exports.object({
|
|
14845
15188
|
slug: ThreadSlug,
|
|
14846
15189
|
title: external_exports.string(),
|
|
15190
|
+
// The target's project URL slug — present only when it is NOT the caller's project, so the receipt's
|
|
15191
|
+
// link can name it (`/project/<project>/thread/<slug>`) and a reply can be addressed back.
|
|
15192
|
+
project: external_exports.string().optional(),
|
|
14847
15193
|
// What the target was doing when the message arrived: `running` ⇒ it reads it at its next tool
|
|
14848
15194
|
// boundary; `resting` ⇒ the message started its next turn; `reopened` ⇒ it was marked done and this
|
|
14849
15195
|
// message put it back on the board.
|
|
14850
15196
|
delivery: external_exports.enum(["running", "resting", "reopened"])
|
|
14851
15197
|
}).strict();
|
|
15198
|
+
SpawnThreadInput = DispatchInput.pick({ title: true, prompt: true, model: true, backend: true, effort: true }).extend({
|
|
15199
|
+
from: ThreadSlug,
|
|
15200
|
+
project: external_exports.string().min(1).max(200)
|
|
15201
|
+
}).strict();
|
|
15202
|
+
SpawnThreadResult = external_exports.object({
|
|
15203
|
+
slug: ThreadSlug,
|
|
15204
|
+
sessionId: external_exports.string(),
|
|
15205
|
+
// The new thread's project URL slug — present only when it is NOT the caller's project, as on a steer.
|
|
15206
|
+
project: external_exports.string().optional()
|
|
15207
|
+
}).strict();
|
|
15208
|
+
SpawnChildInput = DispatchInput.pick({ title: true, prompt: true, model: true, backend: true, effort: true }).extend({
|
|
15209
|
+
from: ThreadSlug
|
|
15210
|
+
}).strict();
|
|
15211
|
+
SpawnChildResult = external_exports.union([
|
|
15212
|
+
external_exports.object({ slug: ThreadSlug, sessionId: external_exports.string() }).strict(),
|
|
15213
|
+
// The experiment is off, or the caller is itself a child. Exit 2 at the command, like every refusal.
|
|
15214
|
+
external_exports.object({ refused: external_exports.string() }).strict()
|
|
15215
|
+
]);
|
|
14852
15216
|
DismissFailedFollowUpInput = external_exports.object({
|
|
14853
15217
|
slug: ThreadSlug,
|
|
14854
15218
|
deliveryId: external_exports.string().min(1).max(200)
|
|
@@ -14867,11 +15231,19 @@ var init_src = __esm({
|
|
|
14867
15231
|
DeliverQueuedNowInput = external_exports.object({
|
|
14868
15232
|
slug: ThreadSlug,
|
|
14869
15233
|
// Same staleness guard as unqueueFollowUp: a stale tab must not preempt a re-dispatched session.
|
|
14870
|
-
sessionId: external_exports.string().min(1)
|
|
15234
|
+
sessionId: external_exports.string().min(1),
|
|
15235
|
+
// Hand over what Frizz is HOLDING for the agent's rest ("Send to queue") and stop there — no
|
|
15236
|
+
// interrupt. The "Send now" under a held bubble: the operator changed their mind about waiting, not
|
|
15237
|
+
// about interrupting, so the message goes in as the steer it would have been.
|
|
15238
|
+
releaseOnly: external_exports.boolean().optional()
|
|
14871
15239
|
}).strict();
|
|
14872
15240
|
DeliverQueuedNowResult = external_exports.object({
|
|
14873
15241
|
interrupted: external_exports.boolean(),
|
|
14874
|
-
reason: external_exports.string().optional()
|
|
15242
|
+
reason: external_exports.string().optional(),
|
|
15243
|
+
// How many messages Frizz was HOLDING for the agent's rest ("Send to queue") and has now handed over.
|
|
15244
|
+
// Those were never in a provider queue, so on a runtime that cannot be interrupted this is the whole
|
|
15245
|
+
// of what "send now" did — and it is a success the surface must not report as a refusal.
|
|
15246
|
+
released: external_exports.number().int().nonnegative().optional()
|
|
14875
15247
|
}).strict();
|
|
14876
15248
|
CompactThreadInput = external_exports.object({
|
|
14877
15249
|
slug: ThreadSlug,
|
|
@@ -14893,6 +15265,17 @@ var init_src = __esm({
|
|
|
14893
15265
|
sessionId: external_exports.string().min(1),
|
|
14894
15266
|
pinned: external_exports.boolean()
|
|
14895
15267
|
}).strict();
|
|
15268
|
+
ReorderPinnedThreadsInput = external_exports.object({
|
|
15269
|
+
slugs: external_exports.array(ThreadSlug).max(500)
|
|
15270
|
+
}).strict();
|
|
15271
|
+
SetThreadConsolePinnedInput = external_exports.object({
|
|
15272
|
+
slug: ThreadSlug,
|
|
15273
|
+
sessionId: external_exports.string().min(1),
|
|
15274
|
+
pinned: external_exports.boolean()
|
|
15275
|
+
}).strict();
|
|
15276
|
+
ReorderConsolePinnedThreadsInput = external_exports.object({
|
|
15277
|
+
pins: external_exports.array(external_exports.object({ slug: ThreadSlug, at: SnoozeUntil }).strict()).max(500)
|
|
15278
|
+
}).strict();
|
|
14896
15279
|
SetThreadRecurringPromptInput = external_exports.object({
|
|
14897
15280
|
slug: ThreadSlug,
|
|
14898
15281
|
sessionId: external_exports.string().min(1),
|
|
@@ -14949,7 +15332,7 @@ var init_src = __esm({
|
|
|
14949
15332
|
timers: external_exports.array(ThreadTimerView)
|
|
14950
15333
|
}).strict();
|
|
14951
15334
|
ThreadActivityItem = external_exports.object({
|
|
14952
|
-
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue"]),
|
|
15335
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "command", "thread"]),
|
|
14953
15336
|
/** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
|
|
14954
15337
|
* was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
|
|
14955
15338
|
id: external_exports.string(),
|
|
@@ -14978,6 +15361,62 @@ var init_src = __esm({
|
|
|
14978
15361
|
* agent to read out the current set of watchers and questions?"). */
|
|
14979
15362
|
questions: external_exports.array(RegisteredQuestionView).default([])
|
|
14980
15363
|
}).strict();
|
|
15364
|
+
ThreadBand = external_exports.enum(["rested", "active", "snoozed", "done"]);
|
|
15365
|
+
ThreadListing = external_exports.object({
|
|
15366
|
+
slug: ThreadSlug,
|
|
15367
|
+
/** The title the sidebar shows (displayTitle). */
|
|
15368
|
+
title: external_exports.string(),
|
|
15369
|
+
band: ThreadBand,
|
|
15370
|
+
runtime: RuntimeState,
|
|
15371
|
+
backend: Backend.optional(),
|
|
15372
|
+
model: external_exports.string().optional(),
|
|
15373
|
+
/** The human pinned it to the top of this board's sidebar. */
|
|
15374
|
+
pinned: external_exports.literal(true).optional(),
|
|
15375
|
+
/** The caption of the tool call in flight, while a turn is running ("Running the focused tests"). */
|
|
15376
|
+
doing: external_exports.string().optional(),
|
|
15377
|
+
/** The first line of its newest message — the verdict line of a handoff. Capped. */
|
|
15378
|
+
lastLine: external_exports.string().optional(),
|
|
15379
|
+
/** The signal fence its last rest ended on, while the board still reads it. */
|
|
15380
|
+
fence: external_exports.enum(["done", "awaiting"]).optional(),
|
|
15381
|
+
/** Questions standing at its current rest (hasStandingQuestion's rule), plus a native ask or a
|
|
15382
|
+
* permission prompt the human has not answered. */
|
|
15383
|
+
questions: external_exports.number().int(),
|
|
15384
|
+
/** Its own sub-agents still running (direct children only, as the board counts them). */
|
|
15385
|
+
subAgents: external_exports.number().int(),
|
|
15386
|
+
/** Background shells still running. */
|
|
15387
|
+
shells: external_exports.number().int(),
|
|
15388
|
+
/** CHILD THREADS it started (`frizzctl spawn-child`) that are still open. A child is never a row of its
|
|
15389
|
+
* own here, as on the board (drawnUnderParent), so this is how another thread finds one to steer. */
|
|
15390
|
+
children: external_exports.array(external_exports.object({ slug: ThreadSlug, title: external_exports.string(), runtime: RuntimeState }).strict()).optional(),
|
|
15391
|
+
/** What its armed watches are on: `owner/repo#N` for a PR or issue, the prompt for a timer, the
|
|
15392
|
+
* command for a command watch. */
|
|
15393
|
+
watches: external_exports.array(external_exports.string()),
|
|
15394
|
+
/** Killed by a usage limit, waiting for the window to reset. */
|
|
15395
|
+
limited: external_exports.literal(true).optional(),
|
|
15396
|
+
/** Its process is gone with the turn unfinished. */
|
|
15397
|
+
crashed: external_exports.literal(true).optional(),
|
|
15398
|
+
spawnedAt: external_exports.string().optional(),
|
|
15399
|
+
/** When its own turn last came to rest (ThreadView.lastAssistantAt). */
|
|
15400
|
+
restedAt: external_exports.string().optional(),
|
|
15401
|
+
lastActivityAt: external_exports.string().optional(),
|
|
15402
|
+
/** A wall-clock snooze still in the future. */
|
|
15403
|
+
snoozedUntil: external_exports.string().optional(),
|
|
15404
|
+
archivedAt: external_exports.string().optional()
|
|
15405
|
+
}).strict();
|
|
15406
|
+
ListThreadsInput = external_exports.object({
|
|
15407
|
+
/** Another project's board — its slug, id or registered name — instead of the one the call lands on. */
|
|
15408
|
+
project: external_exports.string().trim().min(1).optional(),
|
|
15409
|
+
/** Include the Done band (archived threads), which on a long-lived board is nearly every thread. */
|
|
15410
|
+
all: external_exports.boolean().optional()
|
|
15411
|
+
}).strict();
|
|
15412
|
+
ListThreadsResult = external_exports.object({
|
|
15413
|
+
/** The board's project: its URL slug, its registry id, and the name the board shows. */
|
|
15414
|
+
project: external_exports.object({ slug: external_exports.string(), id: external_exports.string(), name: external_exports.string() }).strict(),
|
|
15415
|
+
/** Rested, then active, then snoozed, then done; newest activity first within each. */
|
|
15416
|
+
threads: external_exports.array(ThreadListing),
|
|
15417
|
+
/** Every band's size, the Done band's included when its threads are not listed. */
|
|
15418
|
+
counts: external_exports.object({ rested: external_exports.number().int(), active: external_exports.number().int(), snoozed: external_exports.number().int(), done: external_exports.number().int() }).strict()
|
|
15419
|
+
}).strict();
|
|
14981
15420
|
SetOwnThreadTimerResult = external_exports.object({
|
|
14982
15421
|
id: external_exports.string(),
|
|
14983
15422
|
fireAt: external_exports.string(),
|
|
@@ -14998,13 +15437,6 @@ var init_src = __esm({
|
|
|
14998
15437
|
intervalSeconds: RecurringIntervalSeconds.optional(),
|
|
14999
15438
|
enabled: external_exports.boolean().optional()
|
|
15000
15439
|
}).strict();
|
|
15001
|
-
ThreadPluginReloadResult = external_exports.object({
|
|
15002
|
-
plugins: external_exports.number().int().min(0),
|
|
15003
|
-
commands: external_exports.number().int().min(0),
|
|
15004
|
-
agents: external_exports.number().int().min(0),
|
|
15005
|
-
mcpServers: external_exports.array(external_exports.string()),
|
|
15006
|
-
errorCount: external_exports.number().int().min(0)
|
|
15007
|
-
}).strict();
|
|
15008
15440
|
RenameThreadInput = external_exports.object({
|
|
15009
15441
|
slug: ThreadSlug,
|
|
15010
15442
|
title: external_exports.string().trim().min(1).max(200)
|
|
@@ -15050,6 +15482,42 @@ var init_src = __esm({
|
|
|
15050
15482
|
}).strict();
|
|
15051
15483
|
ThreadSkillsInput = external_exports.object({ slug: ThreadSlug }).strict();
|
|
15052
15484
|
ThreadSkillsResult = external_exports.object({ skills: external_exports.array(ThreadSkill).max(1024) }).strict();
|
|
15485
|
+
McpTarget = external_exports.union([
|
|
15486
|
+
external_exports.object({ slug: ThreadSlug }).strict(),
|
|
15487
|
+
external_exports.object({ backend: Backend, model: external_exports.string().trim().max(200).optional() }).strict()
|
|
15488
|
+
]);
|
|
15489
|
+
McpServerState = external_exports.enum(["connected", "needs-auth", "failed", "pending", "disabled"]);
|
|
15490
|
+
McpServerReading = external_exports.object({
|
|
15491
|
+
name: external_exports.string().min(1).max(256),
|
|
15492
|
+
state: McpServerState,
|
|
15493
|
+
tools: external_exports.number().int().nonnegative().optional(),
|
|
15494
|
+
error: external_exports.string().max(2e3).optional(),
|
|
15495
|
+
remote: external_exports.boolean().optional()
|
|
15496
|
+
}).strict();
|
|
15497
|
+
McpStatusResult = external_exports.object({
|
|
15498
|
+
backend: Backend,
|
|
15499
|
+
/** The agent's display name, for the empty state ("Gemini CLI doesn't support …"). */
|
|
15500
|
+
agent: external_exports.string().max(200),
|
|
15501
|
+
supported: external_exports.boolean(),
|
|
15502
|
+
servers: external_exports.array(McpServerReading).max(256),
|
|
15503
|
+
/**
|
|
15504
|
+
* Why the servers can't be read right now (Claude Code isn't signed in on this machine), drawn as the
|
|
15505
|
+
* menu's note. An answer, not an error: every prompt box asks on page load, and a machine without the
|
|
15506
|
+
* agent's login is an ordinary machine, not a failing server.
|
|
15507
|
+
*/
|
|
15508
|
+
note: external_exports.string().max(500).optional()
|
|
15509
|
+
}).strict();
|
|
15510
|
+
McpSignInInput = external_exports.object({ target: McpTarget, server: external_exports.string().min(1).max(256) }).strict();
|
|
15511
|
+
McpSignInState = external_exports.enum(["waiting", "done", "failed", "expired", "cancelled"]);
|
|
15512
|
+
McpSignInResult = external_exports.object({
|
|
15513
|
+
attemptId: external_exports.string().min(1).max(64),
|
|
15514
|
+
/** Absent when the agent finished without the human (a refresh token was still good). */
|
|
15515
|
+
authUrl: external_exports.string().url().max(8192).optional(),
|
|
15516
|
+
state: McpSignInState
|
|
15517
|
+
}).strict();
|
|
15518
|
+
McpSignInAttemptInput = external_exports.object({ attemptId: external_exports.string().min(1).max(64) }).strict();
|
|
15519
|
+
McpSignInPollResult = external_exports.object({ state: McpSignInState, error: external_exports.string().max(2e3).optional() }).strict();
|
|
15520
|
+
McpSignInFinishInput = external_exports.object({ attemptId: external_exports.string().min(1).max(64), callbackUrl: external_exports.string().trim().min(1).max(8192) }).strict();
|
|
15053
15521
|
SetThreadProfileInput = external_exports.object({
|
|
15054
15522
|
slug: ThreadSlug,
|
|
15055
15523
|
model: external_exports.string().trim().min(1).max(200),
|
|
@@ -15110,6 +15578,21 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15110
15578
|
nameWithOwner: external_exports.string().nullable(),
|
|
15111
15579
|
authed: external_exports.boolean()
|
|
15112
15580
|
});
|
|
15581
|
+
GithubAccount = external_exports.object({
|
|
15582
|
+
signedIn: external_exports.boolean(),
|
|
15583
|
+
/** "app" — Frizz's GitHub App, signed in here; "token" — a stored token; "env" — FRIZZ_GITHUB_TOKEN. */
|
|
15584
|
+
source: external_exports.enum(["app", "token", "env"]).optional(),
|
|
15585
|
+
login: external_exports.string().nullable(),
|
|
15586
|
+
/** Why a stored sign-in stopped working. */
|
|
15587
|
+
problem: external_exports.string().optional(),
|
|
15588
|
+
/** Whether this build knows Frizz's GitHub App, i.e. whether "Sign in" can be offered at all. */
|
|
15589
|
+
appRegistered: external_exports.boolean(),
|
|
15590
|
+
signIn: external_exports.discriminatedUnion("phase", [
|
|
15591
|
+
external_exports.object({ phase: external_exports.literal("idle") }),
|
|
15592
|
+
external_exports.object({ phase: external_exports.literal("pending"), userCode: external_exports.string(), verificationUri: external_exports.string(), expiresAt: external_exports.number() }),
|
|
15593
|
+
external_exports.object({ phase: external_exports.literal("failed"), message: external_exports.string() })
|
|
15594
|
+
])
|
|
15595
|
+
});
|
|
15113
15596
|
GithubRefCard = external_exports.object({
|
|
15114
15597
|
ref: external_exports.string(),
|
|
15115
15598
|
kind: external_exports.enum(["issue", "pr", "commit"]),
|
|
@@ -15209,6 +15692,14 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15209
15692
|
// same pre-restart back-compat reason as on BoardSnapshot.
|
|
15210
15693
|
errorItems: external_exports.array(BoardErrorItem).optional()
|
|
15211
15694
|
});
|
|
15695
|
+
BoardDeltaEvent = external_exports.object({
|
|
15696
|
+
type: external_exports.literal("board-delta"),
|
|
15697
|
+
seq: external_exports.number(),
|
|
15698
|
+
bootId: external_exports.string(),
|
|
15699
|
+
upserts: external_exports.array(ThreadView),
|
|
15700
|
+
removed: external_exports.array(ThreadSlug),
|
|
15701
|
+
meta: BoardMeta.optional()
|
|
15702
|
+
});
|
|
15212
15703
|
ServerEvent = external_exports.discriminatedUnion("type", [
|
|
15213
15704
|
external_exports.object({
|
|
15214
15705
|
type: external_exports.literal("board"),
|
|
@@ -15220,17 +15711,7 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15220
15711
|
seq: external_exports.number().optional(),
|
|
15221
15712
|
bootId: external_exports.string().optional()
|
|
15222
15713
|
}),
|
|
15223
|
-
|
|
15224
|
-
// Keyed per-thread delta. `upserts` are COMPLETE ThreadViews for threads whose serialization
|
|
15225
|
-
// changed (or are new); `removed` are ids gone from the board; `meta` is present only when a
|
|
15226
|
-
// board-level field changed. Emitted only by a post-restart server → seq/bootId are required here.
|
|
15227
|
-
type: external_exports.literal("board-delta"),
|
|
15228
|
-
seq: external_exports.number(),
|
|
15229
|
-
bootId: external_exports.string(),
|
|
15230
|
-
upserts: external_exports.array(ThreadView),
|
|
15231
|
-
removed: external_exports.array(ThreadSlug),
|
|
15232
|
-
meta: BoardMeta.optional()
|
|
15233
|
-
}),
|
|
15714
|
+
BoardDeltaEvent,
|
|
15234
15715
|
external_exports.object({
|
|
15235
15716
|
type: external_exports.literal("notify"),
|
|
15236
15717
|
slug: ThreadSlug,
|
|
@@ -15247,8 +15728,24 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15247
15728
|
interactionId: InteractionOpaqueId,
|
|
15248
15729
|
lifecycle: InteractionLifecycle,
|
|
15249
15730
|
recordRevision: InteractionRevision
|
|
15731
|
+
}).strict(),
|
|
15732
|
+
external_exports.object({
|
|
15733
|
+
// The machine's PROJECT LIST changed: a reorder, a rename, an icon, a project added or forgotten.
|
|
15734
|
+
// Payload-free for the same reason as above — the list is one cheap read, and a client that
|
|
15735
|
+
// re-reads it cannot apply a stale copy out of order. It is the one event that is not about this
|
|
15736
|
+
// stream's own project: the server publishes it on EVERY open project's stream, because the rail
|
|
15737
|
+
// and the grid draw the same list in every tab whichever board that tab is on.
|
|
15738
|
+
type: external_exports.literal("projects-changed")
|
|
15250
15739
|
}).strict()
|
|
15251
15740
|
]);
|
|
15741
|
+
BoardDeltasInput = external_exports.object({
|
|
15742
|
+
bootId: external_exports.string().min(1).max(200),
|
|
15743
|
+
seq: external_exports.number().int().min(0)
|
|
15744
|
+
});
|
|
15745
|
+
BoardDeltasResult = external_exports.union([
|
|
15746
|
+
external_exports.object({ bootId: external_exports.string(), deltas: external_exports.array(BoardDeltaEvent) }),
|
|
15747
|
+
external_exports.object({ bootId: external_exports.string(), resync: external_exports.enum(["boot", "expired", "ahead"]) })
|
|
15748
|
+
]);
|
|
15252
15749
|
TranscriptEdit = external_exports.object({
|
|
15253
15750
|
file: external_exports.string(),
|
|
15254
15751
|
old: external_exports.string(),
|
|
@@ -15490,7 +15987,8 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15490
15987
|
// the server's from the instant it arrives. "failed": the transport threw, or never answered — the
|
|
15491
15988
|
// text is kept until the operator retries, edits or dismisses it, and nothing retries it on its own.
|
|
15492
15989
|
// Additive + optional: an older client renders both as a plain bubble (gray for "sending").
|
|
15493
|
-
|
|
15990
|
+
// "held": the operator sent it TO THE QUEUE — Frizz has it and no transport does, until the agent rests.
|
|
15991
|
+
deliveryState: external_exports.enum(["sending", "pending", "enqueued", "delivered", "unconfirmed", "failed", "held"]).optional(),
|
|
15494
15992
|
// Why a "failed" send failed, verbatim from the transport. Set only alongside deliveryState "failed".
|
|
15495
15993
|
deliveryError: external_exports.string().optional(),
|
|
15496
15994
|
// FRIZZ wrote this user turn, not the human: it is a scheduler wake delivery (isWakeDelivery). The
|
|
@@ -15615,7 +16113,12 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15615
16113
|
external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("file"), path: SocketFilePath }).strict(),
|
|
15616
16114
|
external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("file"), path: SocketFilePath }).strict()
|
|
15617
16115
|
]);
|
|
15618
|
-
|
|
16116
|
+
SocketBoardClientMsg = external_exports.object({
|
|
16117
|
+
t: external_exports.literal("board"),
|
|
16118
|
+
bootId: external_exports.string().min(1).max(200),
|
|
16119
|
+
seq: external_exports.number().int().min(0)
|
|
16120
|
+
}).strict();
|
|
16121
|
+
SocketClientMsg = external_exports.union([SocketTranscriptClientMsg, SocketFileClientMsg, SocketBoardClientMsg]);
|
|
15619
16122
|
ProjectCard = external_exports.object({
|
|
15620
16123
|
id: external_exports.string(),
|
|
15621
16124
|
slug: external_exports.string(),
|
|
@@ -15625,12 +16128,15 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15625
16128
|
/** The directory is gone — moved or deleted. The card stays so it can be reopened or forgotten. */
|
|
15626
16129
|
stale: external_exports.boolean(),
|
|
15627
16130
|
/**
|
|
15628
|
-
*
|
|
16131
|
+
* A hash of the icon's bytes; for a project with no icon, when it was last scanned. Absent if it
|
|
16132
|
+
* never has been.
|
|
15629
16133
|
*
|
|
15630
16134
|
* Carried purely so the client can hang it off the `/_frizz/project-icon` URL: the icon bytes are
|
|
15631
16135
|
* cached hard (a rail of forty squares is forty requests, and none of them should recur), which
|
|
15632
16136
|
* means a newly uploaded icon would otherwise stay invisible behind the cached old one. A changed
|
|
15633
16137
|
* version is a changed URL, so the swap is immediate without weakening the caching for everyone.
|
|
16138
|
+
* It is the HASH, not the scan time, because a rescan restamps the time every ten minutes whether
|
|
16139
|
+
* or not the picture changed, and every restamp was a fresh download of every icon.
|
|
15634
16140
|
*
|
|
15635
16141
|
* Deliberately NOT "does this project have an icon". Answering that for a project nobody has
|
|
15636
16142
|
* scanned yet would mean scanning it, and this list is one file read on purpose.
|
|
@@ -15642,8 +16148,9 @@ ${DISPATCH_TASK_BANNER}
|
|
|
15642
16148
|
* Three states, not a boolean, because the two "no icon to draw right now" cases must behave
|
|
15643
16149
|
* differently and a boolean collapses them:
|
|
15644
16150
|
* · `icon` — one is stored; draw it.
|
|
15645
|
-
* · `none` — scanned, nothing found. Draw the monogram and DO NOT request the icon
|
|
15646
|
-
* which is what stops an iconless project flashing its initials and then
|
|
16151
|
+
* · `none` — scanned recently, nothing found. Draw the monogram and DO NOT request the icon
|
|
16152
|
+
* route, which is what stops an iconless project flashing its initials and then
|
|
16153
|
+
* swapping. An OLD miss reads `unknown` again, so an icon added since is found.
|
|
15647
16154
|
* · `unknown` — never scanned. The monogram shows, but the request MUST still go out, because
|
|
15648
16155
|
* that request is what triggers the (lazy, cached) scan in the first place.
|
|
15649
16156
|
*
|
|
@@ -20120,7 +20627,7 @@ import {
|
|
|
20120
20627
|
realpathSync as realpathSync2,
|
|
20121
20628
|
renameSync,
|
|
20122
20629
|
rmSync as rmSync3,
|
|
20123
|
-
statSync as
|
|
20630
|
+
statSync as statSync4,
|
|
20124
20631
|
writeFileSync as writeFileSync5
|
|
20125
20632
|
} from "node:fs";
|
|
20126
20633
|
import { homedir as homedir7 } from "node:os";
|
|
@@ -20171,7 +20678,7 @@ function syncDirectory(path) {
|
|
|
20171
20678
|
}
|
|
20172
20679
|
function lockOwnerPath(lockPath) {
|
|
20173
20680
|
try {
|
|
20174
|
-
return
|
|
20681
|
+
return statSync4(lockPath).isDirectory() ? join8(lockPath, GLOBAL_LOCK_OWNER) : lockPath;
|
|
20175
20682
|
} catch {
|
|
20176
20683
|
return lockPath;
|
|
20177
20684
|
}
|
|
@@ -20202,10 +20709,10 @@ function observeLock(lockPath, adapter) {
|
|
|
20202
20709
|
}
|
|
20203
20710
|
try {
|
|
20204
20711
|
const ownerPath = lockOwnerPath(lockPath);
|
|
20205
|
-
let modified =
|
|
20712
|
+
let modified = statSync4(lockPath).mtimeMs;
|
|
20206
20713
|
if (ownerPath !== lockPath) {
|
|
20207
20714
|
try {
|
|
20208
|
-
modified = Math.max(modified,
|
|
20715
|
+
modified = Math.max(modified, statSync4(ownerPath).mtimeMs);
|
|
20209
20716
|
} catch {
|
|
20210
20717
|
}
|
|
20211
20718
|
}
|
|
@@ -20536,7 +21043,7 @@ import {
|
|
|
20536
21043
|
readFileSync as readFileSync6,
|
|
20537
21044
|
renameSync as renameSync2,
|
|
20538
21045
|
rmSync as rmSync4,
|
|
20539
|
-
statSync as
|
|
21046
|
+
statSync as statSync5,
|
|
20540
21047
|
writeFileSync as writeFileSync6
|
|
20541
21048
|
} from "node:fs";
|
|
20542
21049
|
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join9 } from "node:path";
|
|
@@ -20603,7 +21110,7 @@ function quarantine(path, suffix) {
|
|
|
20603
21110
|
}
|
|
20604
21111
|
function pathAge(path) {
|
|
20605
21112
|
try {
|
|
20606
|
-
return Date.now() -
|
|
21113
|
+
return Date.now() - statSync5(path).mtimeMs;
|
|
20607
21114
|
} catch {
|
|
20608
21115
|
return void 0;
|
|
20609
21116
|
}
|
|
@@ -21009,7 +21516,7 @@ var init_boot_progress = __esm({
|
|
|
21009
21516
|
|
|
21010
21517
|
// packages/server/src/project-root.ts
|
|
21011
21518
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
21012
|
-
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,
|
|
21519
|
+
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";
|
|
21013
21520
|
import { homedir as homedir8 } from "node:os";
|
|
21014
21521
|
import { dirname as dirname8, join as join11, parse, resolve as resolve3 } from "node:path";
|
|
21015
21522
|
function projectIdPath(root) {
|
|
@@ -21103,17 +21610,6 @@ function isHomeDirectory(dir, home = homedir8()) {
|
|
|
21103
21610
|
return false;
|
|
21104
21611
|
}
|
|
21105
21612
|
}
|
|
21106
|
-
function isWorldWritable(dir, platform = process.platform) {
|
|
21107
|
-
if (platform === "win32") return false;
|
|
21108
|
-
try {
|
|
21109
|
-
return (statSync5(dir).mode & 2) !== 0;
|
|
21110
|
-
} catch {
|
|
21111
|
-
return false;
|
|
21112
|
-
}
|
|
21113
|
-
}
|
|
21114
|
-
function refuseWorldWritableProject(dir) {
|
|
21115
|
-
if (isWorldWritable(dir)) throw new Error(`A folder every account can write to cannot be a project: ${dir}`);
|
|
21116
|
-
}
|
|
21117
21613
|
function existingProjectId(dir) {
|
|
21118
21614
|
const fromFile = readProjectIdFile(dir);
|
|
21119
21615
|
if (fromFile) return fromFile;
|
|
@@ -21173,7 +21669,7 @@ var init_project_root = __esm({
|
|
|
21173
21669
|
});
|
|
21174
21670
|
|
|
21175
21671
|
// packages/server/src/project-registry.ts
|
|
21176
|
-
import { closeSync as closeSync4, existsSync as existsSync5, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
21672
|
+
import { closeSync as closeSync4, existsSync as existsSync5, fsyncSync as fsyncSync4, lstatSync, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
21177
21673
|
import { homedir as homedir9 } from "node:os";
|
|
21178
21674
|
import { basename as basename4, dirname as dirname9, join as join12 } from "node:path";
|
|
21179
21675
|
function registryPath(home = homedir9()) {
|
|
@@ -21190,10 +21686,11 @@ function readRegistry(home = homedir9()) {
|
|
|
21190
21686
|
const parsed = JSON.parse(raw);
|
|
21191
21687
|
if (parsed?.version !== 1 || !Array.isArray(parsed.projects)) return { ...EMPTY, projects: [] };
|
|
21192
21688
|
const forgotten = Array.isArray(parsed.forgotten) ? parsed.forgotten.filter((id) => typeof id === "string") : [];
|
|
21689
|
+
const host = canonicalPath(launchHostDir(home));
|
|
21193
21690
|
return {
|
|
21194
21691
|
version: 1,
|
|
21195
21692
|
projects: parsed.projects.filter(
|
|
21196
|
-
(p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string"
|
|
21693
|
+
(p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string" && p.path !== host
|
|
21197
21694
|
),
|
|
21198
21695
|
...forgotten.length > 0 ? { forgotten } : {}
|
|
21199
21696
|
};
|
|
@@ -21227,6 +21724,12 @@ function writeRegistry(registry, home = homedir9()) {
|
|
|
21227
21724
|
}
|
|
21228
21725
|
throw error;
|
|
21229
21726
|
}
|
|
21727
|
+
for (const listener of writeListeners) {
|
|
21728
|
+
try {
|
|
21729
|
+
listener(path);
|
|
21730
|
+
} catch {
|
|
21731
|
+
}
|
|
21732
|
+
}
|
|
21230
21733
|
}
|
|
21231
21734
|
function slugifyProject(value) {
|
|
21232
21735
|
const s = slugify(value);
|
|
@@ -21297,20 +21800,11 @@ function registerProject(input, home = homedir9()) {
|
|
|
21297
21800
|
writeRegistry(registry, home);
|
|
21298
21801
|
return { entry, action: "created" };
|
|
21299
21802
|
}
|
|
21300
|
-
function listProjects(home = homedir9()) {
|
|
21301
|
-
const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
|
|
21302
|
-
return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync5(p.path) })).sort((a, b) => {
|
|
21303
|
-
if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
|
|
21304
|
-
if (a.order === void 0) return 1;
|
|
21305
|
-
if (b.order === void 0) return -1;
|
|
21306
|
-
return a.order - b.order;
|
|
21307
|
-
});
|
|
21308
|
-
}
|
|
21309
21803
|
function findByPath(path, home = homedir9()) {
|
|
21310
21804
|
const canonical = canonicalPath(path);
|
|
21311
21805
|
return readRegistry(home).projects.find((p) => p.path === canonical || p.path === path);
|
|
21312
21806
|
}
|
|
21313
|
-
var EMPTY, GENERIC, RESERVED;
|
|
21807
|
+
var EMPTY, GENERIC, RESERVED, writeListeners, ICON_RESCAN_AFTER_MS;
|
|
21314
21808
|
var init_project_registry = __esm({
|
|
21315
21809
|
"packages/server/src/project-registry.ts"() {
|
|
21316
21810
|
"use strict";
|
|
@@ -21342,6 +21836,7 @@ var init_project_registry = __esm({
|
|
|
21342
21836
|
"term",
|
|
21343
21837
|
"attach",
|
|
21344
21838
|
"local-image",
|
|
21839
|
+
"local-file",
|
|
21345
21840
|
"local-visualization",
|
|
21346
21841
|
// `control` answers `/control/stop` and `/control/status` — the supervisor channel the board polls
|
|
21347
21842
|
// every few seconds. It was missing from this list, so a directory called `control` could have taken
|
|
@@ -21356,15 +21851,55 @@ var init_project_registry = __esm({
|
|
|
21356
21851
|
"api",
|
|
21357
21852
|
"health"
|
|
21358
21853
|
]);
|
|
21854
|
+
writeListeners = /* @__PURE__ */ new Set();
|
|
21855
|
+
ICON_RESCAN_AFTER_MS = 10 * 60 * 1e3;
|
|
21856
|
+
}
|
|
21857
|
+
});
|
|
21858
|
+
|
|
21859
|
+
// packages/server/src/launch-routing.ts
|
|
21860
|
+
import { realpathSync as realpathSync5 } from "node:fs";
|
|
21861
|
+
import { homedir as homedir10 } from "node:os";
|
|
21862
|
+
function classifyLaunch(dir, home = homedir10()) {
|
|
21863
|
+
const candidate = realpathSync5(discoverProjectRoot(dir, home));
|
|
21864
|
+
const refused = isHomeDirectory(candidate, home);
|
|
21865
|
+
if (!refused && (isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0)) {
|
|
21866
|
+
return { kind: "open", root: candidate };
|
|
21867
|
+
}
|
|
21868
|
+
return !refused && hasProjectMarker(candidate) ? { kind: "offer", directory: candidate } : { kind: "grid" };
|
|
21869
|
+
}
|
|
21870
|
+
function offerPath(directory) {
|
|
21871
|
+
return `/?add=${encodeURIComponent(directory)}`;
|
|
21872
|
+
}
|
|
21873
|
+
function parseLaunchRoute(body) {
|
|
21874
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
21875
|
+
const { protocol, path, project, notice } = body;
|
|
21876
|
+
if (protocol !== LAUNCH_ROUTE_PROTOCOL || typeof path !== "string") return void 0;
|
|
21877
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\") || /[\u0000-\u001f]/u.test(path)) return void 0;
|
|
21878
|
+
const named = project && typeof project === "object" ? project : void 0;
|
|
21879
|
+
return {
|
|
21880
|
+
path,
|
|
21881
|
+
...named && typeof named.name === "string" && typeof named.dir === "string" ? { project: { name: named.name, dir: named.dir } } : {},
|
|
21882
|
+
...typeof notice === "string" && notice.length > 0 ? { notice: notice.slice(0, 500) } : {}
|
|
21883
|
+
};
|
|
21884
|
+
}
|
|
21885
|
+
var LAUNCH_ROUTE_PROTOCOL, LAUNCH_ROUTE_PATH, LAUNCH_TOKEN_HEADER;
|
|
21886
|
+
var init_launch_routing = __esm({
|
|
21887
|
+
"packages/server/src/launch-routing.ts"() {
|
|
21888
|
+
"use strict";
|
|
21889
|
+
init_project_registry();
|
|
21890
|
+
init_project_root();
|
|
21891
|
+
LAUNCH_ROUTE_PROTOCOL = 1;
|
|
21892
|
+
LAUNCH_ROUTE_PATH = "/control/launch";
|
|
21893
|
+
LAUNCH_TOKEN_HEADER = "x-frizz-launch-token";
|
|
21359
21894
|
}
|
|
21360
21895
|
});
|
|
21361
21896
|
|
|
21362
21897
|
// src/production.ts
|
|
21363
21898
|
init_local_origin();
|
|
21364
|
-
import { readFileSync as readFileSync14 } from "node:fs";
|
|
21365
|
-
import { homedir as
|
|
21899
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync14 } from "node:fs";
|
|
21900
|
+
import { homedir as homedir12 } from "node:os";
|
|
21366
21901
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
21367
|
-
import { join as join17 } from "node:path";
|
|
21902
|
+
import { dirname as dirname14, join as join17 } from "node:path";
|
|
21368
21903
|
|
|
21369
21904
|
// src/browser.ts
|
|
21370
21905
|
import { spawn, execFile } from "node:child_process";
|
|
@@ -23879,7 +24414,7 @@ async function runDeviceFlow(options2) {
|
|
|
23879
24414
|
const clientId = options2.clientId === void 0 ? githubClientId() : options2.clientId;
|
|
23880
24415
|
if (!clientId) {
|
|
23881
24416
|
throw new GithubSignInError(
|
|
23882
|
-
"custom frizz.sh
|
|
24417
|
+
"custom frizz.sh subdomains are not available in this version of Frizz yet: its GitHub sign-in is not set up. A private frizz.sh subdomain needs no account and works today.",
|
|
23883
24418
|
"not-configured"
|
|
23884
24419
|
);
|
|
23885
24420
|
}
|
|
@@ -24417,7 +24952,7 @@ var CUSTOM_NAMES_OFFERED = false;
|
|
|
24417
24952
|
function customNamesOffered(env = process.env) {
|
|
24418
24953
|
return CUSTOM_NAMES_OFFERED || env.FRIZZ_CUSTOM_NAMES === "1";
|
|
24419
24954
|
}
|
|
24420
|
-
var CUSTOM_NAMES_PAUSED = "custom frizz.sh
|
|
24955
|
+
var CUSTOM_NAMES_PAUSED = "custom frizz.sh subdomains are paused for now \u2014 press enter for a private subdomain (no account needed), or answer with the hostname of a tunnel you run";
|
|
24421
24956
|
function isClaimedConfig(config) {
|
|
24422
24957
|
return typeof config.claim === "string" && config.claim.length > 0;
|
|
24423
24958
|
}
|
|
@@ -24437,7 +24972,7 @@ async function reconcileCloudConfig(config, port, home = homedir5(), onNotice, o
|
|
|
24437
24972
|
if (!label) return config;
|
|
24438
24973
|
if (!isAnonymousClaimName(label) && !customNamesOffered()) {
|
|
24439
24974
|
throw new Error(
|
|
24440
|
-
`${config.hostname} needs moving onto the Frizz relay, which means claiming it again, and custom frizz.sh
|
|
24975
|
+
`${config.hostname} needs moving onto the Frizz relay, which means claiming it again, and custom frizz.sh subdomains are paused for now \u2014 press R to pick a private subdomain or another setup`
|
|
24441
24976
|
);
|
|
24442
24977
|
}
|
|
24443
24978
|
onNotice?.(
|
|
@@ -24519,7 +25054,7 @@ async function establishCloudConfig(answer, port, home = homedir5(), origin, git
|
|
|
24519
25054
|
}
|
|
24520
25055
|
if (!REGISTRAR_IS_LIVE && !origin && !process.env.FRIZZ_REGISTRAR) {
|
|
24521
25056
|
throw new Error(
|
|
24522
|
-
"claiming a
|
|
25057
|
+
"claiming a frizz.sh subdomain is not available yet.\n Reach this board through a tunnel of your own instead \u2014 see docs/remote-access.md \u2014 and\n answer with its hostname rather than a bare name."
|
|
24523
25058
|
);
|
|
24524
25059
|
}
|
|
24525
25060
|
const anonymous = answer === "" || isAnonymousClaimName(answer);
|
|
@@ -24535,7 +25070,7 @@ async function establishCloudConfig(answer, port, home = homedir5(), origin, git
|
|
|
24535
25070
|
token = authorization.token;
|
|
24536
25071
|
if (authorization.login) console.log(` claiming ${name}.frizz.sh for GitHub user ${authorization.login}`);
|
|
24537
25072
|
} else {
|
|
24538
|
-
console.log(" claiming a private
|
|
25073
|
+
console.log(" claiming a private frizz.sh subdomain \u2014 no account needed");
|
|
24539
25074
|
}
|
|
24540
25075
|
let result;
|
|
24541
25076
|
try {
|
|
@@ -24735,8 +25270,8 @@ async function probeTailscale() {
|
|
|
24735
25270
|
// src/remote-pane.ts
|
|
24736
25271
|
init_src();
|
|
24737
25272
|
var CHOICES = [
|
|
24738
|
-
{ kind: "private", title: "
|
|
24739
|
-
{ kind: "frizz", title: "Custom
|
|
25273
|
+
{ kind: "private", title: "frizz.sh subdomain", blurb: "a private, unguessable address; no account, nothing to install" },
|
|
25274
|
+
{ kind: "frizz", title: "Custom subdomain", blurb: "<name>.frizz.sh of your choosing; needs a GitHub account" },
|
|
24740
25275
|
{ kind: "cloudflare", title: "Cloudflare Tunnel", blurb: "a domain you own on Cloudflare; cloudflared on this machine" },
|
|
24741
25276
|
{ kind: "tailscale", title: "Tailscale", blurb: "your tailnet; tailscale serve does the TLS" },
|
|
24742
25277
|
{ kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
|
|
@@ -24746,7 +25281,7 @@ function choicesFor(current) {
|
|
|
24746
25281
|
if (customNamesOffered()) return CHOICES;
|
|
24747
25282
|
if (kindOf(current) === "frizz") {
|
|
24748
25283
|
return CHOICES.map(
|
|
24749
|
-
(choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom
|
|
25284
|
+
(choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom subdomains are paused` } : choice
|
|
24750
25285
|
);
|
|
24751
25286
|
}
|
|
24752
25287
|
return CHOICES.filter((choice) => choice.kind !== "frizz");
|
|
@@ -24807,7 +25342,7 @@ function createRemotePane(options2) {
|
|
|
24807
25342
|
"",
|
|
24808
25343
|
...choices().map((choice, index) => {
|
|
24809
25344
|
const marker = index === s.index ? "\u276F" : " ";
|
|
24810
|
-
const title = choice.title.padEnd(
|
|
25345
|
+
const title = choice.title.padEnd(20);
|
|
24811
25346
|
const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
|
|
24812
25347
|
return `${marker} ${title}${DIM}${choice.blurb}${RESET2}${now}`;
|
|
24813
25348
|
}),
|
|
@@ -24862,10 +25397,10 @@ function createRemotePane(options2) {
|
|
|
24862
25397
|
const formHead = (kind) => {
|
|
24863
25398
|
if (kind === "frizz") {
|
|
24864
25399
|
return [
|
|
24865
|
-
"Custom frizz.sh
|
|
25400
|
+
"Custom frizz.sh subdomain",
|
|
24866
25401
|
"",
|
|
24867
25402
|
...wrap(
|
|
24868
|
-
"Claims <name>.frizz.sh of your choosing, tied to your GitHub account so names cannot be squatted. The board dials out to frizz.sh \u2014 no port, no DNS record, no tunnel binary. One name per account; a name nobody runs for 30 days is released. (A private
|
|
25403
|
+
"Claims <name>.frizz.sh of your choosing, tied to your GitHub account so names cannot be squatted. The board dials out to frizz.sh \u2014 no port, no DNS record, no tunnel binary. One name per account; a name nobody runs for 30 days is released. (A private subdomain needs none of this.)"
|
|
24869
25404
|
),
|
|
24870
25405
|
...options2.sandbox ? ["", ...wrap("This is a sandbox, but a claim is real: it binds this machine's one name to your account, and your real board keeps it. Only the setup saved here is thrown away.")] : [],
|
|
24871
25406
|
"",
|
|
@@ -24889,7 +25424,7 @@ function createRemotePane(options2) {
|
|
|
24889
25424
|
" cloudflared tunnel route dns my-board board.example.com",
|
|
24890
25425
|
"",
|
|
24891
25426
|
...wrap(
|
|
24892
|
-
`
|
|
25427
|
+
`Point it at this board in ~/.cloudflared/frizz.yml (ingress to http://127.0.0.1:${options2.port}; https://frizz.sh/docs/cloudflare-tunnel has the file), then name them here. Frizz runs the tunnel beside the board, so the two share a lifetime.`
|
|
24893
25428
|
)
|
|
24894
25429
|
];
|
|
24895
25430
|
}
|
|
@@ -24999,7 +25534,7 @@ function createRemotePane(options2) {
|
|
|
24999
25534
|
};
|
|
25000
25535
|
const claimPrivate = async () => {
|
|
25001
25536
|
const back = { name: "menu", index: 0 };
|
|
25002
|
-
screen = { name: "busy", message: "claiming a private
|
|
25537
|
+
screen = { name: "busy", message: "claiming a private frizz.sh subdomain\u2026" };
|
|
25003
25538
|
paint();
|
|
25004
25539
|
try {
|
|
25005
25540
|
const next = await options2.claim("", { signal: new AbortController().signal, onDeviceCode: () => {
|
|
@@ -25459,20 +25994,20 @@ init_frizz_paths();
|
|
|
25459
25994
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
25460
25995
|
import { createServer as createServer2 } from "node:net";
|
|
25461
25996
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
25462
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync10, realpathSync as
|
|
25463
|
-
import { homedir as
|
|
25997
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync10, realpathSync as realpathSync6, rmSync as rmSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
25998
|
+
import { homedir as homedir11, tmpdir as tmpdir2 } from "node:os";
|
|
25464
25999
|
import { basename as basename5, dirname as dirname10, join as join13, resolve as resolve4 } from "node:path";
|
|
25465
26000
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
25466
26001
|
init_project_root();
|
|
26002
|
+
init_launch_routing();
|
|
25467
26003
|
init_logging();
|
|
25468
26004
|
init_src();
|
|
25469
|
-
init_project_registry();
|
|
25470
26005
|
var PORT_SCAN_COUNT = 100;
|
|
25471
26006
|
var LAUNCH_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
|
|
25472
26007
|
var LAUNCH_HARD_TIMEOUT_MS = BOOT_HARD_TIMEOUT_MS;
|
|
25473
26008
|
var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
|
|
25474
26009
|
function retiredFlagMessage(flag) {
|
|
25475
|
-
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
|
|
26010
|
+
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 subdomain, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
|
|
25476
26011
|
}
|
|
25477
26012
|
function parseCliArgs(argv) {
|
|
25478
26013
|
const args2 = new Set(argv);
|
|
@@ -25583,7 +26118,7 @@ function boardAddress(url) {
|
|
|
25583
26118
|
return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
|
|
25584
26119
|
}
|
|
25585
26120
|
var SANDBOX_DROPPED_XDG_ROOTS = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"];
|
|
25586
|
-
function prepareSandbox(env = process.env, realHome =
|
|
26121
|
+
function prepareSandbox(env = process.env, realHome = homedir11()) {
|
|
25587
26122
|
const home = mkdtempSync(join13(tmpdir2(), "frizz-sandbox-"));
|
|
25588
26123
|
const inherited = { ...env };
|
|
25589
26124
|
env.HOME = home;
|
|
@@ -25614,9 +26149,13 @@ function shareCredentials(realHome, home, realEnv, sandboxEnv) {
|
|
|
25614
26149
|
mkdirSync10(dirname10(realKey), { recursive: true });
|
|
25615
26150
|
mkdirSync10(dirname10(sandboxKey), { recursive: true });
|
|
25616
26151
|
link(realKey, sandboxKey);
|
|
25617
|
-
|
|
25618
|
-
|
|
25619
|
-
|
|
26152
|
+
const realGithub = join13(frizzPaths({ home: realHome, env: realEnv }).data, "github");
|
|
26153
|
+
if (existsSync6(realGithub)) {
|
|
26154
|
+
const sandboxGithub = join13(frizzPaths({ home, env: sandboxEnv }).data, "github");
|
|
26155
|
+
mkdirSync10(dirname10(sandboxGithub), { recursive: true });
|
|
26156
|
+
link(realGithub, sandboxGithub);
|
|
26157
|
+
const keychain = join13(realHome, "Library", "Keychains", "login.keychain-db");
|
|
26158
|
+
if (process.platform === "darwin" && existsSync6(keychain)) sandboxEnv.FRIZZ_GITHUB_KEYCHAIN = keychain;
|
|
25620
26159
|
}
|
|
25621
26160
|
const cloudflared = join13(realHome, ".cloudflared");
|
|
25622
26161
|
if (existsSync6(cloudflared)) {
|
|
@@ -25639,34 +26178,46 @@ function cleanupSandbox(home) {
|
|
|
25639
26178
|
} catch {
|
|
25640
26179
|
}
|
|
25641
26180
|
}
|
|
25642
|
-
function resolveLaunchIntent(cwd = process.cwd(), home =
|
|
25643
|
-
const
|
|
25644
|
-
|
|
25645
|
-
const
|
|
25646
|
-
|
|
25647
|
-
|
|
25648
|
-
|
|
26181
|
+
function resolveLaunchIntent(cwd = process.cwd(), home = homedir11(), env = process.env) {
|
|
26182
|
+
const launch = classifyLaunch(cwd, home);
|
|
26183
|
+
if (launch.kind === "open") return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
|
|
26184
|
+
const host = hostWorkspace(home);
|
|
26185
|
+
return launch.kind === "offer" ? { kind: "offer", workspace: host, directory: launch.directory } : { kind: "grid", workspace: host };
|
|
26186
|
+
}
|
|
26187
|
+
async function askLaunchRoute(port, ownerToken, dir, options2 = {}) {
|
|
26188
|
+
const fetcher = options2.fetcher ?? fetch;
|
|
26189
|
+
const deadline = Date.now() + (options2.patienceMs ?? 6e3);
|
|
26190
|
+
for (; ; ) {
|
|
26191
|
+
const controller = new AbortController();
|
|
26192
|
+
const timeout = setTimeout(() => controller.abort(), options2.timeoutMs ?? 5e3);
|
|
26193
|
+
timeout.unref?.();
|
|
26194
|
+
try {
|
|
26195
|
+
const response = await fetcher(`http://127.0.0.1:${port}${FRIZZ_ROUTE_PREFIX}${LAUNCH_ROUTE_PATH}`, {
|
|
26196
|
+
method: "POST",
|
|
26197
|
+
headers: { "content-type": "application/json", [LAUNCH_TOKEN_HEADER]: ownerToken },
|
|
26198
|
+
body: JSON.stringify({ protocol: LAUNCH_ROUTE_PROTOCOL, dir, ...options2.args?.length ? { args: options2.args } : {} }),
|
|
26199
|
+
signal: controller.signal
|
|
26200
|
+
});
|
|
26201
|
+
if (response.status === 200) return parseLaunchRoute(await response.json());
|
|
26202
|
+
if (response.status !== 503 || Date.now() >= deadline) return void 0;
|
|
26203
|
+
} catch {
|
|
26204
|
+
return void 0;
|
|
26205
|
+
} finally {
|
|
26206
|
+
clearTimeout(timeout);
|
|
26207
|
+
}
|
|
26208
|
+
await delay2(250);
|
|
26209
|
+
}
|
|
25649
26210
|
}
|
|
25650
|
-
function hostWorkspace(home =
|
|
26211
|
+
function hostWorkspace(home = homedir11()) {
|
|
25651
26212
|
const dir = launchHostDir(home);
|
|
25652
26213
|
mkdirSync10(dir, { recursive: true });
|
|
25653
|
-
const root =
|
|
26214
|
+
const root = realpathSync6(dir);
|
|
25654
26215
|
const id = ensureProjectIdFile(root, home);
|
|
25655
26216
|
const stateDir = projectStateDir(id, home);
|
|
25656
26217
|
mkdirSync10(stateDir, { recursive: true });
|
|
25657
26218
|
return { root, id, stateDir, name: basename5(root), identityScope: "repository" };
|
|
25658
26219
|
}
|
|
25659
|
-
function
|
|
25660
|
-
for (const entry of listProjects(home)) {
|
|
25661
|
-
if (entry.stale || isHomeDirectory(entry.path, home) || isWorldWritable(entry.path)) continue;
|
|
25662
|
-
try {
|
|
25663
|
-
return resolveWorkspace(entry.path, home, env);
|
|
25664
|
-
} catch {
|
|
25665
|
-
}
|
|
25666
|
-
}
|
|
25667
|
-
return void 0;
|
|
25668
|
-
}
|
|
25669
|
-
function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process.env) {
|
|
26220
|
+
function resolveWorkspace(cwd = process.cwd(), home = homedir11(), env = process.env) {
|
|
25670
26221
|
let gitRoot;
|
|
25671
26222
|
try {
|
|
25672
26223
|
gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -25680,8 +26231,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process
|
|
|
25680
26231
|
if (!isNotAGitWorktree(error)) throw new Error("unable to resolve Git repository root");
|
|
25681
26232
|
gitRoot = void 0;
|
|
25682
26233
|
}
|
|
25683
|
-
const root0 =
|
|
25684
|
-
refuseWorldWritableProject(root0);
|
|
26234
|
+
const root0 = realpathSync6(gitRoot ?? discoverProjectRoot(cwd, home));
|
|
25685
26235
|
const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
|
|
25686
26236
|
const root = identity?.root ?? root0;
|
|
25687
26237
|
const id = ensureProjectIdFile(root, home, identity?.id);
|
|
@@ -25712,7 +26262,7 @@ function workspaceLaunchTarget(workspace2) {
|
|
|
25712
26262
|
function workspaceFromLaunchTarget(target2, env = process.env) {
|
|
25713
26263
|
let root;
|
|
25714
26264
|
try {
|
|
25715
|
-
root =
|
|
26265
|
+
root = realpathSync6(target2.projectDir);
|
|
25716
26266
|
} catch {
|
|
25717
26267
|
throw new Error("pinned Frizz workspace is no longer available");
|
|
25718
26268
|
}
|
|
@@ -26483,9 +27033,25 @@ var Supervisor = class {
|
|
|
26483
27033
|
if (this.restartCompletion === completion) this.restartCompletion = null;
|
|
26484
27034
|
}
|
|
26485
27035
|
}
|
|
27036
|
+
/**
|
|
27037
|
+
* A restart or an update drains the running child and forks its replacement in the project the
|
|
27038
|
+
* server is hosted on. Once that folder is deleted the fork fails with ENOENT, and a child given
|
|
27039
|
+
* any other cwd refuses to boot without its project — and so does the one that would put the old
|
|
27040
|
+
* child back, so the click meant to refresh the board stops it. The running child is unharmed by
|
|
27041
|
+
* the missing folder, so refuse before draining and leave it serving.
|
|
27042
|
+
*
|
|
27043
|
+
* Measured 2026-10-11 on a board hosted on a folder deleted a day later: its Update failed one
|
|
27044
|
+
* step earlier, on npm dying with `uv_cwd ENOENT`, and that accident was all that kept it up.
|
|
27045
|
+
*/
|
|
27046
|
+
missingLaunchFolder() {
|
|
27047
|
+
if (existsSync7(this.cwd)) return void 0;
|
|
27048
|
+
return `Frizz is running on ${this.cwd}, which no longer exists, so a new server cannot start there. Stop Frizz and start it again.`;
|
|
27049
|
+
}
|
|
26486
27050
|
/** Called only by the public supervisor control endpoint, never by ordinary document reloads. */
|
|
26487
27051
|
restartFromBrowser() {
|
|
26488
27052
|
if (this.browserRestart) return this.browserRestart;
|
|
27053
|
+
const missing = this.missingLaunchFolder();
|
|
27054
|
+
if (missing) return Promise.resolve({ state: "failed", message: missing });
|
|
26489
27055
|
const previousBoot = this.boot?.bootId;
|
|
26490
27056
|
const work = (async () => {
|
|
26491
27057
|
this.lastRestartFailure = void 0;
|
|
@@ -26511,6 +27077,8 @@ var Supervisor = class {
|
|
|
26511
27077
|
}
|
|
26512
27078
|
async updateFromBrowser() {
|
|
26513
27079
|
if (!this.updateRestart) return { state: "failed", message: "Update & Restart is unavailable in this launcher mode" };
|
|
27080
|
+
const missing = this.missingLaunchFolder();
|
|
27081
|
+
if (missing) return { state: "failed", message: missing };
|
|
26514
27082
|
if (this.updateMode === "child") return this.updateChildFromBrowser();
|
|
26515
27083
|
this.emitActivity("updating", "preparing the update \u2014 the running Frizz is untouched until it is ready");
|
|
26516
27084
|
const candidate = await this.updateRestart();
|
|
@@ -27172,7 +27740,7 @@ function createNpmRegistryReleaseAdapter(options2 = {}) {
|
|
|
27172
27740
|
return settle({ code: null, signal: null, stdout: "", stderr: error instanceof Error ? error.message : String(error) });
|
|
27173
27741
|
}
|
|
27174
27742
|
const started = Date.now();
|
|
27175
|
-
const child = spawn3(npm.command, [...npm.prefixArgs, "exec", "--yes", `--package=${packageSpec}`, "--", bin, ...args2], {
|
|
27743
|
+
const child = spawn3(npm.command, [...npm.prefixArgs, "exec", "--yes", "--min-release-age=0", `--package=${packageSpec}`, "--", bin, ...args2], {
|
|
27176
27744
|
cwd,
|
|
27177
27745
|
env,
|
|
27178
27746
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -27220,12 +27788,13 @@ import {
|
|
|
27220
27788
|
mkdirSync as mkdirSync12,
|
|
27221
27789
|
openSync as openSync6,
|
|
27222
27790
|
readFileSync as readFileSync12,
|
|
27223
|
-
realpathSync as
|
|
27791
|
+
realpathSync as realpathSync7,
|
|
27224
27792
|
renameSync as renameSync6,
|
|
27225
27793
|
rmSync as rmSync9,
|
|
27226
27794
|
statSync as statSync6,
|
|
27227
27795
|
writeFileSync as writeFileSync11
|
|
27228
27796
|
} from "node:fs";
|
|
27797
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
27229
27798
|
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";
|
|
27230
27799
|
var SERVER_PROTOCOL = 1;
|
|
27231
27800
|
var SERVER_DATA_EPOCH = 1;
|
|
@@ -27262,9 +27831,18 @@ function serverReleaseSpec(manifest, packageOverride) {
|
|
|
27262
27831
|
assertSpec(spec);
|
|
27263
27832
|
return spec;
|
|
27264
27833
|
}
|
|
27834
|
+
function serverReleaseMinimum(manifest) {
|
|
27835
|
+
const metadata = record(record(manifest).frizzServer);
|
|
27836
|
+
const minimum = metadata.minimum;
|
|
27837
|
+
if (minimum === void 0) return void 0;
|
|
27838
|
+
if (typeof minimum !== "string" || !VERSION.test(minimum)) throw new Error("invalid minimum Frizz server version");
|
|
27839
|
+
const order = typeof metadata.version === "string" ? compareReleaseVersions(metadata.version, minimum) : null;
|
|
27840
|
+
if (order === null || order < 0) throw new Error("the pinned Frizz server is older than the minimum this launcher declares");
|
|
27841
|
+
return minimum;
|
|
27842
|
+
}
|
|
27265
27843
|
function containedFile(root, name, directory = false) {
|
|
27266
|
-
const path =
|
|
27267
|
-
const rel = relative3(
|
|
27844
|
+
const path = realpathSync7(join15(root, name));
|
|
27845
|
+
const rel = relative3(realpathSync7(root), path);
|
|
27268
27846
|
if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute6(rel))
|
|
27269
27847
|
throw new Error(`Frizz server artifact escapes its package: ${name}`);
|
|
27270
27848
|
const stat = statSync6(path);
|
|
@@ -27286,7 +27864,7 @@ function validateServerGeneration(root, spec, id, compatibility = CURRENT_COMPAT
|
|
|
27286
27864
|
containedFile(root, "web-dist/index.html");
|
|
27287
27865
|
containedFile(root, "runtime/board/index.mjs");
|
|
27288
27866
|
containedFile(root, "runtime/cc-worker/.claude-plugin/plugin.json");
|
|
27289
|
-
return { ...spec, id, root:
|
|
27867
|
+
return { ...spec, id, root: realpathSync7(root), entry };
|
|
27290
27868
|
}
|
|
27291
27869
|
function serverGenerationLaunch(generation) {
|
|
27292
27870
|
return {
|
|
@@ -27305,7 +27883,7 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
|
|
|
27305
27883
|
for (const directory of [dirname13(executable), ...(env.PATH ?? env.Path ?? "").split(delimiter2).filter(Boolean)]) {
|
|
27306
27884
|
candidates.push(join15(directory, "node_modules/npm/bin/npm-cli.js"), join15(directory, "../lib/node_modules/npm/bin/npm-cli.js"));
|
|
27307
27885
|
try {
|
|
27308
|
-
const path =
|
|
27886
|
+
const path = realpathSync7(join15(directory, "npm"));
|
|
27309
27887
|
if (path.endsWith("npm-cli.js")) candidates.push(path);
|
|
27310
27888
|
} catch {
|
|
27311
27889
|
}
|
|
@@ -27314,7 +27892,15 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
|
|
|
27314
27892
|
if (!found) throw new Error("npm was not found beside Node or on PATH; install Node with npm to install Frizz server releases");
|
|
27315
27893
|
return resolve6(found);
|
|
27316
27894
|
}
|
|
27317
|
-
|
|
27895
|
+
var NPM_OPERATION_TIMEOUT_MS = 18e4;
|
|
27896
|
+
function npmFailureSummary(stderr) {
|
|
27897
|
+
const lines = stderr.trim().split("\n");
|
|
27898
|
+
const tail = lines.slice(-8);
|
|
27899
|
+
const cause = lines.find((line) => /^(?:[A-Z]\w*)?Error(?: \[\w+\])?: /u.test(line));
|
|
27900
|
+
return (cause && !tail.includes(cause) ? [cause, ...tail] : tail).join("\n");
|
|
27901
|
+
}
|
|
27902
|
+
function npmServerPackageInstaller(env = process.env, options2 = {}) {
|
|
27903
|
+
const timeoutMs = options2.timeoutMs ?? NPM_OPERATION_TIMEOUT_MS;
|
|
27318
27904
|
const run2 = (args2, cwd) => new Promise((resolveOutput, reject) => {
|
|
27319
27905
|
let cli;
|
|
27320
27906
|
try {
|
|
@@ -27328,14 +27914,17 @@ function npmServerPackageInstaller(env = process.env) {
|
|
|
27328
27914
|
cwd,
|
|
27329
27915
|
encoding: "utf8",
|
|
27330
27916
|
windowsHide: true,
|
|
27331
|
-
timeout:
|
|
27917
|
+
timeout: timeoutMs,
|
|
27332
27918
|
killSignal: "SIGKILL",
|
|
27333
27919
|
maxBuffer: 4 * 1024 * 1024
|
|
27334
27920
|
}, (error, stdout, stderr) => {
|
|
27335
|
-
if (error) {
|
|
27336
|
-
|
|
27337
|
-
|
|
27338
|
-
}
|
|
27921
|
+
if (!error) {
|
|
27922
|
+
resolveOutput(stdout);
|
|
27923
|
+
return;
|
|
27924
|
+
}
|
|
27925
|
+
const limit = timeoutMs >= 6e4 ? `${Math.ceil(timeoutMs / 6e4)}m` : `${Math.ceil(timeoutMs / 1e3)}s`;
|
|
27926
|
+
const why = error.killed ? `npm ${args2[0]} did not finish within ${limit} and was stopped` : npmFailureSummary(stderr) || error.message;
|
|
27927
|
+
reject(new Error(`Frizz server npm operation failed: ${why}`));
|
|
27339
27928
|
});
|
|
27340
27929
|
});
|
|
27341
27930
|
return {
|
|
@@ -27347,6 +27936,7 @@ function npmServerPackageInstaller(env = process.env) {
|
|
|
27347
27936
|
prefix,
|
|
27348
27937
|
"--global=false",
|
|
27349
27938
|
"--ignore-scripts",
|
|
27939
|
+
"--min-release-age=0",
|
|
27350
27940
|
"--no-audit",
|
|
27351
27941
|
"--no-fund",
|
|
27352
27942
|
"--engine-strict",
|
|
@@ -27359,7 +27949,7 @@ function npmServerPackageInstaller(env = process.env) {
|
|
|
27359
27949
|
},
|
|
27360
27950
|
async latestVersion(packageName) {
|
|
27361
27951
|
if (!PACKAGE.test(packageName)) throw new Error("invalid Frizz server package name");
|
|
27362
|
-
const parsed = JSON.parse(await run2(["view", `${packageName}@latest`, "version", "--json"]));
|
|
27952
|
+
const parsed = JSON.parse(await run2(["view", `${packageName}@latest`, "version", "--json"], tmpdir3()));
|
|
27363
27953
|
const version = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
|
|
27364
27954
|
if (typeof version !== "string" || !VERSION.test(version)) throw new Error("npm returned an invalid Frizz server version");
|
|
27365
27955
|
return version;
|
|
@@ -27393,11 +27983,12 @@ function atomicJson2(path, value) {
|
|
|
27393
27983
|
}
|
|
27394
27984
|
}
|
|
27395
27985
|
var ServerReleaseStore = class {
|
|
27396
|
-
constructor(spec, installer, roots = frizzPaths(), compatibility = CURRENT_COMPATIBILITY) {
|
|
27986
|
+
constructor(spec, installer, roots = frizzPaths(), compatibility = CURRENT_COMPATIBILITY, minimum) {
|
|
27397
27987
|
this.spec = spec;
|
|
27398
27988
|
this.installer = installer;
|
|
27399
27989
|
this.roots = roots;
|
|
27400
27990
|
this.compatibility = compatibility;
|
|
27991
|
+
this.minimum = minimum;
|
|
27401
27992
|
assertSpec(spec, compatibility);
|
|
27402
27993
|
const key = createHash4("sha256").update(spec.package).digest("hex").slice(0, 16);
|
|
27403
27994
|
this.generations = join15(roots.cache, "server-releases", key);
|
|
@@ -27447,6 +28038,22 @@ var ServerReleaseStore = class {
|
|
|
27447
28038
|
this.markCompatibilityBeforeLaunch();
|
|
27448
28039
|
return generation;
|
|
27449
28040
|
}
|
|
28041
|
+
/**
|
|
28042
|
+
* The selected release, when it is older than this shell works with: `load` will start the pinned
|
|
28043
|
+
* one instead. Undefined for a machine with no selection, an unreadable one (`load` reports that),
|
|
28044
|
+
* and any selection at or above the minimum.
|
|
28045
|
+
*/
|
|
28046
|
+
belowMinimum() {
|
|
28047
|
+
if (this.minimum === void 0 || !existsSync9(this.selection)) return void 0;
|
|
28048
|
+
let selected;
|
|
28049
|
+
try {
|
|
28050
|
+
selected = record(readJson(this.selection)).version;
|
|
28051
|
+
} catch {
|
|
28052
|
+
return void 0;
|
|
28053
|
+
}
|
|
28054
|
+
if (typeof selected !== "string" || compareReleaseVersions(selected, this.minimum) !== -1) return void 0;
|
|
28055
|
+
return { selected, minimum: this.minimum, pinned: this.spec.version };
|
|
28056
|
+
}
|
|
27450
28057
|
async load() {
|
|
27451
28058
|
this.assertMarkerIsSupported();
|
|
27452
28059
|
if (!existsSync9(this.selection)) return this.prepareInitial(this.spec.version);
|
|
@@ -27460,6 +28067,7 @@ var ServerReleaseStore = class {
|
|
|
27460
28067
|
if (spec.dataEpoch < this.compatibility.dataEpoch) return this.prepareInitial(this.spec.version);
|
|
27461
28068
|
if (spec.dataEpoch > this.compatibility.dataEpoch)
|
|
27462
28069
|
throw new Error("this Frizz server requires a newer launcher or a data migration; restart with a compatible Frizz release");
|
|
28070
|
+
if (this.belowMinimum()) return this.prepareInitial(this.spec.version);
|
|
27463
28071
|
const root = this.packageRoot(selected.id);
|
|
27464
28072
|
const generation = !existsSync9(root) ? await this.prepare(spec.version) : validateServerGeneration(root, spec, selected.id, this.compatibility);
|
|
27465
28073
|
this.markCompatibilityBeforeLaunch();
|
|
@@ -27484,7 +28092,7 @@ var ServerReleaseStore = class {
|
|
|
27484
28092
|
}
|
|
27485
28093
|
}
|
|
27486
28094
|
commit(generation) {
|
|
27487
|
-
if (generation.package !== this.spec.package || generation.root !==
|
|
28095
|
+
if (generation.package !== this.spec.package || generation.root !== realpathSync7(this.packageRoot(generation.id)))
|
|
27488
28096
|
throw new Error("cannot select a Frizz server outside this release store");
|
|
27489
28097
|
validateServerGeneration(generation.root, generation, generation.id, this.compatibility);
|
|
27490
28098
|
const { package: packageName, version, protocol, dataEpoch, id } = generation;
|
|
@@ -27496,7 +28104,7 @@ var ServerReleaseStore = class {
|
|
|
27496
28104
|
init_frizz_paths();
|
|
27497
28105
|
init_project_launch();
|
|
27498
28106
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
27499
|
-
import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as
|
|
28107
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as realpathSync8 } from "node:fs";
|
|
27500
28108
|
import { join as join16 } from "node:path";
|
|
27501
28109
|
var STABLE_SERVER_OWNER_NAME = "frizz-server";
|
|
27502
28110
|
var STABLE_SERVER_OWNER_PROJECT_ID = "c1fd5810-0f8a-4c1d-91a0-6d7445d28e5a";
|
|
@@ -27510,7 +28118,7 @@ function stableServerOwnerTarget(roots) {
|
|
|
27510
28118
|
mkdirSync13(resolved.data, { recursive: true, mode: 448 });
|
|
27511
28119
|
return {
|
|
27512
28120
|
projectId: STABLE_SERVER_OWNER_PROJECT_ID,
|
|
27513
|
-
projectDir:
|
|
28121
|
+
projectDir: realpathSync8(resolved.data),
|
|
27514
28122
|
stateDir: join16(resolved.state, STABLE_SERVER_OWNER_NAME)
|
|
27515
28123
|
};
|
|
27516
28124
|
}
|
|
@@ -27587,6 +28195,9 @@ function releaseHeldLease(lease) {
|
|
|
27587
28195
|
return true;
|
|
27588
28196
|
}
|
|
27589
28197
|
|
|
28198
|
+
// src/production.ts
|
|
28199
|
+
init_launch_routing();
|
|
28200
|
+
|
|
27590
28201
|
// src/preflight.ts
|
|
27591
28202
|
import { spawnSync } from "node:child_process";
|
|
27592
28203
|
var SUPPORTED_NODE_LINES = [
|
|
@@ -27681,8 +28292,9 @@ Options:
|
|
|
27681
28292
|
--no-app print the URL without opening a browser
|
|
27682
28293
|
--port <port> request a fixed port for a new workspace server
|
|
27683
28294
|
--sandbox a disposable Frizz to try things in: throwaway home and project, its
|
|
27684
|
-
own port, deleted when this terminal closes; credentials (
|
|
27685
|
-
cloudflared, Claude, Codex, the machine's frizz.sh
|
|
28295
|
+
own port, deleted when this terminal closes; credentials (Frizz's
|
|
28296
|
+
GitHub sign-in, cloudflared, Claude, Codex, the machine's frizz.sh
|
|
28297
|
+
key) are shared
|
|
27686
28298
|
--link print a fresh single-use access link for the running board
|
|
27687
28299
|
--sessions list the devices signed in to the running board
|
|
27688
28300
|
--sign-out <id|all> sign one device out, or every one of them
|
|
@@ -27693,10 +28305,10 @@ Options:
|
|
|
27693
28305
|
|
|
27694
28306
|
|
|
27695
28307
|
To reach the board from a phone or another machine, press R in the terminal running it: a short
|
|
27696
|
-
walkthrough sets up a private frizz.sh
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
28308
|
+
walkthrough sets up a private frizz.sh subdomain (no account needed), a Cloudflare Tunnel,
|
|
28309
|
+
Tailscale, or a proxy of your own, and remembers the choice, so a plain launch serves it from then
|
|
28310
|
+
on. The board stays on loopback and shows a single-use sign-in link as a QR; press L for a fresh
|
|
28311
|
+
one, or run --link from another shell.`
|
|
27700
28312
|
);
|
|
27701
28313
|
process.exit(0);
|
|
27702
28314
|
}
|
|
@@ -27713,7 +28325,13 @@ var serverOwner;
|
|
|
27713
28325
|
process.on("exit", () => {
|
|
27714
28326
|
if (serverOwner) releaseStableServerOwner(serverOwner);
|
|
27715
28327
|
});
|
|
27716
|
-
var
|
|
28328
|
+
var launchDir = (() => {
|
|
28329
|
+
try {
|
|
28330
|
+
return process.cwd();
|
|
28331
|
+
} catch {
|
|
28332
|
+
return homedir12();
|
|
28333
|
+
}
|
|
28334
|
+
})();
|
|
27717
28335
|
var workspace = (() => {
|
|
27718
28336
|
try {
|
|
27719
28337
|
if (!reexec) {
|
|
@@ -27725,21 +28343,26 @@ var workspace = (() => {
|
|
|
27725
28343
|
if (!pinned) throw new Error("registry successor is missing its pinned project identity");
|
|
27726
28344
|
return workspaceFromLaunchTarget(pinned);
|
|
27727
28345
|
}
|
|
27728
|
-
|
|
27729
|
-
launchIntent = intent;
|
|
27730
|
-
return intent.workspace;
|
|
28346
|
+
return hostWorkspace();
|
|
27731
28347
|
} catch (error) {
|
|
27732
28348
|
return fail(error);
|
|
27733
28349
|
}
|
|
27734
28350
|
})();
|
|
27735
|
-
|
|
28351
|
+
function runningBoardPort() {
|
|
28352
|
+
const machine = readStableServerOwner();
|
|
28353
|
+
if (machine.kind === "running") return machine.port;
|
|
28354
|
+
return liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace))?.port;
|
|
28355
|
+
}
|
|
28356
|
+
function controlPorts() {
|
|
28357
|
+
const machine = readStableServerOwner();
|
|
28358
|
+
return [.../* @__PURE__ */ new Set([...machine.kind === "running" ? [machine.port] : [], DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])];
|
|
28359
|
+
}
|
|
27736
28360
|
if (options.stop) {
|
|
27737
28361
|
try {
|
|
27738
|
-
const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace) });
|
|
27739
|
-
const where = control.host ? workspace.root : `${workspace.root} (the board was launched from ${control.target.projectDir})`;
|
|
28362
|
+
const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace), ports: controlPorts() });
|
|
27740
28363
|
const result = await stopProjectLaunch({ stateDir: control.stateDir, target: control.target });
|
|
27741
28364
|
console.log(
|
|
27742
|
-
result.kind !== "stopped" ?
|
|
28365
|
+
result.kind !== "stopped" ? "Frizz is not running" : result.stale ? "Frizz was not running; cleared the record a dead board left behind" : "stopped Frizz; running agents keep going \u2014 they are detached daemons"
|
|
27743
28366
|
);
|
|
27744
28367
|
process.exit(0);
|
|
27745
28368
|
} catch (error) {
|
|
@@ -27747,14 +28370,13 @@ if (options.stop) {
|
|
|
27747
28370
|
}
|
|
27748
28371
|
}
|
|
27749
28372
|
if (options.status) {
|
|
27750
|
-
const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace) });
|
|
28373
|
+
const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace), ports: controlPorts() });
|
|
27751
28374
|
const running = await runningFrizzStatus({ stateDir: control.stateDir, target: control.target });
|
|
27752
28375
|
if (!running) {
|
|
27753
|
-
console.log(
|
|
28376
|
+
console.log("Frizz is not running");
|
|
27754
28377
|
process.exit(1);
|
|
27755
28378
|
}
|
|
27756
28379
|
console.log(`running: http://127.0.0.1:${running.port}`);
|
|
27757
|
-
console.log(`workspace: ${workspace.root}`);
|
|
27758
28380
|
if (!control.host) console.log(`launched from: ${control.target.projectDir} (one server serves every project)`);
|
|
27759
28381
|
console.log(`supervisor pid: ${running.pid}`);
|
|
27760
28382
|
console.log(`version: ${running.version ?? "unknown"}`);
|
|
@@ -27762,23 +28384,23 @@ if (options.status) {
|
|
|
27762
28384
|
process.exit(0);
|
|
27763
28385
|
}
|
|
27764
28386
|
if (options.sessions || options.signOut !== void 0) {
|
|
27765
|
-
const
|
|
27766
|
-
if (!
|
|
27767
|
-
console.error("frizz:
|
|
28387
|
+
const port = runningBoardPort();
|
|
28388
|
+
if (!port) {
|
|
28389
|
+
console.error("frizz: Frizz is not running");
|
|
27768
28390
|
process.exit(1);
|
|
27769
28391
|
}
|
|
27770
|
-
if (options.signOut !== void 0) await signOutSession(
|
|
27771
|
-
await listSessions(
|
|
28392
|
+
if (options.signOut !== void 0) await signOutSession(port, options.signOut);
|
|
28393
|
+
await listSessions(port);
|
|
27772
28394
|
}
|
|
27773
28395
|
if (options.link) {
|
|
27774
|
-
const
|
|
27775
|
-
if (!
|
|
27776
|
-
console.error("frizz:
|
|
28396
|
+
const port = runningBoardPort();
|
|
28397
|
+
if (!port) {
|
|
28398
|
+
console.error("frizz: Frizz is not running");
|
|
27777
28399
|
process.exit(1);
|
|
27778
28400
|
}
|
|
27779
|
-
const response = await fetch(`http://127.0.0.1:${
|
|
28401
|
+
const response = await fetch(`http://127.0.0.1:${port}${SUPERVISOR_ACCESS_CODE_PATH}`, {
|
|
27780
28402
|
method: "POST",
|
|
27781
|
-
headers: { origin: `http://127.0.0.1:${
|
|
28403
|
+
headers: { origin: `http://127.0.0.1:${port}` }
|
|
27782
28404
|
}).catch(() => void 0);
|
|
27783
28405
|
if (!response?.ok) {
|
|
27784
28406
|
const detail = response ? (await response.json().catch(() => void 0))?.error : void 0;
|
|
@@ -27802,7 +28424,7 @@ readout?.plan([
|
|
|
27802
28424
|
{ key: "browser", label: options.noApp ? "Address" : "Browser" }
|
|
27803
28425
|
]);
|
|
27804
28426
|
readout?.begin("server", "starting");
|
|
27805
|
-
logger.info("launcher", `Frizz launcher ${PACKAGE_VERSION} starting
|
|
28427
|
+
logger.info("launcher", `Frizz launcher ${PACKAGE_VERSION} starting in ${launchDir}`);
|
|
27806
28428
|
var target = workspaceLaunchTarget(workspace);
|
|
27807
28429
|
var expected = expectedOwnerHealth(target, readProjectLaunchOwner(workspace.stateDir));
|
|
27808
28430
|
async function existingPort() {
|
|
@@ -27811,39 +28433,54 @@ async function existingPort() {
|
|
|
27811
28433
|
for (const port of new Set(ports)) if (await probeFrizz(port, expected)) return port;
|
|
27812
28434
|
return void 0;
|
|
27813
28435
|
}
|
|
27814
|
-
|
|
28436
|
+
var legacyIntentCache;
|
|
28437
|
+
function legacyIntent() {
|
|
28438
|
+
return legacyIntentCache ??= resolveLaunchIntent(launchDir);
|
|
28439
|
+
}
|
|
28440
|
+
function legacyProject() {
|
|
28441
|
+
const intent = legacyIntent();
|
|
28442
|
+
if (intent.kind !== "open") return void 0;
|
|
28443
|
+
const { id, root, name } = intent.workspace;
|
|
27815
28444
|
try {
|
|
27816
|
-
|
|
27817
|
-
|
|
27818
|
-
homedir11()
|
|
27819
|
-
).entry?.slug;
|
|
28445
|
+
const slug = registerProject({ dir: root, id, remoteOwner: resolveProjectLabel(root)?.split("/")[0] }, homedir12()).entry?.slug;
|
|
28446
|
+
return slug ? { id, root, name, slug } : void 0;
|
|
27820
28447
|
} catch {
|
|
27821
28448
|
return void 0;
|
|
27822
28449
|
}
|
|
27823
28450
|
}
|
|
27824
|
-
|
|
27825
|
-
|
|
27826
|
-
|
|
27827
|
-
if (
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
}
|
|
28451
|
+
function legacyLanding() {
|
|
28452
|
+
try {
|
|
28453
|
+
const intent = legacyIntent();
|
|
28454
|
+
if (intent.kind === "grid") return { path: "/" };
|
|
28455
|
+
if (intent.kind === "offer") return { path: offerPath(intent.directory) };
|
|
28456
|
+
const project = legacyProject();
|
|
28457
|
+
return project ? { path: `/project/${project.slug}`, project: { name: project.name, dir: project.root } } : { path: "/" };
|
|
28458
|
+
} catch (error) {
|
|
28459
|
+
return { path: "/", notice: `Could not open ${launchDir}: ${error instanceof Error ? error.message : String(error)}` };
|
|
27833
28460
|
}
|
|
27834
|
-
return cachedSlugPath;
|
|
27835
28461
|
}
|
|
27836
|
-
async function
|
|
27837
|
-
const
|
|
27838
|
-
if (
|
|
28462
|
+
async function resolveLanding(port, ownerToken) {
|
|
28463
|
+
const answered = ownerToken ? await askLaunchRoute(port, ownerToken, launchDir) : void 0;
|
|
28464
|
+
if (answered) return answered;
|
|
28465
|
+
logger.info("launcher", "the running server did not say where this launch lands; using this launcher's own rules");
|
|
28466
|
+
return legacyLanding();
|
|
28467
|
+
}
|
|
28468
|
+
async function joinPreSplitFrizz() {
|
|
27839
28469
|
for (const port of /* @__PURE__ */ new Set([DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])) {
|
|
27840
|
-
if (await
|
|
27841
|
-
|
|
28470
|
+
if (!await readFrizzHealth(port)) continue;
|
|
28471
|
+
let project;
|
|
28472
|
+
try {
|
|
28473
|
+
project = legacyProject();
|
|
28474
|
+
} catch {
|
|
28475
|
+
return void 0;
|
|
28476
|
+
}
|
|
28477
|
+
if (!project) return void 0;
|
|
28478
|
+
if (await probeFrizz(port, { projectId: project.id, projectDir: project.root, slug: project.slug })) return port;
|
|
27842
28479
|
}
|
|
27843
28480
|
return void 0;
|
|
27844
28481
|
}
|
|
27845
|
-
async function openOrPrint(port, reused,
|
|
27846
|
-
const url = `http://127.0.0.1:${port}${path}`;
|
|
28482
|
+
async function openOrPrint(port, reused, landing) {
|
|
28483
|
+
const url = `http://127.0.0.1:${port}${landing.path}`;
|
|
27847
28484
|
logger.info("launcher", `${reused ? "reusing" : "started"} Frizz at ${url}`);
|
|
27848
28485
|
readout?.settle("server", "done", reused ? `already running on port ${port}` : `port ${port}`);
|
|
27849
28486
|
let browser;
|
|
@@ -27870,26 +28507,27 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
27870
28507
|
const warnings = [];
|
|
27871
28508
|
if (sandbox) warnings.push(`Sandbox: everything here is throwaway (${sandbox.home}) and is deleted when this terminal closes.`);
|
|
27872
28509
|
if (sessionNotice && !reused) warnings.push(sessionNotice);
|
|
28510
|
+
if (landing.notice) warnings.push(landing.notice);
|
|
27873
28511
|
if (!readout) {
|
|
27874
|
-
console.log(`${reused ? "reusing" : "started"} Frizz${
|
|
28512
|
+
console.log(`${reused ? "reusing" : "started"} Frizz${landing.project ? ` for ${landing.project.dir}` : ""}`);
|
|
27875
28513
|
console.log(url);
|
|
27876
28514
|
if (publicOrigin) console.log(activeAccessLink?.url ?? `${publicOrigin}/`);
|
|
27877
28515
|
for (const warning of warnings) console.log(warning);
|
|
27878
28516
|
return;
|
|
27879
28517
|
}
|
|
27880
|
-
const home =
|
|
28518
|
+
const home = homedir12();
|
|
27881
28519
|
readout.ready(
|
|
27882
28520
|
[
|
|
27883
28521
|
{ label: "Local", value: boardAddress(url), accent: true },
|
|
27884
28522
|
...publicOrigin ? [{ label: "Public", value: activeAccessLink?.url ?? `${publicOrigin}/`, accent: true }] : [],
|
|
27885
|
-
// Only
|
|
27886
|
-
//
|
|
27887
|
-
...
|
|
28523
|
+
// Only a launch that OPENED a project names one: the dashboard and an offer are about no
|
|
28524
|
+
// project, and the folder the server runs on is Frizz's own.
|
|
28525
|
+
...landing.project ? [{ label: "Project", value: `${landing.project.name} \u2014 ${tildePath(landing.project.dir, home)}` }] : [],
|
|
27888
28526
|
...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
|
|
27889
28527
|
],
|
|
27890
28528
|
reused ? (
|
|
27891
28529
|
// This launch owns nothing and exits immediately, so ctrl-c would not stop what it reopened.
|
|
27892
|
-
"reopened the
|
|
28530
|
+
"reopened the Frizz already running on this machine \xB7 stop it from the terminal that started it"
|
|
27893
28531
|
) : options.debug ? void 0 : publicOrigin ? "press L for a fresh sign-in link \xB7 R to change how this board is reached \xB7 ctrl-c to stop" : "press R to reach this board from a phone or another machine \xB7 ctrl-c to stop",
|
|
27894
28532
|
{
|
|
27895
28533
|
...reused ? { status: `already running on port ${port}` } : {},
|
|
@@ -27914,32 +28552,49 @@ async function runSupervisor(port, token, onPrepared = () => {
|
|
|
27914
28552
|
target,
|
|
27915
28553
|
owner.token
|
|
27916
28554
|
);
|
|
27917
|
-
const
|
|
27918
|
-
|
|
27919
|
-
process.env.FRIZZ_SERVER_PACKAGE
|
|
27920
|
-
);
|
|
28555
|
+
const manifest = JSON.parse(readFileSync14(join17(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
28556
|
+
const spec = serverReleaseSpec(manifest, process.env.FRIZZ_SERVER_PACKAGE);
|
|
27921
28557
|
const installer = npmServerPackageInstaller(env);
|
|
27922
|
-
const store = new ServerReleaseStore(spec, installer);
|
|
27923
|
-
|
|
28558
|
+
const store = new ServerReleaseStore(spec, installer, void 0, void 0, serverReleaseMinimum(manifest));
|
|
28559
|
+
const raise = store.belowMinimum();
|
|
28560
|
+
if (raise)
|
|
28561
|
+
logger.info("launcher", `the installed Frizz server ${raise.selected} is older than this launcher works with (${raise.minimum}); starting on ${raise.pinned}`);
|
|
28562
|
+
activityReadout?.notice(
|
|
28563
|
+
"progress",
|
|
28564
|
+
"Starting",
|
|
28565
|
+
raise ? `installing Frizz server ${raise.pinned}: ${raise.selected} is older than this launcher works with` : "loading the selected Frizz server release"
|
|
28566
|
+
);
|
|
27924
28567
|
let active = await store.load();
|
|
27925
28568
|
let pending;
|
|
27926
28569
|
let previous = active;
|
|
27927
28570
|
onPrepared();
|
|
27928
28571
|
let updateAvailable = false;
|
|
27929
28572
|
let updateVersion;
|
|
28573
|
+
let probeFailure;
|
|
27930
28574
|
const refreshUpdateAvailable = async () => {
|
|
27931
28575
|
try {
|
|
27932
28576
|
const checkedVersion = active.version;
|
|
27933
28577
|
const plan = await planRegistryUpdate(spec.package, checkedVersion, installer);
|
|
28578
|
+
probeFailure = void 0;
|
|
27934
28579
|
if (active.version !== checkedVersion) return;
|
|
27935
28580
|
updateAvailable = plan !== null;
|
|
27936
28581
|
updateVersion = plan?.latestVersion;
|
|
27937
|
-
} catch {
|
|
28582
|
+
} catch (error) {
|
|
28583
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28584
|
+
if (message !== probeFailure) logger.warn("launcher", `could not check npm for a newer Frizz server: ${message}`);
|
|
28585
|
+
probeFailure = message;
|
|
27938
28586
|
}
|
|
27939
28587
|
};
|
|
27940
28588
|
void refreshUpdateAvailable();
|
|
27941
28589
|
const updateAvailablePoll = setInterval(() => void refreshUpdateAvailable(), 30 * 60 * 1e3);
|
|
27942
28590
|
updateAvailablePoll.unref();
|
|
28591
|
+
try {
|
|
28592
|
+
const projects = dirname14(workspace.stateDir);
|
|
28593
|
+
const adopted = adoptSessionState(workspace.stateDir, readdirSync4(projects).map((name) => join17(projects, name)));
|
|
28594
|
+
if (adopted) logger.info("launcher", `carried the remote sign-in state over from ${adopted}`);
|
|
28595
|
+
} catch (error) {
|
|
28596
|
+
logger.warn("launcher", `could not carry the remote sign-in state over: ${error instanceof Error ? error.message : String(error)}`);
|
|
28597
|
+
}
|
|
27943
28598
|
const sessions = loadSessionState(workspace.stateDir);
|
|
27944
28599
|
const epochRecord = sessionEpochLogLine(sessions.advance, sessions.signedOut);
|
|
27945
28600
|
if (epochRecord) logger.warn("launcher", epochRecord);
|
|
@@ -28043,7 +28698,7 @@ async function runSupervisor(port, token, onPrepared = () => {
|
|
|
28043
28698
|
paneHost?.dispose();
|
|
28044
28699
|
remote?.stop();
|
|
28045
28700
|
const suffix = logger.file ? `
|
|
28046
|
-
log: ${tildePath(logger.file,
|
|
28701
|
+
log: ${tildePath(logger.file, homedir12())}` : "";
|
|
28047
28702
|
process.stdout.write(
|
|
28048
28703
|
code === 0 ? `
|
|
28049
28704
|
Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
|
|
@@ -28080,7 +28735,7 @@ try {
|
|
|
28080
28735
|
if (globalClaim.kind === "running") {
|
|
28081
28736
|
if (options.port && options.port !== globalClaim.port)
|
|
28082
28737
|
throw new Error(`Frizz is already running on port ${globalClaim.port}; --port cannot change a running server`);
|
|
28083
|
-
await openOrPrint(globalClaim.port, true,
|
|
28738
|
+
await openOrPrint(globalClaim.port, true, await resolveLanding(globalClaim.port, globalClaim.owner.token));
|
|
28084
28739
|
process.exit(0);
|
|
28085
28740
|
}
|
|
28086
28741
|
serverOwner = globalClaim.lease;
|
|
@@ -28092,16 +28747,16 @@ try {
|
|
|
28092
28747
|
}
|
|
28093
28748
|
const existing = await existingPort();
|
|
28094
28749
|
if (existing) {
|
|
28095
|
-
await openOrPrint(existing, true,
|
|
28750
|
+
await openOrPrint(existing, true, await resolveLanding(existing, readProjectLaunchOwner(workspace.stateDir)?.token));
|
|
28096
28751
|
process.exit(0);
|
|
28097
28752
|
}
|
|
28098
|
-
const
|
|
28099
|
-
if (
|
|
28100
|
-
await openOrPrint(
|
|
28753
|
+
const preSplit = await joinPreSplitFrizz();
|
|
28754
|
+
if (preSplit) {
|
|
28755
|
+
await openOrPrint(preSplit, true, legacyLanding());
|
|
28101
28756
|
process.exit(0);
|
|
28102
28757
|
}
|
|
28103
28758
|
const claim = tryAcquireProjectLaunchOwner(target, "launcher");
|
|
28104
|
-
if (claim.kind !== "acquired") throw new Error("Frizz is starting
|
|
28759
|
+
if (claim.kind !== "acquired") throw new Error("Frizz is starting on this machine; retry shortly");
|
|
28105
28760
|
let release = await acquireGlobalLaunchLock();
|
|
28106
28761
|
let portReservation;
|
|
28107
28762
|
try {
|
|
@@ -28127,7 +28782,7 @@ try {
|
|
|
28127
28782
|
]);
|
|
28128
28783
|
portReservation();
|
|
28129
28784
|
portReservation = void 0;
|
|
28130
|
-
await openOrPrint(port, false,
|
|
28785
|
+
await openOrPrint(port, false, await resolveLanding(port, serverOwner.token));
|
|
28131
28786
|
await running;
|
|
28132
28787
|
} finally {
|
|
28133
28788
|
release?.();
|