@staff0rd/assist 0.749.0 → 0.751.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -4
- package/dist/commands/sessions/web/bundle.js +1 -1
- package/dist/index.js +307 -101
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@staff0rd/assist",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.751.0",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -10696,7 +10696,7 @@ function startWebServer(label2, port, handler, initialPath, open = true) {
|
|
|
10696
10696
|
const server = createServer((req, res) => {
|
|
10697
10697
|
runHandler(handler, req, res, port);
|
|
10698
10698
|
});
|
|
10699
|
-
server.listen(port, () => {
|
|
10699
|
+
server.listen(port, "127.0.0.1", () => {
|
|
10700
10700
|
console.log(chalk103.green(`${label2}: ${url}`));
|
|
10701
10701
|
console.log(chalk103.dim("Press Ctrl+C to stop"));
|
|
10702
10702
|
if (open) {
|
|
@@ -12903,8 +12903,8 @@ var seqConfigHelp = [
|
|
|
12903
12903
|
var nodesConfigHelp = [
|
|
12904
12904
|
{
|
|
12905
12905
|
key: "sessions.links",
|
|
12906
|
-
setter: "assist sessions nodes link <name> http://127.0.0.1:<port> (or --ssh <alias> --port <port>)",
|
|
12907
|
-
note: "linked nodes whose sessions merge into this node's web UI, each direct (url) or over an ssh tunnel (ssh alias, peer port, localPort); each name must match the peer's sessions.nodeName"
|
|
12906
|
+
setter: "assist sessions nodes link <name> http://127.0.0.1:<port> (or --tailscale <host> --port <port>, or --ssh <alias> --port <port>)",
|
|
12907
|
+
note: "linked nodes whose sessions merge into this node's web UI, each direct (url, including a Tailscale <host>.<tailnet>.ts.net https url) or over an ssh tunnel (ssh alias, peer port, localPort); each name must match the peer's sessions.nodeName"
|
|
12908
12908
|
},
|
|
12909
12909
|
{
|
|
12910
12910
|
key: "sessions.linkVersionCheck",
|
|
@@ -47295,6 +47295,73 @@ async function sshHop(ssh, probes) {
|
|
|
47295
47295
|
);
|
|
47296
47296
|
}
|
|
47297
47297
|
|
|
47298
|
+
// src/commands/sessions/nodes/tailscaleStatus.ts
|
|
47299
|
+
import { execFile as execFile13 } from "child_process";
|
|
47300
|
+
import { promisify as promisify12 } from "util";
|
|
47301
|
+
var execFileAsync11 = promisify12(execFile13);
|
|
47302
|
+
var STATUS_TIMEOUT_MS2 = 1e4;
|
|
47303
|
+
function tailscaleCli() {
|
|
47304
|
+
return detectPlatform() === "wsl" ? "tailscale.exe" : "tailscale";
|
|
47305
|
+
}
|
|
47306
|
+
async function tailscaleStatus() {
|
|
47307
|
+
const { stdout } = await execFileAsync11(tailscaleCli(), ["status", "--json"], {
|
|
47308
|
+
encoding: "utf8",
|
|
47309
|
+
timeout: STATUS_TIMEOUT_MS2,
|
|
47310
|
+
windowsHide: true
|
|
47311
|
+
});
|
|
47312
|
+
return JSON.parse(stdout);
|
|
47313
|
+
}
|
|
47314
|
+
|
|
47315
|
+
// src/commands/sessions/nodes/findTailscalePeer.ts
|
|
47316
|
+
function findTailscalePeer(status3, hostname2) {
|
|
47317
|
+
const nodes = [
|
|
47318
|
+
...status3.Self ? [{ ...status3.Self, Online: true }] : [],
|
|
47319
|
+
...Object.values(status3.Peer ?? {})
|
|
47320
|
+
];
|
|
47321
|
+
return nodes.find(
|
|
47322
|
+
(node) => node.DNSName?.replace(/\.$/, "").toLowerCase() === hostname2
|
|
47323
|
+
);
|
|
47324
|
+
}
|
|
47325
|
+
|
|
47326
|
+
// src/commands/sessions/nodes/tailscaleHop.ts
|
|
47327
|
+
async function tailscaleHop(spec, probes) {
|
|
47328
|
+
const cli = tailscaleCli();
|
|
47329
|
+
let status3;
|
|
47330
|
+
try {
|
|
47331
|
+
status3 = await probes.tailscale();
|
|
47332
|
+
} catch (error) {
|
|
47333
|
+
return failed2(
|
|
47334
|
+
"tailscale",
|
|
47335
|
+
describePeerError(error),
|
|
47336
|
+
`\`${cli} status --json\` failed \u2014 install Tailscale on this node and sign in with \`${cli} up\``
|
|
47337
|
+
);
|
|
47338
|
+
}
|
|
47339
|
+
if (status3.BackendState !== "Running")
|
|
47340
|
+
return failed2(
|
|
47341
|
+
"tailscale",
|
|
47342
|
+
`Tailscale on this node is ${status3.BackendState ?? "not running"}`,
|
|
47343
|
+
`run \`${cli} up\` on this node`
|
|
47344
|
+
);
|
|
47345
|
+
const { hostname: hostname2 } = new URL(spec.url);
|
|
47346
|
+
const peer = findTailscalePeer(status3, hostname2);
|
|
47347
|
+
if (!peer)
|
|
47348
|
+
return failed2(
|
|
47349
|
+
"tailscale",
|
|
47350
|
+
`${hostname2} is not in this node's tailnet`,
|
|
47351
|
+
`check the host in \`${cli} status\`, then relink with \`assist sessions nodes link ${spec.name} --tailscale <host> --port <port>\``
|
|
47352
|
+
);
|
|
47353
|
+
if (!peer.Online)
|
|
47354
|
+
return failed2(
|
|
47355
|
+
"tailscale",
|
|
47356
|
+
`${hostname2} is offline in the tailnet`,
|
|
47357
|
+
`is ${spec.name}'s machine awake with Tailscale running and signed in?`
|
|
47358
|
+
);
|
|
47359
|
+
return passed(
|
|
47360
|
+
"tailscale",
|
|
47361
|
+
`${hostname2} online${peer.TailscaleIPs?.[0] ? ` (${peer.TailscaleIPs[0]})` : ""}`
|
|
47362
|
+
);
|
|
47363
|
+
}
|
|
47364
|
+
|
|
47298
47365
|
// src/commands/sessions/nodes/tunnelHop.ts
|
|
47299
47366
|
async function tunnelHop(spec, ssh, probes) {
|
|
47300
47367
|
const bound = `127.0.0.1:${ssh.localPort}`;
|
|
@@ -47307,10 +47374,26 @@ async function tunnelHop(spec, ssh, probes) {
|
|
|
47307
47374
|
);
|
|
47308
47375
|
}
|
|
47309
47376
|
|
|
47377
|
+
// src/commands/sessions/nodes/tailscaleServeRemediation.ts
|
|
47378
|
+
function tailscaleServeRemediation(spec, error) {
|
|
47379
|
+
const status3 = error.status;
|
|
47380
|
+
if (status3 !== void 0 && status3 !== 502) return void 0;
|
|
47381
|
+
const { hostname: hostname2, port } = new URL(spec.url);
|
|
47382
|
+
const servePort = port || "443";
|
|
47383
|
+
return status3 === 502 ? `tailscale serve on ${hostname2} answered but nothing listens on 127.0.0.1:${servePort} behind it \u2014 is ${spec.name}'s web server running (project-switch webservers)?` : `nothing serves https on ${servePort} on ${hostname2} \u2014 run \`assist sessions nodes serve --port ${servePort}\` on ${spec.name}`;
|
|
47384
|
+
}
|
|
47385
|
+
|
|
47386
|
+
// src/commands/sessions/nodes/isTailscaleLink.ts
|
|
47387
|
+
function isTailscaleLink(spec) {
|
|
47388
|
+
return new URL(spec.url).hostname.endsWith(".ts.net");
|
|
47389
|
+
}
|
|
47390
|
+
|
|
47310
47391
|
// src/commands/sessions/nodes/webHop.ts
|
|
47311
47392
|
function webRemediation(spec, error) {
|
|
47312
47393
|
if (spec.ssh && error.status === void 0)
|
|
47313
47394
|
return `nothing listening on ${spec.ssh.port} on ${spec.ssh.alias} \u2014 is project-switch running ${spec.name}'s web server?`;
|
|
47395
|
+
const serve = isTailscaleLink(spec) ? tailscaleServeRemediation(spec, error) : void 0;
|
|
47396
|
+
if (serve) return serve;
|
|
47314
47397
|
const { hostname: hostname2, port } = new URL(spec.url);
|
|
47315
47398
|
switch (peerErrorCode(error)) {
|
|
47316
47399
|
case "ECONNREFUSED":
|
|
@@ -47350,7 +47433,7 @@ async function webHop(spec, probes) {
|
|
|
47350
47433
|
};
|
|
47351
47434
|
}
|
|
47352
47435
|
|
|
47353
|
-
// src/commands/sessions/nodes/
|
|
47436
|
+
// src/commands/sessions/nodes/peerDaemonHop.ts
|
|
47354
47437
|
function peerDaemonHop(spec, health2) {
|
|
47355
47438
|
if (health2.daemon?.reachable) return passed("daemon", "reachable");
|
|
47356
47439
|
return failed2(
|
|
@@ -47359,6 +47442,8 @@ function peerDaemonHop(spec, health2) {
|
|
|
47359
47442
|
`run \`assist daemon status\` on ${spec.name}; restarting its web server starts the daemon`
|
|
47360
47443
|
);
|
|
47361
47444
|
}
|
|
47445
|
+
|
|
47446
|
+
// src/commands/sessions/nodes/diagnoseLink.ts
|
|
47362
47447
|
async function diagnoseLink(spec, probes) {
|
|
47363
47448
|
const hops = [];
|
|
47364
47449
|
const done2 = () => ({ ...spec, ok: hops.every((h) => h.ok), hops });
|
|
@@ -47373,6 +47458,10 @@ async function diagnoseLink(spec, probes) {
|
|
|
47373
47458
|
if (!hops.at(-1)?.ok) return done2();
|
|
47374
47459
|
}
|
|
47375
47460
|
}
|
|
47461
|
+
if (isTailscaleLink(spec)) {
|
|
47462
|
+
hops.push(await tailscaleHop(spec, probes));
|
|
47463
|
+
if (!hops.at(-1)?.ok) return done2();
|
|
47464
|
+
}
|
|
47376
47465
|
const web3 = await webHop(spec, probes);
|
|
47377
47466
|
hops.push(web3.hop);
|
|
47378
47467
|
if (!web3.health) return done2();
|
|
@@ -47520,7 +47609,7 @@ function printDiagnosis(diagnosis) {
|
|
|
47520
47609
|
console.log(`${chalk253.bold(diagnosis.name)} ${chalk253.dim(diagnosis.url)}`);
|
|
47521
47610
|
for (const hop of diagnosis.hops) {
|
|
47522
47611
|
const mark = hop.ok ? chalk253.green("\u2713") : chalk253.red("\u2717");
|
|
47523
|
-
console.log(` ${mark} ${hop.hop.padEnd(
|
|
47612
|
+
console.log(` ${mark} ${hop.hop.padEnd(9)} ${hop.detail ?? hop.error}`);
|
|
47524
47613
|
if (hop.remediation) console.log(` ${chalk253.yellow(hop.remediation)}`);
|
|
47525
47614
|
}
|
|
47526
47615
|
}
|
|
@@ -47533,6 +47622,7 @@ function doctorProbes(live) {
|
|
|
47533
47622
|
sshAgent: probeSshAgent,
|
|
47534
47623
|
ssh: probeSsh,
|
|
47535
47624
|
tunnel: (localPort) => portAccepts(localPort),
|
|
47625
|
+
tailscale: tailscaleStatus,
|
|
47536
47626
|
linkState: (name) => {
|
|
47537
47627
|
if (!live) return "no-daemon";
|
|
47538
47628
|
return live.links.find((l) => l.name === name) ?? "unknown-link";
|
|
@@ -47553,15 +47643,74 @@ async function doctorNodes(name, options2) {
|
|
|
47553
47643
|
else printReport(report2);
|
|
47554
47644
|
}
|
|
47555
47645
|
|
|
47646
|
+
// src/commands/sessions/nodes/listNodes.ts
|
|
47647
|
+
import chalk254 from "chalk";
|
|
47648
|
+
async function nodesReport() {
|
|
47649
|
+
const live = await queryNodes();
|
|
47650
|
+
if (live)
|
|
47651
|
+
return { local: live.local, daemonRunning: true, links: live.links };
|
|
47652
|
+
return {
|
|
47653
|
+
local: resolveNodeName(),
|
|
47654
|
+
daemonRunning: false,
|
|
47655
|
+
links: loadLinkSpecs().map((link3) => ({ ...link3, state: "disconnected" }))
|
|
47656
|
+
};
|
|
47657
|
+
}
|
|
47658
|
+
async function listNodes(options2) {
|
|
47659
|
+
const report2 = await nodesReport();
|
|
47660
|
+
if (options2.json) {
|
|
47661
|
+
console.log(JSON.stringify(report2, null, 2));
|
|
47662
|
+
return;
|
|
47663
|
+
}
|
|
47664
|
+
console.log(`${report2.local} ${chalk254.dim("(this node)")}`);
|
|
47665
|
+
if (!report2.daemonRunning)
|
|
47666
|
+
console.log(chalk254.dim(" daemon not running; showing configured links"));
|
|
47667
|
+
if (report2.links.length === 0) console.log(chalk254.dim(" no links"));
|
|
47668
|
+
for (const link3 of report2.links) console.log(formatLink(link3));
|
|
47669
|
+
}
|
|
47670
|
+
|
|
47671
|
+
// src/commands/sessions/nodes/nodeLogs.ts
|
|
47672
|
+
async function fetchLogTail(name, lines2) {
|
|
47673
|
+
if (name === resolveNodeName())
|
|
47674
|
+
return {
|
|
47675
|
+
nodeName: name,
|
|
47676
|
+
path: daemonPaths.log,
|
|
47677
|
+
lines: readLogTail(daemonPaths.log, lines2)
|
|
47678
|
+
};
|
|
47679
|
+
const link3 = findLinkSpec(name);
|
|
47680
|
+
if (!link3)
|
|
47681
|
+
throw new Error(`No link named ${name}; see assist sessions nodes`);
|
|
47682
|
+
try {
|
|
47683
|
+
return await fetchPeerJson(
|
|
47684
|
+
link3.url,
|
|
47685
|
+
`/api/daemon-log?lines=${lines2}`
|
|
47686
|
+
);
|
|
47687
|
+
} catch (error) {
|
|
47688
|
+
throw new Error(
|
|
47689
|
+
`Could not read ${name}'s daemon log: ${describePeerError(error)}. Run assist sessions nodes doctor ${name}`
|
|
47690
|
+
);
|
|
47691
|
+
}
|
|
47692
|
+
}
|
|
47693
|
+
async function nodeLogs(name, options2) {
|
|
47694
|
+
const lines2 = Number.parseInt(options2.lines, 10);
|
|
47695
|
+
if (!Number.isInteger(lines2) || lines2 < 1)
|
|
47696
|
+
throw new Error(`-n must be a positive integer, got ${options2.lines}`);
|
|
47697
|
+
const tail = await fetchLogTail(name, lines2);
|
|
47698
|
+
if (options2.json) {
|
|
47699
|
+
console.log(JSON.stringify(tail, null, 2));
|
|
47700
|
+
return;
|
|
47701
|
+
}
|
|
47702
|
+
for (const line of tail.lines) console.log(line);
|
|
47703
|
+
}
|
|
47704
|
+
|
|
47556
47705
|
// src/commands/sessions/nodes/linkNode.ts
|
|
47557
|
-
import
|
|
47706
|
+
import chalk256 from "chalk";
|
|
47558
47707
|
|
|
47559
47708
|
// src/commands/sessions/nodes/notifyDaemonLinks.ts
|
|
47560
|
-
import
|
|
47709
|
+
import chalk255 from "chalk";
|
|
47561
47710
|
async function notifyDaemonLinks() {
|
|
47562
47711
|
if (!await isDaemonRunning()) {
|
|
47563
47712
|
console.log(
|
|
47564
|
-
|
|
47713
|
+
chalk255.yellow(
|
|
47565
47714
|
"The sessions daemon is not running; it loads the links when it starts."
|
|
47566
47715
|
)
|
|
47567
47716
|
);
|
|
@@ -47569,11 +47718,11 @@ async function notifyDaemonLinks() {
|
|
|
47569
47718
|
}
|
|
47570
47719
|
try {
|
|
47571
47720
|
await sendToDaemonAwaitAck({ type: "reload-links" });
|
|
47572
|
-
console.log(
|
|
47721
|
+
console.log(chalk255.dim("The sessions daemon reloaded its links."));
|
|
47573
47722
|
} catch (error) {
|
|
47574
47723
|
const message3 = error instanceof Error ? error.message : String(error);
|
|
47575
47724
|
console.log(
|
|
47576
|
-
|
|
47725
|
+
chalk255.yellow(
|
|
47577
47726
|
`Could not notify the sessions daemon (${message3}); run \`assist daemon restart\` to apply the change.`
|
|
47578
47727
|
)
|
|
47579
47728
|
);
|
|
@@ -47596,134 +47745,191 @@ async function writeLinks(links2) {
|
|
|
47596
47745
|
saveGlobalConfig(updated2);
|
|
47597
47746
|
}
|
|
47598
47747
|
|
|
47599
|
-
// src/commands/sessions/nodes/
|
|
47600
|
-
function
|
|
47601
|
-
|
|
47602
|
-
|
|
47603
|
-
|
|
47604
|
-
|
|
47748
|
+
// src/commands/sessions/nodes/tailnetSuffix.ts
|
|
47749
|
+
function tailnetSuffix(status3) {
|
|
47750
|
+
if (status3.BackendState !== "Running")
|
|
47751
|
+
throw new Error(
|
|
47752
|
+
`Tailscale is not running on this node (${status3.BackendState ?? "unknown state"}) \u2014 run \`${tailscaleCli()} up\``
|
|
47753
|
+
);
|
|
47754
|
+
const suffix = status3.CurrentTailnet?.MagicDNSSuffix ?? status3.MagicDNSSuffix ?? "";
|
|
47755
|
+
if (status3.CurrentTailnet?.MagicDNSEnabled === false || !suffix)
|
|
47756
|
+
throw new Error(
|
|
47757
|
+
"MagicDNS is off in this tailnet \u2014 turn on MagicDNS and HTTPS certificates on the DNS page of the Tailscale admin console"
|
|
47758
|
+
);
|
|
47759
|
+
return suffix.replace(/\.$/, "");
|
|
47760
|
+
}
|
|
47761
|
+
|
|
47762
|
+
// src/commands/sessions/nodes/tailscaleLinkUrl.ts
|
|
47763
|
+
async function localTailnetSuffix() {
|
|
47764
|
+
return tailnetSuffix(await tailscaleStatus());
|
|
47605
47765
|
}
|
|
47766
|
+
async function tailscaleLinkUrl(host, port, resolveSuffix = localTailnetSuffix) {
|
|
47767
|
+
const suffix = await resolveSuffix();
|
|
47768
|
+
const fqdn = host.replace(/\.$/, "");
|
|
47769
|
+
const bare = fqdn.endsWith(`.${suffix}`) ? fqdn.slice(0, -suffix.length - 1) : fqdn;
|
|
47770
|
+
return `https://${bare}.${suffix}:${port}`;
|
|
47771
|
+
}
|
|
47772
|
+
|
|
47773
|
+
// src/commands/sessions/nodes/parsePort.ts
|
|
47606
47774
|
function parsePort(flag, value) {
|
|
47607
47775
|
const port = Number(value);
|
|
47608
47776
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
47609
47777
|
throw new Error(`${flag} must be a port number, got ${value}`);
|
|
47610
47778
|
return port;
|
|
47611
47779
|
}
|
|
47612
|
-
|
|
47613
|
-
|
|
47614
|
-
|
|
47615
|
-
|
|
47616
|
-
|
|
47780
|
+
|
|
47781
|
+
// src/commands/sessions/nodes/parseLinkUrl.ts
|
|
47782
|
+
function parseLinkUrl(url) {
|
|
47783
|
+
const parsed = new URL(url);
|
|
47784
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
47785
|
+
throw new Error(`link url must be http(s): ${url}`);
|
|
47786
|
+
return parsed.origin;
|
|
47617
47787
|
}
|
|
47618
|
-
|
|
47619
|
-
|
|
47620
|
-
|
|
47788
|
+
|
|
47789
|
+
// src/commands/sessions/nodes/buildLink.ts
|
|
47790
|
+
async function buildLink(name, url, options2, others, resolveTailnetSuffix) {
|
|
47791
|
+
if ([url, options2.ssh, options2.tailscale].filter(Boolean).length > 1)
|
|
47792
|
+
throw new Error(
|
|
47793
|
+
"pass only one of <url>, --tailscale <host> or --ssh <alias>"
|
|
47794
|
+
);
|
|
47621
47795
|
if (url) return { name, url: parseLinkUrl(url) };
|
|
47796
|
+
if (options2.tailscale) {
|
|
47797
|
+
const port2 = parsePort("--port", options2.port);
|
|
47798
|
+
return {
|
|
47799
|
+
name,
|
|
47800
|
+
url: await tailscaleLinkUrl(
|
|
47801
|
+
options2.tailscale,
|
|
47802
|
+
port2,
|
|
47803
|
+
resolveTailnetSuffix
|
|
47804
|
+
)
|
|
47805
|
+
};
|
|
47806
|
+
}
|
|
47622
47807
|
if (!options2.ssh)
|
|
47623
|
-
throw new Error(
|
|
47808
|
+
throw new Error(
|
|
47809
|
+
"pass the peer's <url>, --tailscale <host> --port <port> or --ssh <alias> --port <port>"
|
|
47810
|
+
);
|
|
47624
47811
|
const port = parsePort("--port", options2.port);
|
|
47625
47812
|
const localPort = options2.localPort ? parsePort("--local-port", options2.localPort) : freeTunnelPort(port, others);
|
|
47626
47813
|
return { name, ssh: options2.ssh, port, localPort };
|
|
47627
47814
|
}
|
|
47815
|
+
function freeTunnelPort(port, others) {
|
|
47816
|
+
const taken = new Set(others.map((link3) => toLinkSpec(link3).ssh?.localPort));
|
|
47817
|
+
let candidate = defaultTunnelPort(port);
|
|
47818
|
+
while (taken.has(candidate)) candidate++;
|
|
47819
|
+
return candidate;
|
|
47820
|
+
}
|
|
47821
|
+
|
|
47822
|
+
// src/commands/sessions/nodes/linkNode.ts
|
|
47628
47823
|
async function linkNode(name, url, options2) {
|
|
47629
47824
|
if (name.includes(":")) throw new Error("node names cannot contain ':'");
|
|
47630
47825
|
if (name === resolveNodeName())
|
|
47631
47826
|
throw new Error(`${name} is this node; link to a different node`);
|
|
47632
47827
|
const others = readLinks().filter((link4) => link4.name !== name);
|
|
47633
|
-
const link3 = buildLink(name, url, options2, others);
|
|
47828
|
+
const link3 = await buildLink(name, url, options2, others);
|
|
47634
47829
|
await writeLinks([...others, link3]);
|
|
47635
47830
|
console.log(
|
|
47636
|
-
|
|
47831
|
+
chalk256.green(`Linked ${name} (${describeTarget(toLinkSpec(link3))})`)
|
|
47637
47832
|
);
|
|
47638
47833
|
await notifyDaemonLinks();
|
|
47639
47834
|
}
|
|
47640
47835
|
|
|
47641
|
-
// src/commands/sessions/nodes/
|
|
47642
|
-
|
|
47643
|
-
|
|
47644
|
-
|
|
47645
|
-
|
|
47646
|
-
|
|
47647
|
-
|
|
47648
|
-
|
|
47649
|
-
|
|
47650
|
-
|
|
47651
|
-
|
|
47652
|
-
|
|
47653
|
-
|
|
47654
|
-
|
|
47655
|
-
|
|
47656
|
-
|
|
47657
|
-
|
|
47658
|
-
}
|
|
47659
|
-
console.log(`${report2.local} ${chalk256.dim("(this node)")}`);
|
|
47660
|
-
if (!report2.daemonRunning)
|
|
47661
|
-
console.log(chalk256.dim(" daemon not running; showing configured links"));
|
|
47662
|
-
if (report2.links.length === 0) console.log(chalk256.dim(" no links"));
|
|
47663
|
-
for (const link3 of report2.links) console.log(formatLink(link3));
|
|
47836
|
+
// src/commands/sessions/nodes/registerLinkCommand.ts
|
|
47837
|
+
function registerLinkCommand(nodes) {
|
|
47838
|
+
nodes.command("link <name> [url]").description(
|
|
47839
|
+
"Link a peer node by its web server URL, by its Tailscale name with --tailscale and --port, or over ssh with --ssh and --port (name must match the peer's sessions.nodeName)"
|
|
47840
|
+
).option(
|
|
47841
|
+
"--tailscale <host>",
|
|
47842
|
+
"Dial the peer at https://<host>.<tailnet>.ts.net:<port> (the peer runs tailscale serve for its web server port)"
|
|
47843
|
+
).option(
|
|
47844
|
+
"--ssh <alias>",
|
|
47845
|
+
"Tunnel to the peer through this ~/.ssh/config alias"
|
|
47846
|
+
).option(
|
|
47847
|
+
"--port <port>",
|
|
47848
|
+
"The peer's web server port (with --tailscale or --ssh)"
|
|
47849
|
+
).option(
|
|
47850
|
+
"--local-port <port>",
|
|
47851
|
+
"Local end of the ssh tunnel (default: 43000 + port % 1000, bumped past other links)"
|
|
47852
|
+
).action(linkNode);
|
|
47664
47853
|
}
|
|
47665
47854
|
|
|
47666
|
-
// src/commands/sessions/nodes/
|
|
47667
|
-
|
|
47668
|
-
|
|
47669
|
-
|
|
47670
|
-
|
|
47671
|
-
|
|
47672
|
-
|
|
47673
|
-
|
|
47674
|
-
const
|
|
47675
|
-
if (!
|
|
47676
|
-
throw new Error(`No link named ${name}; see assist sessions nodes`);
|
|
47677
|
-
try {
|
|
47678
|
-
return await fetchPeerJson(
|
|
47679
|
-
link3.url,
|
|
47680
|
-
`/api/daemon-log?lines=${lines2}`
|
|
47681
|
-
);
|
|
47682
|
-
} catch (error) {
|
|
47855
|
+
// src/commands/sessions/nodes/serveNode.ts
|
|
47856
|
+
import { execFile as execFile14, spawnSync as spawnSync12 } from "child_process";
|
|
47857
|
+
import { promisify as promisify13 } from "util";
|
|
47858
|
+
import chalk257 from "chalk";
|
|
47859
|
+
|
|
47860
|
+
// src/commands/sessions/nodes/planServe.ts
|
|
47861
|
+
function planServe(status3, serveConfig2, port) {
|
|
47862
|
+
tailnetSuffix(status3);
|
|
47863
|
+
const fqdn = status3.Self?.DNSName?.replace(/\.$/, "");
|
|
47864
|
+
if (!fqdn)
|
|
47683
47865
|
throw new Error(
|
|
47684
|
-
|
|
47866
|
+
`\`${tailscaleCli()} status --json\` reports no DNS name for this machine`
|
|
47685
47867
|
);
|
|
47686
|
-
}
|
|
47868
|
+
const target = `http://127.0.0.1:${port}`;
|
|
47869
|
+
const served = serveConfig2.Web?.[`${fqdn}:${port}`]?.Handlers?.["/"]?.Proxy;
|
|
47870
|
+
return {
|
|
47871
|
+
host: fqdn.split(".")[0],
|
|
47872
|
+
url: `https://${fqdn}:${port}`,
|
|
47873
|
+
target,
|
|
47874
|
+
alreadyServing: served?.replace(/\/$/, "") === target,
|
|
47875
|
+
args: ["serve", "--bg", `--https=${port}`, target]
|
|
47876
|
+
};
|
|
47687
47877
|
}
|
|
47688
|
-
|
|
47689
|
-
|
|
47690
|
-
|
|
47691
|
-
|
|
47692
|
-
const
|
|
47693
|
-
|
|
47694
|
-
|
|
47695
|
-
|
|
47878
|
+
|
|
47879
|
+
// src/commands/sessions/nodes/serveNode.ts
|
|
47880
|
+
var execFileAsync12 = promisify13(execFile14);
|
|
47881
|
+
async function serveConfig() {
|
|
47882
|
+
const { stdout } = await execFileAsync12(
|
|
47883
|
+
tailscaleCli(),
|
|
47884
|
+
["serve", "status", "--json"],
|
|
47885
|
+
{ encoding: "utf8", timeout: 1e4, windowsHide: true }
|
|
47886
|
+
);
|
|
47887
|
+
return stdout.trim() ? JSON.parse(stdout) : {};
|
|
47888
|
+
}
|
|
47889
|
+
function runServe(args) {
|
|
47890
|
+
console.log(chalk257.dim(`${tailscaleCli()} ${args.join(" ")}`));
|
|
47891
|
+
const result = spawnSync12(tailscaleCli(), args, { stdio: "inherit" });
|
|
47892
|
+
if (result.error) throw result.error;
|
|
47893
|
+
if (result.status !== 0)
|
|
47894
|
+
throw new Error(`tailscale serve exited with ${result.status}`);
|
|
47895
|
+
}
|
|
47896
|
+
async function serveNode(options2) {
|
|
47897
|
+
const port = parsePort("--port", options2.port);
|
|
47898
|
+
const plan2 = planServe(await tailscaleStatus(), await serveConfig(), port);
|
|
47899
|
+
if (plan2.alreadyServing) {
|
|
47900
|
+
console.log(chalk257.dim(`${plan2.url} already serves ${plan2.target}`));
|
|
47901
|
+
} else {
|
|
47902
|
+
runServe(plan2.args);
|
|
47903
|
+
console.log(chalk257.green(`Serving ${plan2.target} at ${plan2.url}`));
|
|
47696
47904
|
}
|
|
47697
|
-
|
|
47905
|
+
console.log("Link this node from another node with:");
|
|
47906
|
+
console.log(
|
|
47907
|
+
` assist sessions nodes link ${resolveNodeName()} --tailscale ${plan2.host} --port ${port}`
|
|
47908
|
+
);
|
|
47698
47909
|
}
|
|
47699
47910
|
|
|
47700
47911
|
// src/commands/sessions/nodes/unlinkNode.ts
|
|
47701
|
-
import
|
|
47912
|
+
import chalk258 from "chalk";
|
|
47702
47913
|
async function unlinkNode(name) {
|
|
47703
47914
|
const links2 = readLinks();
|
|
47704
47915
|
const remaining = links2.filter((link3) => link3.name !== name);
|
|
47705
47916
|
if (remaining.length === links2.length) {
|
|
47706
|
-
console.log(
|
|
47917
|
+
console.log(chalk258.yellow(`No link named ${name}`));
|
|
47707
47918
|
return;
|
|
47708
47919
|
}
|
|
47709
47920
|
await writeLinks(remaining);
|
|
47710
|
-
console.log(
|
|
47921
|
+
console.log(chalk258.green(`Unlinked ${name}`));
|
|
47711
47922
|
await notifyDaemonLinks();
|
|
47712
47923
|
}
|
|
47713
47924
|
|
|
47714
47925
|
// src/commands/sessions/nodes/registerNodes.ts
|
|
47715
47926
|
function registerNodes(sessions) {
|
|
47716
47927
|
const cmd = sessions.command("nodes").description("List this node and every linked node with its link state").option("--json", "Output as JSON").action(listNodes);
|
|
47717
|
-
cmd
|
|
47718
|
-
"Link a peer node by its web server URL, or over ssh with --ssh and --port (name must match the peer's sessions.nodeName)"
|
|
47719
|
-
).option(
|
|
47720
|
-
"--ssh <alias>",
|
|
47721
|
-
"Tunnel to the peer through this ~/.ssh/config alias"
|
|
47722
|
-
).option("--port <port>", "The peer's loopback web server port (with --ssh)").option(
|
|
47723
|
-
"--local-port <port>",
|
|
47724
|
-
"Local end of the ssh tunnel (default: 43000 + port % 1000, bumped past other links)"
|
|
47725
|
-
).action(linkNode);
|
|
47928
|
+
registerLinkCommand(cmd);
|
|
47726
47929
|
cmd.command("unlink <name>").description("Remove a linked node").action(unlinkNode);
|
|
47930
|
+
cmd.command("serve").description(
|
|
47931
|
+
"Expose this node's web server on the tailnet with tailscale serve (idempotent) and print the command that links it"
|
|
47932
|
+
).option("-p, --port <port>", "This node's web server port", "3100").action(serveNode);
|
|
47727
47933
|
cmd.command("doctor [name]").description(
|
|
47728
47934
|
"Probe each hop of every link (or one) in order and stop at the first failure with a remediation"
|
|
47729
47935
|
).option("--json", "Output as JSON").action(
|
|
@@ -47846,7 +48052,7 @@ async function renameSession(title) {
|
|
|
47846
48052
|
|
|
47847
48053
|
// src/commands/sessions/summarise.ts
|
|
47848
48054
|
import * as fs58 from "fs";
|
|
47849
|
-
import
|
|
48055
|
+
import chalk259 from "chalk";
|
|
47850
48056
|
|
|
47851
48057
|
// src/commands/sessions/summarise/shared.ts
|
|
47852
48058
|
import * as fs57 from "fs";
|
|
@@ -47905,22 +48111,22 @@ ${firstMessage}`);
|
|
|
47905
48111
|
async function summarise2(options2) {
|
|
47906
48112
|
const files = await discoverSessionFiles();
|
|
47907
48113
|
if (files.length === 0) {
|
|
47908
|
-
console.log(
|
|
48114
|
+
console.log(chalk259.yellow("No sessions found."));
|
|
47909
48115
|
return;
|
|
47910
48116
|
}
|
|
47911
48117
|
const toProcess = selectCandidates(files, options2);
|
|
47912
48118
|
if (toProcess.length === 0) {
|
|
47913
|
-
console.log(
|
|
48119
|
+
console.log(chalk259.green("All sessions already summarised."));
|
|
47914
48120
|
return;
|
|
47915
48121
|
}
|
|
47916
48122
|
console.log(
|
|
47917
|
-
|
|
48123
|
+
chalk259.cyan(
|
|
47918
48124
|
`Summarising ${toProcess.length} session(s) (${files.length} total)\u2026`
|
|
47919
48125
|
)
|
|
47920
48126
|
);
|
|
47921
48127
|
const { succeeded, failed: failed3 } = processSessions(toProcess);
|
|
47922
48128
|
console.log(
|
|
47923
|
-
|
|
48129
|
+
chalk259.green(`Done: ${succeeded} summarised`) + (failed3 > 0 ? chalk259.yellow(`, ${failed3} skipped`) : "")
|
|
47924
48130
|
);
|
|
47925
48131
|
}
|
|
47926
48132
|
function selectCandidates(files, options2) {
|
|
@@ -47940,16 +48146,16 @@ function processSessions(files) {
|
|
|
47940
48146
|
let failed3 = 0;
|
|
47941
48147
|
for (let i = 0; i < files.length; i++) {
|
|
47942
48148
|
const file = files[i];
|
|
47943
|
-
process.stdout.write(
|
|
48149
|
+
process.stdout.write(chalk259.dim(` [${i + 1}/${files.length}] `));
|
|
47944
48150
|
const summary = summariseSession(file);
|
|
47945
48151
|
if (summary) {
|
|
47946
48152
|
writeSummary(file, summary);
|
|
47947
48153
|
succeeded++;
|
|
47948
|
-
process.stdout.write(`${
|
|
48154
|
+
process.stdout.write(`${chalk259.green("\u2713")} ${summary}
|
|
47949
48155
|
`);
|
|
47950
48156
|
} else {
|
|
47951
48157
|
failed3++;
|
|
47952
|
-
process.stdout.write(` ${
|
|
48158
|
+
process.stdout.write(` ${chalk259.yellow("skip")}
|
|
47953
48159
|
`);
|
|
47954
48160
|
}
|
|
47955
48161
|
}
|
|
@@ -47979,10 +48185,10 @@ function registerSessions(program2) {
|
|
|
47979
48185
|
}
|
|
47980
48186
|
|
|
47981
48187
|
// src/reportCliError.ts
|
|
47982
|
-
import
|
|
48188
|
+
import chalk260 from "chalk";
|
|
47983
48189
|
function reportCliError(error) {
|
|
47984
48190
|
if (error instanceof InvalidItemIdError || error instanceof AmbiguousRepoConfigError || error instanceof UnknownRepoConfigError || error instanceof MissingRunCwdError || error instanceof MiroExtractError) {
|
|
47985
|
-
console.error(
|
|
48191
|
+
console.error(chalk260.red(error.message));
|
|
47986
48192
|
} else {
|
|
47987
48193
|
console.error(error);
|
|
47988
48194
|
}
|