@join-pantheon/cli 0.32.1 → 0.33.0-rc.2
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/accounts.js +1 -1
- package/dist/board.js +1417 -0
- package/dist/claude-config.js +189 -0
- package/dist/cli.js +351 -101
- package/dist/client.js +16 -0
- package/dist/command-state.js +2 -1
- package/dist/dbus.js +92 -13
- package/dist/doctor.js +36 -5
- package/dist/folder-guard.js +89 -0
- package/dist/git-safe.js +3 -3
- package/dist/host-env.js +47 -1
- package/dist/install.js +76 -10
- package/dist/installer.js +2 -1
- package/dist/ledger.js +2 -2
- package/dist/machine.js +174 -3
- package/dist/managed.js +27 -1
- package/dist/notice-linux.js +501 -0
- package/dist/notice-windows.js +323 -0
- package/dist/notifier.js +12 -3
- package/dist/quickstart.js +28 -11
- package/dist/relay.js +64 -9
- package/dist/service.js +455 -29
- package/dist/setup-guide.js +22 -5
- package/dist/store.js +18 -2
- package/dist/uninstall.js +1 -1
- package/dist/upgrade.js +249 -4
- package/dist/uri-handler.js +335 -0
- package/dist/win-host.js +993 -0
- package/dist/worker-policy.js +3 -0
- package/dist/worker.js +11 -11
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/windows/pantheon-host.cs +170 -0
- package/helper/windows/pantheon-notice.ps1 +127 -0
- package/helper/windows/pantheon-tray.ps1 +731 -0
- package/helper/windows/pantheon.ico +0 -0
- package/installer/install.ps1 +238 -19
- package/installer/install.sh +36 -1
- package/package.json +2 -2
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve, win32 } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { CONFIG_DIR } from "./config.js";
|
|
8
|
+
import { userEntry } from "./install.js";
|
|
9
|
+
import { childNodeFlags } from "./managed.js";
|
|
10
|
+
import { borrowedProfile } from "./service.js";
|
|
11
|
+
import { CLM_GUARD, GIVE_UP_CODE, hostChildRunning, hostFiles, userSid } from "./win-host.js";
|
|
12
|
+
import { aumidRegistered, buildUri, handlerCommand, handlerEntry, PANTHEON_AUMID, realRegIo, registerUriHandler, uriKey, } from "./uri-handler.js";
|
|
13
|
+
const HERE = fileURLToPath(new URL(".", import.meta.url));
|
|
14
|
+
export const POWERSHELL_AUMID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
15
|
+
export const windowsIconPath = () => resolve(HERE, "..", "helper", "windows", "pantheon.ico");
|
|
16
|
+
export const windowsTrayScriptPath = () => resolve(HERE, "..", "helper", "windows", "pantheon-tray.ps1");
|
|
17
|
+
export const TOAST_CAPS = { heading: 80, context: 120, body: 320, who: 80, button: 24 };
|
|
18
|
+
export const BALLOON_CAPS = { title: 63, text: 255 };
|
|
19
|
+
export function toastText(s, max) {
|
|
20
|
+
const clean = (s ?? "")
|
|
21
|
+
.replace(/[\t\n\r\v\f]/g, " ")
|
|
22
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\ufffe\uffff]/g, "")
|
|
23
|
+
.replace(/[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g, "")
|
|
24
|
+
.replace(/\s+/g, " ")
|
|
25
|
+
.trim();
|
|
26
|
+
const chars = [...clean];
|
|
27
|
+
return chars.length <= max ? clean : `${chars.slice(0, max - 1).join("").trimEnd()}…`;
|
|
28
|
+
}
|
|
29
|
+
export function xmlEscape(s) {
|
|
30
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
31
|
+
}
|
|
32
|
+
export function toastButtons(notice, key) {
|
|
33
|
+
const space = notice.space;
|
|
34
|
+
const d = notice.details ?? {};
|
|
35
|
+
const out = [];
|
|
36
|
+
const add = (content, uri) => { if (uri)
|
|
37
|
+
out.push({ content, uri }); };
|
|
38
|
+
if (notice.kind === "decision" && d.decision) {
|
|
39
|
+
add("Done", buildUri("done", { decision: d.decision, act: d.act, space }, key));
|
|
40
|
+
add("Explain", buildUri("explain", { decision: d.decision, act: d.act, space }, key));
|
|
41
|
+
}
|
|
42
|
+
else if (d.act) {
|
|
43
|
+
add("Show details", buildUri("details", { act: d.act, decision: d.decision, space }, key));
|
|
44
|
+
}
|
|
45
|
+
add("Open board", buildUri("board", {}, key));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function toastTag(notice) {
|
|
49
|
+
const id = notice.details?.decision ?? notice.details?.act ?? "";
|
|
50
|
+
return /^[A-Za-z0-9_]{1,60}$/.test(id) ? id : "";
|
|
51
|
+
}
|
|
52
|
+
export function toastFields(notice, key) {
|
|
53
|
+
const lines = [toastText(notice.heading, TOAST_CAPS.heading) || "Pantheon"];
|
|
54
|
+
const context = toastText(notice.context, TOAST_CAPS.context);
|
|
55
|
+
const body = toastText(notice.body, TOAST_CAPS.body);
|
|
56
|
+
if (context)
|
|
57
|
+
lines.push(context);
|
|
58
|
+
if (body)
|
|
59
|
+
lines.push(body);
|
|
60
|
+
return {
|
|
61
|
+
lines,
|
|
62
|
+
who: toastText(notice.who, TOAST_CAPS.who),
|
|
63
|
+
launch: buildUri("board", {}, key),
|
|
64
|
+
buttons: toastButtons(notice, key).map((b) => ({ content: toastText(b.content, TOAST_CAPS.button), uri: b.uri })),
|
|
65
|
+
scenario: notice.ttl === 0 ? "reminder" : "",
|
|
66
|
+
duration: notice.ttl !== 0 && notice.ttl > 7 ? "long" : "",
|
|
67
|
+
silent: !notice.sound,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function toastXmlFrom(t) {
|
|
71
|
+
const attrs = [
|
|
72
|
+
`activationType="protocol"`, `launch="${xmlEscape(t.launch)}"`,
|
|
73
|
+
t.scenario ? `scenario="${t.scenario}"` : t.duration ? `duration="${t.duration}"` : "",
|
|
74
|
+
].filter(Boolean).join(" ");
|
|
75
|
+
return [
|
|
76
|
+
`<toast ${attrs}>`,
|
|
77
|
+
`<visual><binding template="ToastGeneric">`,
|
|
78
|
+
...t.lines.map((l) => `<text>${xmlEscape(l)}</text>`),
|
|
79
|
+
...(t.who ? [`<text placement="attribution">${xmlEscape(t.who)}</text>`] : []),
|
|
80
|
+
`</binding></visual>`,
|
|
81
|
+
...(t.buttons.length ? [
|
|
82
|
+
`<actions>`,
|
|
83
|
+
...t.buttons.map((b) => `<action content="${xmlEscape(b.content)}" activationType="protocol" arguments="${xmlEscape(b.uri)}"/>`),
|
|
84
|
+
`</actions>`,
|
|
85
|
+
] : []),
|
|
86
|
+
t.silent ? `<audio silent="true"/>` : "",
|
|
87
|
+
`</toast>`,
|
|
88
|
+
].filter(Boolean).join("");
|
|
89
|
+
}
|
|
90
|
+
export function toastXml(notice, key) {
|
|
91
|
+
return toastXmlFrom(toastFields(notice, key));
|
|
92
|
+
}
|
|
93
|
+
export const windowsNoticeScriptPath = () => resolve(HERE, "..", "helper", "windows", "pantheon-notice.ps1");
|
|
94
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
95
|
+
const psQuote = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
96
|
+
export function powershellPath(env = process.env) {
|
|
97
|
+
return win32.join(env.SystemRoot || env.windir || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
98
|
+
}
|
|
99
|
+
export function powershellArgs(script, mode) {
|
|
100
|
+
return ["-NoProfile", "-NonInteractive", "-Command", `${CLM_GUARD}; & ([scriptblock]::Create([IO.File]::ReadAllText(${psQuote(script)}))) -Mode ${mode}`];
|
|
101
|
+
}
|
|
102
|
+
export function balloonText(notice) {
|
|
103
|
+
const title = toastText(notice.heading, BALLOON_CAPS.title) || "Pantheon";
|
|
104
|
+
const text = toastText([notice.context, notice.body].filter(Boolean).join(" — "), BALLOON_CAPS.text) || toastText(notice.who, BALLOON_CAPS.text) || "Open Pantheon";
|
|
105
|
+
return { title, text };
|
|
106
|
+
}
|
|
107
|
+
const aboutOf = (notice) => `${notice.kind}${notice.details?.decision ? ` ${notice.details.decision}` : notice.details?.act ? ` ${notice.details.act}` : ""}`;
|
|
108
|
+
export function queuedNotice(notice, key, aumids, now = Date.now()) {
|
|
109
|
+
return { v: 2, at: now, about: aboutOf(notice), toast: toastFields(notice, key), tag: toastTag(notice), aumids, balloon: balloonText(notice) };
|
|
110
|
+
}
|
|
111
|
+
export function toastCall(notice, key, aumids, env = process.env, script = windowsNoticeScriptPath()) {
|
|
112
|
+
return {
|
|
113
|
+
file: powershellPath(env),
|
|
114
|
+
args: powershellArgs(script, "toast"),
|
|
115
|
+
env: { PANTHEON_TOAST_XML: b64(toastXml(notice, key)), PANTHEON_TOAST_TAG: toastTag(notice), PANTHEON_TOAST_AUMIDS: aumids.join(";") },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function balloonCall(notice, icon, env = process.env, script = windowsNoticeScriptPath()) {
|
|
119
|
+
const { title, text } = balloonText(notice);
|
|
120
|
+
return {
|
|
121
|
+
file: powershellPath(env),
|
|
122
|
+
args: powershellArgs(script, "balloon"),
|
|
123
|
+
env: { PANTHEON_BALLOON_TITLE: b64(title), PANTHEON_BALLOON_TEXT: b64(text), ...(icon ? { PANTHEON_BALLOON_ICON: icon } : {}) },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export const QUEUE_STALE_MS = 30_000;
|
|
127
|
+
export function trayTakesNotices(home, now = Date.now()) {
|
|
128
|
+
if (!hostChildRunning("tray", home, now))
|
|
129
|
+
return false;
|
|
130
|
+
const dir = hostFiles(home).notices;
|
|
131
|
+
const skipped = skippedNotices(home);
|
|
132
|
+
try {
|
|
133
|
+
for (const n of readdirSync(dir)) {
|
|
134
|
+
if (!n.endsWith(".json"))
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
const mtime = statSync(join(dir, n)).mtimeMs;
|
|
138
|
+
if (now - mtime <= QUEUE_STALE_MS)
|
|
139
|
+
continue;
|
|
140
|
+
const s = skipped.get(n);
|
|
141
|
+
if (s !== undefined && Math.abs(s - Math.floor(mtime)) <= 1)
|
|
142
|
+
continue;
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
export function skippedNotices(home) {
|
|
152
|
+
const out = new Map();
|
|
153
|
+
let text = "";
|
|
154
|
+
try {
|
|
155
|
+
text = readFileSync(hostFiles(home).noticesSkipped, "utf8");
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
for (const line of text.split(/\r?\n/)) {
|
|
161
|
+
const m = /^([0-9A-Za-z._-]+\.json)\t(\d{1,16})$/.exec(line.trim());
|
|
162
|
+
if (m)
|
|
163
|
+
out.set(m[1], Number(m[2]));
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
export function ownerOnlyAclArgs(dir, sid) {
|
|
168
|
+
return [dir, "/inheritance:r", "/grant:r", `*${sid}:(OI)(CI)F`];
|
|
169
|
+
}
|
|
170
|
+
function realQueueDirDeps() {
|
|
171
|
+
const exec = (c, a) => execFileSync(c, a, { encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"], timeout: 15_000 });
|
|
172
|
+
return {
|
|
173
|
+
platform: process.platform,
|
|
174
|
+
sid: () => userSid(exec),
|
|
175
|
+
icacls: (args) => { exec(win32.join(process.env.SystemRoot || process.env.windir || "C:\\Windows", "System32", "icacls.exe"), args); },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function ensureQueueDir(dir, deps = realQueueDirDeps()) {
|
|
179
|
+
if (existsSync(dir))
|
|
180
|
+
return;
|
|
181
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
182
|
+
if (deps.platform !== "win32")
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
const sid = deps.sid();
|
|
186
|
+
if (!sid)
|
|
187
|
+
throw new Error("could not tell which user this is (whoami), so the alert queue cannot be made private");
|
|
188
|
+
deps.icacls(ownerOnlyAclArgs(dir, sid));
|
|
189
|
+
for (const n of readdirSync(dir))
|
|
190
|
+
rmSync(join(dir, n), { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
rmSync(dir, { recursive: true, force: true });
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function enqueueNotice(home, q, deps) {
|
|
198
|
+
const dir = hostFiles(home).notices;
|
|
199
|
+
ensureQueueDir(dir, deps);
|
|
200
|
+
const name = `${q.at}-${randomBytes(4).toString("hex")}`;
|
|
201
|
+
const tmp = join(dir, `${name}.tmp`);
|
|
202
|
+
const f = join(dir, `${name}.json`);
|
|
203
|
+
writeFileSync(tmp, JSON.stringify(q), { mode: 0o600, flag: "wx" });
|
|
204
|
+
try {
|
|
205
|
+
renameSync(tmp, f);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
rmSync(tmp, { force: true });
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
return f;
|
|
212
|
+
}
|
|
213
|
+
export const NOTICE_COOLDOWN_MS = 5 * 60_000;
|
|
214
|
+
export const CLM_COOLDOWN_MS = 6 * 3600_000;
|
|
215
|
+
let coolUntil = 0;
|
|
216
|
+
let handler = null;
|
|
217
|
+
export function resetWindowsNoticeState() { coolUntil = 0; handler = null; }
|
|
218
|
+
export function ensureUriHandler(io = realRegIo(() => borrowedProfile()), entry = userEntry("win32"), o = {}) {
|
|
219
|
+
const command = handlerCommand(handlerEntry(entry, o.flags ?? childNodeFlags()), o.home === undefined ? nonDefaultPantheonHome() : o.home);
|
|
220
|
+
if (!command)
|
|
221
|
+
return { ok: false, aumid: false, line: "pantheon: links not registered — no node.exe and cli.js to name (a pantheon.cmd is never registered)" };
|
|
222
|
+
const icon = windowsIconPath();
|
|
223
|
+
const r = registerUriHandler(command, io, existsSync(icon) ? icon : undefined);
|
|
224
|
+
return { ok: r.ok, aumid: r.ok && aumidRegistered(io), line: r.line };
|
|
225
|
+
}
|
|
226
|
+
function nonDefaultPantheonHome() {
|
|
227
|
+
return resolve(CONFIG_DIR).toLowerCase() === resolve(join(homedir(), ".pantheon")).toLowerCase() ? null : resolve(CONFIG_DIR);
|
|
228
|
+
}
|
|
229
|
+
function logTo(home) {
|
|
230
|
+
return (line) => {
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync(join(home, "logs"), { recursive: true });
|
|
233
|
+
appendFileSync(join(home, "logs", "alerts.log"), `${new Date().toISOString()} ${line}\n`, { mode: 0o600 });
|
|
234
|
+
}
|
|
235
|
+
catch { }
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function realDeps(home) {
|
|
239
|
+
const icon = windowsIconPath();
|
|
240
|
+
const childEnv = (call) => ({ ...process.env, PANTHEON_HOME: home, ...call.env });
|
|
241
|
+
return {
|
|
242
|
+
trayReady: () => trayTakesNotices(home),
|
|
243
|
+
enqueue: (q) => { enqueueNotice(home, q); },
|
|
244
|
+
run: (call, timeoutMs) => {
|
|
245
|
+
const r = spawnSync(call.file, call.args, { env: childEnv(call), windowsHide: true, timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
246
|
+
const said = (r.stdout ?? "").trim().split(/\r?\n/).filter(Boolean).join(" | ");
|
|
247
|
+
if (said)
|
|
248
|
+
logTo(home)(`toast: ${said.slice(0, 300)}`);
|
|
249
|
+
return r.error ? null : r.status;
|
|
250
|
+
},
|
|
251
|
+
detach: (call) => {
|
|
252
|
+
try {
|
|
253
|
+
const c = spawn(call.file, call.args, { env: childEnv(call), windowsHide: true, detached: true, stdio: "ignore" });
|
|
254
|
+
c.on("error", () => { });
|
|
255
|
+
c.unref();
|
|
256
|
+
return typeof c.pid === "number";
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
ensureHandler: () => {
|
|
263
|
+
const r = ensureUriHandler();
|
|
264
|
+
logTo(home)(`links: ${r.line}`);
|
|
265
|
+
return { aumid: r.aumid };
|
|
266
|
+
},
|
|
267
|
+
markClm: () => { try {
|
|
268
|
+
const f = hostFiles(home);
|
|
269
|
+
mkdirSync(f.dir, { recursive: true });
|
|
270
|
+
writeFileSync(f.clm, new Date().toISOString());
|
|
271
|
+
}
|
|
272
|
+
catch { } },
|
|
273
|
+
log: logTo(home),
|
|
274
|
+
icon: existsSync(icon) ? icon : null,
|
|
275
|
+
now: Date.now,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export function showWindowsNotice(notice, home, deps = realDeps(home)) {
|
|
279
|
+
if (deps.now() < coolUntil)
|
|
280
|
+
return false;
|
|
281
|
+
if (!handler) {
|
|
282
|
+
try {
|
|
283
|
+
handler = deps.ensureHandler();
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
handler = { aumid: false };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const key = uriKey(home);
|
|
290
|
+
const aumids = handler.aumid ? [PANTHEON_AUMID, POWERSHELL_AUMID] : [POWERSHELL_AUMID];
|
|
291
|
+
const about = aboutOf(notice);
|
|
292
|
+
if (deps.trayReady()) {
|
|
293
|
+
try {
|
|
294
|
+
deps.enqueue(queuedNotice(notice, key, aumids, deps.now()));
|
|
295
|
+
deps.log(`handed to the tray: ${about}`);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
deps.log(`could not hand ${about} to the tray (${e.message.split("\n")[0]}) — showing it directly`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const code = deps.run(toastCall(notice, key, aumids), 15_000);
|
|
303
|
+
if (code === 0) {
|
|
304
|
+
deps.log(`shown: ${about}`);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (code === GIVE_UP_CODE) {
|
|
308
|
+
deps.markClm();
|
|
309
|
+
coolUntil = deps.now() + CLM_COOLDOWN_MS;
|
|
310
|
+
deps.log(`not shown: ${about} — PowerShell runs in Constrained Language Mode here (an IT policy), which refuses Windows alerts; pantheon board shows the same. Next try in ${CLM_COOLDOWN_MS / 3600_000} hours`);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (code === 3 || code === 4) {
|
|
314
|
+
if (deps.detach(balloonCall(notice, deps.icon))) {
|
|
315
|
+
deps.log(`handed to a balloon: ${about} (toast exit ${code})`);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
coolUntil = deps.now() + NOTICE_COOLDOWN_MS;
|
|
320
|
+
deps.log(`not shown: ${about} — toast exit ${code ?? "timeout or no PowerShell"}; next try in ${NOTICE_COOLDOWN_MS / 60_000} minutes`);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=notice-windows.js.map
|
package/dist/notifier.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { showWindowsNotice } from "./notice-windows.js";
|
|
2
|
+
import { showLinuxNotice } from "./notice-linux.js";
|
|
1
3
|
import { spawn } from "node:child_process";
|
|
2
4
|
import { helperStale, installHelper } from "./helpers-build.js";
|
|
3
5
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -107,6 +109,10 @@ export function preferredTool(profile, tools, home = CONFIG_DIR) {
|
|
|
107
109
|
return profile.agent.primary_surface;
|
|
108
110
|
}
|
|
109
111
|
export function sendDesktopNotification(notice, home = CONFIG_DIR) {
|
|
112
|
+
if (platform() === "win32")
|
|
113
|
+
return showWindowsNotice(notice, home);
|
|
114
|
+
if (platform() === "linux")
|
|
115
|
+
return showLinuxNotice(notice, home);
|
|
110
116
|
if (platform() !== "darwin")
|
|
111
117
|
return false;
|
|
112
118
|
const binary = hudBinaryPath(home);
|
|
@@ -154,12 +160,15 @@ export class HumanNotifier {
|
|
|
154
160
|
: [];
|
|
155
161
|
const tools = installedTools();
|
|
156
162
|
const peerOf = (from) => from ? { member: from, agent: client.roster.find((m) => m.id === from)?.agent } : undefined;
|
|
157
|
-
const withLabel = (n) =>
|
|
163
|
+
const withLabel = (n) => {
|
|
164
|
+
const labelled = this.opts.label ? { ...n, context: n.context ? `${this.opts.label} · ${n.context}` : this.opts.label } : n;
|
|
165
|
+
return platform() === "darwin" ? labelled : { ...labelled, space: this.cfg.space };
|
|
166
|
+
};
|
|
158
167
|
for (const obligation of due) {
|
|
159
168
|
const isReminder = state.notified_at[obligation.id] !== undefined;
|
|
160
169
|
const act = client.seenAct(obligation.ref);
|
|
161
170
|
const notice = withLabel(composeDecision(obligation, act, obligation.goal ? client.goals.get(obligation.goal) : undefined, peerOf(act?.from), { tools, preferred: preferredTool(profile, tools), now: now(), member: this.cfg.member }));
|
|
162
|
-
const ok = send(notice);
|
|
171
|
+
const ok = await send(notice);
|
|
163
172
|
if (ok) {
|
|
164
173
|
state.notified_at[obligation.id] = now();
|
|
165
174
|
if (isReminder)
|
|
@@ -182,7 +191,7 @@ export class HumanNotifier {
|
|
|
182
191
|
const forMe = (r.to?.includes(this.cfg.member) ?? false) || (goal?.members.includes(this.cfg.member) ?? false);
|
|
183
192
|
if (!forMe)
|
|
184
193
|
continue;
|
|
185
|
-
const ok = send(withLabel(composeDelivery(r, goal, peerOf(r.from), { tools, preferred: preferredTool(profile, tools), now: now() })));
|
|
194
|
+
const ok = await send(withLabel(composeDelivery(r, goal, peerOf(r.from), { tools, preferred: preferredTool(profile, tools), now: now() })));
|
|
186
195
|
if (ok) {
|
|
187
196
|
state.notified_at[`act:${r.id}`] = now();
|
|
188
197
|
writeState(stateFile, state);
|
package/dist/quickstart.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
|
-
import { basename,
|
|
4
|
+
import { basename, join as pathJoin, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
6
6
|
import { lookup } from "node:dns/promises";
|
|
7
7
|
import { describeNetError } from "./network.js";
|
|
8
8
|
import { PantheonClient } from "./client.js";
|
|
9
9
|
import { holdsRepo } from "./repo-find.js";
|
|
10
|
+
import { folderFacts, folderRefusal, folderRefusalHere, isInside, isTemporaryRefusal } from "./folder-guard.js";
|
|
10
11
|
import { load, loadAll, identity, save, toInvite, resolveFor, repoInfo, bindRepo, bindFolder, folderBinding, DEFAULT_RELAY } from "./config.js";
|
|
11
12
|
import { commandsChosen, loadProfile, wakeChosen, wakeDefault } from "./profile.js";
|
|
12
13
|
import { newKeypair } from "./identity.js";
|
|
@@ -14,6 +15,7 @@ import { installForUser } from "./install.js";
|
|
|
14
15
|
import { writeIntent, installHooks } from "./notify.js";
|
|
15
16
|
import { detectAvailable, agentById, agentsNeedingUnsandboxed } from "./agents.js";
|
|
16
17
|
import { installService, servicesUnavailable } from "./service.js";
|
|
18
|
+
import { clearTrayQuit } from "./win-host.js";
|
|
17
19
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
18
20
|
import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
|
|
19
21
|
import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
|
|
@@ -39,7 +41,7 @@ export function wireUp(opts, deps = {}) {
|
|
|
39
41
|
const ensureTray = deps.ensureTray ?? ensureTrayBinary;
|
|
40
42
|
const host = deps.host ?? hostEnv();
|
|
41
43
|
const noServices = servicesUnavailable(host.platform);
|
|
42
|
-
const tools = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
|
|
44
|
+
const tools = installForUser(undefined, undefined, { direct: true }).map((t) => ({ tool: t.tool, status: t.status }));
|
|
43
45
|
const granted = [];
|
|
44
46
|
const warnings = [];
|
|
45
47
|
if (host.ephemeral)
|
|
@@ -66,9 +68,12 @@ export function wireUp(opts, deps = {}) {
|
|
|
66
68
|
}
|
|
67
69
|
else if (opts.notifications) {
|
|
68
70
|
try {
|
|
69
|
-
|
|
71
|
+
const mac = host.platform === "darwin";
|
|
72
|
+
if (mac)
|
|
73
|
+
ensureNotifier();
|
|
70
74
|
const why = notLoaded(installSvc("notifier", homedir(), []));
|
|
71
|
-
|
|
75
|
+
if (mac)
|
|
76
|
+
writeTrayCommands();
|
|
72
77
|
if (why) {
|
|
73
78
|
tools.push({ tool: "notifications", status: `installed, not running: ${why}` });
|
|
74
79
|
warnings.push(`human notifications are installed but not running (${why}). Try \`pantheon service restart notifier\``);
|
|
@@ -84,8 +89,13 @@ export function wireUp(opts, deps = {}) {
|
|
|
84
89
|
else {
|
|
85
90
|
tools.push({ tool: "notifications", status: "off (--no-notifications) — human escalations only appear in pantheon status" });
|
|
86
91
|
}
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
const noMenubar = desktopUnavailable(host, "the menu bar");
|
|
93
|
+
if (opts.menubar && noMenubar) {
|
|
94
|
+
tools.push({ tool: "menu bar", status: noMenubar });
|
|
95
|
+
}
|
|
96
|
+
else if (opts.menubar && host.platform === "win32") {
|
|
97
|
+
const wasQuit = clearTrayQuit();
|
|
98
|
+
tools.push({ tool: "tray", status: `comes with the background host — the Pantheon icon in the notification area${wasQuit ? " (you had quit it; it shows again)" : ""}` });
|
|
89
99
|
}
|
|
90
100
|
else if (opts.menubar) {
|
|
91
101
|
try {
|
|
@@ -491,6 +501,8 @@ export async function start(opts) {
|
|
|
491
501
|
bindFolder(process.cwd(), c.spaceId);
|
|
492
502
|
folderBound = process.cwd();
|
|
493
503
|
}
|
|
504
|
+
else if (!repo && c.spaceId)
|
|
505
|
+
temporaryFolderNote(process.cwd(), opts.log, c.space);
|
|
494
506
|
if (repo && c.spaceId) {
|
|
495
507
|
const { replaced } = bindRepo(repo, c.spaceId);
|
|
496
508
|
const was = replaced ?? (here.binding && here.binding.space !== c.spaceId ? here.binding : null);
|
|
@@ -578,6 +590,8 @@ export async function join(opts) {
|
|
|
578
590
|
bindFolder(process.cwd(), cfg.spaceId);
|
|
579
591
|
opts.log(` folder ${process.cwd()} → bound to "${cfg.space}" (agents opened here speak in it; no git needed)`);
|
|
580
592
|
}
|
|
593
|
+
else if (!repo && cfg.spaceId)
|
|
594
|
+
temporaryFolderNote(process.cwd(), opts.log, cfg.space);
|
|
581
595
|
if (repo && cfg.spaceId) {
|
|
582
596
|
const { replaced } = bindRepo(repo, cfg.spaceId);
|
|
583
597
|
if (replaced)
|
|
@@ -605,10 +619,8 @@ export function bindableFolder(dir) {
|
|
|
605
619
|
catch {
|
|
606
620
|
return resolvePath(d);
|
|
607
621
|
} };
|
|
608
|
-
const here = real(dir)
|
|
609
|
-
if (
|
|
610
|
-
return false;
|
|
611
|
-
if (["Downloads", "Desktop", "Documents", "Library", "tmp"].some((n) => here === pathJoin(home, n)))
|
|
622
|
+
const here = real(dir);
|
|
623
|
+
if (folderRefusal(here, folderFacts()))
|
|
612
624
|
return false;
|
|
613
625
|
try {
|
|
614
626
|
if (holdsRepo(here))
|
|
@@ -619,12 +631,17 @@ export function bindableFolder(dir) {
|
|
|
619
631
|
}
|
|
620
632
|
try {
|
|
621
633
|
const f = loadAll();
|
|
622
|
-
if (Object.values(f?.bindings ?? {}).some((b) => { const p = real(b.path); return p
|
|
634
|
+
if (Object.values(f?.bindings ?? {}).some((b) => { const p = real(b.path); return isInside(p, here) && !isInside(here, p); }))
|
|
623
635
|
return false;
|
|
624
636
|
}
|
|
625
637
|
catch { }
|
|
626
638
|
return true;
|
|
627
639
|
}
|
|
640
|
+
function temporaryFolderNote(dir, log, team) {
|
|
641
|
+
const r = folderRefusalHere(dir);
|
|
642
|
+
if (isTemporaryRefusal(r))
|
|
643
|
+
log(` folder not bound — ${r.why.replace("<team>", team)}`);
|
|
644
|
+
}
|
|
628
645
|
function detectAgent() {
|
|
629
646
|
const found = detectAvailable();
|
|
630
647
|
return found[0]?.backend.id ?? "unknown";
|
package/dist/relay.js
CHANGED
|
@@ -6,14 +6,14 @@ import { join } from "node:path";
|
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import { badRemote, Inbound, isContinuationFor, LIMITS, newId, PROTOCOL_VERSION, RELAY_FEATURES, SENDABLE, shouldDeferToHuman, } from "./protocol.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
|
-
import { installerScript, InstallerGate } from "./installer.js";
|
|
9
|
+
import { compareVersions, INSTALLER_MIN_CLI, isVersion, installerScript, InstallerGate } from "./installer.js";
|
|
10
10
|
import { installerLine } from "./managed.js";
|
|
11
11
|
import { Ledger, SpaceFull } from "./ledger.js";
|
|
12
12
|
import { fingerprint, newNonce, verifyChallenge, verifyActivity, verifyAccount, verifyRead, actPayload, goalPayload, verifyBytes, verifyDelegate, canonicalJson } from "./identity.js";
|
|
13
13
|
import { Accounts, normalizeUsername, validEmail, validUsername } from "./accounts.js";
|
|
14
14
|
import { emailSenderFromEnv } from "./email.js";
|
|
15
15
|
import { Allowlist, RosterUnreadable } from "./roster-file.js";
|
|
16
|
-
import { StateStore, ensurePrivateDir } from "./store.js";
|
|
16
|
+
import { StateStore, ensurePrivateDir, setWaitForDisk, takeSlowestDiskWait, waitsForDisk } from "./store.js";
|
|
17
17
|
import { activity, mergeActivity, feed, noteRefusal, refusalsSince, countActs, spaceHealth, fleetReport, ESCALATIONS, MESSAGE_ACTS, ACT_TONE, DAY } from "./admin-stats.js";
|
|
18
18
|
import { fmtLimit, inviteLimit, parseLimits, planLimits, PLAN_NAME } from "./plans.js";
|
|
19
19
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
@@ -366,8 +366,35 @@ function withoutFromRepo(meta) {
|
|
|
366
366
|
const { from_repo: _f, ...rest } = meta;
|
|
367
367
|
return rest;
|
|
368
368
|
}
|
|
369
|
+
export function upgradeHoldFrom(raw, relayVersion, log = () => { }) {
|
|
370
|
+
const v = (raw ?? "").trim();
|
|
371
|
+
if (!v)
|
|
372
|
+
return null;
|
|
373
|
+
if (!isVersion(v)) {
|
|
374
|
+
log(`! PANTHEON_HOLD_UPGRADES_AT is "${v}", which is not a version — ignored, upgrades are not held`);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (v.includes("-")) {
|
|
378
|
+
log(`! PANTHEON_HOLD_UPGRADES_AT is ${v}, a prerelease — ignored; hold at a release (x.y.z)`);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if (compareVersions(v, relayVersion) > 0) {
|
|
382
|
+
log(`! PANTHEON_HOLD_UPGRADES_AT is ${v}, newer than this relay (${relayVersion}) — ignored`);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
if (compareVersions(v, INSTALLER_MIN_CLI) < 0) {
|
|
386
|
+
log(`! PANTHEON_HOLD_UPGRADES_AT is ${v}, older than ${INSTALLER_MIN_CLI}, the oldest release the installer serves — ignored`);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
log(`* upgrades held at ${v}: workers will not move past it, and new installs get it (this relay runs ${relayVersion})`);
|
|
390
|
+
return v;
|
|
391
|
+
}
|
|
392
|
+
export function advertisedPkg(hold, relayVersion) {
|
|
393
|
+
return hold ?? (relayVersion.includes("-") ? null : relayVersion);
|
|
394
|
+
}
|
|
369
395
|
export class Relay {
|
|
370
396
|
opts;
|
|
397
|
+
advertisedPkg() { return advertisedPkg(this.upgradeHold, VERSION); }
|
|
371
398
|
spaces = new Map();
|
|
372
399
|
dataDir;
|
|
373
400
|
log;
|
|
@@ -410,6 +437,9 @@ export class Relay {
|
|
|
410
437
|
});
|
|
411
438
|
wss = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 });
|
|
412
439
|
heartbeat;
|
|
440
|
+
lagTimer;
|
|
441
|
+
lagMax = 0;
|
|
442
|
+
fsyncOff = false;
|
|
413
443
|
compactedAt = new Map();
|
|
414
444
|
sessionSeq = 0;
|
|
415
445
|
spaceCreates = new Map();
|
|
@@ -437,11 +467,17 @@ export class Relay {
|
|
|
437
467
|
rotateOnRemove;
|
|
438
468
|
backupTimer;
|
|
439
469
|
installerGate;
|
|
470
|
+
upgradeHold;
|
|
440
471
|
get features() { return this.opts.features ?? RELAY_FEATURES; }
|
|
441
472
|
constructor(opts = {}) {
|
|
442
473
|
this.opts = opts;
|
|
443
474
|
this.dataDir = opts.dataDir ?? "./pantheon-data";
|
|
444
475
|
this.log = opts.log ?? ((l) => console.error(l));
|
|
476
|
+
if ((opts.fsync ?? process.env.PANTHEON_RELAY_FSYNC) === "off" && waitsForDisk()) {
|
|
477
|
+
setWaitForDisk(false);
|
|
478
|
+
this.fsyncOff = true;
|
|
479
|
+
this.log("* PANTHEON_RELAY_FSYNC=off: writes go to the operating system at once but the relay does not wait for the disk to confirm them (a relay crash loses nothing; a host power loss can lose the last seconds)");
|
|
480
|
+
}
|
|
445
481
|
ensurePrivateDir(this.dataDir);
|
|
446
482
|
this.maxLedgerBytes = opts.spaceMaxBytes
|
|
447
483
|
?? (Number(process.env.PANTHEON_SPACE_MAX_BYTES) > 0 ? Number(process.env.PANTHEON_SPACE_MAX_BYTES) : SPACE_MAX_BYTES_DEFAULT);
|
|
@@ -474,7 +510,8 @@ export class Relay {
|
|
|
474
510
|
spaceMinAgeMs: opts.spaceLimits?.spaceMinAgeMs ?? LIMITS.spaceMinAgeMs,
|
|
475
511
|
};
|
|
476
512
|
this.rotateOnRemove = opts.rotateOnRemove ?? process.env.PANTHEON_ROTATE_ON_REMOVE === "1";
|
|
477
|
-
this.
|
|
513
|
+
this.upgradeHold = upgradeHoldFrom(opts.upgradeHoldAt ?? process.env.PANTHEON_HOLD_UPGRADES_AT, VERSION, this.log);
|
|
514
|
+
this.installerGate = opts.installerGate ?? new InstallerGate({ log: this.log, ...(this.upgradeHold ? { version: this.upgradeHold } : {}) });
|
|
478
515
|
this.busyTtlMs = opts.activityBusyTtlMs ?? LIMITS.activityBusyTtlMs;
|
|
479
516
|
this.pruneMs = opts.activityPruneMs ?? LIMITS.activityPruneMs;
|
|
480
517
|
const readsEnv = (process.env.PANTHEON_READS ?? "").trim().toLowerCase();
|
|
@@ -495,7 +532,10 @@ export class Relay {
|
|
|
495
532
|
const host = this.opts.host ?? "0.0.0.0";
|
|
496
533
|
return new Promise((resolve) => {
|
|
497
534
|
this.http.listen(want, host, () => {
|
|
498
|
-
this.heartbeat = setInterval(() => this.sweep(), 15_000);
|
|
535
|
+
this.heartbeat = setInterval(() => { this.sweep(); this.noteStalls(); }, 15_000);
|
|
536
|
+
let last = Date.now();
|
|
537
|
+
this.lagTimer = setInterval(() => { const now = Date.now(); this.lagMax = Math.max(this.lagMax, now - last - 500); last = now; }, 500);
|
|
538
|
+
this.lagTimer.unref?.();
|
|
499
539
|
this.startBackups();
|
|
500
540
|
const addr = this.http.address();
|
|
501
541
|
const port = typeof addr === "object" && addr ? addr.port : want;
|
|
@@ -519,6 +559,12 @@ export class Relay {
|
|
|
519
559
|
this.accounts.close();
|
|
520
560
|
if (this.heartbeat)
|
|
521
561
|
clearInterval(this.heartbeat);
|
|
562
|
+
if (this.lagTimer)
|
|
563
|
+
clearInterval(this.lagTimer);
|
|
564
|
+
if (this.fsyncOff) {
|
|
565
|
+
setWaitForDisk(true);
|
|
566
|
+
this.fsyncOff = false;
|
|
567
|
+
}
|
|
522
568
|
for (const ws of this.wss.clients)
|
|
523
569
|
ws.terminate();
|
|
524
570
|
await new Promise((r) => this.wss.close(() => r()));
|
|
@@ -591,7 +637,9 @@ export class Relay {
|
|
|
591
637
|
ok: !sf,
|
|
592
638
|
...(sf ? { storage: { error: sf.message, where: sf.where, at: new Date(sf.at).toISOString() } } : {}),
|
|
593
639
|
v: PROTOCOL_VERSION,
|
|
594
|
-
pkg:
|
|
640
|
+
...(this.advertisedPkg() ? { pkg: this.advertisedPkg() } : {}),
|
|
641
|
+
...(this.advertisedPkg() !== VERSION ? { relay_pkg: VERSION } : {}),
|
|
642
|
+
...(this.upgradeHold ? { upgrades_held_at: this.upgradeHold } : {}),
|
|
595
643
|
...(commit ? { commit } : {}),
|
|
596
644
|
...(adminView && this.unbound.size ? { unbound_spaces: [...this.unbound].sort() } : {}),
|
|
597
645
|
space_count: this.spaces.size,
|
|
@@ -781,7 +829,7 @@ export class Relay {
|
|
|
781
829
|
if (url.searchParams.get("format") === "json")
|
|
782
830
|
return this.json(res, 200, { relay: VERSION, relay_age_ms: relayAgeMs, generated: Date.now(), spaces: rows });
|
|
783
831
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
784
|
-
return void res.end(fleetReport(rows, VERSION, Date.now(), relayAgeMs) + "\n");
|
|
832
|
+
return void res.end(fleetReport(rows, this.upgradeHold ?? VERSION, Date.now(), relayAgeMs) + "\n");
|
|
785
833
|
}
|
|
786
834
|
if (url.pathname === "/admin/export") {
|
|
787
835
|
if (!authed)
|
|
@@ -1288,7 +1336,7 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
1288
1336
|
out.push(spaceHealth({
|
|
1289
1337
|
name: sp.name, records: sp.ledger.all(), members: sp.allow.all(),
|
|
1290
1338
|
online: sessions.length, interactive: sessions.filter((x) => x.role === "interactive").length,
|
|
1291
|
-
relayPkg: VERSION, now, relayAgeMs: now - this.startedAt,
|
|
1339
|
+
relayPkg: this.upgradeHold ?? VERSION, now, relayAgeMs: now - this.startedAt,
|
|
1292
1340
|
}));
|
|
1293
1341
|
}
|
|
1294
1342
|
let files = [];
|
|
@@ -1307,7 +1355,7 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
1307
1355
|
members = new Allowlist(this.dataDir, safe).all();
|
|
1308
1356
|
}
|
|
1309
1357
|
catch { }
|
|
1310
|
-
out.push(spaceHealth({ name: safe, records: this.sleepingRecords(safe), members, online: 0, interactive: 0, relayPkg: VERSION, now, relayAgeMs: now - this.startedAt }));
|
|
1358
|
+
out.push(spaceHealth({ name: safe, records: this.sleepingRecords(safe), members, online: 0, interactive: 0, relayPkg: this.upgradeHold ?? VERSION, now, relayAgeMs: now - this.startedAt }));
|
|
1311
1359
|
}
|
|
1312
1360
|
return out;
|
|
1313
1361
|
}
|
|
@@ -1951,7 +1999,7 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
1951
1999
|
${ADMIN_CSS}
|
|
1952
2000
|
<header>
|
|
1953
2001
|
<h1>pantheon · admin</h1>
|
|
1954
|
-
<span class=meta>${esc(this.opts.host ?? "relay")} · pkg ${VERSION} · up ${upMin < 60 ? upMin + "m" : Math.round(upMin / 60) + "h"}</span>
|
|
2002
|
+
<span class=meta>${esc(this.opts.host ?? "relay")} · pkg ${VERSION}${this.upgradeHold ? ` · upgrades held at ${esc(this.upgradeHold)}` : ""} · up ${upMin < 60 ? upMin + "m" : Math.round(upMin / 60) + "h"}</span>
|
|
1955
2003
|
<span class=links><a href="/admin/export?table=audit&format=csv">export audit CSV</a><a href="/admin/export?table=accounts&format=json">accounts JSON</a><a href="/admin/backup">download backup</a><a href="/admin/logout">sign out</a></span>
|
|
1956
2004
|
</header>
|
|
1957
2005
|
<nav class=jump aria-label="Sections"><a href="#s-activity">Activity</a><a href="#s-spaces">Spaces</a><a href="#s-accounts">Accounts</a><a href="#s-plans">Plans</a><a href="#s-sessions">Sessions</a><a href="#s-blocks">Blocked</a><a href="#s-codes">Codes</a><a href="#s-audit">Audit log</a></nav>
|
|
@@ -3244,6 +3292,13 @@ pantheon start</code></pre>
|
|
|
3244
3292
|
} });
|
|
3245
3293
|
ws.on("error", () => { });
|
|
3246
3294
|
}
|
|
3295
|
+
noteStalls() {
|
|
3296
|
+
const worst = this.lagMax;
|
|
3297
|
+
this.lagMax = 0;
|
|
3298
|
+
const disk = takeSlowestDiskWait();
|
|
3299
|
+
if (worst >= 1000)
|
|
3300
|
+
this.log(`! the relay could not answer for up to ${(worst / 1000).toFixed(1)} s in the last 15 s${disk ? ` (longest disk wait ${(disk / 1000).toFixed(1)} s)` : ""}`);
|
|
3301
|
+
}
|
|
3247
3302
|
sweep() {
|
|
3248
3303
|
for (const ws of this.wss.clients) {
|
|
3249
3304
|
const s = ws;
|