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.
- package/dist/claude-agent-broker.js +19 -3
- package/dist/codex-app-server-daemon.js +1 -1
- package/dist/dev-child.js +766 -329
- package/dist/frayui.js +1291 -193
- package/package.json +2 -2
- package/runtime/cc-worker/DECISIONS.md +86 -0
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +18 -4
- package/runtime/cc-worker/hooks/fence-stop.mjs +290 -0
- package/runtime/cc-worker/hooks/hooks.json +2 -1
- package/runtime/cc-worker/hooks/scratchpad.mjs +10 -9
- package/runtime/cc-worker/hooks/session-seed.mjs +2 -1
- package/runtime/cc-worker/skills/handoff/SKILL.md +6 -0
- package/web-dist/assets/{TerminalPane-DyK0ncN8.js → TerminalPane-hMf6ifuS.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-DtotTgyK.js → abnfDiagram-VRR7QNED-CQj6fwXy.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-JX8_jS4I.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CVl8mUt0.js → architectureDiagram-ZJ3FMSHR-CoE5ct03.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-VUZEsYBW.js → blockDiagram-677ZJIJ3-CEudy3zd.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-Dlg18lBF.js → c4Diagram-LMCZKHZV-ChW62IOH.js} +1 -1
- package/web-dist/assets/channel-y3u1yiAC.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-DG8gd151.js → chunk-32BRIVSS-Bh-A2le2.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-DKlahFJ2.js → chunk-52WLFC77-7Gia-F6f.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-Bk-BFDsQ.js → chunk-C7G6YPKG-C0MCv0Ym.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-DnXqd-jJ.js → chunk-EX3LRPZG-DPRO7Ss0.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-ujYyBebT.js → chunk-FWX5IMBZ-Cod9MA7c.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-Dk2XhPWQ.js → chunk-HOUHSVGY-D1oHrXQx.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-DoseDoV4.js → chunk-ICXQ74PX-DBLMA0ZN.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-B8dog4zz.js → chunk-MOJQB5TN-jiLvxNvx.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-D4GkHktS.js → chunk-OGEWGWER-w1RtF7G0.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-ChEIayRe.js → chunk-PUDLZKDR-DQlFMKm5.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-BtzMOr97.js → chunk-Q4XR5HBZ-CfD6vdr0.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-DQ7c8QAd.js → chunk-V7JOEXUC-BRB1iO1P.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-6jWzOU_J.js → chunk-VAUOI2AC-CCeJEpxW.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-TB0TNuya.js → chunk-VR4S4FIN-DfPRj9a1.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CY7fL6F5.js → chunk-WYO6CB5R-BWd84n6L.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-vZO6zPtA.js → chunk-ZGVPDNZ5-Bm-3TA9N.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-BL8mggXA.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-BL8mggXA.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-D4NcAp7V.js → cynefin-VYW2F7L2-BylEqg5y.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-hLO8f53U.js → cynefinDiagram-TSTJHNR4-CRfGS7Ou.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-86z9AWRw.js → dagre-VKFMJZFB-CNeTu13Z.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-DXw3bj8G.js → diagram-FQU43EPY-BYZKetkJ.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-BtH3_eiQ.js → diagram-G47NLZAW-DSOzyl5U.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-BxWq2H6D.js → diagram-NH7WQ7WH-DFZCvfJN.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-BoU3I6Xx.js → diagram-OA4YK3LP-m0qY8Zlg.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-CkPD-vrA.js → diagram-WEI45ONY-Cg7Qz-0T.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-BudQTnvf.js → ebnfDiagram-CCIWWBDH-DGvTjoXK.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-CRFLgkaW.js → erDiagram-Q63AITRT-BFqTK5TB.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-BaCHCepY.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-Cjl6weoT.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-DFmOc45y.js → ganttDiagram-NO4QXBWP-KbR9IE0O.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-CMW6meDa.js → gitGraph-TEB2WS4Q-hCbzZO_l.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-CHelbJfg.js → gitGraphDiagram-IHSO6WYX-CNPRTwQG.js} +1 -1
- package/web-dist/assets/index-BUY5ijyI.css +1 -0
- package/web-dist/assets/{index-C2Tf3Ihs.js → index-Bj6YWd3g.js} +68 -68
- package/web-dist/assets/{info-DKCQHKI2-DZT4W61r.js → info-DKCQHKI2-Zd_TXMfr.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-Dj0tLhVO.js → infoDiagram-FWYZ7A6U-DQ9zmVhJ.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-D825ES0c.js → ishikawaDiagram-FXEZZL3T-BFyJ5E_L.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-D53Igq5c.js → journeyDiagram-5HDEW3XC-BgjbY7_T.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-CJ-F0vAo.js → kanban-definition-HUTT4EX6-mpUEl8xx.js} +1 -1
- package/web-dist/assets/{line-CuiC27jO.js → line-CM_FUYGm.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-rG6qAIn0.js → mermaid-parser.core-CTVz2V70.js} +3 -3
- package/web-dist/assets/{mermaid.core-DzNUOuDq.js → mermaid.core-BCLNmVvR.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-DHfhQue9.js → mindmap-definition-LN4V7U3C-CgJU-6O7.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-BiUlKhVG.js → packet-7NZHBO7P-rkLFb4ZX.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-MQdiKZx3.js → pegDiagram-2B236MQR-Sy2oYuu1.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-dNHUvvE3.js → pie-RZYD4A2V-0yTxNtqZ.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-BwFxe1x1.js → pieDiagram-ENE6RG2P-CQ7YFWhH.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-9dic-8vv.js → quadrantDiagram-ABIIQ3AL-ClCTxrOj.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-CK3dl20X.js → radar-I7S5WNFK-JUOW5mdN.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-BuB2LkFj.js → railroad-3IZDKUUU-DJ2IS1Jg.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-CU4HK7Ek.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-CarBAd7R.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-ByEhVLyD.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-CuuspS_3.js → railroadDiagram-RFXS5EU6-BNsVGeA3.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-DHAILLdh.js → requirementDiagram-TGXJPOKE-BHa3IX0k.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-CztQ2t1b.js → sankeyDiagram-HTMAVEWB-CSbsLbtw.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-jk56eo0l.js → sequenceDiagram-DBY2YBRQ-BgVWj1nN.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-uwqBWnrI.js → stateDiagram-2N3HPSRC-BKHYNxA3.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-C-k0bbcG.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-BzuJ77j6.js → swimlanes-5IMT3BWC-BluloqBJ.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-DVjLYZbp.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-CVDNkzi9.js → timeline-definition-FHXFAJF6-Bj1WKNgB.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-CdkPxaDl.js → treeView-QDETBFTQ-Dj5T24Qr.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-lnksCHdn.js → treemap-6X3UGDF4-BPqewrXp.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-B533fRM0.js → vennDiagram-L72KCM5P-BhggGhQv.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-DwKf-2YJ.js → wardley-OPB4EBWU-Di9GWKYt.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-BDDN72B1.js → wardleyDiagram-EHGQE667-D-mtWLKf.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-CkvHmNVt.js → xychartDiagram-FW5EYKEG-CU0us6D_.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-B23bEjC4.js +0 -1
- package/web-dist/assets/channel-BMFwXMMq.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-DiWatUgG.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-DiWatUgG.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-C7ZRnE7-.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-DKS-kFcF.js +0 -1
- package/web-dist/assets/index-CeAC3wUW.css +0 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-C1-MJAeY.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-crQCD9rJ.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Ba-_QGKi.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DOPbAEV3.js +0 -1
- 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 =
|
|
145
|
-
return
|
|
331
|
+
function globalLaunchLockPath(home = homedir3()) {
|
|
332
|
+
return join3(canonicalHome(home), ".fray", GLOBAL_LOCK_NAME);
|
|
146
333
|
}
|
|
147
334
|
function namedLaunchLockPath(home, name) {
|
|
148
|
-
return
|
|
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 =
|
|
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
|
|
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 =
|
|
394
|
+
let modified = statSync2(lockPath).mtimeMs;
|
|
208
395
|
if (ownerPath !== lockPath) {
|
|
209
396
|
try {
|
|
210
|
-
modified = Math.max(modified,
|
|
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
|
-
|
|
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
|
-
|
|
421
|
+
mkdirSync2(dirname(lockPath), { recursive: true, mode: 448 });
|
|
235
422
|
let fd;
|
|
236
423
|
try {
|
|
237
|
-
fd =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
495
|
-
openSync as
|
|
693
|
+
mkdirSync as mkdirSync3,
|
|
694
|
+
openSync as openSync3,
|
|
496
695
|
readFileSync as readFileSync3,
|
|
497
696
|
renameSync as renameSync2,
|
|
498
|
-
rmSync as
|
|
499
|
-
statSync as
|
|
500
|
-
writeFileSync as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
526
|
-
const temp =
|
|
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 =
|
|
530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() -
|
|
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 =
|
|
586
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
709
|
-
|
|
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
|
|
964
|
-
openSync as
|
|
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
|
|
969
|
-
statSync as
|
|
970
|
-
writeFileSync as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
1058
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
1087
|
-
|
|
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
|
-
|
|
1290
|
+
mkdirSync4(path, { mode: 448 });
|
|
1092
1291
|
const generation = currentProcessGeneration();
|
|
1093
1292
|
const owner2 = { version: 1, token: randomUUID4(), ...generation };
|
|
1094
|
-
atomicJson2(
|
|
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
|
-
|
|
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() -
|
|
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
|
-
|
|
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
|
|
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(`..${
|
|
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(
|
|
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
|
|
1354
|
-
import { basename as
|
|
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
|
|
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
|
-
//
|
|
7032
|
-
//
|
|
7033
|
-
//
|
|
7034
|
-
//
|
|
7035
|
-
|
|
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
|
|
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 (!
|
|
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 {
|
|
7219
|
-
import {
|
|
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
|
|
7222
|
-
|
|
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
|
|
7233
|
-
import { homedir as
|
|
7234
|
-
import { basename as
|
|
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 =
|
|
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 =
|
|
7323
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
8393
|
+
join7(stateDir, "dev-supervisor.lock"),
|
|
7407
8394
|
authoritative,
|
|
7408
8395
|
expected2,
|
|
7409
8396
|
adapter
|
|
7410
8397
|
) ?? parseStatusFile(
|
|
7411
|
-
|
|
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(
|
|
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
|
-
|
|
7471
|
-
|
|
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
|
-
|
|
7484
|
-
|
|
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
|
-
|
|
7487
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(`..${
|
|
8837
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute4(rel);
|
|
7828
8838
|
}
|
|
7829
8839
|
function ignoredDevPath(path) {
|
|
7830
|
-
const parts = resolve4(path).split(
|
|
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 =
|
|
8849
|
+
const name = basename5(absolute);
|
|
7840
8850
|
const relToWorkspace = relative2(resolve4(root), absolute);
|
|
7841
|
-
const parts = relToWorkspace.split(
|
|
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 =
|
|
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) =>
|
|
8000
|
-
this.errorLine = opts.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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9533
|
+
import { chmodSync, statSync as statSync6 } from "node:fs";
|
|
8523
9534
|
import { createRequire } from "node:module";
|
|
8524
|
-
import { dirname as dirname6, join as
|
|
8525
|
-
var MINIMUM_NODE = { major:
|
|
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) =>
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
8645
|
-
|
|
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(
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
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)
|
|
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({
|
|
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
|
-
|
|
9798
|
+
let release = await acquireGlobalLaunchLock();
|
|
9799
|
+
let portReservation;
|
|
8708
9800
|
try {
|
|
8709
|
-
const
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
const
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
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
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
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
|
}
|