frayui 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/claude-agent-broker.js +19 -3
  2. package/dist/codex-app-server-daemon.js +1 -1
  3. package/dist/dev-child.js +766 -329
  4. package/dist/frayui.js +1291 -193
  5. package/package.json +2 -2
  6. package/runtime/cc-worker/DECISIONS.md +86 -0
  7. package/runtime/cc-worker/hooks/agent-dispatch.mjs +18 -4
  8. package/runtime/cc-worker/hooks/fence-stop.mjs +290 -0
  9. package/runtime/cc-worker/hooks/hooks.json +2 -1
  10. package/runtime/cc-worker/hooks/scratchpad.mjs +10 -9
  11. package/runtime/cc-worker/hooks/session-seed.mjs +2 -1
  12. package/runtime/cc-worker/skills/handoff/SKILL.md +6 -0
  13. package/web-dist/assets/{TerminalPane-DyK0ncN8.js → TerminalPane-hMf6ifuS.js} +1 -1
  14. package/web-dist/assets/{abnfDiagram-VRR7QNED-DtotTgyK.js → abnfDiagram-VRR7QNED-CQj6fwXy.js} +1 -1
  15. package/web-dist/assets/architecture-TIHT7OUA-JX8_jS4I.js +1 -0
  16. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CVl8mUt0.js → architectureDiagram-ZJ3FMSHR-CoE5ct03.js} +1 -1
  17. package/web-dist/assets/{blockDiagram-677ZJIJ3-VUZEsYBW.js → blockDiagram-677ZJIJ3-CEudy3zd.js} +1 -1
  18. package/web-dist/assets/{c4Diagram-LMCZKHZV-Dlg18lBF.js → c4Diagram-LMCZKHZV-ChW62IOH.js} +1 -1
  19. package/web-dist/assets/channel-y3u1yiAC.js +1 -0
  20. package/web-dist/assets/{chunk-32BRIVSS-DG8gd151.js → chunk-32BRIVSS-Bh-A2le2.js} +1 -1
  21. package/web-dist/assets/{chunk-52WLFC77-DKlahFJ2.js → chunk-52WLFC77-7Gia-F6f.js} +1 -1
  22. package/web-dist/assets/{chunk-C7G6YPKG-Bk-BFDsQ.js → chunk-C7G6YPKG-C0MCv0Ym.js} +1 -1
  23. package/web-dist/assets/{chunk-EX3LRPZG-DnXqd-jJ.js → chunk-EX3LRPZG-DPRO7Ss0.js} +1 -1
  24. package/web-dist/assets/{chunk-FWX5IMBZ-ujYyBebT.js → chunk-FWX5IMBZ-Cod9MA7c.js} +2 -2
  25. package/web-dist/assets/{chunk-HOUHSVGY-Dk2XhPWQ.js → chunk-HOUHSVGY-D1oHrXQx.js} +1 -1
  26. package/web-dist/assets/{chunk-ICXQ74PX-DoseDoV4.js → chunk-ICXQ74PX-DBLMA0ZN.js} +1 -1
  27. package/web-dist/assets/{chunk-MOJQB5TN-B8dog4zz.js → chunk-MOJQB5TN-jiLvxNvx.js} +1 -1
  28. package/web-dist/assets/{chunk-OGEWGWER-D4GkHktS.js → chunk-OGEWGWER-w1RtF7G0.js} +1 -1
  29. package/web-dist/assets/{chunk-PUDLZKDR-ChEIayRe.js → chunk-PUDLZKDR-DQlFMKm5.js} +1 -1
  30. package/web-dist/assets/{chunk-Q4XR5HBZ-BtzMOr97.js → chunk-Q4XR5HBZ-CfD6vdr0.js} +1 -1
  31. package/web-dist/assets/{chunk-V7JOEXUC-DQ7c8QAd.js → chunk-V7JOEXUC-BRB1iO1P.js} +1 -1
  32. package/web-dist/assets/{chunk-VAUOI2AC-6jWzOU_J.js → chunk-VAUOI2AC-CCeJEpxW.js} +1 -1
  33. package/web-dist/assets/{chunk-VR4S4FIN-TB0TNuya.js → chunk-VR4S4FIN-DfPRj9a1.js} +1 -1
  34. package/web-dist/assets/{chunk-WYO6CB5R-CY7fL6F5.js → chunk-WYO6CB5R-BWd84n6L.js} +1 -1
  35. package/web-dist/assets/{chunk-ZGVPDNZ5-vZO6zPtA.js → chunk-ZGVPDNZ5-Bm-3TA9N.js} +1 -1
  36. package/web-dist/assets/classDiagram-OUVF2IWQ-BL8mggXA.js +1 -0
  37. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BL8mggXA.js +1 -0
  38. package/web-dist/assets/{cynefin-VYW2F7L2-D4NcAp7V.js → cynefin-VYW2F7L2-BylEqg5y.js} +1 -1
  39. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-hLO8f53U.js → cynefinDiagram-TSTJHNR4-CRfGS7Ou.js} +1 -1
  40. package/web-dist/assets/{dagre-VKFMJZFB-86z9AWRw.js → dagre-VKFMJZFB-CNeTu13Z.js} +1 -1
  41. package/web-dist/assets/{diagram-FQU43EPY-DXw3bj8G.js → diagram-FQU43EPY-BYZKetkJ.js} +1 -1
  42. package/web-dist/assets/{diagram-G47NLZAW-BtH3_eiQ.js → diagram-G47NLZAW-DSOzyl5U.js} +1 -1
  43. package/web-dist/assets/{diagram-NH7WQ7WH-BxWq2H6D.js → diagram-NH7WQ7WH-DFZCvfJN.js} +1 -1
  44. package/web-dist/assets/{diagram-OA4YK3LP-BoU3I6Xx.js → diagram-OA4YK3LP-m0qY8Zlg.js} +1 -1
  45. package/web-dist/assets/{diagram-WEI45ONY-CkPD-vrA.js → diagram-WEI45ONY-Cg7Qz-0T.js} +1 -1
  46. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-BudQTnvf.js → ebnfDiagram-CCIWWBDH-DGvTjoXK.js} +1 -1
  47. package/web-dist/assets/{erDiagram-Q63AITRT-CRFLgkaW.js → erDiagram-Q63AITRT-BFqTK5TB.js} +1 -1
  48. package/web-dist/assets/eventmodeling-45OFAUF4-BaCHCepY.js +1 -0
  49. package/web-dist/assets/flowDiagram-23GEKE2U-Cjl6weoT.js +1 -0
  50. package/web-dist/assets/{ganttDiagram-NO4QXBWP-DFmOc45y.js → ganttDiagram-NO4QXBWP-KbR9IE0O.js} +1 -1
  51. package/web-dist/assets/{gitGraph-TEB2WS4Q-CMW6meDa.js → gitGraph-TEB2WS4Q-hCbzZO_l.js} +1 -1
  52. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-CHelbJfg.js → gitGraphDiagram-IHSO6WYX-CNPRTwQG.js} +1 -1
  53. package/web-dist/assets/index-BUY5ijyI.css +1 -0
  54. package/web-dist/assets/{index-C2Tf3Ihs.js → index-Bj6YWd3g.js} +68 -68
  55. package/web-dist/assets/{info-DKCQHKI2-DZT4W61r.js → info-DKCQHKI2-Zd_TXMfr.js} +1 -1
  56. package/web-dist/assets/{infoDiagram-FWYZ7A6U-Dj0tLhVO.js → infoDiagram-FWYZ7A6U-DQ9zmVhJ.js} +1 -1
  57. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-D825ES0c.js → ishikawaDiagram-FXEZZL3T-BFyJ5E_L.js} +1 -1
  58. package/web-dist/assets/{journeyDiagram-5HDEW3XC-D53Igq5c.js → journeyDiagram-5HDEW3XC-BgjbY7_T.js} +1 -1
  59. package/web-dist/assets/{kanban-definition-HUTT4EX6-CJ-F0vAo.js → kanban-definition-HUTT4EX6-mpUEl8xx.js} +1 -1
  60. package/web-dist/assets/{line-CuiC27jO.js → line-CM_FUYGm.js} +1 -1
  61. package/web-dist/assets/{mermaid-parser.core-rG6qAIn0.js → mermaid-parser.core-CTVz2V70.js} +3 -3
  62. package/web-dist/assets/{mermaid.core-DzNUOuDq.js → mermaid.core-BCLNmVvR.js} +3 -3
  63. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DHfhQue9.js → mindmap-definition-LN4V7U3C-CgJU-6O7.js} +1 -1
  64. package/web-dist/assets/{packet-7NZHBO7P-BiUlKhVG.js → packet-7NZHBO7P-rkLFb4ZX.js} +1 -1
  65. package/web-dist/assets/{pegDiagram-2B236MQR-MQdiKZx3.js → pegDiagram-2B236MQR-Sy2oYuu1.js} +1 -1
  66. package/web-dist/assets/{pie-RZYD4A2V-dNHUvvE3.js → pie-RZYD4A2V-0yTxNtqZ.js} +1 -1
  67. package/web-dist/assets/{pieDiagram-ENE6RG2P-BwFxe1x1.js → pieDiagram-ENE6RG2P-CQ7YFWhH.js} +1 -1
  68. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-9dic-8vv.js → quadrantDiagram-ABIIQ3AL-ClCTxrOj.js} +1 -1
  69. package/web-dist/assets/{radar-I7S5WNFK-CK3dl20X.js → radar-I7S5WNFK-JUOW5mdN.js} +1 -1
  70. package/web-dist/assets/{railroad-3IZDKUUU-BuB2LkFj.js → railroad-3IZDKUUU-DJ2IS1Jg.js} +1 -1
  71. package/web-dist/assets/railroad-abnf-AHOZXSZD-CU4HK7Ek.js +1 -0
  72. package/web-dist/assets/railroad-ebnf-EBAXGLYW-CarBAd7R.js +1 -0
  73. package/web-dist/assets/railroad-peg-LSFZ7HO6-ByEhVLyD.js +1 -0
  74. package/web-dist/assets/{railroadDiagram-RFXS5EU6-CuuspS_3.js → railroadDiagram-RFXS5EU6-BNsVGeA3.js} +1 -1
  75. package/web-dist/assets/{requirementDiagram-TGXJPOKE-DHAILLdh.js → requirementDiagram-TGXJPOKE-BHa3IX0k.js} +1 -1
  76. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-CztQ2t1b.js → sankeyDiagram-HTMAVEWB-CSbsLbtw.js} +1 -1
  77. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-jk56eo0l.js → sequenceDiagram-DBY2YBRQ-BgVWj1nN.js} +1 -1
  78. package/web-dist/assets/{stateDiagram-2N3HPSRC-uwqBWnrI.js → stateDiagram-2N3HPSRC-BKHYNxA3.js} +1 -1
  79. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-C-k0bbcG.js +1 -0
  80. package/web-dist/assets/{swimlanes-5IMT3BWC-BzuJ77j6.js → swimlanes-5IMT3BWC-BluloqBJ.js} +1 -1
  81. package/web-dist/assets/swimlanesDiagram-G3AALYLV-DVjLYZbp.js +8 -0
  82. package/web-dist/assets/{timeline-definition-FHXFAJF6-CVDNkzi9.js → timeline-definition-FHXFAJF6-Bj1WKNgB.js} +1 -1
  83. package/web-dist/assets/{treeView-QDETBFTQ-CdkPxaDl.js → treeView-QDETBFTQ-Dj5T24Qr.js} +1 -1
  84. package/web-dist/assets/{treemap-6X3UGDF4-lnksCHdn.js → treemap-6X3UGDF4-BPqewrXp.js} +1 -1
  85. package/web-dist/assets/{vennDiagram-L72KCM5P-B533fRM0.js → vennDiagram-L72KCM5P-BhggGhQv.js} +1 -1
  86. package/web-dist/assets/{wardley-OPB4EBWU-DwKf-2YJ.js → wardley-OPB4EBWU-Di9GWKYt.js} +1 -1
  87. package/web-dist/assets/{wardleyDiagram-EHGQE667-BDDN72B1.js → wardleyDiagram-EHGQE667-D-mtWLKf.js} +1 -1
  88. package/web-dist/assets/{xychartDiagram-FW5EYKEG-CkvHmNVt.js → xychartDiagram-FW5EYKEG-CU0us6D_.js} +1 -1
  89. package/web-dist/index.html +2 -2
  90. package/web-dist/assets/architecture-TIHT7OUA-B23bEjC4.js +0 -1
  91. package/web-dist/assets/channel-BMFwXMMq.js +0 -1
  92. package/web-dist/assets/classDiagram-OUVF2IWQ-DiWatUgG.js +0 -1
  93. package/web-dist/assets/classDiagram-v2-EOCWNBFH-DiWatUgG.js +0 -1
  94. package/web-dist/assets/eventmodeling-45OFAUF4-C7ZRnE7-.js +0 -1
  95. package/web-dist/assets/flowDiagram-23GEKE2U-DKS-kFcF.js +0 -1
  96. package/web-dist/assets/index-CeAC3wUW.css +0 -1
  97. package/web-dist/assets/railroad-abnf-AHOZXSZD-C1-MJAeY.js +0 -1
  98. package/web-dist/assets/railroad-ebnf-EBAXGLYW-crQCD9rJ.js +0 -1
  99. package/web-dist/assets/railroad-peg-LSFZ7HO6-Ba-_QGKi.js +0 -1
  100. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DOPbAEV3.js +0 -1
  101. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BRh1ca21.js +0 -8
package/dist/frayui.js CHANGED
@@ -10,6 +10,193 @@ var __export = (target2, all) => {
10
10
  __defProp(target2, name, { get: all[name], enumerable: true });
11
11
  };
12
12
 
13
+ // packages/server/src/logging.ts
14
+ import {
15
+ appendFileSync,
16
+ mkdirSync,
17
+ openSync,
18
+ readdirSync,
19
+ rmSync,
20
+ statSync,
21
+ symlinkSync,
22
+ writeFileSync,
23
+ writeSync
24
+ } from "node:fs";
25
+ import { homedir as homedir2 } from "node:os";
26
+ import { join as join2 } from "node:path";
27
+ function defaultLogRoot(stateDir, home = homedir2()) {
28
+ return stateDir ? join2(stateDir, "logs") : join2(home, ".fray", "logs");
29
+ }
30
+ function runStamp(at) {
31
+ const pad = (value) => String(value).padStart(2, "0");
32
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
33
+ }
34
+ function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir2(), env = process.env) {
35
+ const name = `fray-${runStamp(at)}-${pid}.log`;
36
+ const override = env[LOG_PATH_ENV]?.trim();
37
+ if (override) return override.endsWith(".log") ? override : join2(override, name);
38
+ return join2(defaultLogRoot(stateDir, home), name);
39
+ }
40
+ function latestLogPath(dir) {
41
+ return join2(dir, "latest.log");
42
+ }
43
+ function linkLatest(dir, target2) {
44
+ const link = latestLogPath(dir);
45
+ try {
46
+ rmSync(link, { force: true });
47
+ symlinkSync(target2, link);
48
+ } catch {
49
+ try {
50
+ writeFileSync(link, `${target2}
51
+ `, { mode: 384 });
52
+ } catch {
53
+ }
54
+ }
55
+ }
56
+ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
57
+ let entries;
58
+ try {
59
+ entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
60
+ } catch {
61
+ return;
62
+ }
63
+ const dated = entries.map((name) => {
64
+ try {
65
+ return { name, at: statSync(join2(dir, name)).mtimeMs };
66
+ } catch {
67
+ return { name, at: 0 };
68
+ }
69
+ }).sort((a, b) => b.at - a.at);
70
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
71
+ const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
72
+ for (const entry of stale) {
73
+ try {
74
+ rmSync(join2(dir, entry.name), { force: true });
75
+ } catch {
76
+ }
77
+ }
78
+ }
79
+ function formatDiskLine(record) {
80
+ const at = new Date(record.at);
81
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
82
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
83
+ return `${clock} ${record.level.toUpperCase().padEnd(5)} ${record.scope.padEnd(12)} ${record.message}
84
+ `;
85
+ }
86
+ function openLogFile(path) {
87
+ try {
88
+ mkdirSync(join2(path, ".."), { recursive: true, mode: 448 });
89
+ return openSync(path, "a", 384);
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ function createLogger(options2 = {}) {
95
+ const path = options2.file === null ? null : options2.file ?? runLogPath(options2.stateDir);
96
+ const level = options2.level ?? "debug";
97
+ const now = options2.now ?? Date.now;
98
+ const threshold = LEVEL_ORDER[level];
99
+ const maxBytes = options2.maxBytes ?? MAX_LOG_BYTES;
100
+ let fd = path === null ? null : openLogFile(path);
101
+ if (fd !== null && path !== null && options2.owner !== false) {
102
+ const dir = join2(path, "..");
103
+ pruneRunLogs(dir);
104
+ linkLatest(dir, path);
105
+ }
106
+ let written = 0;
107
+ const listeners = /* @__PURE__ */ new Set();
108
+ const emit = (recordLevel, scope, message) => {
109
+ if (LEVEL_ORDER[recordLevel] < threshold) return;
110
+ const record = { at: now(), level: recordLevel, scope, message };
111
+ if (fd !== null) {
112
+ try {
113
+ const line = formatDiskLine(record);
114
+ if (written + line.length > maxBytes) {
115
+ writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
116
+ `);
117
+ fd = null;
118
+ } else {
119
+ writeSync(fd, line);
120
+ written += line.length;
121
+ }
122
+ } catch {
123
+ fd = null;
124
+ }
125
+ }
126
+ for (const listener of listeners) {
127
+ try {
128
+ listener(record);
129
+ } catch {
130
+ }
131
+ }
132
+ };
133
+ return {
134
+ debug: (scope, message) => emit("debug", scope, message),
135
+ info: (scope, message) => emit("info", scope, message),
136
+ warn: (scope, message) => emit("warn", scope, message),
137
+ error: (scope, message) => emit("error", scope, message),
138
+ get file() {
139
+ return fd === null ? null : path;
140
+ },
141
+ onRecord(listener) {
142
+ listeners.add(listener);
143
+ return () => listeners.delete(listener);
144
+ },
145
+ close() {
146
+ listeners.clear();
147
+ fd = null;
148
+ }
149
+ };
150
+ }
151
+ function setAmbientLogger(logger2) {
152
+ ambient = logger2;
153
+ return logger2;
154
+ }
155
+ function formatFeedLine(record) {
156
+ return formatDiskLine(record).trimEnd();
157
+ }
158
+ function attachTerminalMirror(logger2, enabled = process.env.FRAY_DEBUG === "1") {
159
+ if (!enabled) return () => {
160
+ };
161
+ return logger2.onRecord((record) => process.stderr.write(`${formatFeedLine(record)}
162
+ `));
163
+ }
164
+ function ambientLogger() {
165
+ if (ambient) return ambient;
166
+ const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
167
+ const level = process.env[LOG_LEVEL_ENV];
168
+ const logger2 = createLogger({
169
+ ...inherited ? { file: inherited, owner: false } : { file: null },
170
+ ...level && level in LEVEL_ORDER ? { level } : {}
171
+ });
172
+ attachTerminalMirror(logger2);
173
+ ambient = logger2;
174
+ return ambient;
175
+ }
176
+ function logEnvironment(logger2, level = "debug") {
177
+ return logger2.file ? { [LOG_FILE_ENV]: logger2.file, [LOG_LEVEL_ENV]: level } : {};
178
+ }
179
+ var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
180
+ var init_logging = __esm({
181
+ "packages/server/src/logging.ts"() {
182
+ "use strict";
183
+ LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
184
+ LOG_FILE_ENV = "FRAY_LOG_FILE";
185
+ LOG_LEVEL_ENV = "FRAY_LOG_LEVEL";
186
+ LOG_PATH_ENV = "FRAY_LOG_PATH";
187
+ RETAINED_RUNS = 20;
188
+ RETAINED_DAYS = 14;
189
+ MAX_LOG_BYTES = 32 * 1024 * 1024;
190
+ ambient = null;
191
+ log = {
192
+ debug: (scope, message) => ambientLogger().debug(scope, message),
193
+ info: (scope, message) => ambientLogger().info(scope, message),
194
+ warn: (scope, message) => ambientLogger().warn(scope, message),
195
+ error: (scope, message) => ambientLogger().error(scope, message)
196
+ };
197
+ }
198
+ });
199
+
13
200
  // packages/server/src/process-generation.ts
14
201
  import { execFileSync } from "node:child_process";
15
202
  import { randomUUID } from "node:crypto";
@@ -116,17 +303,17 @@ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
116
303
  import {
117
304
  closeSync,
118
305
  fsyncSync,
119
- mkdirSync,
120
- openSync,
306
+ mkdirSync as mkdirSync2,
307
+ openSync as openSync2,
121
308
  readFileSync as readFileSync2,
122
309
  realpathSync,
123
310
  renameSync,
124
- rmSync,
125
- statSync,
126
- writeFileSync
311
+ rmSync as rmSync2,
312
+ statSync as statSync2,
313
+ writeFileSync as writeFileSync2
127
314
  } from "node:fs";
128
- import { homedir } from "node:os";
129
- import { dirname, join, resolve } from "node:path";
315
+ import { homedir as homedir3 } from "node:os";
316
+ import { dirname, join as join3, resolve } from "node:path";
130
317
  import { setTimeout as delay } from "node:timers/promises";
131
318
  function errorCode2(error) {
132
319
  return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
@@ -141,11 +328,11 @@ function canonicalHome(home) {
141
328
  return resolve(home);
142
329
  }
143
330
  }
144
- function globalLaunchLockPath(home = homedir()) {
145
- return join(canonicalHome(home), ".fray", GLOBAL_LOCK_NAME);
331
+ function globalLaunchLockPath(home = homedir3()) {
332
+ return join3(canonicalHome(home), ".fray", GLOBAL_LOCK_NAME);
146
333
  }
147
334
  function namedLaunchLockPath(home, name) {
148
- return join(canonicalHome(home), ".fray", name);
335
+ return join3(canonicalHome(home), ".fray", name);
149
336
  }
150
337
  function pidIsAlive(pid) {
151
338
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -159,7 +346,7 @@ function pidIsAlive(pid) {
159
346
  function syncDirectory(path) {
160
347
  let fd;
161
348
  try {
162
- fd = openSync(path, "r");
349
+ fd = openSync2(path, "r");
163
350
  fsyncSync(fd);
164
351
  } catch {
165
352
  } finally {
@@ -173,7 +360,7 @@ function syncDirectory(path) {
173
360
  }
174
361
  function lockOwnerPath(lockPath) {
175
362
  try {
176
- return statSync(lockPath).isDirectory() ? join(lockPath, GLOBAL_LOCK_OWNER) : lockPath;
363
+ return statSync2(lockPath).isDirectory() ? join3(lockPath, GLOBAL_LOCK_OWNER) : lockPath;
177
364
  } catch {
178
365
  return lockPath;
179
366
  }
@@ -204,10 +391,10 @@ function observeLock(lockPath, adapter) {
204
391
  }
205
392
  try {
206
393
  const ownerPath = lockOwnerPath(lockPath);
207
- let modified = statSync(lockPath).mtimeMs;
394
+ let modified = statSync2(lockPath).mtimeMs;
208
395
  if (ownerPath !== lockPath) {
209
396
  try {
210
- modified = Math.max(modified, statSync(ownerPath).mtimeMs);
397
+ modified = Math.max(modified, statSync2(ownerPath).mtimeMs);
211
398
  } catch {
212
399
  }
213
400
  }
@@ -225,16 +412,16 @@ function quarantineLock(lockPath) {
225
412
  return false;
226
413
  }
227
414
  try {
228
- rmSync(quarantine2, { recursive: true, force: true });
415
+ rmSync2(quarantine2, { recursive: true, force: true });
229
416
  } catch {
230
417
  }
231
418
  return true;
232
419
  }
233
420
  function tryAcquireLock(lockPath, adapter) {
234
- mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
421
+ mkdirSync2(dirname(lockPath), { recursive: true, mode: 448 });
235
422
  let fd;
236
423
  try {
237
- fd = openSync(lockPath, "wx", 384);
424
+ fd = openSync2(lockPath, "wx", 384);
238
425
  } catch (error) {
239
426
  if (errorCode2(error) === "EEXIST") return void 0;
240
427
  throw error;
@@ -248,7 +435,7 @@ function tryAcquireLock(lockPath, adapter) {
248
435
  at: new Date(adapter.now()).toISOString()
249
436
  };
250
437
  try {
251
- writeFileSync(fd, `${JSON.stringify(owner)}
438
+ writeFileSync2(fd, `${JSON.stringify(owner)}
252
439
  `, "utf8");
253
440
  fsyncSync(fd);
254
441
  closeSync(fd);
@@ -271,7 +458,7 @@ function tryAcquireLock(lockPath, adapter) {
271
458
  const quarantine2 = `${lockPath}.release-${process.pid}-${owner.token}`;
272
459
  try {
273
460
  renameSync(lockPath, quarantine2);
274
- rmSync(quarantine2, { recursive: true, force: true });
461
+ rmSync2(quarantine2, { recursive: true, force: true });
275
462
  } catch {
276
463
  }
277
464
  };
@@ -279,12 +466,23 @@ function tryAcquireLock(lockPath, adapter) {
279
466
  function tryAcquireGlobalLaunchLock(home, adapter) {
280
467
  return tryAcquireLock(globalLaunchLockPath(home), adapter);
281
468
  }
469
+ function portReservationPath(port, home = homedir3()) {
470
+ return join3(canonicalHome(home), ".fray", PORT_LOCK_DIR, `${port}.lock`);
471
+ }
472
+ function tryReservePort(port, home = homedir3(), adapter = defaultProcessPlatformAdapter) {
473
+ const lockPath = portReservationPath(port, home);
474
+ const release = tryAcquireLock(lockPath, adapter);
475
+ if (release) return release;
476
+ const observation = observeLock(lockPath, adapter);
477
+ if (!observation.stale || !quarantineLock(lockPath)) return void 0;
478
+ return tryAcquireLock(lockPath, adapter);
479
+ }
282
480
  function lockTimeoutError(observation) {
283
481
  return new Error(
284
482
  observation.ownerPid === void 0 ? "another Fray launch still owns the global launch lock" : `another Fray launch (pid ${observation.ownerPid}) still owns the global launch lock`
285
483
  );
286
484
  }
287
- async function acquireGlobalLaunchLock(home = homedir(), timeoutMs = 1e4, adapter = defaultProcessPlatformAdapter) {
485
+ async function acquireGlobalLaunchLock(home = homedir3(), timeoutMs = 1e4, adapter = defaultProcessPlatformAdapter) {
288
486
  const deadline = Date.now() + Math.max(0, timeoutMs);
289
487
  let observation = { stale: false };
290
488
  for (; ; ) {
@@ -350,7 +548,7 @@ function resolveGitWorktree(dir) {
350
548
  // Do not enable Git's repository-wide `extensions.worktreeConfig` merely to hold one private Fray
351
549
  // value. A config file inside the linked worktree's own administrative directory has Git's atomic
352
550
  // config-lock behavior, survives `git worktree move`, and disappears with `git worktree remove`.
353
- ...scope === "worktree" ? { identityConfig: join(gitDir, "fray.config") } : {}
551
+ ...scope === "worktree" ? { identityConfig: join3(gitDir, "fray.config") } : {}
354
552
  };
355
553
  }
356
554
  function readProjectIdConfig(dir, args2, description) {
@@ -385,7 +583,7 @@ function readGitWorktreeProjectId(worktree) {
385
583
  );
386
584
  }
387
585
  function syncGitConfigPath(path) {
388
- const fd = openSync(path, "r");
586
+ const fd = openSync2(path, "r");
389
587
  try {
390
588
  fsyncSync(fd);
391
589
  } finally {
@@ -452,7 +650,7 @@ function resolvedIdentity(worktree) {
452
650
  }
453
651
  return { ...worktree, id: worktreeId };
454
652
  }
455
- function resolveGitProjectIdentity(dir, home = homedir()) {
653
+ function resolveGitProjectIdentity(dir, home = homedir3()) {
456
654
  let worktree = resolveGitWorktree(dir);
457
655
  const existing = resolvedIdentity(worktree);
458
656
  if (existing) return existing;
@@ -472,13 +670,14 @@ function resolveGitProjectIdentity(dir, home = homedir()) {
472
670
  release();
473
671
  }
474
672
  }
475
- var GLOBAL_LOCK_NAME, GLOBAL_LOCK_OWNER, INCOMPLETE_LOCK_GRACE_MS, LOCK_POLL_MS, PROJECT_ID_RE, SYNC_WAIT2;
673
+ var GLOBAL_LOCK_NAME, GLOBAL_LOCK_OWNER, PORT_LOCK_DIR, INCOMPLETE_LOCK_GRACE_MS, LOCK_POLL_MS, PROJECT_ID_RE, SYNC_WAIT2;
476
674
  var init_project_identity = __esm({
477
675
  "packages/server/src/project-identity.ts"() {
478
676
  "use strict";
479
677
  init_process_generation();
480
678
  GLOBAL_LOCK_NAME = "dev-launch.lock";
481
679
  GLOBAL_LOCK_OWNER = "owner.json";
680
+ PORT_LOCK_DIR = "ports";
482
681
  INCOMPLETE_LOCK_GRACE_MS = 1e3;
483
682
  LOCK_POLL_MS = 50;
484
683
  PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
@@ -491,15 +690,15 @@ import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypt
491
690
  import {
492
691
  closeSync as closeSync2,
493
692
  fsyncSync as fsyncSync2,
494
- mkdirSync as mkdirSync2,
495
- openSync as openSync2,
693
+ mkdirSync as mkdirSync3,
694
+ openSync as openSync3,
496
695
  readFileSync as readFileSync3,
497
696
  renameSync as renameSync2,
498
- rmSync as rmSync2,
499
- statSync as statSync2,
500
- writeFileSync as writeFileSync2
697
+ rmSync as rmSync3,
698
+ statSync as statSync3,
699
+ writeFileSync as writeFileSync3
501
700
  } from "node:fs";
502
- import { basename, dirname as dirname2, isAbsolute, join as join2 } from "node:path";
701
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join4 } from "node:path";
503
702
  function errorCode3(error) {
504
703
  return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
505
704
  }
@@ -509,7 +708,7 @@ function validText(value, max = 4096) {
509
708
  function syncDirectory2(path) {
510
709
  let fd;
511
710
  try {
512
- fd = openSync2(path, "r");
711
+ fd = openSync3(path, "r");
513
712
  fsyncSync2(fd);
514
713
  } catch {
515
714
  } finally {
@@ -522,12 +721,12 @@ function syncDirectory2(path) {
522
721
  }
523
722
  }
524
723
  function atomicJson(path, value) {
525
- mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
526
- const temp = join2(dirname2(path), `.${basename(path)}.${process.pid}.${randomUUID3()}.tmp`);
724
+ mkdirSync3(dirname2(path), { recursive: true, mode: 448 });
725
+ const temp = join4(dirname2(path), `.${basename2(path)}.${process.pid}.${randomUUID3()}.tmp`);
527
726
  let fd;
528
727
  try {
529
- fd = openSync2(temp, "wx", 384);
530
- writeFileSync2(fd, `${JSON.stringify(value)}
728
+ fd = openSync3(temp, "wx", 384);
729
+ writeFileSync3(fd, `${JSON.stringify(value)}
531
730
  `, "utf8");
532
731
  fsyncSync2(fd);
533
732
  closeSync2(fd);
@@ -542,7 +741,7 @@ function atomicJson(path, value) {
542
741
  }
543
742
  }
544
743
  try {
545
- rmSync2(temp, { force: true });
744
+ rmSync3(temp, { force: true });
546
745
  } catch {
547
746
  }
548
747
  throw error;
@@ -556,14 +755,14 @@ function quarantine(path, suffix) {
556
755
  return false;
557
756
  }
558
757
  try {
559
- rmSync2(moved, { recursive: true, force: true });
758
+ rmSync3(moved, { recursive: true, force: true });
560
759
  } catch {
561
760
  }
562
761
  return true;
563
762
  }
564
763
  function pathAge(path) {
565
764
  try {
566
- return Date.now() - statSync2(path).mtimeMs;
765
+ return Date.now() - statSync3(path).mtimeMs;
567
766
  } catch {
568
767
  return void 0;
569
768
  }
@@ -582,22 +781,22 @@ function parseGuard(path) {
582
781
  }
583
782
  }
584
783
  function acquireMutationGuard(stateDir, timeoutMs = GUARD_TIMEOUT_MS, adapter = defaultProcessPlatformAdapter) {
585
- const path = join2(stateDir, MUTATION_GUARD_NAME);
586
- mkdirSync2(stateDir, { recursive: true, mode: 448 });
784
+ const path = join4(stateDir, MUTATION_GUARD_NAME);
785
+ mkdirSync3(stateDir, { recursive: true, mode: 448 });
587
786
  const deadline = adapter.now() + Math.max(0, timeoutMs);
588
787
  for (; ; ) {
589
788
  const generation = currentProcessGeneration(adapter);
590
789
  const token = randomUUID3();
591
790
  let fd;
592
791
  try {
593
- fd = openSync2(path, "wx", 384);
792
+ fd = openSync3(path, "wx", 384);
594
793
  const record = {
595
794
  version: 1,
596
795
  token,
597
796
  at: new Date(adapter.now()).toISOString(),
598
797
  ...generation
599
798
  };
600
- writeFileSync2(fd, `${JSON.stringify(record)}
799
+ writeFileSync3(fd, `${JSON.stringify(record)}
601
800
  `, "utf8");
602
801
  fsyncSync2(fd);
603
802
  closeSync2(fd);
@@ -677,7 +876,7 @@ function parseOwner(path) {
677
876
  }
678
877
  }
679
878
  function projectLaunchOwnerPath(stateDir) {
680
- return join2(stateDir, OWNER_NAME);
879
+ return join4(stateDir, OWNER_NAME);
681
880
  }
682
881
  function readProjectLaunchOwner(stateDir) {
683
882
  return parseOwner(projectLaunchOwnerPath(stateDir));
@@ -694,7 +893,7 @@ function projectLaunchTokenProof(target2, token) {
694
893
  }
695
894
  function removeStatusesForToken(stateDir, token) {
696
895
  for (const name of ["dev-supervisor.lock", "server.lock"]) {
697
- const path = join2(stateDir, name);
896
+ const path = join4(stateDir, name);
698
897
  try {
699
898
  const value = JSON.parse(readFileSync3(path, "utf8"));
700
899
  if (value.ownerToken === token) quarantine(path, "stale");
@@ -705,8 +904,8 @@ function removeStatusesForToken(stateDir, token) {
705
904
  function writeNewOwner(path, record) {
706
905
  let fd;
707
906
  try {
708
- fd = openSync2(path, "wx", 384);
709
- writeFileSync2(fd, `${JSON.stringify(record)}
907
+ fd = openSync3(path, "wx", 384);
908
+ writeFileSync3(fd, `${JSON.stringify(record)}
710
909
  `, "utf8");
711
910
  fsyncSync2(fd);
712
911
  closeSync2(fd);
@@ -960,16 +1159,16 @@ import {
960
1159
  closeSync as closeSync3,
961
1160
  existsSync,
962
1161
  fsyncSync as fsyncSync3,
963
- mkdirSync as mkdirSync3,
964
- openSync as openSync3,
1162
+ mkdirSync as mkdirSync4,
1163
+ openSync as openSync4,
965
1164
  readFileSync as readFileSync4,
966
1165
  realpathSync as realpathSync2,
967
1166
  renameSync as renameSync3,
968
- rmSync as rmSync3,
969
- statSync as statSync3,
970
- writeFileSync as writeFileSync3
1167
+ rmSync as rmSync4,
1168
+ statSync as statSync4,
1169
+ writeFileSync as writeFileSync4
971
1170
  } from "node:fs";
972
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "node:path";
1171
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2, sep as sep2 } from "node:path";
973
1172
  function canonicalUuid(value) {
974
1173
  return value && UUID_RE2.test(value) ? value.toLowerCase() : null;
975
1174
  }
@@ -1039,7 +1238,7 @@ function parseTmuxSocketInspection(output) {
1039
1238
  function syncDirectory3(path) {
1040
1239
  let fd;
1041
1240
  try {
1042
- fd = openSync3(path, "r");
1241
+ fd = openSync4(path, "r");
1043
1242
  fsyncSync3(fd);
1044
1243
  } catch {
1045
1244
  } finally {
@@ -1050,12 +1249,12 @@ function syncDirectory3(path) {
1050
1249
  }
1051
1250
  }
1052
1251
  function atomicJson2(path, value) {
1053
- mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
1252
+ mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
1054
1253
  const temp = `${path}.${process.pid}.${randomUUID4()}.tmp`;
1055
1254
  let fd;
1056
1255
  try {
1057
- fd = openSync3(temp, "wx", 384);
1058
- writeFileSync3(fd, `${JSON.stringify(value)}
1256
+ fd = openSync4(temp, "wx", 384);
1257
+ writeFileSync4(fd, `${JSON.stringify(value)}
1059
1258
  `, "utf8");
1060
1259
  fsyncSync3(fd);
1061
1260
  closeSync3(fd);
@@ -1068,14 +1267,14 @@ function atomicJson2(path, value) {
1068
1267
  } catch {
1069
1268
  }
1070
1269
  try {
1071
- rmSync3(temp, { force: true });
1270
+ rmSync4(temp, { force: true });
1072
1271
  } catch {
1073
1272
  }
1074
1273
  }
1075
1274
  }
1076
1275
  function readGuard(path) {
1077
1276
  try {
1078
- const value = JSON.parse(readFileSync4(join3(path, GUARD_OWNER), "utf8"));
1277
+ const value = JSON.parse(readFileSync4(join5(path, GUARD_OWNER), "utf8"));
1079
1278
  if (value.version !== 1 || !UUID_RE2.test(value.token ?? "") || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.processStart !== "string") return null;
1080
1279
  return value;
1081
1280
  } catch {
@@ -1083,15 +1282,15 @@ function readGuard(path) {
1083
1282
  }
1084
1283
  }
1085
1284
  function acquireGuard(stateDir) {
1086
- const path = join3(stateDir, GUARD_NAME);
1087
- mkdirSync3(stateDir, { recursive: true, mode: 448 });
1285
+ const path = join5(stateDir, GUARD_NAME);
1286
+ mkdirSync4(stateDir, { recursive: true, mode: 448 });
1088
1287
  const deadline = Date.now() + GUARD_TIMEOUT_MS2;
1089
1288
  for (; ; ) {
1090
1289
  try {
1091
- mkdirSync3(path, { mode: 448 });
1290
+ mkdirSync4(path, { mode: 448 });
1092
1291
  const generation = currentProcessGeneration();
1093
1292
  const owner2 = { version: 1, token: randomUUID4(), ...generation };
1094
- atomicJson2(join3(path, GUARD_OWNER), owner2);
1293
+ atomicJson2(join5(path, GUARD_OWNER), owner2);
1095
1294
  let released = false;
1096
1295
  return () => {
1097
1296
  if (released) return;
@@ -1101,7 +1300,7 @@ function acquireGuard(stateDir) {
1101
1300
  const quarantine2 = `${path}.release-${owner2.token}`;
1102
1301
  try {
1103
1302
  renameSync3(path, quarantine2);
1104
- rmSync3(quarantine2, { recursive: true, force: true });
1303
+ rmSync4(quarantine2, { recursive: true, force: true });
1105
1304
  } catch {
1106
1305
  }
1107
1306
  };
@@ -1112,7 +1311,7 @@ function acquireGuard(stateDir) {
1112
1311
  let stale = owner ? processGenerationIsStale(owner) : false;
1113
1312
  if (!owner) {
1114
1313
  try {
1115
- const age = Date.now() - statSync3(path).mtimeMs;
1314
+ const age = Date.now() - statSync4(path).mtimeMs;
1116
1315
  stale = age >= PARTIAL_GRACE_MS2 || age < -PARTIAL_GRACE_MS2;
1117
1316
  } catch {
1118
1317
  stale = false;
@@ -1122,7 +1321,7 @@ function acquireGuard(stateDir) {
1122
1321
  try {
1123
1322
  const quarantine2 = `${path}.stale-${process.pid}-${randomUUID4()}`;
1124
1323
  renameSync3(path, quarantine2);
1125
- rmSync3(quarantine2, { recursive: true, force: true });
1324
+ rmSync4(quarantine2, { recursive: true, force: true });
1126
1325
  continue;
1127
1326
  } catch {
1128
1327
  }
@@ -1132,7 +1331,7 @@ function acquireGuard(stateDir) {
1132
1331
  }
1133
1332
  }
1134
1333
  function tmuxSocketMigrationPath(stateDir) {
1135
- return join3(stateDir, MIGRATION_NAME);
1334
+ return join5(stateDir, MIGRATION_NAME);
1136
1335
  }
1137
1336
  function readTmuxSocketMigration(stateDir) {
1138
1337
  try {
@@ -1171,7 +1370,7 @@ function pathBelongsToProject(path, projectDir) {
1171
1370
  canonical = resolve2(path);
1172
1371
  }
1173
1372
  const rel = relative(projectDir, canonical);
1174
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute2(rel);
1373
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
1175
1374
  }
1176
1375
  function markerlessLegacyIsAttributable(observation, target2) {
1177
1376
  return observation.panes.length > 0 && observation.panes.every(
@@ -1262,7 +1461,7 @@ function resolveProjectTmuxSocket(target2, options2 = {}) {
1262
1461
  record.phase === "claiming"
1263
1462
  );
1264
1463
  }
1265
- if (!existsSync(join3(target2.stateDir, "ui.db")) && !oldOwnerPinsLegacy) {
1464
+ if (!existsSync(join5(target2.stateDir, "ui.db")) && !oldOwnerPinsLegacy) {
1266
1465
  validateFullSocket(runtime, fullSocket, target2);
1267
1466
  return writeMigration(target2, legacySocket, fullSocket, "full").selectedSocket;
1268
1467
  }
@@ -1350,10 +1549,10 @@ var init_tmux_socket = __esm({
1350
1549
  });
1351
1550
 
1352
1551
  // packages/server/src/boot-progress.ts
1353
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
1354
- import { basename as basename2, dirname as dirname4, join as join4 } from "node:path";
1552
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
1553
+ import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
1355
1554
  function bootProgressPath(stateDir) {
1356
- return join4(stateDir, BOOT_PROGRESS_NAME);
1555
+ return join6(stateDir, BOOT_PROGRESS_NAME);
1357
1556
  }
1358
1557
  function readBootProgress(stateDir) {
1359
1558
  try {
@@ -5797,6 +5996,14 @@ var init_interactions = __esm({
5797
5996
  ...CommonPayload,
5798
5997
  kind: external_exports.literal("permission-approval"),
5799
5998
  permission: InteractionOpaqueId,
5999
+ // The exact text of what is being authorized — a command, a path, a request body. Multi-line and
6000
+ // rendered monospace, because a Bash command is the one thing the operator actually reads and a
6001
+ // single-line label cannot hold a heredoc. Adapters redact and clamp it before it gets here.
6002
+ preview: InteractionPreview.optional(),
6003
+ // A shell prompt to render in FRONT of `preview`, which is how a terminal shows a command and where
6004
+ // the working directory belongs when there is one. Set it only when the preview genuinely IS a shell
6005
+ // command line: a file path or an MCP argument behind a `❯` would be a lie about what runs.
6006
+ promptLabel: plainText(256, 512).optional(),
5800
6007
  resourceLabel: plainText(1024, 2048).optional(),
5801
6008
  workingDirectoryLabel: plainText(1024, 2048).optional(),
5802
6009
  scopeLabel: InteractionDescription.optional(),
@@ -6031,7 +6238,7 @@ var init_receipt_bus = __esm({
6031
6238
  });
6032
6239
 
6033
6240
  // packages/shared/src/index.ts
6034
- var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrayStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, SetThreadSnoozeInput, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT;
6241
+ var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrayStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, SetThreadSnoozeInput, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT;
6035
6242
  var init_src = __esm({
6036
6243
  "packages/shared/src/index.ts"() {
6037
6244
  "use strict";
@@ -6664,7 +6871,13 @@ var init_src = __esm({
6664
6871
  sessionId: external_exports.string().min(1),
6665
6872
  message: external_exports.string().min(1),
6666
6873
  // Generated once before the optimistic clear so a transport replay can be idempotent.
6667
- deliveryId: external_exports.string().min(1).max(200).optional()
6874
+ deliveryId: external_exports.string().min(1).max(200).optional(),
6875
+ // Retire the worker's live process before delivering, so this message lands in a `claude` that has
6876
+ // just started. The operator's "Restart worker" verb — the ONLY caller that sets it — exists because
6877
+ // a worker inherits its plugin/hooks AND its system prompt at process start and can never pick up a
6878
+ // newer fray build in place (hooks are read once, at startup). Everything else that needs a fresh
6879
+ // process derives it server-side; see needsFreshProcessForLimit.
6880
+ freshProcess: external_exports.boolean().optional()
6668
6881
  });
6669
6882
  UnqueueFollowUpInput = external_exports.object({
6670
6883
  slug: ThreadSlug,
@@ -6732,6 +6945,25 @@ var init_src = __esm({
6732
6945
  ${DISPATCH_TASK_BANNER}
6733
6946
 
6734
6947
  `;
6948
+ GithubWakeItem = external_exports.object({
6949
+ label: external_exports.string(),
6950
+ // the activity's noun ("comment", "approval", "change request", …)
6951
+ actor: external_exports.string(),
6952
+ // GitHub login, no leading @
6953
+ bot: external_exports.boolean(),
6954
+ // drives the 🤖/👤 icon; an app files most of what wakes this watcher
6955
+ at: external_exports.string().optional(),
6956
+ // ISO8601
6957
+ url: external_exports.string().optional()
6958
+ // the item's own permalink
6959
+ });
6960
+ GithubWakeSteer = external_exports.object({
6961
+ ref: external_exports.string(),
6962
+ // owner/repo#N
6963
+ items: external_exports.array(GithubWakeItem),
6964
+ omitted: external_exports.number()
6965
+ // fresh items counted but not named (the enumeration cap)
6966
+ });
6735
6967
  WAKE_SCOPE = "ignore older activity you have already handled";
6736
6968
  WAKE_REF = String.raw`[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*#\d+`;
6737
6969
  WAKE_SINGLE = new RegExp(
@@ -7028,11 +7260,20 @@ ${DISPATCH_TASK_BANNER}
7028
7260
  // non-reasoning messages and on any reasoning block whose timing couldn't be derived.
7029
7261
  durationMs: external_exports.number().nonnegative().optional(),
7030
7262
  // A turn-BOUNDARY marker: this `kind:"event"` line was emitted at the position an EXTERNAL wake — a
7031
- // background task/shell completion `<task-notification>` re-invoked the agent and started a fresh
7032
- // turn. The client renders it as a centered divider rule carrying the cause label, so two consecutive
7033
- // assistant turns (each with its own trailing signal) no longer paint as one seamless bubble. Additive
7034
- // + optional: an old client ignores it and shows the plain quiet event line (graceful degrade).
7035
- boundary: external_exports.boolean().optional(),
7263
+ // This event opened a fresh turn, so it renders as a centered divider rule carrying the cause label —
7264
+ // without it, two consecutive assistant turns (each with its own trailing signal) paint as one
7265
+ // seamless bubble. Additive + optional: an old client ignores it and shows the plain quiet event line
7266
+ // (graceful degrade).
7267
+ //
7268
+ // It names WHICH KIND of boundary, because two unrelated events earn the divider and the client has to
7269
+ // tell them apart to put the right glyph on each:
7270
+ // wake — a background task/shell completion `<task-notification>` re-invoked the agent
7271
+ // compaction — the provider rewrote the conversation and dropped everything above this point
7272
+ // It was a bare boolean until the dividers grew icons (a shell glyph on a compaction line is simply
7273
+ // wrong), and the kind has to come from the SERVER: the alternative is the client sniffing the label
7274
+ // text, which is the guess this codebase refuses everywhere else. A string stays truthy, so any
7275
+ // surviving `if (boundary)` reads exactly as it did.
7276
+ boundary: external_exports.enum(["wake", "compaction"]).optional(),
7036
7277
  // Block-ordered content for an assistant turn (see TranscriptPart). Defaults to [] so a pre-restart
7037
7278
  // server (which ships only text/tools) parses; the client renders `parts` when non-empty and falls
7038
7279
  // back to the legacy tools-then-text layout when it's empty. `text`/`tools` stay populated for that
@@ -7061,6 +7302,17 @@ ${DISPATCH_TASK_BANNER}
7061
7302
  // old client ignores it and shows the plain bubble (the previous behavior), and an old server simply
7062
7303
  // never sets it.
7063
7304
  wake: external_exports.boolean().optional(),
7305
+ // The STRUCTURED wake, parsed by the server from the same text the same build formatted. The chat
7306
+ // renders the divider from this rather than re-deriving it from prose in the browser.
7307
+ //
7308
+ // It exists because re-deriving it in the browser is version-skewed by construction. `web/api/boot.ts`
7309
+ // adopts a new server boot id IN PLACE (so an unsent draft survives a restart), so a promoted artifact
7310
+ // swaps the server under tabs that keep their old bundle — and on 2026-07-31 a steer that gained two
7311
+ // agent-facing lines met parsers that predated them, which cost every open tab its card and dumped the
7312
+ // raw `gh api …` text into the transcript instead. Server-side, formatter and parser can never
7313
+ // disagree. Additive + optional: absent from a legacy transcript or an older server, and the client
7314
+ // falls back to `parseGithubWakeSteer` on the text.
7315
+ wakeSteer: GithubWakeSteer.optional(),
7064
7316
  // A SUB-AGENT (or peer session) wrote this user turn, not the human — the same defect class `wake`
7065
7317
  // above corrects. Claude Code's agent-to-agent channel (a background child calling
7066
7318
  // `SendMessage({to:"main"})`) delivers UPWARD into the parent's queue like any follow-up, so the
@@ -7181,7 +7433,7 @@ var init_local_origin = __esm({
7181
7433
  });
7182
7434
 
7183
7435
  // packages/server/src/local-image.ts
7184
- import { readFileSync as readFileSync7, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
7436
+ import { readFileSync as readFileSync7, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
7185
7437
  import { extname, isAbsolute as isAbsolute3 } from "node:path";
7186
7438
  function resolveLocalImage(rawPath) {
7187
7439
  if (!rawPath || !isAbsolute3(rawPath)) return { status: 400 };
@@ -7194,7 +7446,7 @@ function resolveLocalImage(rawPath) {
7194
7446
  return { status: 404 };
7195
7447
  }
7196
7448
  try {
7197
- if (!statSync4(real).isFile()) return { status: 404 };
7449
+ if (!statSync5(real).isFile()) return { status: 404 };
7198
7450
  return { status: 200, contentType, body: readFileSync7(real) };
7199
7451
  } catch {
7200
7452
  return { status: 404 };
@@ -7215,23 +7467,756 @@ var init_local_image = __esm({
7215
7467
  });
7216
7468
 
7217
7469
  // src/production.ts
7218
- import { spawn as spawn2 } from "node:child_process";
7219
- import { closeSync as closeSync4, openSync as openSync4 } from "node:fs";
7470
+ import { readFileSync as readFileSync9 } from "node:fs";
7471
+ import { homedir as homedir5 } from "node:os";
7220
7472
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7221
- import { join as join7 } from "node:path";
7222
- import { setTimeout as delay3 } from "node:timers/promises";
7473
+ import { join as join9 } from "node:path";
7474
+
7475
+ // src/browser.ts
7476
+ import { spawn, execFile } from "node:child_process";
7477
+ import { access, readdir, mkdir, writeFile, readFile, rm } from "node:fs/promises";
7478
+ import { homedir } from "node:os";
7479
+ import { join, delimiter, sep } from "node:path";
7480
+ function defaultBrowserOpenCommand(rawUrl, platform = process.platform) {
7481
+ let url;
7482
+ try {
7483
+ url = new URL(rawUrl);
7484
+ } catch {
7485
+ throw new Error(`Cannot open invalid browser URL: ${rawUrl}`);
7486
+ }
7487
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7488
+ throw new Error(`Cannot open unsupported browser URL scheme: ${url.protocol}`);
7489
+ }
7490
+ if (platform === "darwin") return { command: "/usr/bin/open", args: [url.toString()] };
7491
+ if (platform === "win32") {
7492
+ return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url.toString()] };
7493
+ }
7494
+ if (platform === "linux" || platform === "freebsd" || platform === "openbsd") {
7495
+ return { command: "xdg-open", args: [url.toString()] };
7496
+ }
7497
+ throw new Error(`Opening the default browser is not supported on ${platform}`);
7498
+ }
7499
+ async function launchBrowserTab(rawUrl, options2 = {}) {
7500
+ const platform = options2.platform ?? process.platform;
7501
+ const runCommand = options2.runCommand ?? execFileP;
7502
+ const launch = defaultBrowserOpenCommand(rawUrl, platform);
7503
+ await runCommand(launch.command, launch.args);
7504
+ }
7505
+ var browserPaths = {
7506
+ win32: process.platform === "win32" && {
7507
+ chrome: [
7508
+ join("Google", "Chrome", "Application", "chrome.exe"),
7509
+ join(process.env.USERPROFILE ?? "", "scoop", "apps", "googlechrome", "current", "chrome.exe")
7510
+ ],
7511
+ chrome_canary: join("Google", "Chrome SxS", "Application", "chrome.exe"),
7512
+ chromium: [
7513
+ join("Chromium", "Application", "chrome.exe"),
7514
+ join(process.env.USERPROFILE ?? "", "scoop", "apps", "chromium", "current", "chrome.exe")
7515
+ ],
7516
+ edge: join("Microsoft", "Edge", "Application", "msedge.exe"),
7517
+ brave: join("BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
7518
+ vivaldi: join("Vivaldi", "Application", "vivaldi.exe"),
7519
+ thorium: join("Thorium", "Application", "thorium.exe"),
7520
+ firefox: [
7521
+ join("Mozilla Firefox", "firefox.exe"),
7522
+ join(process.env.USERPROFILE ?? "", "scoop", "apps", "firefox", "current", "firefox.exe")
7523
+ ],
7524
+ firefox_developer: join("Firefox Developer Edition", "firefox.exe"),
7525
+ firefox_nightly: join("Firefox Nightly", "firefox.exe"),
7526
+ librewolf: join("LibreWolf", "librewolf.exe"),
7527
+ waterfox: join("Waterfox", "waterfox.exe")
7528
+ },
7529
+ linux: {
7530
+ chrome: ["chrome", "google-chrome", "chrome-browser", "google-chrome-stable"],
7531
+ chrome_canary: ["chrome-canary", "google-chrome-canary"],
7532
+ chromium: ["chromium", "chromium-browser"],
7533
+ edge: ["microsoft-edge", "microsoft-edge-stable"],
7534
+ brave: ["brave", "brave-browser"],
7535
+ vivaldi: ["vivaldi", "vivaldi-browser"],
7536
+ thorium: ["thorium", "thorium-browser"],
7537
+ firefox: ["firefox", "firefox-browser"],
7538
+ firefox_nightly: ["firefox-nightly"],
7539
+ librewolf: ["librewolf", "librewolf-browser"],
7540
+ waterfox: ["waterfox", "waterfox-browser"]
7541
+ },
7542
+ darwin: {
7543
+ chrome: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
7544
+ chrome_canary: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
7545
+ chromium: "/Applications/Chromium.app/Contents/MacOS/Chromium",
7546
+ edge: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
7547
+ brave: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
7548
+ vivaldi: "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
7549
+ thorium: "/Applications/Thorium.app/Contents/MacOS/Thorium",
7550
+ firefox: "/Applications/Firefox.app/Contents/MacOS/firefox",
7551
+ firefox_nightly: "/Applications/Firefox Nightly.app/Contents/MacOS/firefox",
7552
+ librewolf: "/Applications/LibreWolf.app/Contents/MacOS/librewolf",
7553
+ waterfox: "/Applications/Waterfox.app/Contents/MacOS/waterfox"
7554
+ }
7555
+ }[process.platform] || void 0;
7556
+ if (process.platform === "win32" && browserPaths) {
7557
+ for (const browser in browserPaths) {
7558
+ const val = browserPaths[browser];
7559
+ const isArray = Array.isArray(val);
7560
+ const basePath = isArray ? val[0] : val;
7561
+ if (!basePath) continue;
7562
+ browserPaths[browser] = [
7563
+ join(process.env.PROGRAMFILES ?? "", basePath),
7564
+ join(process.env.LOCALAPPDATA ?? "", basePath),
7565
+ join(process.env["PROGRAMFILES(X86)"] ?? "", basePath),
7566
+ ...isArray ? val.slice(1) : []
7567
+ ];
7568
+ }
7569
+ }
7570
+ var _binariesInPath;
7571
+ async function getBinariesInPath() {
7572
+ if (_binariesInPath) return _binariesInPath;
7573
+ _binariesInPath = (await Promise.all(
7574
+ (process.env.PATH ?? "").replaceAll('"', "").split(delimiter).filter(Boolean).map((x) => readdir(x.replace(/"+/g, "")).catch(() => []))
7575
+ )).flat();
7576
+ return _binariesInPath;
7577
+ }
7578
+ async function pathExists(path) {
7579
+ if (path.includes(sep)) return access(path).then(() => true).catch(() => false);
7580
+ return (await getBinariesInPath()).includes(path);
7581
+ }
7582
+ async function getBrowserPath(browser) {
7583
+ if (!browserPaths) return null;
7584
+ const paths = browserPaths[browser];
7585
+ if (!paths) return null;
7586
+ for (const p of Array.isArray(paths) ? paths : [paths]) {
7587
+ if (await pathExists(p)) return p;
7588
+ }
7589
+ return null;
7590
+ }
7591
+ function getBrowserType(name) {
7592
+ if (name.startsWith("firefox") || ["librewolf", "waterfox"].includes(name)) return "firefox";
7593
+ return "chromium";
7594
+ }
7595
+ async function findBrowser(forceBrowser) {
7596
+ if (!browserPaths) return null;
7597
+ if (forceBrowser) {
7598
+ const path = await getBrowserPath(forceBrowser);
7599
+ if (path) return { path, name: forceBrowser, type: getBrowserType(forceBrowser) };
7600
+ return null;
7601
+ }
7602
+ for (const name in browserPaths) {
7603
+ const path = await getBrowserPath(name);
7604
+ if (path) return { path, name, type: getBrowserType(name) };
7605
+ }
7606
+ return null;
7607
+ }
7608
+ async function launchApp(url, opts = {}) {
7609
+ const { windowSize = [1400, 900], dataPath } = opts;
7610
+ const browser = await findBrowser();
7611
+ if (!browser) {
7612
+ throw new Error("No supported browser found for --app mode");
7613
+ }
7614
+ if (browser.type === "chromium") {
7615
+ if (process.platform === "darwin" && dataPath) {
7616
+ const shim = await ensureAppShim(browser.path, url, dataPath);
7617
+ if (shim && await openShimVerified(shim, dataPath)) return;
7618
+ }
7619
+ launchChromium(browser.path, url, windowSize, dataPath);
7620
+ } else {
7621
+ await launchFirefox(browser.path, url, windowSize, dataPath);
7622
+ }
7623
+ }
7624
+ var CHROME_APPS_DIR = join(homedir(), "Applications", "Chrome Apps.localized");
7625
+ var EXPECTED_BUNDLE_NAME = "Fray";
7626
+ function bundleNameMatchesManifest(bundleName) {
7627
+ if (bundleName === EXPECTED_BUNDLE_NAME) return true;
7628
+ return new RegExp(`^${EXPECTED_BUNDLE_NAME} \\d+$`).test(bundleName);
7629
+ }
7630
+ var STANDALONE_SENTINEL = ".fray-pwa-standalone";
7631
+ function manifestIdFor(url) {
7632
+ return url.endsWith("/") ? url : `${url}/`;
7633
+ }
7634
+ function execFileP(cmd, args2) {
7635
+ return new Promise((resolve5, reject) => {
7636
+ execFile(
7637
+ cmd,
7638
+ args2,
7639
+ { encoding: "utf8", timeout: 1e4 },
7640
+ (err, stdout) => err ? reject(err) : resolve5(stdout)
7641
+ );
7642
+ });
7643
+ }
7644
+ async function findAppShim(url, dataPath) {
7645
+ const manifestId = manifestIdFor(url);
7646
+ const bundles = await readdir(CHROME_APPS_DIR).catch(() => []);
7647
+ for (const name of bundles) {
7648
+ if (!name.endsWith(".app")) continue;
7649
+ const plist = join(CHROME_APPS_DIR, name, "Contents", "Info.plist");
7650
+ try {
7651
+ const info = JSON.parse(await execFileP("plutil", ["-convert", "json", "-o", "-", plist]));
7652
+ if (info.CrAppModeShortcutURL === manifestId && info.CrAppModeUserDataDir?.startsWith(dataPath + sep)) {
7653
+ return { path: join(CHROME_APPS_DIR, name), bundleName: info.CFBundleName ?? "" };
7654
+ }
7655
+ } catch {
7656
+ }
7657
+ }
7658
+ return null;
7659
+ }
7660
+ function escapeERE(s) {
7661
+ return s.replace(/[.^$*+?()[\]{}|\\]/g, "\\$&");
7662
+ }
7663
+ async function pgrepMatches(pattern) {
7664
+ try {
7665
+ const out = await execFileP("pgrep", ["-f", "--", escapeERE(pattern)]);
7666
+ return out.trim().length > 0;
7667
+ } catch {
7668
+ return false;
7669
+ }
7670
+ }
7671
+ async function profileInUse(dataPath) {
7672
+ return pgrepMatches(`--user-data-dir=${dataPath}`);
7673
+ }
7674
+ function openCdpSession(browserPath, dataPath) {
7675
+ const child = spawn(
7676
+ browserPath,
7677
+ [
7678
+ `--user-data-dir=${dataPath}`,
7679
+ "--remote-debugging-pipe",
7680
+ "--no-startup-window",
7681
+ "--no-first-run",
7682
+ "--no-default-browser-check",
7683
+ "--disable-sync",
7684
+ "--disable-background-networking"
7685
+ ],
7686
+ { stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"] }
7687
+ );
7688
+ const cmdPipe = child.stdio[3];
7689
+ const resPipe = child.stdio[4];
7690
+ let seq = 0;
7691
+ const pending = /* @__PURE__ */ new Map();
7692
+ let buf = Buffer.alloc(0);
7693
+ resPipe.on("data", (chunk) => {
7694
+ buf = Buffer.concat([buf, chunk]);
7695
+ let nul;
7696
+ while ((nul = buf.indexOf(0)) !== -1) {
7697
+ const raw = buf.subarray(0, nul).toString("utf8");
7698
+ buf = buf.subarray(nul + 1);
7699
+ try {
7700
+ const msg = JSON.parse(raw);
7701
+ if (msg.id !== void 0 && pending.has(msg.id)) {
7702
+ const p = pending.get(msg.id);
7703
+ pending.delete(msg.id);
7704
+ if (msg.error) p.reject(new Error(msg.error.message));
7705
+ else p.resolve(msg.result);
7706
+ }
7707
+ } catch {
7708
+ }
7709
+ }
7710
+ });
7711
+ const failAll = (e) => {
7712
+ for (const [, p] of pending) p.reject(e);
7713
+ pending.clear();
7714
+ };
7715
+ child.on("error", failAll);
7716
+ cmdPipe.on("error", failAll);
7717
+ resPipe.on("error", failAll);
7718
+ child.on("exit", () => failAll(new Error("chrome exited before the CDP handshake completed")));
7719
+ const call = (method, params = {}, timeoutMs = 2e4) => new Promise((resolve5, reject) => {
7720
+ const id = ++seq;
7721
+ pending.set(id, { resolve: resolve5, reject });
7722
+ cmdPipe.write(JSON.stringify({ id, method, params }) + "\0");
7723
+ setTimeout(() => {
7724
+ if (pending.has(id)) {
7725
+ pending.delete(id);
7726
+ reject(new Error(`${method}: timed out`));
7727
+ }
7728
+ }, timeoutMs).unref();
7729
+ });
7730
+ const close = async () => {
7731
+ await call("Browser.close").catch(() => {
7732
+ });
7733
+ if (child.exitCode !== null || child.signalCode !== null) return;
7734
+ await new Promise((resolve5) => {
7735
+ const t = setTimeout(() => {
7736
+ child.kill();
7737
+ resolve5();
7738
+ }, 1e4);
7739
+ t.unref();
7740
+ child.once("exit", () => {
7741
+ clearTimeout(t);
7742
+ resolve5();
7743
+ });
7744
+ });
7745
+ };
7746
+ return { call, close, kill: () => child.kill() };
7747
+ }
7748
+ async function cdpInstallPwa(browserPath, url, dataPath) {
7749
+ const manifestId = manifestIdFor(url);
7750
+ const s = openCdpSession(browserPath, dataPath);
7751
+ try {
7752
+ await s.call("Browser.getVersion");
7753
+ await s.call("PWA.install", { manifestId, installUrlOrBundleUrl: manifestId }, 3e4);
7754
+ await s.call("PWA.changeAppUserSettings", { manifestId, displayMode: "standalone" });
7755
+ await writeFile(join(dataPath, STANDALONE_SENTINEL), "");
7756
+ await s.close();
7757
+ } catch (err) {
7758
+ s.kill();
7759
+ throw err;
7760
+ }
7761
+ }
7762
+ async function cdpUninstallPwa(browserPath, url, dataPath) {
7763
+ const manifestId = manifestIdFor(url);
7764
+ const s = openCdpSession(browserPath, dataPath);
7765
+ try {
7766
+ await s.call("Browser.getVersion");
7767
+ await s.call("PWA.uninstall", { manifestId });
7768
+ await s.close();
7769
+ } catch (err) {
7770
+ s.kill();
7771
+ throw err;
7772
+ }
7773
+ await rm(join(dataPath, STANDALONE_SENTINEL), { force: true }).catch(() => {
7774
+ });
7775
+ }
7776
+ async function waitForShimIdle(shimPath) {
7777
+ const appearBy = Date.now() + 6e3;
7778
+ let appeared = false;
7779
+ while (Date.now() < appearBy) {
7780
+ if (await pgrepMatches(shimPath)) {
7781
+ appeared = true;
7782
+ break;
7783
+ }
7784
+ await new Promise((r) => setTimeout(r, 100));
7785
+ }
7786
+ if (!appeared) return;
7787
+ const idleBy = Date.now() + 15e3;
7788
+ while (Date.now() < idleBy) {
7789
+ if (!await pgrepMatches(shimPath)) return;
7790
+ await new Promise((r) => setTimeout(r, 250));
7791
+ }
7792
+ }
7793
+ async function openShimVerified(shimPath, dataPath) {
7794
+ spawn("open", [shimPath], { detached: true, stdio: "ignore" }).unref();
7795
+ const deadline = Date.now() + 1e4;
7796
+ while (Date.now() < deadline) {
7797
+ if (await profileInUse(dataPath)) return true;
7798
+ await new Promise((r) => setTimeout(r, 150));
7799
+ }
7800
+ return false;
7801
+ }
7802
+ async function standaloneSentinelExists(dataPath) {
7803
+ return access(join(dataPath, STANDALONE_SENTINEL)).then(() => true).catch(() => false);
7804
+ }
7805
+ async function seedFirstRunState(dataPath) {
7806
+ await writeFile(join(dataPath, "First Run"), "", { flag: "wx" }).catch(() => {
7807
+ });
7808
+ if (await profileInUse(dataPath)) return;
7809
+ const prefsPath = join(dataPath, "Default", "Preferences");
7810
+ try {
7811
+ const prefs = JSON.parse(await readFile(prefsPath, "utf8"));
7812
+ if (prefs.browser?.check_default_browser === false) return;
7813
+ prefs.browser = { ...prefs.browser ?? {}, check_default_browser: false };
7814
+ await writeFile(prefsPath, JSON.stringify(prefs));
7815
+ } catch {
7816
+ }
7817
+ }
7818
+ async function ensureAppShim(browserPath, url, dataPath) {
7819
+ try {
7820
+ const existing = await findAppShim(url, dataPath);
7821
+ if (existing && bundleNameMatchesManifest(existing.bundleName) && await standaloneSentinelExists(dataPath)) {
7822
+ await seedFirstRunState(dataPath);
7823
+ return existing.path;
7824
+ }
7825
+ if (await profileInUse(dataPath)) return null;
7826
+ if (existing) {
7827
+ await cdpUninstallPwa(browserPath, url, dataPath).catch(() => {
7828
+ });
7829
+ }
7830
+ await cdpInstallPwa(browserPath, url, dataPath);
7831
+ await seedFirstRunState(dataPath);
7832
+ const installed = await findAppShim(url, dataPath);
7833
+ if (installed) await waitForShimIdle(installed.path);
7834
+ return installed?.path ?? null;
7835
+ } catch {
7836
+ return null;
7837
+ }
7838
+ }
7839
+ function launchChromium(browserPath, url, windowSize, dataPath) {
7840
+ const args2 = [
7841
+ `--app=${url}`,
7842
+ `--window-size=${windowSize.join(",")}`,
7843
+ "--new-window",
7844
+ "--no-first-run",
7845
+ "--no-default-browser-check",
7846
+ "--disable-translate",
7847
+ "--disable-sync",
7848
+ "--disable-popup-blocking",
7849
+ "--disable-background-networking",
7850
+ "--disable-default-apps",
7851
+ "--disable-component-extensions-with-background-pages",
7852
+ "--autoplay-policy=no-user-gesture-required"
7853
+ ];
7854
+ if (dataPath) {
7855
+ args2.push(`--user-data-dir=${dataPath}`);
7856
+ }
7857
+ spawn(browserPath, args2, { detached: true, stdio: "ignore" }).unref();
7858
+ }
7859
+ async function launchFirefox(browserPath, url, windowSize, dataPath) {
7860
+ const profileDir = dataPath ?? join(
7861
+ process.env.HOME ?? process.env.USERPROFILE ?? ".",
7862
+ ".gent",
7863
+ "firefox-profile"
7864
+ );
7865
+ await mkdir(profileDir, { recursive: true });
7866
+ await writeFile(
7867
+ join(profileDir, "user.js"),
7868
+ `
7869
+ user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
7870
+ user_pref("browser.shell.checkDefaultBrowser", false);
7871
+ user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
7872
+ user_pref("privacy.resistFingerprinting", false);
7873
+ user_pref("ui.key.menuAccessKeyFocuses", false);
7874
+ user_pref("media.autoplay.blocking_policy", 0);
7875
+ ${process.platform === "darwin" ? 'user_pref("browser.tabs.inTitlebar", 0);' : ""}
7876
+ `
7877
+ );
7878
+ await mkdir(join(profileDir, "chrome"), { recursive: true });
7879
+ await writeFile(
7880
+ join(profileDir, "chrome", "userChrome.css"),
7881
+ `
7882
+ .titlebar-spacer, #firefox-view-button, #alltabs-button,
7883
+ #tabbrowser-arrowscrollbox-periphery, .tab-close-button {
7884
+ display: none;
7885
+ }
7886
+ #nav-bar, #urlbar-container, #searchbar { visibility: collapse !important; }
7887
+ .tab-background, .tab-content, #tabbrowser-tabs {
7888
+ background: none !important;
7889
+ margin: 0 !important;
7890
+ padding: 0 !important;
7891
+ border: none !important;
7892
+ box-shadow: none !important;
7893
+ }
7894
+ #tabbrowser-tabs { margin: 0 6px !important; }
7895
+ .tabbrowser-tab { pointer-events: none; }
7896
+ #titlebar, .tabbrowser-tab { height: 20px; }
7897
+ .tab-content { height: 42px; }
7898
+ html:not([tabsintitlebar="true"]) #titlebar,
7899
+ html:not([tabsintitlebar="true"]) .tabbrowser-tab,
7900
+ html:not([tabsintitlebar="true"]) .tab-background,
7901
+ html:not([tabsintitlebar="true"]) .tab-content,
7902
+ html:not([tabsintitlebar="true"]) #tabbrowser-tabs {
7903
+ display: none !important;
7904
+ }
7905
+ `
7906
+ );
7907
+ spawn(
7908
+ browserPath,
7909
+ [
7910
+ "-width",
7911
+ String(windowSize[0]),
7912
+ "-height",
7913
+ String(windowSize[1]),
7914
+ "-profile",
7915
+ profileDir,
7916
+ "-new-window",
7917
+ url,
7918
+ "-new-instance",
7919
+ "-no-remote"
7920
+ ],
7921
+ { detached: true, stdio: "ignore" }
7922
+ ).unref();
7923
+ }
7924
+
7925
+ // src/readout.ts
7926
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7927
+ var SYNC_BEGIN = "\x1B[?2026h";
7928
+ var SYNC_END = "\x1B[?2026l";
7929
+ var ANSI = {
7930
+ reset: "\x1B[0m",
7931
+ bold: "\x1B[1m",
7932
+ dim: "\x1B[2m",
7933
+ green: "\x1B[32m",
7934
+ yellow: "\x1B[33m",
7935
+ blue: "\x1B[34m",
7936
+ magenta: "\x1B[35m",
7937
+ cyan: "\x1B[36m",
7938
+ red: "\x1B[31m"
7939
+ };
7940
+ function formatDuration(ms) {
7941
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
7942
+ if (ms < 1e4) return `${(ms / 1e3).toFixed(1)}s`;
7943
+ return `${Math.round(ms / 1e3)}s`;
7944
+ }
7945
+ function tildePath(path, home) {
7946
+ if (!home || !path.startsWith(home)) return path;
7947
+ const rest = path.slice(home.length);
7948
+ return rest === "" ? "~" : rest.startsWith("/") ? `~${rest}` : path;
7949
+ }
7950
+ var Readout = class {
7951
+ out;
7952
+ tty;
7953
+ colorEnabled;
7954
+ debug;
7955
+ version;
7956
+ now;
7957
+ startedAt;
7958
+ steps = [];
7959
+ frame = 0;
7960
+ timer;
7961
+ /** Rows currently occupied by the repainting region, so the next paint knows what to erase. */
7962
+ painted = 0;
7963
+ stepStartedAt = /* @__PURE__ */ new Map();
7964
+ settled = false;
7965
+ /** Non-TTY only: suppress repeating an identical status row. */
7966
+ lastPlainLine = "";
7967
+ constructor(options2 = {}) {
7968
+ this.out = options2.output ?? process.stdout;
7969
+ this.tty = this.out.isTTY === true;
7970
+ this.debug = options2.debug === true;
7971
+ this.colorEnabled = options2.color ?? (this.tty && !process.env.NO_COLOR && process.env.TERM !== "dumb");
7972
+ this.version = options2.version;
7973
+ this.now = options2.now ?? Date.now;
7974
+ this.startedAt = this.now();
7975
+ if (this.tty && !this.debug) {
7976
+ const timer = setInterval(() => this.paint(), options2.tickMs ?? 80);
7977
+ timer.unref?.();
7978
+ this.timer = timer;
7979
+ }
7980
+ }
7981
+ c(code, text) {
7982
+ return this.colorEnabled ? `${code}${text}${ANSI.reset}` : text;
7983
+ }
7984
+ get width() {
7985
+ return Math.max(40, this.out.columns ?? 80);
7986
+ }
7987
+ /** Truncate to the terminal width so a row can never wrap — wrapping desynchronizes the repaint. */
7988
+ fit(line) {
7989
+ const limit = this.width - 1;
7990
+ if (visibleLength(line) <= limit) return line;
7991
+ return `${sliceVisible(line, limit - 1)}\u2026`;
7992
+ }
7993
+ // ── Steps ────────────────────────────────────────────────────────────────────────────────────────
7994
+ /** Declare the boot's shape up front so the operator can see what is still ahead. */
7995
+ plan(steps) {
7996
+ for (const step of steps) {
7997
+ if (!this.steps.some((existing) => existing.key === step.key)) {
7998
+ this.steps.push({ ...step, state: "pending" });
7999
+ }
8000
+ }
8001
+ this.paint(true);
8002
+ }
8003
+ begin(key, detail) {
8004
+ const step = this.steps.find((candidate) => candidate.key === key);
8005
+ if (!step) return;
8006
+ for (const earlier of this.steps) {
8007
+ if (earlier === step) break;
8008
+ if (earlier.state === "active") this.settle(earlier.key, "done");
8009
+ }
8010
+ step.state = "active";
8011
+ if (detail !== void 0) step.detail = detail;
8012
+ this.stepStartedAt.set(key, this.now());
8013
+ this.emitPlain(step);
8014
+ this.paint(true);
8015
+ }
8016
+ /** Update the live sub-phase of the active step without changing which step is active. */
8017
+ detail(key, detail) {
8018
+ const step = this.steps.find((candidate) => candidate.key === key);
8019
+ if (!step || step.state !== "active") return;
8020
+ if (step.detail === detail) return;
8021
+ step.detail = detail;
8022
+ this.emitPlain(step);
8023
+ this.paint(true);
8024
+ }
8025
+ settle(key, state, detail) {
8026
+ const step = this.steps.find((candidate) => candidate.key === key);
8027
+ if (!step) return;
8028
+ step.state = state;
8029
+ step.detail = detail;
8030
+ const startedAt = this.stepStartedAt.get(key);
8031
+ if (startedAt !== void 0) step.ms = this.now() - startedAt;
8032
+ this.emitPlain(step);
8033
+ this.paint(true);
8034
+ }
8035
+ /** The step currently doing work, for a failure message that can name where the boot stopped. */
8036
+ activeStep() {
8037
+ return this.steps.find((step) => step.state === "active");
8038
+ }
8039
+ // ── Painting ─────────────────────────────────────────────────────────────────────────────────────
8040
+ glyph(step) {
8041
+ switch (step.state) {
8042
+ case "done":
8043
+ return this.c(ANSI.green, "\u2713");
8044
+ case "failed":
8045
+ return this.c(ANSI.red, "\u2717");
8046
+ case "skipped":
8047
+ return this.c(ANSI.dim, "\u2212");
8048
+ case "active":
8049
+ return this.c(ANSI.cyan, SPINNER[this.frame % SPINNER.length]);
8050
+ default:
8051
+ return this.c(ANSI.dim, "\xB7");
8052
+ }
8053
+ }
8054
+ stepRow(step) {
8055
+ const elapsed = step.state === "active" ? formatDuration(this.now() - (this.stepStartedAt.get(step.key) ?? this.now())) : step.ms !== void 0 && step.ms >= 1e3 ? formatDuration(step.ms) : "";
8056
+ const tail = [step.detail, elapsed].filter(Boolean).join(" ");
8057
+ const label = tail ? step.label.padEnd(18) : step.label;
8058
+ const styled = step.state === "pending" ? this.c(ANSI.dim, label) : label;
8059
+ return this.fit(` ${this.glyph(step)} ${styled}${tail ? this.c(ANSI.dim, tail) : ""}`);
8060
+ }
8061
+ header() {
8062
+ const name = this.c(`${ANSI.bold}${ANSI.magenta}`, "FRAY");
8063
+ const version = this.version ? ` ${this.c(ANSI.dim, `v${this.version}`)}` : "";
8064
+ return ["", ` ${name}${version}`, ""];
8065
+ }
8066
+ /** Return the cursor to the top of the painted region and clear everything below it. */
8067
+ rewind() {
8068
+ return this.painted > 0 ? `\x1B[${this.painted}A\r\x1B[0J` : "\r\x1B[0J";
8069
+ }
8070
+ paint(force = false) {
8071
+ if (this.settled) return;
8072
+ if (!this.tty || this.debug) return;
8073
+ if (!force) this.frame++;
8074
+ const lines = [...this.header(), ...this.steps.map((step) => this.stepRow(step))];
8075
+ this.out.write(`${SYNC_BEGIN}${this.rewind()}${lines.join("\n")}
8076
+ ${SYNC_END}`);
8077
+ this.painted = lines.length;
8078
+ }
8079
+ /**
8080
+ * Non-TTY transcript: one settled, parseable row per state change, so a pipe still sees progress.
8081
+ *
8082
+ * Silent under `--debug`, where the log feed is the authoritative account and these rows were
8083
+ * duplicating it in a second format ("fray: ··· artifact — checking …" immediately beside
8084
+ * "INFO artifact Checking …"). The final ready/fail summary still prints in both modes.
8085
+ */
8086
+ emitPlain(step) {
8087
+ if (this.tty || this.debug) return;
8088
+ const mark = step.state === "done" ? "done" : step.state === "failed" ? "failed" : step.state === "skipped" ? "skipped" : "\xB7\xB7\xB7";
8089
+ const detail = step.detail ? ` \u2014 ${step.detail}` : "";
8090
+ const line = `fray: ${mark} ${step.label.toLowerCase()}${detail}`;
8091
+ if (line === this.lastPlainLine) return;
8092
+ this.lastPlainLine = line;
8093
+ this.out.write(`${line}
8094
+ `);
8095
+ }
8096
+ /** Print a line without disturbing the repaint region (used for the debug feed and warnings). */
8097
+ note(line) {
8098
+ if (this.settled || !this.tty || this.debug) {
8099
+ this.out.write(`${line}
8100
+ `);
8101
+ return;
8102
+ }
8103
+ this.out.write(`${SYNC_BEGIN}${this.rewind()}${this.fit(line)}
8104
+ ${SYNC_END}`);
8105
+ this.painted = 0;
8106
+ this.paint(true);
8107
+ }
8108
+ // ── The final block ──────────────────────────────────────────────────────────────────────────────
8109
+ /**
8110
+ * Replace the boot region with the block that stays on screen, then stop repainting for good.
8111
+ * `entries` are the label/value rows: the URL first, then project, source, log path.
8112
+ */
8113
+ ready(entries, hint) {
8114
+ for (const step of this.steps) if (step.state === "active") this.settle(step.key, "done");
8115
+ const elapsed = formatDuration(this.now() - this.startedAt);
8116
+ this.stop();
8117
+ if (!this.tty || this.debug) {
8118
+ this.out.write(`fray: ready in ${elapsed}
8119
+ `);
8120
+ for (const entry of entries) this.out.write(`fray: ${entry.label.toLowerCase()}: ${entry.value}
8121
+ `);
8122
+ return;
8123
+ }
8124
+ const width = entries.reduce((max, entry) => Math.max(max, entry.label.length), 0) + 1;
8125
+ const lines = [
8126
+ "",
8127
+ ` ${this.c(`${ANSI.bold}${ANSI.magenta}`, "FRAY")}${this.version ? ` ${this.c(ANSI.dim, `v${this.version}`)}` : ""} ${this.c(ANSI.dim, `ready in ${elapsed}`)}`,
8128
+ "",
8129
+ // Deliberately NOT truncated. These rows carry an address and a log path the operator has to
8130
+ // be able to copy, and a clipped path is worse than a wrapped one. Truncation exists to stop a
8131
+ // wrap desynchronizing the repaint region — and this is the last paint, so there is no region
8132
+ // left to protect.
8133
+ ...entries.map((entry) => {
8134
+ const arrow = this.c(ANSI.green, "\u279C");
8135
+ const label = this.c(ANSI.bold, `${entry.label}:`.padEnd(width + 1));
8136
+ const value = entry.accent ? this.c(ANSI.cyan, entry.value) : this.c(ANSI.dim, entry.value);
8137
+ return ` ${arrow} ${label} ${value}`;
8138
+ }),
8139
+ ...hint ? ["", ` ${this.c(ANSI.dim, hint)}`] : [],
8140
+ ""
8141
+ ];
8142
+ const rewind = this.painted > 0 ? `\x1B[${this.painted}A\r\x1B[0J` : "\r\x1B[0J";
8143
+ this.out.write(`${rewind}${lines.join("\n")}
8144
+ `);
8145
+ this.painted = 0;
8146
+ }
8147
+ /** Terminal failure: settle the active step as failed, then print the reason and where to look. */
8148
+ fail(message, logPath) {
8149
+ const active = this.activeStep();
8150
+ if (active) this.settle(active.key, "failed");
8151
+ this.stop();
8152
+ if (!this.tty || this.debug) {
8153
+ this.out.write(`fray: failed: ${message}
8154
+ `);
8155
+ if (logPath) this.out.write(`fray: log: ${logPath}
8156
+ `);
8157
+ return;
8158
+ }
8159
+ const lines = [
8160
+ "",
8161
+ ` ${this.c(ANSI.red, "\u2717")} ${this.c(ANSI.bold, "Fray could not start")}`,
8162
+ "",
8163
+ // Not truncated, for the same reason as `ready()`: the operator has to be able to read the whole
8164
+ // error and copy the whole path, and nothing repaints after this.
8165
+ ...message.split("\n").map((row) => ` ${this.c(ANSI.red, row)}`),
8166
+ ...logPath ? ["", ` ${this.c(ANSI.dim, `Full log: ${logPath}`)}`] : [],
8167
+ ""
8168
+ ];
8169
+ const rewind = this.painted > 0 ? `\x1B[${this.painted}A\r\x1B[0J` : "\r\x1B[0J";
8170
+ this.out.write(`${rewind}${lines.join("\n")}
8171
+ `);
8172
+ this.painted = 0;
8173
+ }
8174
+ /** Stop repainting without printing anything, e.g. before ordinary console output takes over. */
8175
+ stop() {
8176
+ if (this.settled) return;
8177
+ this.settled = true;
8178
+ if (this.timer) clearInterval(this.timer);
8179
+ this.timer = void 0;
8180
+ }
8181
+ };
8182
+ var SGR = /\x1b\[[0-9;]*m/g;
8183
+ function visibleLength(line) {
8184
+ return line.replace(SGR, "").length;
8185
+ }
8186
+ function sliceVisible(line, limit) {
8187
+ let visible = 0;
8188
+ let out = "";
8189
+ for (let index = 0; index < line.length; ) {
8190
+ SGR.lastIndex = index;
8191
+ const match = /^\x1b\[[0-9;]*m/.exec(line.slice(index));
8192
+ if (match) {
8193
+ out += match[0];
8194
+ index += match[0].length;
8195
+ continue;
8196
+ }
8197
+ if (visible >= limit) break;
8198
+ out += line[index];
8199
+ visible++;
8200
+ index++;
8201
+ }
8202
+ return `${out}${ANSI.reset}`;
8203
+ }
8204
+
8205
+ // src/production.ts
8206
+ init_logging();
7223
8207
 
7224
8208
  // src/launcher.ts
7225
8209
  init_project_identity();
7226
8210
  init_project_launch();
7227
8211
  init_tmux_socket();
7228
8212
  init_boot_progress();
8213
+ init_logging();
7229
8214
  init_src();
7230
8215
  import { execFileSync as execFileSync4 } from "node:child_process";
7231
8216
  import { createServer } from "node:net";
7232
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
7233
- import { homedir as homedir2 } from "node:os";
7234
- import { basename as basename3, join as join5, resolve as resolve3 } from "node:path";
8217
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, realpathSync as realpathSync3, writeFileSync as writeFileSync6 } from "node:fs";
8218
+ import { homedir as homedir4 } from "node:os";
8219
+ import { basename as basename4, join as join7, resolve as resolve3 } from "node:path";
7235
8220
  import { setTimeout as delay2 } from "node:timers/promises";
7236
8221
  var PORT_SCAN_COUNT = 100;
7237
8222
  var LAUNCH_TIMEOUT_MS = 3e4;
@@ -7274,7 +8259,8 @@ function parseCliArgs(argv) {
7274
8259
  "-h",
7275
8260
  "--dev",
7276
8261
  "--prod",
7277
- "--port"
8262
+ "--port",
8263
+ "--debug"
7278
8264
  ]);
7279
8265
  for (let index = 0; index < argv.length; index++) {
7280
8266
  const arg = argv[index];
@@ -7299,11 +8285,12 @@ function parseCliArgs(argv) {
7299
8285
  status: args2.has("--status"),
7300
8286
  help: args2.has("--help") || args2.has("-h"),
7301
8287
  dev: args2.has("--dev"),
8288
+ debug: args2.has("--debug"),
7302
8289
  port,
7303
8290
  repoPath
7304
8291
  };
7305
8292
  }
7306
- function resolveWorkspace(cwd = process.cwd(), home = homedir2(), env = process.env) {
8293
+ function resolveWorkspace(cwd = process.cwd(), home = homedir4(), env = process.env) {
7307
8294
  let gitRoot;
7308
8295
  try {
7309
8296
  gitRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
@@ -7319,8 +8306,8 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir2(), env = process.
7319
8306
  const identity = resolveGitProjectIdentity(realpathSync3(gitRoot), home);
7320
8307
  const root = identity.root;
7321
8308
  const id = identity.id;
7322
- const stateDir = join5(home, ".fray", "projects", id);
7323
- mkdirSync5(stateDir, { recursive: true });
8309
+ const stateDir = join7(home, ".fray", "projects", id);
8310
+ mkdirSync6(stateDir, { recursive: true });
7324
8311
  const target2 = {
7325
8312
  projectId: id,
7326
8313
  projectDir: root,
@@ -7334,7 +8321,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir2(), env = process.
7334
8321
  root,
7335
8322
  id,
7336
8323
  stateDir,
7337
- name: basename3(root),
8324
+ name: basename4(root),
7338
8325
  identityScope: identity.scope,
7339
8326
  tmuxSocket: selected.socket,
7340
8327
  tmuxSocketManaged: selected.managed
@@ -7366,7 +8353,7 @@ function workspaceFromLaunchTarget(target2, env = process.env) {
7366
8353
  root,
7367
8354
  id: target2.projectId,
7368
8355
  stateDir: target2.stateDir,
7369
- name: basename3(root),
8356
+ name: basename4(root),
7370
8357
  identityScope: target2.identityScope === "worktree" ? "worktree" : "repository",
7371
8358
  tmuxSocket: selected.socket,
7372
8359
  tmuxSocketManaged: selected.managed
@@ -7403,12 +8390,12 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
7403
8390
  if (authoritative && processGenerationIsStale(authoritative, adapter))
7404
8391
  return null;
7405
8392
  return parseStatusFile(
7406
- join5(stateDir, "dev-supervisor.lock"),
8393
+ join7(stateDir, "dev-supervisor.lock"),
7407
8394
  authoritative,
7408
8395
  expected2,
7409
8396
  adapter
7410
8397
  ) ?? parseStatusFile(
7411
- join5(stateDir, "server.lock"),
8398
+ join7(stateDir, "server.lock"),
7412
8399
  authoritative,
7413
8400
  expected2,
7414
8401
  adapter
@@ -7417,7 +8404,7 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
7417
8404
  function readPreferredPort(stateDir) {
7418
8405
  try {
7419
8406
  const value = JSON.parse(
7420
- readFileSync6(join5(stateDir, "launcher.json"), "utf8")
8407
+ readFileSync6(join7(stateDir, "launcher.json"), "utf8")
7421
8408
  );
7422
8409
  return Number.isInteger(value.port) && value.port > 0 && value.port <= 65535 ? value.port : void 0;
7423
8410
  } catch {
@@ -7467,25 +8454,38 @@ async function canBindPort(port) {
7467
8454
  });
7468
8455
  return bind(port);
7469
8456
  }
7470
- async function choosePort(explicit, preferred, available = canBindPort) {
7471
- if (explicit !== void 0) {
7472
- if (!await available(explicit))
7473
- throw new Error(`port ${explicit} is already in use`);
7474
- return explicit;
7475
- }
7476
- const candidates = [
8457
+ function portCandidates(preferred) {
8458
+ return [
7477
8459
  preferred,
7478
8460
  ...Array.from(
7479
8461
  { length: PORT_SCAN_COUNT },
7480
8462
  (_, index) => DEFAULT_PORT + index
7481
8463
  )
7482
- ];
7483
- for (const port of candidates) {
7484
- if (port && port <= 65535 && await available(port)) return port;
8464
+ ].filter((port) => !!port && port <= 65535);
8465
+ }
8466
+ function noFreePortMessage() {
8467
+ return `no free Fray development port found in ${DEFAULT_PORT}-${DEFAULT_PORT + PORT_SCAN_COUNT - 1}`;
8468
+ }
8469
+ async function allocatePort(explicit, preferred, options2 = {}) {
8470
+ const available = options2.available ?? canBindPort;
8471
+ const reserve = options2.reserve ?? ((port) => tryReservePort(port));
8472
+ const claim = async (port) => {
8473
+ const release = reserve(port);
8474
+ if (!release) return void 0;
8475
+ if (await available(port)) return { port, release };
8476
+ release();
8477
+ return void 0;
8478
+ };
8479
+ if (explicit !== void 0) {
8480
+ const allocated = await claim(explicit);
8481
+ if (!allocated) throw new Error(`port ${explicit} is already in use`);
8482
+ return allocated;
7485
8483
  }
7486
- throw new Error(
7487
- `no free Fray development port found in ${DEFAULT_PORT}-${DEFAULT_PORT + PORT_SCAN_COUNT - 1}`
7488
- );
8484
+ for (const port of portCandidates(preferred)) {
8485
+ const allocated = await claim(port);
8486
+ if (allocated) return allocated;
8487
+ }
8488
+ throw new Error(noFreePortMessage());
7489
8489
  }
7490
8490
  async function waitForWorkspace(port, expected2, timeoutMs = LAUNCH_TIMEOUT_MS, options2 = {}) {
7491
8491
  const started = Date.now();
@@ -7522,9 +8522,9 @@ init_project_launch();
7522
8522
  init_project_launch();
7523
8523
  import { fork } from "node:child_process";
7524
8524
  import { randomUUID as randomUUID5 } from "node:crypto";
7525
- import { existsSync as existsSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
8525
+ import { existsSync as existsSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8 } from "node:fs";
7526
8526
  import { createServer as createNetServer } from "node:net";
7527
- import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
8527
+ import { basename as basename5, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
7528
8528
  import { fileURLToPath } from "node:url";
7529
8529
  import watcher from "@parcel/watcher";
7530
8530
 
@@ -7601,14 +8601,20 @@ var RestartSupervisorProxy = class {
7601
8601
  ...ownerTransition ? { state: this.state, ...this.message ? { message: this.message } : {} } : delegated ?? { state: this.state, ...this.message ? { message: this.message } : {} },
7602
8602
  // Never infer this from the generic protocol: legacy/static supervisors can recover a child
7603
8603
  // but cannot build and promote the canonical Fray artifact.
7604
- updateRestart: typeof this.options.updateRestart === "function"
8604
+ updateRestart: typeof this.options.updateRestart === "function",
8605
+ // Sent ONLY when the launcher can actually answer it, so an older client — and fray-dev, which
8606
+ // has no notion of "already current" — keeps today's behaviour on its absence.
8607
+ ...this.options.updateAvailable ? { updateAvailable: this.options.updateAvailable() === true } : {}
7605
8608
  };
7606
8609
  }
7607
8610
  async runAction(action) {
7608
8611
  if (this.restartInFlight) return this.restartInFlight;
7609
8612
  this.state = "restarting";
7610
8613
  this.message = void 0;
7611
- const work = action().then(
8614
+ const work = (async () => {
8615
+ await new Promise((resume) => setImmediate(resume));
8616
+ return action();
8617
+ })().then(
7612
8618
  (result) => {
7613
8619
  this.state = result.state;
7614
8620
  this.message = result.message;
@@ -7742,6 +8748,7 @@ var RestartSupervisorProxy = class {
7742
8748
  };
7743
8749
 
7744
8750
  // packages/server/src/dev-supervisor.ts
8751
+ init_logging();
7745
8752
  var DEV_RESTART_DEBOUNCE_MS = 180;
7746
8753
  var DEV_CRASH_STABLE_MS = 5e3;
7747
8754
  var DEV_CRASH_RETRY_BASE_MS = 500;
@@ -7777,6 +8784,9 @@ function devCrashRetryDelay(attempt) {
7777
8784
  return Math.min(DEV_CRASH_RETRY_BASE_MS * 2 ** exponent, DEV_CRASH_RETRY_MAX_MS);
7778
8785
  }
7779
8786
  var SUPERVISOR_ESCALATE_GRACE_MS = 500;
8787
+ function stripPrefix(line) {
8788
+ return line.startsWith("[fray-ui] ") ? line.slice("[fray-ui] ".length) : line;
8789
+ }
7780
8790
  function createSupervisorShutdownHandler(options2) {
7781
8791
  const now = options2.now ?? (() => Date.now());
7782
8792
  const escalateAfterMs = options2.escalateAfterMs ?? SUPERVISOR_ESCALATE_GRACE_MS;
@@ -7824,10 +8834,10 @@ function defaultDevWatchRoots() {
7824
8834
  }
7825
8835
  function isWithin(path, root) {
7826
8836
  const rel = relative2(root, path);
7827
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute4(rel);
8837
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute4(rel);
7828
8838
  }
7829
8839
  function ignoredDevPath(path) {
7830
- const parts = resolve4(path).split(sep2);
8840
+ const parts = resolve4(path).split(sep3);
7831
8841
  const name = parts.at(-1) ?? "";
7832
8842
  return parts.some((part) => GENERATED_DIRS.has(part) || part === "fixtures" || part.endsWith(".fixtures")) || /\.(?:test|spec)\.[^.]+$/.test(name) || name.includes(".golden.");
7833
8843
  }
@@ -7836,9 +8846,9 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
7836
8846
  const root = roots.find((candidate) => isWithin(absolute, resolve4(candidate)));
7837
8847
  if (!root) return null;
7838
8848
  if (ignoredDevPath(absolute)) return null;
7839
- const name = basename4(absolute);
8849
+ const name = basename5(absolute);
7840
8850
  const relToWorkspace = relative2(resolve4(root), absolute);
7841
- const parts = relToWorkspace.split(sep2);
8851
+ const parts = relToWorkspace.split(sep3);
7842
8852
  const packageName = parts[0] === "packages" ? parts[1] : void 0;
7843
8853
  if (name === "package.json" && parts.length === 3 && packageName && CHILD_PACKAGE_METADATA.has(packageName)) {
7844
8854
  return "child";
@@ -7858,7 +8868,7 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
7858
8868
  return null;
7859
8869
  }
7860
8870
  function devConfigSyntaxError(path) {
7861
- const name = basename4(path);
8871
+ const name = basename5(path);
7862
8872
  const isTsconfig = /^tsconfig(?:\.[^.]+)?\.json$/.test(name);
7863
8873
  if (name !== "package.json" && !isTsconfig) return null;
7864
8874
  let text;
@@ -7996,8 +9006,8 @@ var Supervisor = class {
7996
9006
  }
7997
9007
  this.supervisorLock = resolve4(opts.launchTarget.stateDir, "dev-supervisor.lock");
7998
9008
  this.ownerToken = opts.launchOwnerToken;
7999
- this.logLine = opts.log ?? ((line) => console.log(line));
8000
- this.errorLine = opts.error ?? ((line) => console.error(line));
9009
+ this.logLine = opts.log ?? ((line) => log.info("supervisor", stripPrefix(line)));
9010
+ this.errorLine = opts.error ?? ((line) => log.error("supervisor", stripPrefix(line)));
8001
9011
  this.updateRestart = opts.updateRestart;
8002
9012
  this.rollbackUpdate = opts.rollbackUpdate;
8003
9013
  this.durableReexec = opts.durableReexec;
@@ -8006,6 +9016,7 @@ var Supervisor = class {
8006
9016
  childPort: () => this.childPort,
8007
9017
  restart: () => this.restartFromBrowser(),
8008
9018
  updateRestart: this.updateRestart ? () => this.updateFromBrowser() : void 0,
9019
+ updateAvailable: opts.updateAvailable,
8009
9020
  status: () => {
8010
9021
  if (this.browserRestart || this.restartRunning) return { state: "restarting" };
8011
9022
  const failed = this.child === null && this.boot === null;
@@ -8025,7 +9036,7 @@ var Supervisor = class {
8025
9036
  }
8026
9037
  async start() {
8027
9038
  if (this.supervisorLock) {
8028
- mkdirSync6(dirname5(this.supervisorLock), { recursive: true });
9039
+ mkdirSync7(dirname5(this.supervisorLock), { recursive: true });
8029
9040
  this.writeStatus("starting", "watcher initializing");
8030
9041
  }
8031
9042
  try {
@@ -8447,7 +9458,7 @@ async function startDevSupervisor(opts) {
8447
9458
  }
8448
9459
 
8449
9460
  // src/production-update.ts
8450
- import { execFile, spawn } from "node:child_process";
9461
+ import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
8451
9462
  var PRODUCTION_REEXEC_FLAG = "--_fray-production-reexec";
8452
9463
  function compareReleaseVersions(a, b) {
8453
9464
  const parse = (value) => {
@@ -8496,7 +9507,7 @@ function handoffToRegistrySuccessor(plan, request, adapter) {
8496
9507
  var npmRegistryReleaseAdapter = {
8497
9508
  latestVersion(packageName) {
8498
9509
  return new Promise((resolveVersion, reject) => {
8499
- execFile("npm", ["view", `${packageName}@latest`, "version", "--json"], { encoding: "utf8" }, (error, stdout) => {
9510
+ execFile2("npm", ["view", `${packageName}@latest`, "version", "--json"], { encoding: "utf8" }, (error, stdout) => {
8500
9511
  if (error) return reject(new Error(`could not check npm for ${packageName}: ${error.message}`));
8501
9512
  try {
8502
9513
  const parsed = JSON.parse(stdout);
@@ -8508,7 +9519,7 @@ var npmRegistryReleaseAdapter = {
8508
9519
  });
8509
9520
  },
8510
9521
  spawnNpmExec({ packageSpec, bin, args: args2, cwd, env }) {
8511
- return spawn("npm", ["exec", "--yes", `--package=${packageSpec}`, "--", bin, ...args2], {
9522
+ return spawn2("npm", ["exec", "--yes", `--package=${packageSpec}`, "--", bin, ...args2], {
8512
9523
  cwd,
8513
9524
  env,
8514
9525
  detached: true,
@@ -8519,10 +9530,10 @@ var npmRegistryReleaseAdapter = {
8519
9530
 
8520
9531
  // src/preflight.ts
8521
9532
  import { spawnSync } from "node:child_process";
8522
- import { chmodSync, statSync as statSync5 } from "node:fs";
9533
+ import { chmodSync, statSync as statSync6 } from "node:fs";
8523
9534
  import { createRequire } from "node:module";
8524
- import { dirname as dirname6, join as join6 } from "node:path";
8525
- var MINIMUM_NODE = { major: 20, minor: 19 };
9535
+ import { dirname as dirname6, join as join8 } from "node:path";
9536
+ var MINIMUM_NODE = { major: 22, minor: 12 };
8526
9537
  function commandIsAvailable(command) {
8527
9538
  const versionArg = command === "tmux" ? "-V" : "--version";
8528
9539
  const result = spawnSync(command, [versionArg], {
@@ -8549,11 +9560,11 @@ function assertLaunchPrerequisites(options2 = {}) {
8549
9560
  function ensureNativeHelperPermissions(options2 = {}) {
8550
9561
  const platform = options2.platform ?? process.platform;
8551
9562
  if (platform === "win32") return;
8552
- const stat = options2.stat ?? ((path) => statSync5(path));
9563
+ const stat = options2.stat ?? ((path) => statSync6(path));
8553
9564
  const chmod = options2.chmod ?? ((path, mode) => chmodSync(path, mode));
8554
9565
  const resolvePty = options2.resolvePty ?? (() => createRequire(import.meta.url).resolve("node-pty/package.json"));
8555
9566
  try {
8556
- const helper = join6(
9567
+ const helper = join8(
8557
9568
  dirname6(resolvePty()),
8558
9569
  "prebuilds",
8559
9570
  `${platform}-${options2.arch ?? process.arch}`,
@@ -8567,9 +9578,16 @@ function ensureNativeHelperPermissions(options2 = {}) {
8567
9578
  }
8568
9579
 
8569
9580
  // src/production.ts
8570
- var ABANDON_GRACE_MS = 3e3;
8571
9581
  var PACKAGE_NAME = process.env.FRAY_REGISTRY_PACKAGE ?? "frayui";
8572
- var PACKAGE_VERSION = process.env.npm_package_version ?? "0.0.1";
9582
+ function resolvePackageVersion() {
9583
+ try {
9584
+ const manifest = JSON.parse(readFileSync9(join9(import.meta.dirname, "..", "package.json"), "utf8"));
9585
+ if (typeof manifest.version === "string" && manifest.version.trim()) return manifest.version.trim();
9586
+ } catch {
9587
+ }
9588
+ return process.env.npm_package_version ?? "0.0.1";
9589
+ }
9590
+ var PACKAGE_VERSION = resolvePackageVersion();
8573
9591
  var rawArgs = process.argv.slice(2);
8574
9592
  var reexec = rawArgs.includes(PRODUCTION_REEXEC_FLAG);
8575
9593
  var args = rawArgs.filter((arg) => arg !== PRODUCTION_REEXEC_FLAG);
@@ -8585,7 +9603,21 @@ var options = (() => {
8585
9603
  }
8586
9604
  })();
8587
9605
  if (options.help) {
8588
- console.log("Fray production launcher\n\nUsage: npx frayui [options] [repository]\n\nRuns the npm-resolved immutable Fray package. Use fray-dev only for a source checkout.");
9606
+ console.log(
9607
+ `Fray production launcher
9608
+
9609
+ Usage: npx ${PACKAGE_NAME} [options] [repository]
9610
+
9611
+ Runs the npm-resolved immutable Fray package, then opens it in your default browser. Use fray-dev
9612
+ only for a source checkout.
9613
+
9614
+ Options:
9615
+ --app use the legacy dedicated app window instead of a browser tab
9616
+ --no-app print the URL without opening a browser
9617
+ --port <port> request a fixed port for a new workspace server
9618
+ --debug stream the full event feed to the terminal instead of the compact readout
9619
+ -h, --help show this help`
9620
+ );
8589
9621
  process.exit(0);
8590
9622
  }
8591
9623
  if (options.dev || rawArgs.includes("--prod")) fail("--dev and --prod are not available from the registry launcher");
@@ -8602,59 +9634,94 @@ var workspace = (() => {
8602
9634
  }
8603
9635
  })();
8604
9636
  process.chdir(workspace.root);
9637
+ var logger = setAmbientLogger(
9638
+ process.env.FRAY_LOG_FILE ? createLogger({ file: process.env.FRAY_LOG_FILE, owner: false }) : createLogger({ file: runLogPath(workspace.stateDir) })
9639
+ );
9640
+ var readout = reexec || process.env.FRAY_PRODUCTION_SUPERVISOR === "1" ? void 0 : new Readout({ debug: options.debug, version: PACKAGE_VERSION });
9641
+ attachTerminalMirror(logger, options.debug || process.env.FRAY_DEBUG === "1");
9642
+ readout?.plan([
9643
+ { key: "server", label: "Server" },
9644
+ { key: "browser", label: options.noApp ? "Address" : "Browser" }
9645
+ ]);
9646
+ readout?.begin("server", "starting");
9647
+ logger.info("launcher", `frayui ${PACKAGE_VERSION} starting for ${workspace.root}`);
8605
9648
  var target = workspaceLaunchTarget(workspace);
8606
9649
  var expected = expectedOwnerHealth(target, readProjectLaunchOwner(workspace.stateDir));
8607
- async function abandonLaunchedChild(pid, port) {
8608
- if (!pid) return;
8609
- const signalGroup = (signal) => {
8610
- try {
8611
- process.kill(-pid, signal);
8612
- return true;
8613
- } catch {
8614
- }
8615
- try {
8616
- process.kill(pid, signal);
8617
- return true;
8618
- } catch {
8619
- return false;
8620
- }
8621
- };
8622
- if (!signalGroup("SIGTERM")) return;
8623
- const deadline = Date.now() + ABANDON_GRACE_MS;
8624
- while (Date.now() < deadline) {
8625
- if (!pidIsAlive(pid)) return;
8626
- await delay3(100);
8627
- }
8628
- signalGroup("SIGKILL");
8629
- await delay3(200);
8630
- if (pidIsAlive(pid)) {
8631
- console.error(
8632
- `fray: could not stop the abandoned Fray supervisor (pid ${pid}, port ${port}); stop it manually before retrying`
8633
- );
8634
- }
8635
- }
8636
9650
  async function existingPort() {
8637
9651
  const owner = liveWorkspaceOwner(workspace.stateDir, target);
8638
9652
  const ports = [owner?.port, readPreferredPort(workspace.stateDir)].filter((value) => !!value);
8639
9653
  for (const port of new Set(ports)) if (await probeFray(port, expected)) return port;
8640
9654
  return void 0;
8641
9655
  }
8642
- function openOrPrint(port, reused) {
9656
+ async function openOrPrint(port, reused) {
8643
9657
  const url = `http://127.0.0.1:${port}`;
8644
- console.log(`${reused ? "reusing" : "started"} Fray ${PACKAGE_VERSION} for ${workspace.root}`);
8645
- console.log(url);
9658
+ logger.info("launcher", `${reused ? "reusing" : "started"} Fray at ${url}`);
9659
+ readout?.settle("server", "done", `port ${port}`);
9660
+ let browser;
9661
+ if (!options.noApp) {
9662
+ readout?.begin("browser", options.appMode ? "requesting app window" : "requesting default browser");
9663
+ try {
9664
+ if (options.appMode) {
9665
+ await launchApp(url, { dataPath: join9(workspace.stateDir, "browser-profile") });
9666
+ browser = reused ? "focused the Fray app window" : "opened the Fray app window";
9667
+ } else {
9668
+ await launchBrowserTab(url);
9669
+ browser = "opened in your default browser";
9670
+ }
9671
+ logger.info("launcher", browser);
9672
+ } catch (error) {
9673
+ browser = `could not open the ${options.appMode ? "app window" : "default browser"} \u2014 open the address above`;
9674
+ logger.warn("launcher", `${browser}: ${error instanceof Error ? error.message : error}`);
9675
+ }
9676
+ readout?.settle("browser", "done", browser);
9677
+ } else {
9678
+ readout?.settle("browser", "done", url);
9679
+ }
9680
+ if (!readout) {
9681
+ console.log(`${reused ? "reusing" : "started"} Fray ${PACKAGE_VERSION} for ${workspace.root}`);
9682
+ console.log(url);
9683
+ return;
9684
+ }
9685
+ const home = homedir5();
9686
+ readout.ready(
9687
+ [
9688
+ { label: "Local", value: `${url}/`, accent: true },
9689
+ { label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
9690
+ ...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
9691
+ ],
9692
+ options.debug ? void 0 : "press ctrl-c to stop \xB7 run with --debug for the full event feed"
9693
+ );
8646
9694
  }
8647
9695
  async function runSupervisor(port, token) {
8648
9696
  assertLaunchPrerequisites();
8649
9697
  ensureNativeHelperPermissions();
8650
9698
  const owner = adoptProjectLaunchOwner(target, token, "supervisor");
8651
- const env = projectLaunchEnvironment({ ...process.env, FRAY_PRODUCTION_SUPERVISOR: "1" }, target, owner.token);
8652
- const webDist = join7(import.meta.dirname, "..", "web-dist");
8653
- const runtimeDir = join7(import.meta.dirname, "..", "runtime");
8654
- const scriptsDir = join7(runtimeDir, "board");
8655
- const workerPluginDir = join7(runtimeDir, "cc-worker");
9699
+ const env = projectLaunchEnvironment(
9700
+ {
9701
+ ...process.env,
9702
+ FRAY_PRODUCTION_SUPERVISOR: "1",
9703
+ ...logEnvironment(logger, options.debug ? "debug" : "info"),
9704
+ ...options.debug ? { FRAY_DEBUG: "1" } : {}
9705
+ },
9706
+ target,
9707
+ owner.token
9708
+ );
9709
+ const webDist = join9(import.meta.dirname, "..", "web-dist");
9710
+ const runtimeDir = join9(import.meta.dirname, "..", "runtime");
9711
+ const scriptsDir = join9(runtimeDir, "board");
9712
+ const workerPluginDir = join9(runtimeDir, "cc-worker");
8656
9713
  const childEntry = fileURLToPath2(new URL("./dev-child.js", import.meta.url));
8657
9714
  let plannedUpdate;
9715
+ let updateAvailable = true;
9716
+ const refreshUpdateAvailable = async () => {
9717
+ try {
9718
+ updateAvailable = await planRegistryUpdate(PACKAGE_NAME, PACKAGE_VERSION, npmRegistryReleaseAdapter) !== null;
9719
+ } catch {
9720
+ }
9721
+ };
9722
+ void refreshUpdateAvailable();
9723
+ const updateAvailablePoll = setInterval(() => void refreshUpdateAvailable(), 30 * 60 * 1e3);
9724
+ updateAvailablePoll.unref();
8658
9725
  const supervisor = await startDevSupervisor({
8659
9726
  port,
8660
9727
  cwd: workspace.root,
@@ -8665,10 +9732,14 @@ async function runSupervisor(port, token) {
8665
9732
  watch: false,
8666
9733
  childEntry,
8667
9734
  childEnvironment: () => ({ FRAY_STABLE_WEB_DIST: webDist, FRAY_STABLE_ARTIFACT: `npm:${PACKAGE_NAME}@${PACKAGE_VERSION}`, FRAY_SCRIPTS_DIR: scriptsDir, FRAY_WORKER_PLUGIN_DIR: workerPluginDir }),
9735
+ updateAvailable: () => updateAvailable,
8668
9736
  updateRestart: async () => {
8669
9737
  try {
8670
9738
  const plan = await planRegistryUpdate(PACKAGE_NAME, PACKAGE_VERSION, npmRegistryReleaseAdapter);
8671
- if (!plan) return { state: "failed", message: `Fray ${PACKAGE_VERSION} is already current` };
9739
+ if (!plan) {
9740
+ updateAvailable = false;
9741
+ return { state: "failed", message: `Fray ${PACKAGE_VERSION} is already current` };
9742
+ }
8672
9743
  plannedUpdate = plan;
8673
9744
  return { state: "ready", message: `Fray ${plan.latestVersion} will start in a new npm execution cache` };
8674
9745
  } catch (error) {
@@ -8682,7 +9753,27 @@ async function runSupervisor(port, token) {
8682
9753
  process.exit(0);
8683
9754
  }
8684
9755
  });
8685
- const stop = createSupervisorShutdownHandler({ close: () => supervisor.close(), force: () => supervisor.forceStop(), release: () => owner.release(), exit: (code) => process.exit(code), error: (line) => console.error(line) });
9756
+ const stop = createSupervisorShutdownHandler({
9757
+ close: () => supervisor.close(),
9758
+ force: () => supervisor.forceStop(),
9759
+ release: () => owner.release(),
9760
+ exit: (code) => {
9761
+ logger.info("launcher", `stopped with code ${code}`);
9762
+ const suffix = logger.file ? `
9763
+ log: ${tildePath(logger.file, homedir5())}` : "";
9764
+ process.stdout.write(
9765
+ code === 0 ? `
9766
+ Fray stopped. Agent sessions in tmux keep running.${suffix}
9767
+
9768
+ ` : `
9769
+ Fray stopped with errors (exit ${code}).${suffix}
9770
+
9771
+ `
9772
+ );
9773
+ process.exit(code);
9774
+ },
9775
+ error: (line) => logger.error("supervisor", line.startsWith("[fray-ui] ") ? line.slice(10) : line)
9776
+ });
8686
9777
  process.on("SIGINT", stop);
8687
9778
  process.on("SIGTERM", stop);
8688
9779
  void supervisor.stopRequested.then(stop);
@@ -8699,40 +9790,47 @@ try {
8699
9790
  }
8700
9791
  const existing = await existingPort();
8701
9792
  if (existing) {
8702
- openOrPrint(existing, true);
9793
+ await openOrPrint(existing, true);
8703
9794
  process.exit(0);
8704
9795
  }
8705
9796
  const claim = tryAcquireProjectLaunchOwner(target, "launcher");
8706
9797
  if (claim.kind !== "acquired") throw new Error("Fray is starting for this project; retry shortly");
8707
- const release = await acquireGlobalLaunchLock();
9798
+ let release = await acquireGlobalLaunchLock();
9799
+ let portReservation;
8708
9800
  try {
8709
- const port = await choosePort(options.port, readPreferredPort(workspace.stateDir));
8710
- if (options.foreground) await runSupervisor(port, claim.lease.token);
8711
- const log = openSync4(join7(workspace.stateDir, "fray.log"), "a");
8712
- const child = spawn2(process.execPath, [process.argv[1], "--foreground", "--no-app", "--port", String(port)], {
8713
- cwd: workspace.root,
8714
- detached: true,
8715
- env: projectLaunchEnvironment({ ...process.env, FRAY_PRODUCTION_SUPERVISOR: "1" }, target, claim.lease.token),
8716
- stdio: ["ignore", log, log]
8717
- });
8718
- child.unref();
8719
- closeSync4(log);
8720
- try {
8721
- await waitForWorkspace(
9801
+ const allocation = await allocatePort(options.port, readPreferredPort(workspace.stateDir));
9802
+ const port = allocation.port;
9803
+ portReservation = allocation.release;
9804
+ const running = runSupervisor(port, claim.lease.token);
9805
+ release();
9806
+ release = void 0;
9807
+ await Promise.race([
9808
+ waitForWorkspace(
8722
9809
  port,
8723
9810
  expectedOwnerHealth(target, readProjectLaunchOwner(workspace.stateDir)),
8724
9811
  void 0,
8725
9812
  { stateDir: workspace.stateDir }
8726
- );
8727
- } catch (error) {
8728
- await abandonLaunchedChild(child.pid, port);
8729
- throw error;
8730
- }
8731
- openOrPrint(port, false);
9813
+ ),
9814
+ running
9815
+ ]);
9816
+ portReservation();
9817
+ portReservation = void 0;
9818
+ await openOrPrint(port, false);
9819
+ await running;
8732
9820
  } finally {
8733
- release();
9821
+ release?.();
9822
+ portReservation?.();
8734
9823
  claim.lease.release();
8735
9824
  }
8736
9825
  } catch (error) {
9826
+ logger.error("launcher", `startup failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
9827
+ if (readout) {
9828
+ const where = readout.activeStep();
9829
+ readout.fail(
9830
+ `${where ? `${where.label.toLowerCase()}: ` : ""}${error instanceof Error ? error.message : String(error)}`,
9831
+ logger.file ?? void 0
9832
+ );
9833
+ process.exit(1);
9834
+ }
8737
9835
  fail(error);
8738
9836
  }