@join-pantheon/cli 0.28.0 → 0.29.1
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 +13 -2
- package/dist/agents.js +377 -9
- package/dist/cli.js +378 -192
- package/dist/client.js +73 -12
- package/dist/config.js +15 -0
- package/dist/doctor.js +13 -10
- package/dist/grants.js +16 -1
- package/dist/hooks-install.js +4 -2
- package/dist/host-env.js +51 -6
- package/dist/install.js +216 -16
- package/dist/kiro-files.js +23 -6
- package/dist/ledger.js +8 -2
- package/dist/machine.js +286 -60
- package/dist/mcp-supervisor.js +5 -0
- package/dist/mcp.js +33 -6
- package/dist/network.js +258 -57
- package/dist/pending.js +14 -7
- package/dist/quickstart.js +186 -22
- package/dist/refusal.js +29 -0
- package/dist/relay.js +738 -447
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +192 -40
- package/dist/setup-guide.js +21 -4
- package/dist/store.js +9 -3
- package/dist/which.js +66 -4
- package/dist/workdir.js +7 -1
- package/dist/worker.js +522 -101
- package/helper/macos/menubar.swift +11 -3
- package/package.json +1 -1
package/dist/quickstart.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { basename, join as pathJoin } from "node:path";
|
|
5
|
+
import { homedir, hostname, platform } from "node:os";
|
|
6
|
+
import { lookup } from "node:dns/promises";
|
|
7
|
+
import { describeNetError } from "./network.js";
|
|
5
8
|
import { PantheonClient } from "./client.js";
|
|
6
|
-
import { load, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
|
|
9
|
+
import { load, identity, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
|
|
7
10
|
import { loadProfile, wakeDefault } from "./profile.js";
|
|
8
11
|
import { newKeypair } from "./identity.js";
|
|
9
12
|
import { installForUser } from "./install.js";
|
|
@@ -13,7 +16,8 @@ import { installService, servicesUnavailable } from "./service.js";
|
|
|
13
16
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
14
17
|
import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
|
|
15
18
|
import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
|
|
16
|
-
import { existsSync } from "node:fs";
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { localAddresses } from "./machine.js";
|
|
17
21
|
import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
|
|
18
22
|
import { Relay } from "./relay.js";
|
|
19
23
|
import { which } from "./which.js";
|
|
@@ -98,7 +102,12 @@ export function wireUp(opts, deps = {}) {
|
|
|
98
102
|
}
|
|
99
103
|
else if (opts.unattended) {
|
|
100
104
|
const project = opts.unattended;
|
|
101
|
-
const isRepo =
|
|
105
|
+
const isRepo = (() => { try {
|
|
106
|
+
return execFileSync("git", ["-C", project, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim() === "true";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
} })();
|
|
102
111
|
if (!isRepo && how === "explicit") {
|
|
103
112
|
tools.push({ tool: "worker", status: `not installed — ${project} is not a git repo` });
|
|
104
113
|
warnings.push(`--unattended needs a git repo to work in; pass --project <a clone>. Nothing was installed.`);
|
|
@@ -126,7 +135,136 @@ export function wireUp(opts, deps = {}) {
|
|
|
126
135
|
}
|
|
127
136
|
return { tools, granted, warnings };
|
|
128
137
|
}
|
|
129
|
-
|
|
138
|
+
const TRANSLIT = {
|
|
139
|
+
а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "yo", ж: "zh", з: "z", и: "i", й: "y", к: "k", л: "l", м: "m",
|
|
140
|
+
н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f", х: "kh", ц: "ts", ч: "ch", ш: "sh", щ: "shch",
|
|
141
|
+
ъ: "", ы: "y", ь: "", э: "e", ю: "yu", я: "ya", є: "ye", і: "i", ї: "yi", ґ: "g", ў: "u",
|
|
142
|
+
α: "a", β: "v", γ: "g", δ: "d", ε: "e", ζ: "z", η: "i", θ: "th", ι: "i", κ: "k", λ: "l", μ: "m", ν: "n", ξ: "x",
|
|
143
|
+
ο: "o", π: "p", ρ: "r", σ: "s", ς: "s", τ: "t", υ: "y", φ: "f", χ: "ch", ψ: "ps", ω: "o",
|
|
144
|
+
ß: "ss", æ: "ae", ø: "o", đ: "d", ł: "l", þ: "th", œ: "oe",
|
|
145
|
+
};
|
|
146
|
+
export function slugName(raw, fallback) {
|
|
147
|
+
const s = raw.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "")
|
|
148
|
+
.replace(/./gu, (ch) => TRANSLIT[ch] ?? ch)
|
|
149
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
150
|
+
.replace(/-{2,}/g, "-")
|
|
151
|
+
.replace(/^[-._]+|[-._]+$/g, "");
|
|
152
|
+
return /[a-z0-9]/.test(s) ? s : fallback;
|
|
153
|
+
}
|
|
154
|
+
export function teamNameFor(dir, repo = repoInfo(dir)) {
|
|
155
|
+
const fromOrigin = repo?.origin ? (repo.origin.replace(/\.git$/i, "").split(/[/:]/).filter(Boolean).pop() ?? "") : "";
|
|
156
|
+
return slugName(fromOrigin || basename(repo?.root ?? dir), "team");
|
|
157
|
+
}
|
|
158
|
+
export function isLocalRelay(url) {
|
|
159
|
+
let host = "";
|
|
160
|
+
try {
|
|
161
|
+
host = new URL(url.replace(/^ws/i, "http")).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return host === "localhost" || host === "::1" || host === "0.0.0.0" || host === "::" || /^127\./.test(host);
|
|
167
|
+
}
|
|
168
|
+
const hostOf = (url) => {
|
|
169
|
+
try {
|
|
170
|
+
return new URL(url.replace(/^ws/i, "http")).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
export async function relayPlace(url, localPort, deps = {}) {
|
|
177
|
+
if (isLocalRelay(url))
|
|
178
|
+
return "loopback";
|
|
179
|
+
const host = hostOf(url);
|
|
180
|
+
const hosted = new Set([hostOf(deps.hostedRelay ?? "wss://relay.joinpantheon.network"), hostOf(DEFAULT_RELAY)].filter(Boolean));
|
|
181
|
+
if (!host || hosted.has(host))
|
|
182
|
+
return "hosted";
|
|
183
|
+
const addrs = new Set((deps.localAddrs ?? localAddresses()).map((a) => a.toLowerCase()));
|
|
184
|
+
const name = (deps.hostname ?? hostname()).toLowerCase();
|
|
185
|
+
if (addrs.has(host) || host === name || host === `${name}.local`)
|
|
186
|
+
return "this-machine";
|
|
187
|
+
const resolved = await (deps.resolve ?? resolveHost)(host);
|
|
188
|
+
if (resolved?.some((a) => addrs.has(a.toLowerCase()) || /^127\./.test(a) || a === "::1"))
|
|
189
|
+
return "this-machine";
|
|
190
|
+
if ((deps.tunnelTo ?? tunnelRunningTo)(localPort) || (deps.hostedBefore ?? (() => false))(url))
|
|
191
|
+
return "tunnel";
|
|
192
|
+
return "remote";
|
|
193
|
+
}
|
|
194
|
+
async function resolveHost(host) {
|
|
195
|
+
try {
|
|
196
|
+
const all = await Promise.race([
|
|
197
|
+
lookup(host, { all: true }).then((r) => r.map((x) => x.address)),
|
|
198
|
+
new Promise((res) => setTimeout(() => res(null), 1500).unref()),
|
|
199
|
+
]);
|
|
200
|
+
return all;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export function tunnelRunningTo(port, ps = () => sh("ps -Ao command= 2>/dev/null")) {
|
|
207
|
+
const out = ps() ?? "";
|
|
208
|
+
const re = new RegExp(`\\b(cloudflared|ngrok|tailscale\\s+(funnel|serve)|bore)\\b.*[:\\s]${port}\\b`);
|
|
209
|
+
return out.split("\n").some((l) => re.test(l));
|
|
210
|
+
}
|
|
211
|
+
const HOSTED_FOR = "hosted-for.json";
|
|
212
|
+
export function hostedBefore(dataDir, url) {
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(readFileSync(pathJoin(dataDir, HOSTED_FOR), "utf8")).includes(url.replace(/\/+$/, ""));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export function rememberHosted(dataDir, url) {
|
|
221
|
+
try {
|
|
222
|
+
const f = pathJoin(dataDir, HOSTED_FOR);
|
|
223
|
+
let list = [];
|
|
224
|
+
try {
|
|
225
|
+
list = JSON.parse(readFileSync(f, "utf8"));
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
const u = url.replace(/\/+$/, "");
|
|
229
|
+
if (list.includes(u))
|
|
230
|
+
return;
|
|
231
|
+
mkdirSync(dataDir, { recursive: true });
|
|
232
|
+
writeFileSync(f, JSON.stringify([...list, u]));
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
}
|
|
236
|
+
export async function probeRelay(url, timeoutMs, fetchImpl = fetch) {
|
|
237
|
+
const health = url.replace(/^ws/i, "http").replace(/\/+$/, "") + "/health";
|
|
238
|
+
try {
|
|
239
|
+
const r = await fetchImpl(health, { signal: AbortSignal.timeout(timeoutMs) });
|
|
240
|
+
if (r.ok)
|
|
241
|
+
return { ok: true };
|
|
242
|
+
return { ok: false, error: new Error(`${health} answered HTTP ${r.status}`) };
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
246
|
+
return { ok: false, error: Object.assign(new Error(`no answer from ${health} in ${Math.round(timeoutMs / 1000)}s`), { code: "ETIMEDOUT" }) };
|
|
247
|
+
}
|
|
248
|
+
const cause = e?.cause;
|
|
249
|
+
if (cause?.message && /fetch failed/.test(e?.message ?? "")) {
|
|
250
|
+
return { ok: false, error: Object.assign(new Error(`fetch failed: ${cause.message}`), { cause, ...(cause.code ? { code: cause.code } : {}) }) };
|
|
251
|
+
}
|
|
252
|
+
return { ok: false, error: e };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function portFree(port) {
|
|
256
|
+
return new Promise((res) => {
|
|
257
|
+
const s = createServer();
|
|
258
|
+
s.once("error", (e) => res(e.code ?? e.message));
|
|
259
|
+
s.listen(port, "0.0.0.0", () => s.close(() => res(true)));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
export function cloudflaredInstall(os = platform()) {
|
|
263
|
+
return os === "darwin" ? "brew install cloudflared"
|
|
264
|
+
: os === "win32" ? "winget install --id Cloudflare.cloudflared"
|
|
265
|
+
: "install cloudflared (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)";
|
|
266
|
+
}
|
|
267
|
+
export function findReach(port, os = platform()) {
|
|
130
268
|
const ts = sh("tailscale ip -4 2>/dev/null | head -1");
|
|
131
269
|
if (ts && /^\d+\.\d+\.\d+\.\d+$/.test(ts)) {
|
|
132
270
|
return { url: `ws://${ts}:${port}`, how: `tailscale (${ts})`, shareable: true };
|
|
@@ -143,33 +281,58 @@ export function findReach(port) {
|
|
|
143
281
|
url: `ws://127.0.0.1:${port}`, how: "localhost", shareable: false,
|
|
144
282
|
hint: `Only reachable on this machine. For a teammate elsewhere, easiest is Tailscale\n` +
|
|
145
283
|
` on both machines (then re-run \`pantheon start\`), or a tunnel:\n` +
|
|
146
|
-
|
|
284
|
+
(os === "darwin" || os === "win32"
|
|
285
|
+
? ` ${cloudflaredInstall(os)} && cloudflared tunnel --url http://localhost:${port}`
|
|
286
|
+
: ` ${cloudflaredInstall(os)}, then: cloudflared tunnel --url http://localhost:${port}`),
|
|
147
287
|
};
|
|
148
288
|
}
|
|
149
289
|
export async function start(opts) {
|
|
150
290
|
const warnings = [];
|
|
151
291
|
const here = resolveFor(process.cwd());
|
|
152
|
-
const priorCfg = here.cfg;
|
|
292
|
+
const priorCfg = here.cfg ?? identity();
|
|
153
293
|
const repo = repoInfo(process.cwd());
|
|
154
294
|
const signedInRelay = priorCfg?.account ? (priorCfg.relay ?? DEFAULT_RELAY) : undefined;
|
|
155
295
|
const chosen = opts.relay ?? signedInRelay;
|
|
296
|
+
const place = chosen ? await relayPlace(chosen, opts.port, { ...opts.place, hostedBefore: (u) => hostedBefore(opts.dataDir, u) }) : null;
|
|
297
|
+
const hostHere = place === "loopback" || place === "this-machine" || place === "tunnel";
|
|
156
298
|
const reach = chosen
|
|
157
|
-
? (
|
|
299
|
+
? (place === "loopback"
|
|
158
300
|
? { url: chosen, how: "as given", shareable: false,
|
|
159
301
|
hint: `That address is this machine only — a teammate cannot reach it.\n` +
|
|
160
302
|
` Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
|
|
161
|
-
: { url: chosen, how: opts.relay ? "as given" : "your signed-in relay", shareable: true })
|
|
303
|
+
: { url: chosen, how: place === "this-machine" ? "this machine's address" : place === "tunnel" ? "a tunnel to this machine" : opts.relay ? "as given" : "your signed-in relay", shareable: true })
|
|
162
304
|
: findReach(opts.port);
|
|
305
|
+
const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
|
|
163
306
|
let relay = null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
307
|
+
if (chosen && !hostHere) {
|
|
308
|
+
const probe = await probeRelay(reach.url, opts.remoteTimeoutMs ?? 10_000, opts.fetchImpl);
|
|
309
|
+
if (!probe.ok) {
|
|
310
|
+
return nothing(`cannot reach the relay at ${reach.url}: ${describeNetError(probe.error, reach.url)}` +
|
|
311
|
+
(place === "remote" ? `\n If that address is a tunnel to this machine, start the tunnel first (cloudflared tunnel --url http://localhost:${opts.port}) and run this again — Pantheon hosts the relay here when it sees the tunnel.` : ""));
|
|
312
|
+
}
|
|
313
|
+
opts.log(` relay ${reach.url} (${reach.how})`);
|
|
168
314
|
}
|
|
169
315
|
else {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
316
|
+
const urlPort = chosen && place !== "tunnel" ? Number(new URL(chosen.replace(/^ws/i, "http")).port) : 0;
|
|
317
|
+
const listenPort = urlPort || opts.port;
|
|
318
|
+
const local = `ws://127.0.0.1:${listenPort}`;
|
|
319
|
+
const probe = await probeRelay(place === "tunnel" ? local : reach.url, 1500);
|
|
320
|
+
if (probe.ok) {
|
|
321
|
+
opts.log(` relay already running — nothing to start ${reach.url}`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const free = await portFree(listenPort);
|
|
325
|
+
if (free !== true) {
|
|
326
|
+
return nothing(free === "EADDRINUSE"
|
|
327
|
+
? `port ${listenPort} is taken by another program — it did not answer as a Pantheon relay (${local}/health). Free it, or pick another port: pantheon start --port <n>`
|
|
328
|
+
: `the relay could not start on port ${listenPort}: ${free}`);
|
|
329
|
+
}
|
|
330
|
+
relay = new Relay({ port: listenPort, host: "0.0.0.0", dataDir: opts.dataDir, log: () => { } });
|
|
331
|
+
await relay.listen();
|
|
332
|
+
opts.log(` relay started on ${reach.url} (${reach.how})`);
|
|
333
|
+
}
|
|
334
|
+
if (chosen && place !== "loopback")
|
|
335
|
+
rememberHosted(opts.dataDir, chosen);
|
|
173
336
|
}
|
|
174
337
|
const existing = priorCfg;
|
|
175
338
|
const hasOwnSpace = !!existing?.space && !!existing?.token;
|
|
@@ -180,9 +343,9 @@ export async function start(opts) {
|
|
|
180
343
|
? { ...existing, relay: reach.url, agent: detectAgent() }
|
|
181
344
|
: {
|
|
182
345
|
relay: reach.url,
|
|
183
|
-
space: opts.space ?? (
|
|
346
|
+
space: opts.space ?? teamNameFor(process.cwd(), repo),
|
|
184
347
|
token: randomBytes(16).toString("hex"),
|
|
185
|
-
member: opts.me ?? (keepIdentity ? existing.member : (process.env.USER ?? "me"
|
|
348
|
+
member: opts.me ?? (keepIdentity ? existing.member : slugName(process.env.USER ?? "me", "me")),
|
|
186
349
|
agent: detectAgent(),
|
|
187
350
|
...(keepIdentity ? { publicKey: existing.publicKey, privateKey: existing.privateKey } : newKeypair()),
|
|
188
351
|
...(keepIdentity && existing.account ? { account: existing.account } : {}),
|
|
@@ -220,7 +383,8 @@ export async function start(opts) {
|
|
|
220
383
|
warnings.push(`this repo was bound to another team (${replaced.space}); it now belongs to "${c.space}"`);
|
|
221
384
|
}
|
|
222
385
|
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
223
|
-
const
|
|
386
|
+
const mayWake = opts.wake ? opts.wake === "anyone" : wakeDefault(loadProfile(c));
|
|
387
|
+
const inv = await client.makeInvite(mayWake, opts.ttlMinutes);
|
|
224
388
|
invite = toInvite({ ...c, token: inv.url });
|
|
225
389
|
claimed = true;
|
|
226
390
|
}
|
|
@@ -265,7 +429,7 @@ export async function start(opts) {
|
|
|
265
429
|
return { cfg, invite, claimed, reach, tools: wired.tools, granted: wired.granted, relay, warnings };
|
|
266
430
|
}
|
|
267
431
|
export async function join(opts) {
|
|
268
|
-
const existing = opts.reset ? null : load();
|
|
432
|
+
const existing = opts.reset ? null : (load() ?? identity());
|
|
269
433
|
const repo = repoInfo(process.cwd());
|
|
270
434
|
const keys = existing?.publicKey && existing.privateKey
|
|
271
435
|
? { publicKey: existing.publicKey, privateKey: existing.privateKey }
|
|
@@ -277,7 +441,7 @@ export async function join(opts) {
|
|
|
277
441
|
relay: opts.invite.relay,
|
|
278
442
|
space: opts.invite.space,
|
|
279
443
|
token: "",
|
|
280
|
-
member: opts.me ?? existing?.member ?? (process.env.USER ?? "me"
|
|
444
|
+
member: opts.me ?? existing?.member ?? slugName(process.env.USER ?? "me", "me"),
|
|
281
445
|
agent: detectAgent(),
|
|
282
446
|
...keys,
|
|
283
447
|
...(existing?.account ? { account: existing.account } : {}),
|
package/dist/refusal.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const NET_CODE = /^(ECONN\w*|ENOTFOUND|ETIMEDOUT|EAI_\w+|EPROTO|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNRESET|UND_ERR_\w*|ERR_\w+)$/;
|
|
2
|
+
const FIX = {
|
|
3
|
+
NOT_SIGNED_IN: "sign in on this machine: pantheon login --email you@work.com (or pantheon signup), then try again",
|
|
4
|
+
KEY_MISMATCH: "a member of the team runs pantheon admit <you> --forget and sends you a new invite (or pick another name with --me)",
|
|
5
|
+
REVOKED: "ask a member of the team to let you back in: pantheon admit <you>",
|
|
6
|
+
PLAN_LIMIT: "the team's owner frees a seat (pantheon invite --revoke all cancels unused invites) or upgrades the plan",
|
|
7
|
+
BAD_TOKEN: "this machine's copy of the team token is wrong — rejoin with a fresh invite: pantheon join <invite>",
|
|
8
|
+
NO_SUCH_SPACE: "the team does not exist on this relay — check pantheon spaces, or join with a fresh invite",
|
|
9
|
+
SPACE_DELETED: "the team was deleted — remove it here with pantheon leave <team>",
|
|
10
|
+
REPO_MISMATCH: "run this from the team's own repo, or bind this one: pantheon use <team> --here",
|
|
11
|
+
BAD_INVITE: "ask for a fresh invite: pantheon invite",
|
|
12
|
+
};
|
|
13
|
+
export function refusalOf(e) {
|
|
14
|
+
const text = typeof e === "string" ? e : String(e?.message ?? e ?? "");
|
|
15
|
+
const m = /^([A-Z][A-Z0-9_]{2,}): ([\s\S]*)$/.exec(text.trim());
|
|
16
|
+
if (!m || NET_CODE.test(m[1]) || m[1] === "PENDING")
|
|
17
|
+
return null;
|
|
18
|
+
return { code: m[1], message: m[2].trim(), fix: FIX[m[1]] ?? null };
|
|
19
|
+
}
|
|
20
|
+
export function connectFailure(e, relay) {
|
|
21
|
+
const r = refusalOf(e);
|
|
22
|
+
if (r) {
|
|
23
|
+
const fix = r.fix && !/pantheon \w/.test(r.message) ? `\n ${r.fix}` : "";
|
|
24
|
+
return `the relay refused this machine (${r.code}): ${r.message}${fix}`;
|
|
25
|
+
}
|
|
26
|
+
const text = typeof e === "string" ? e : String(e?.message ?? e ?? "");
|
|
27
|
+
return `cannot reach the relay at ${relay} — ${text}`;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=refusal.js.map
|