@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.
Files changed (2) hide show
  1. package/dist/index.js +59 -356
  2. 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 createRequire2 } from "module";
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 { chmod, mkdir, writeFile } from "fs/promises";
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
- await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
917
- endpointUrl: localUrl,
918
- token: runnerToken,
919
- placement: "local",
920
- capabilities: caps,
921
- runnerVersion: "",
922
- runnerNode: hostname()
923
- }).catch((e) => writeError(`register ${id.slice(0, 8)}\u2026 failed: ${e instanceof Error ? e.message : String(e)}`));
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(" \u2705 CONNECTED \u2014 WebSocket relay (no tunnel)");
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, open a Cloudflare quick tunnel, 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("--cloudflared <path>", "cloudflared executable").option("--runner-version <version>", "Runner version").option("--skip-probe", "Skip PAGS FAGS-runtime probe after registration").option("--tunnel <mode>", "Tunnel mode: 'ws' (WebSocket relay), 'named' (production), or 'quick' (default)", "quick").action(async (instanceIds, opts) => {
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
- if (opts.tunnel === "ws") {
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 CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
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("--tunnel <mode>", "Tunnel mode: 'ws' (WebSocket relay, recommended), 'named' (production, stable), or 'quick' (default, trycloudflare.com)", "quick").action(async (opts) => {
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: CLI_VERSION
1217
+ version: CLI_VERSION2
1506
1218
  };
1507
1219
  clearScreen();
1508
- printLogo(CLI_VERSION);
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.tunnel === "named") args.push("--tunnel", "named");
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 (no tunnel)")) {
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 (will retry on next task)";
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).filter((l) => !l.includes("INF "));
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 (cleans up any stuck processes)").action(async () => {
1366
+ var downCommand = new Command7("down").description("Stop the browser runner and disconnect").action(async () => {
1664
1367
  clearScreen();
1665
- printLogo(CLI_VERSION);
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 = createRequire2(import.meta.url);
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.2.10",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",