@proagentstore/cli 0.2.10 → 0.3.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/dist/index.js +59 -356
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
|
-
import { createRequire as
|
|
10
|
+
import { createRequire as createRequire3 } from "module";
|
|
11
11
|
import { Command as Command8 } from "commander";
|
|
12
12
|
|
|
13
13
|
// src/commands/check.ts
|
|
@@ -614,13 +614,19 @@ var publishCommand = new Command5("publish").description("Publish an agent to Pr
|
|
|
614
614
|
import { spawn as spawn2 } from "child_process";
|
|
615
615
|
import { randomUUID } from "crypto";
|
|
616
616
|
import { existsSync as existsSync5 } from "fs";
|
|
617
|
-
import {
|
|
618
|
-
import { gunzipSync } from "zlib";
|
|
619
|
-
import { arch, homedir as homedir2, hostname, platform } from "os";
|
|
617
|
+
import { hostname } from "os";
|
|
620
618
|
import { resolve as resolve4 } from "path";
|
|
621
619
|
import { createServer as createServer2 } from "net";
|
|
622
620
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
621
|
+
import { createRequire } from "module";
|
|
623
622
|
import { Command as Command6 } from "commander";
|
|
623
|
+
var CLI_VERSION = (() => {
|
|
624
|
+
try {
|
|
625
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
626
|
+
} catch {
|
|
627
|
+
return "";
|
|
628
|
+
}
|
|
629
|
+
})();
|
|
624
630
|
async function findFreePort2(start) {
|
|
625
631
|
for (let p = start; p < start + 25; p++) {
|
|
626
632
|
const free = await new Promise((res) => {
|
|
@@ -664,122 +670,6 @@ function buildRunnerArgs(opts) {
|
|
|
664
670
|
if (opts.headless) args.push("--headless");
|
|
665
671
|
return args;
|
|
666
672
|
}
|
|
667
|
-
function buildCloudflaredArgs(localUrl) {
|
|
668
|
-
return ["tunnel", "--url", localUrl];
|
|
669
|
-
}
|
|
670
|
-
function cloudflaredAssetForPlatform(os = platform(), cpu = arch()) {
|
|
671
|
-
if (os === "darwin") {
|
|
672
|
-
if (cpu === "arm64") {
|
|
673
|
-
return {
|
|
674
|
-
asset: "cloudflared-darwin-arm64.tgz",
|
|
675
|
-
executableName: "cloudflared",
|
|
676
|
-
archive: true
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
if (cpu === "x64") {
|
|
680
|
-
return {
|
|
681
|
-
asset: "cloudflared-darwin-amd64.tgz",
|
|
682
|
-
executableName: "cloudflared",
|
|
683
|
-
archive: true
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (os === "linux") {
|
|
688
|
-
if (cpu === "x64") {
|
|
689
|
-
return {
|
|
690
|
-
asset: "cloudflared-linux-amd64",
|
|
691
|
-
executableName: "cloudflared",
|
|
692
|
-
archive: false
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
if (cpu === "arm64") {
|
|
696
|
-
return {
|
|
697
|
-
asset: "cloudflared-linux-arm64",
|
|
698
|
-
executableName: "cloudflared",
|
|
699
|
-
archive: false
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
if (os === "win32" && cpu === "x64") {
|
|
704
|
-
return {
|
|
705
|
-
asset: "cloudflared-windows-amd64.exe",
|
|
706
|
-
executableName: "cloudflared.exe",
|
|
707
|
-
archive: false
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
throw new Error(
|
|
711
|
-
`Automatic cloudflared download is not supported on ${os}/${cpu}. Install cloudflared and pass --cloudflared <path>.`
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
function cloudflaredDownloadUrl(asset) {
|
|
715
|
-
return `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`;
|
|
716
|
-
}
|
|
717
|
-
function extractCloudflaredBinary(asset, bytes) {
|
|
718
|
-
const buffer = Buffer.from(bytes);
|
|
719
|
-
if (!asset.archive) return buffer;
|
|
720
|
-
const tar = gunzipSync(buffer);
|
|
721
|
-
for (let offset = 0; offset + 512 <= tar.length; ) {
|
|
722
|
-
const header = tar.subarray(offset, offset + 512);
|
|
723
|
-
if (header.every((byte) => byte === 0)) break;
|
|
724
|
-
const rawName = header.subarray(0, 100).toString("utf8");
|
|
725
|
-
const name = rawName.slice(0, rawName.indexOf("\0") === -1 ? void 0 : rawName.indexOf("\0"));
|
|
726
|
-
const rawSize = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim();
|
|
727
|
-
const size = Number.parseInt(rawSize || "0", 8);
|
|
728
|
-
const contentStart = offset + 512;
|
|
729
|
-
const contentEnd = contentStart + size;
|
|
730
|
-
if (name.split("/").pop() === asset.executableName) {
|
|
731
|
-
return tar.subarray(contentStart, contentEnd);
|
|
732
|
-
}
|
|
733
|
-
offset = contentStart + Math.ceil(size / 512) * 512;
|
|
734
|
-
}
|
|
735
|
-
throw new Error(`Downloaded ${asset.asset} did not contain ${asset.executableName}`);
|
|
736
|
-
}
|
|
737
|
-
function cloudflaredCacheDir() {
|
|
738
|
-
return resolve4(homedir2(), ".config", "proagentstore", "bin");
|
|
739
|
-
}
|
|
740
|
-
function cachedCloudflaredPath(asset) {
|
|
741
|
-
const suffix = asset.asset.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
742
|
-
return resolve4(cloudflaredCacheDir(), `${asset.executableName}-${suffix}`);
|
|
743
|
-
}
|
|
744
|
-
async function commandAvailable(command) {
|
|
745
|
-
return new Promise((resolvePromise) => {
|
|
746
|
-
const child = spawn2(command, ["--version"], {
|
|
747
|
-
stdio: "ignore",
|
|
748
|
-
shell: process.platform === "win32"
|
|
749
|
-
});
|
|
750
|
-
child.on("error", () => resolvePromise(false));
|
|
751
|
-
child.on("exit", (code) => resolvePromise(code === 0));
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
async function downloadCloudflared(asset) {
|
|
755
|
-
const target = cachedCloudflaredPath(asset);
|
|
756
|
-
if (existsSync5(target)) return target;
|
|
757
|
-
await mkdir(cloudflaredCacheDir(), { recursive: true });
|
|
758
|
-
const url = cloudflaredDownloadUrl(asset.asset);
|
|
759
|
-
writeLine(`cloudflared not found; downloading ${asset.asset}...`);
|
|
760
|
-
const res = await fetch(url);
|
|
761
|
-
if (!res.ok) {
|
|
762
|
-
throw new Error(`cloudflared download failed: ${res.status} ${res.statusText}`);
|
|
763
|
-
}
|
|
764
|
-
const binary = extractCloudflaredBinary(asset, new Uint8Array(await res.arrayBuffer()));
|
|
765
|
-
await writeFile(target, binary);
|
|
766
|
-
if (process.platform !== "win32") await chmod(target, 493);
|
|
767
|
-
writeLine(`cloudflared cached at ${target}`);
|
|
768
|
-
return target;
|
|
769
|
-
}
|
|
770
|
-
async function resolveCloudflaredCommand(requested) {
|
|
771
|
-
const explicit = clean(requested);
|
|
772
|
-
if (explicit && explicit !== "cloudflared") return explicit;
|
|
773
|
-
if (await commandAvailable("cloudflared")) return "cloudflared";
|
|
774
|
-
return downloadCloudflared(cloudflaredAssetForPlatform());
|
|
775
|
-
}
|
|
776
|
-
function parseCloudflaredTunnelUrl(text) {
|
|
777
|
-
const matches = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/gi) || [];
|
|
778
|
-
for (const m of matches) {
|
|
779
|
-
if (!/^https:\/\/api\.trycloudflare\.com/i.test(m)) return m;
|
|
780
|
-
}
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
783
673
|
function buildRuntimeRegistrationBody(opts, capabilities = []) {
|
|
784
674
|
return {
|
|
785
675
|
endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
|
|
@@ -906,29 +796,42 @@ function responseErrorMessage(data, text, statusText) {
|
|
|
906
796
|
function collectCapability(value, previous = []) {
|
|
907
797
|
return [...previous, value];
|
|
908
798
|
}
|
|
909
|
-
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts) {
|
|
799
|
+
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false) {
|
|
910
800
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
911
801
|
const pagsToken = clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
|
|
912
802
|
if (!pagsToken) throw new Error("PAGS token required for WebSocket relay");
|
|
913
803
|
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
|
|
914
804
|
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
915
805
|
for (const id of instanceIds) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
806
|
+
try {
|
|
807
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
808
|
+
endpointUrl: localUrl,
|
|
809
|
+
token: runnerToken,
|
|
810
|
+
placement: "local",
|
|
811
|
+
capabilities: caps,
|
|
812
|
+
runnerVersion: CLI_VERSION,
|
|
813
|
+
runnerNode: hostname(),
|
|
814
|
+
force
|
|
815
|
+
});
|
|
816
|
+
} catch (e) {
|
|
817
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
818
|
+
if (msg.includes("409") && msg.includes("Another machine")) {
|
|
819
|
+
writeError(msg);
|
|
820
|
+
writeLine("");
|
|
821
|
+
writeLine(" To take over, run: pags up --tunnel ws --force");
|
|
822
|
+
writeLine("");
|
|
823
|
+
throw new Error("Another machine is already connected");
|
|
824
|
+
}
|
|
825
|
+
writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
|
|
826
|
+
}
|
|
924
827
|
}
|
|
925
828
|
for (const id of instanceIds) {
|
|
926
|
-
openRelaySocket(id, apiBase, pagsToken, localUrl, runnerToken);
|
|
829
|
+
openRelaySocket(id, apiBase, pagsToken, localUrl, runnerToken, force);
|
|
927
830
|
}
|
|
928
831
|
writeLine("Runtime registered with PAGS \u2713");
|
|
929
832
|
writeLine("");
|
|
930
833
|
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
931
|
-
writeLine(
|
|
834
|
+
writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname()}`);
|
|
932
835
|
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
|
|
933
836
|
writeLine(" No cloudflared needed. Ctrl+C to disconnect.");
|
|
934
837
|
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
@@ -943,11 +846,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts) {
|
|
|
943
846
|
};
|
|
944
847
|
heartbeat();
|
|
945
848
|
}
|
|
946
|
-
function openRelaySocket(instanceId, wsBase, pagsToken, localUrl, runnerToken) {
|
|
849
|
+
function openRelaySocket(instanceId, wsBase, pagsToken, localUrl, runnerToken, force = false) {
|
|
947
850
|
let backoffMs = 1e3;
|
|
948
851
|
let reconnecting = false;
|
|
949
852
|
const connect = () => {
|
|
950
|
-
const url = `${wsBase}/v1/relay/${encodeURIComponent(instanceId)}/connect?token=${encodeURIComponent(pagsToken)}`;
|
|
853
|
+
const url = `${wsBase}/v1/relay/${encodeURIComponent(instanceId)}/connect?token=${encodeURIComponent(pagsToken)}${force ? "&force=1" : ""}`;
|
|
951
854
|
const ws = new WebSocket(url);
|
|
952
855
|
ws.onopen = () => {
|
|
953
856
|
backoffMs = 1e3;
|
|
@@ -1022,18 +925,13 @@ function createRunnerCommand() {
|
|
|
1022
925
|
command.command("start").description("Start the local FAGS browser runtime in the foreground").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Require this bearer token").option("--instance-id <id>", "Bind runner requests to a PAGS instance id").option("--headless", "Run Playwright headless").action(async (opts) => {
|
|
1023
926
|
await startRunnerForeground(opts);
|
|
1024
927
|
});
|
|
1025
|
-
command.command("connect <instanceIds...>").description("Start ONE local runtime,
|
|
928
|
+
command.command("connect <instanceIds...>").description("Start ONE local runtime, connect via WebSocket relay, and register it for every given PAGS instance").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--runner-version <version>", "Runner version").option("--force", "Take over from another connected machine").action(async (instanceIds, opts) => {
|
|
1026
929
|
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
|
|
1027
930
|
const host = clean(opts.host) || "127.0.0.1";
|
|
1028
931
|
const port = clean(opts.port) || String(await findFreePort2(49171));
|
|
1029
932
|
const localUrl = `http://${host}:${port}`;
|
|
1030
933
|
const primary = instanceIds[0];
|
|
1031
|
-
const runnerOpts = {
|
|
1032
|
-
...opts,
|
|
1033
|
-
host,
|
|
1034
|
-
port,
|
|
1035
|
-
token: runnerToken
|
|
1036
|
-
};
|
|
934
|
+
const runnerOpts = { ...opts, host, port, token: runnerToken };
|
|
1037
935
|
const spec = runnerSpawnSpec(runnerOpts);
|
|
1038
936
|
const runner = spawn2(spec.command, spec.args, {
|
|
1039
937
|
cwd: spec.cwd,
|
|
@@ -1045,9 +943,7 @@ function createRunnerCommand() {
|
|
|
1045
943
|
runner.on("exit", (code) => {
|
|
1046
944
|
if (code && code !== 0) writeError(`runner exited with code ${code}`);
|
|
1047
945
|
});
|
|
1048
|
-
let tunnel = null;
|
|
1049
946
|
const shutdown = () => {
|
|
1050
|
-
if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
|
|
1051
947
|
if (!runner.killed) runner.kill("SIGTERM");
|
|
1052
948
|
};
|
|
1053
949
|
process.once("SIGINT", () => {
|
|
@@ -1061,193 +957,10 @@ function createRunnerCommand() {
|
|
|
1061
957
|
try {
|
|
1062
958
|
await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId: primary });
|
|
1063
959
|
writeLine(`Local FAGS runtime healthy at ${localUrl}`);
|
|
1064
|
-
|
|
1065
|
-
await connectViaRelay(instanceIds, localUrl, runnerToken, opts);
|
|
1066
|
-
await new Promise((resolvePromise) => {
|
|
1067
|
-
runner.on("exit", () => resolvePromise());
|
|
1068
|
-
});
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
if (opts.tunnel === "named") {
|
|
1072
|
-
writeLine("Provisioning named tunnel\u2026");
|
|
1073
|
-
const provRes = await requestPags("POST", "/v1/tunnel/provision", opts, {});
|
|
1074
|
-
const prov = provRes;
|
|
1075
|
-
if (!prov.connectorToken) {
|
|
1076
|
-
writeError(`Named tunnel provisioning failed: ${prov.error || "no token returned"}. Falling back to quick tunnel.`);
|
|
1077
|
-
} else {
|
|
1078
|
-
const tunnelUrl2 = prov.endpointUrl || `https://${prov.hostname}`;
|
|
1079
|
-
writeLine(`Named tunnel: ${tunnelUrl2}`);
|
|
1080
|
-
const cloudflaredPath2 = await resolveCloudflaredCommand(opts.cloudflared);
|
|
1081
|
-
const tunnelProc = spawn2(cloudflaredPath2, ["tunnel", "run", "--token", prov.connectorToken], {
|
|
1082
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1083
|
-
shell: process.platform === "win32"
|
|
1084
|
-
});
|
|
1085
|
-
tunnel = tunnelProc;
|
|
1086
|
-
tunnelProc.stdout?.on("data", (d2) => {
|
|
1087
|
-
const t = d2.toString("utf-8");
|
|
1088
|
-
if (/registered/.test(t) || /CONNECTED/.test(t)) writeLine("Named tunnel connected \u2713");
|
|
1089
|
-
});
|
|
1090
|
-
tunnelProc.stderr?.on("data", (d2) => {
|
|
1091
|
-
const t = d2.toString("utf-8");
|
|
1092
|
-
if (/registered/.test(t) || /CONNECTED/.test(t)) writeLine("Named tunnel connected \u2713");
|
|
1093
|
-
});
|
|
1094
|
-
await new Promise((r) => setTimeout(r, 4e3));
|
|
1095
|
-
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: primary });
|
|
1096
|
-
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
1097
|
-
for (const id of instanceIds) {
|
|
1098
|
-
try {
|
|
1099
|
-
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
1100
|
-
endpointUrl: tunnelUrl2,
|
|
1101
|
-
token: runnerToken,
|
|
1102
|
-
placement: "local",
|
|
1103
|
-
capabilities: caps,
|
|
1104
|
-
runnerVersion: clean(opts.runnerVersion) || "",
|
|
1105
|
-
runnerNode: hostname()
|
|
1106
|
-
});
|
|
1107
|
-
} catch (error) {
|
|
1108
|
-
writeError(`register ${id.slice(0, 8)}\u2026 failed (${error instanceof Error ? error.message : String(error)})`);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
writeLine("Runtime registered with PAGS \u2713");
|
|
1112
|
-
writeLine("");
|
|
1113
|
-
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1114
|
-
writeLine(" \u2705 CONNECTED \u2014 named tunnel (production)");
|
|
1115
|
-
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
|
|
1116
|
-
writeLine(` Endpoint: ${tunnelUrl2}`);
|
|
1117
|
-
writeLine(" Stable URL \u2014 no reconnect churn. Ctrl+C to disconnect.");
|
|
1118
|
-
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1119
|
-
let stopped2 = false;
|
|
1120
|
-
void (async () => {
|
|
1121
|
-
while (!stopped2) {
|
|
1122
|
-
await new Promise((r) => setTimeout(r, 3e4));
|
|
1123
|
-
if (stopped2) break;
|
|
1124
|
-
for (const id of instanceIds) {
|
|
1125
|
-
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).catch(() => void 0);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
})();
|
|
1129
|
-
await new Promise((resolvePromise) => {
|
|
1130
|
-
runner.on("exit", () => resolvePromise());
|
|
1131
|
-
});
|
|
1132
|
-
stopped2 = true;
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
const cloudflaredPath = await resolveCloudflaredCommand(opts.cloudflared);
|
|
1137
|
-
const openTunnel = () => new Promise((resolveTunnel, reject) => {
|
|
1138
|
-
let output = "";
|
|
1139
|
-
const proc = spawn2(cloudflaredPath, buildCloudflaredArgs(localUrl), {
|
|
1140
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1141
|
-
shell: process.platform === "win32"
|
|
1142
|
-
});
|
|
1143
|
-
const onData = (data) => {
|
|
1144
|
-
output += data.toString("utf-8");
|
|
1145
|
-
const parsed = parseCloudflaredTunnelUrl(output);
|
|
1146
|
-
if (parsed) resolveTunnel({ url: parsed, proc });
|
|
1147
|
-
};
|
|
1148
|
-
proc.stdout?.on("data", onData);
|
|
1149
|
-
proc.stderr?.on("data", onData);
|
|
1150
|
-
proc.on("error", reject);
|
|
1151
|
-
setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
|
|
1152
|
-
});
|
|
1153
|
-
const registerRuntime = async (url) => {
|
|
1154
|
-
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: primary });
|
|
1155
|
-
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
1156
|
-
for (const id of instanceIds) {
|
|
1157
|
-
try {
|
|
1158
|
-
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
1159
|
-
endpointUrl: url,
|
|
1160
|
-
token: runnerToken,
|
|
1161
|
-
placement: "local",
|
|
1162
|
-
capabilities: caps,
|
|
1163
|
-
runnerVersion: clean(opts.runnerVersion) || "",
|
|
1164
|
-
runnerNode: hostname()
|
|
1165
|
-
});
|
|
1166
|
-
} catch (error) {
|
|
1167
|
-
writeError(`register ${id.slice(0, 8)}\u2026 failed (${error instanceof Error ? error.message : String(error)}); will retry`);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
const first = await openTunnel();
|
|
1172
|
-
tunnel = first.proc;
|
|
1173
|
-
let tunnelUrl = first.url;
|
|
1174
|
-
await registerRuntime(tunnelUrl).catch((error) => {
|
|
1175
|
-
writeError(`registration will retry (${error instanceof Error ? error.message : String(error)})`);
|
|
1176
|
-
});
|
|
1177
|
-
writeLine("Runtime registered with PAGS \u2713");
|
|
1178
|
-
writeLine("");
|
|
1179
|
-
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1180
|
-
writeLine(" \u2705 CONNECTED \u2014 runner is live");
|
|
1181
|
-
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"} served by this runner`);
|
|
1182
|
-
writeLine(` Tunnel: ${tunnelUrl}`);
|
|
1183
|
-
writeLine(" Auto-reconnect is on \u2014 keep this terminal open. Ctrl+C to disconnect.");
|
|
1184
|
-
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1185
|
-
let stopped = false;
|
|
1186
|
-
let consecutiveFailures = 0;
|
|
1187
|
-
let respawnBackoffMs = 3e4;
|
|
1188
|
-
const PROBE_FAIL_THRESHOLD = 3;
|
|
1189
|
-
const MAX_BACKOFF_MS = 5 * 6e4;
|
|
1190
|
-
const probeTunnel = async (url) => {
|
|
1191
|
-
try {
|
|
1192
|
-
const res = await fetch(`${url.replace(/\/$/, "")}/health`, {
|
|
1193
|
-
headers: { Authorization: `Bearer ${runnerToken}`, "X-PAGS-Instance-Id": primary },
|
|
1194
|
-
signal: AbortSignal.timeout(1e4)
|
|
1195
|
-
});
|
|
1196
|
-
return res.ok;
|
|
1197
|
-
} catch {
|
|
1198
|
-
return false;
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
const heartbeatAll = async () => {
|
|
1202
|
-
let allOk = true;
|
|
1203
|
-
for (const id of instanceIds) {
|
|
1204
|
-
const ok = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).then(() => true).catch(() => false);
|
|
1205
|
-
if (!ok) allOk = false;
|
|
1206
|
-
}
|
|
1207
|
-
return allOk;
|
|
1208
|
-
};
|
|
1209
|
-
void (async () => {
|
|
1210
|
-
while (!stopped) {
|
|
1211
|
-
await new Promise((r) => setTimeout(r, 3e4));
|
|
1212
|
-
if (stopped) break;
|
|
1213
|
-
if (await probeTunnel(tunnelUrl)) {
|
|
1214
|
-
consecutiveFailures = 0;
|
|
1215
|
-
respawnBackoffMs = 3e4;
|
|
1216
|
-
const beat = await heartbeatAll();
|
|
1217
|
-
if (!beat) {
|
|
1218
|
-
await registerRuntime(tunnelUrl).then(() => writeLine("\u2705 Re-registered with PAGS")).catch(() => void 0);
|
|
1219
|
-
}
|
|
1220
|
-
continue;
|
|
1221
|
-
}
|
|
1222
|
-
consecutiveFailures++;
|
|
1223
|
-
if (consecutiveFailures < PROBE_FAIL_THRESHOLD) {
|
|
1224
|
-
writeLine(`\u26A0 Tunnel probe failed (${consecutiveFailures}/${PROBE_FAIL_THRESHOLD}) \u2014 will retry`);
|
|
1225
|
-
continue;
|
|
1226
|
-
}
|
|
1227
|
-
writeLine("\u26A0 Tunnel unreachable \u2014 reconnecting\u2026");
|
|
1228
|
-
consecutiveFailures = 0;
|
|
1229
|
-
try {
|
|
1230
|
-
if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
|
|
1231
|
-
} catch {
|
|
1232
|
-
}
|
|
1233
|
-
try {
|
|
1234
|
-
const next = await openTunnel();
|
|
1235
|
-
tunnel = next.proc;
|
|
1236
|
-
tunnelUrl = next.url;
|
|
1237
|
-
await registerRuntime(tunnelUrl);
|
|
1238
|
-
respawnBackoffMs = 3e4;
|
|
1239
|
-
writeLine(`\u2705 Reconnected: ${tunnelUrl}`);
|
|
1240
|
-
} catch (error) {
|
|
1241
|
-
writeError(`reconnect failed (${error instanceof Error ? error.message : String(error)}); retrying in ${Math.round(respawnBackoffMs / 1e3)}s`);
|
|
1242
|
-
await new Promise((r) => setTimeout(r, respawnBackoffMs));
|
|
1243
|
-
respawnBackoffMs = Math.min(respawnBackoffMs * 2, MAX_BACKOFF_MS);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
})();
|
|
960
|
+
await connectViaRelay(instanceIds, localUrl, runnerToken, opts, Boolean(opts.force));
|
|
1247
961
|
await new Promise((resolvePromise) => {
|
|
1248
962
|
runner.on("exit", () => resolvePromise());
|
|
1249
963
|
});
|
|
1250
|
-
stopped = true;
|
|
1251
964
|
} catch (error) {
|
|
1252
965
|
shutdown();
|
|
1253
966
|
throw error;
|
|
@@ -1363,7 +1076,7 @@ function createRunnerCommand() {
|
|
|
1363
1076
|
}
|
|
1364
1077
|
|
|
1365
1078
|
// src/commands/up.ts
|
|
1366
|
-
import { createRequire } from "module";
|
|
1079
|
+
import { createRequire as createRequire2 } from "module";
|
|
1367
1080
|
import { Command as Command7 } from "commander";
|
|
1368
1081
|
|
|
1369
1082
|
// src/tui.ts
|
|
@@ -1470,15 +1183,14 @@ async function waitForKey(keys) {
|
|
|
1470
1183
|
|
|
1471
1184
|
// src/commands/up.ts
|
|
1472
1185
|
var API_BASE2 = "https://api.proagentstore.online";
|
|
1473
|
-
var
|
|
1186
|
+
var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
|
|
1474
1187
|
async function stopRunnerProcesses() {
|
|
1475
1188
|
if (process.platform === "win32") return false;
|
|
1476
1189
|
const { execSync } = await import("child_process");
|
|
1477
1190
|
const patterns = [
|
|
1478
1191
|
"dist/browser-runner/index.js",
|
|
1479
1192
|
"browser-runner/src/index",
|
|
1480
|
-
"runner connect"
|
|
1481
|
-
"tunnel --url http://127.0.0.1"
|
|
1193
|
+
"runner connect"
|
|
1482
1194
|
];
|
|
1483
1195
|
let stopped = false;
|
|
1484
1196
|
for (const p of patterns) {
|
|
@@ -1490,7 +1202,7 @@ async function stopRunnerProcesses() {
|
|
|
1490
1202
|
}
|
|
1491
1203
|
return stopped;
|
|
1492
1204
|
}
|
|
1493
|
-
var upCommand = new Command7("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--
|
|
1205
|
+
var upCommand = new Command7("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--force", "Take over from another connected machine").action(async (opts) => {
|
|
1494
1206
|
const session = requireSession();
|
|
1495
1207
|
const state = {
|
|
1496
1208
|
user: session.user.login,
|
|
@@ -1502,10 +1214,10 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1502
1214
|
registration: "pending",
|
|
1503
1215
|
lastEvent: "Fetching instances...",
|
|
1504
1216
|
taskCount: 0,
|
|
1505
|
-
version:
|
|
1217
|
+
version: CLI_VERSION2
|
|
1506
1218
|
};
|
|
1507
1219
|
clearScreen();
|
|
1508
|
-
printLogo(
|
|
1220
|
+
printLogo(CLI_VERSION2);
|
|
1509
1221
|
printStep("Signed in as " + session.user.login, "ok");
|
|
1510
1222
|
printStep("Fetching instances...", "wait");
|
|
1511
1223
|
const res = await fetch(`${API_BASE2}/v1/instances/my/instances`, {
|
|
@@ -1537,8 +1249,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1537
1249
|
const cliPath = process.argv[1];
|
|
1538
1250
|
const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
|
|
1539
1251
|
if (opts.headless) args.push("--headless");
|
|
1540
|
-
if (opts.
|
|
1541
|
-
if (opts.tunnel === "ws") args.push("--tunnel", "ws");
|
|
1252
|
+
if (opts.force) args.push("--force");
|
|
1542
1253
|
const child = spawn3(process.execPath, args, {
|
|
1543
1254
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1544
1255
|
env: { ...process.env, PAGS_TOKEN: session.token }
|
|
@@ -1551,14 +1262,6 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1551
1262
|
if (!trimmed) continue;
|
|
1552
1263
|
logs.push(trimmed);
|
|
1553
1264
|
if (logs.length > 200) logs.shift();
|
|
1554
|
-
const tunnelMatch = trimmed.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
1555
|
-
if (tunnelMatch && !/^https:\/\/api\.trycloudflare\.com/i.test(tunnelMatch[0])) {
|
|
1556
|
-
state.tunnel = "online";
|
|
1557
|
-
state.tunnelUrl = tunnelMatch[0];
|
|
1558
|
-
state.lastEvent = "Tunnel created";
|
|
1559
|
-
printStatus(state);
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
1265
|
if (trimmed.includes("Relay connected:")) {
|
|
1563
1266
|
state.tunnel = "online";
|
|
1564
1267
|
state.tunnelUrl = "WebSocket relay";
|
|
@@ -1566,7 +1269,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1566
1269
|
printStatus(state);
|
|
1567
1270
|
continue;
|
|
1568
1271
|
}
|
|
1569
|
-
if (trimmed.includes("WebSocket relay
|
|
1272
|
+
if (trimmed.includes("WebSocket relay")) {
|
|
1570
1273
|
state.tunnel = "online";
|
|
1571
1274
|
state.tunnelUrl = "WebSocket relay";
|
|
1572
1275
|
state.registration = "registered";
|
|
@@ -1586,13 +1289,18 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1586
1289
|
printStatus(state);
|
|
1587
1290
|
continue;
|
|
1588
1291
|
}
|
|
1292
|
+
if (trimmed.includes("Another machine")) {
|
|
1293
|
+
state.registration = "failed";
|
|
1294
|
+
state.lastEvent = trimmed.slice(0, 80);
|
|
1295
|
+
printStatus(state);
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1589
1298
|
if (trimmed.includes("fetch failed")) {
|
|
1590
1299
|
state.registration = "failed";
|
|
1591
|
-
state.lastEvent = "PAGS registration failed
|
|
1300
|
+
state.lastEvent = "PAGS registration failed";
|
|
1592
1301
|
printStatus(state);
|
|
1593
1302
|
continue;
|
|
1594
1303
|
}
|
|
1595
|
-
if (trimmed.includes("INF ")) continue;
|
|
1596
1304
|
if (/error|Error|EADDRINUSE|ECONNREFUSED|failed/i.test(trimmed)) {
|
|
1597
1305
|
state.lastEvent = trimmed.slice(0, 80);
|
|
1598
1306
|
printStatus(state);
|
|
@@ -1607,7 +1315,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1607
1315
|
if (code && code !== 0) {
|
|
1608
1316
|
state.runner = "error";
|
|
1609
1317
|
state.lastEvent = `Runner exited (code ${code})`;
|
|
1610
|
-
const recent = logs.slice(-5)
|
|
1318
|
+
const recent = logs.slice(-5);
|
|
1611
1319
|
if (recent.length) state.lastEvent += ": " + recent[recent.length - 1].slice(0, 60);
|
|
1612
1320
|
printStatus(state);
|
|
1613
1321
|
}
|
|
@@ -1641,14 +1349,9 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1641
1349
|
if (key === "r") {
|
|
1642
1350
|
if (childDead) {
|
|
1643
1351
|
writeLine(" Restarting runner...");
|
|
1644
|
-
state.runner = "starting";
|
|
1645
|
-
state.tunnel = "offline";
|
|
1646
|
-
state.tunnelUrl = "";
|
|
1647
|
-
state.registration = "pending";
|
|
1648
|
-
state.lastEvent = "Restarting...";
|
|
1649
1352
|
const { execSync } = await import("child_process");
|
|
1650
1353
|
try {
|
|
1651
|
-
execSync(`${process.execPath} ${process.argv[1]} up${opts.headless ? " --headless" : ""}`, {
|
|
1354
|
+
execSync(`${process.execPath} ${process.argv[1]} up${opts.headless ? " --headless" : ""}${opts.force ? " --force" : ""}`, {
|
|
1652
1355
|
stdio: "inherit",
|
|
1653
1356
|
env: process.env
|
|
1654
1357
|
});
|
|
@@ -1660,9 +1363,9 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1660
1363
|
}
|
|
1661
1364
|
}
|
|
1662
1365
|
});
|
|
1663
|
-
var downCommand = new Command7("down").description("Stop the browser runner and disconnect
|
|
1366
|
+
var downCommand = new Command7("down").description("Stop the browser runner and disconnect").action(async () => {
|
|
1664
1367
|
clearScreen();
|
|
1665
|
-
printLogo(
|
|
1368
|
+
printLogo(CLI_VERSION2);
|
|
1666
1369
|
if (process.platform === "win32") {
|
|
1667
1370
|
writeLine(" On Windows: switch to the 'pags up' window and press Ctrl+C to disconnect.");
|
|
1668
1371
|
writeLine("");
|
|
@@ -1680,7 +1383,7 @@ var downCommand = new Command7("down").description("Stop the browser runner and
|
|
|
1680
1383
|
});
|
|
1681
1384
|
|
|
1682
1385
|
// src/index.ts
|
|
1683
|
-
var require2 =
|
|
1386
|
+
var require2 = createRequire3(import.meta.url);
|
|
1684
1387
|
var { version } = require2("../package.json");
|
|
1685
1388
|
var program = new Command8();
|
|
1686
1389
|
program.name("pags").description(
|