@proagentstore/cli 0.4.46 → 0.4.47
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/browser-runner/coding/terminal.js +104 -1
- package/dist/index.js +137 -32
- package/package.json +1 -1
|
@@ -76,7 +76,14 @@ export function activeTerminalCommand(target, backend) {
|
|
|
76
76
|
switch (t.backend) {
|
|
77
77
|
case "tmux": {
|
|
78
78
|
const out = tmuxExec(["display-message", "-p", "-t", t.id, "#{pane_current_command}"]).trim();
|
|
79
|
-
|
|
79
|
+
// `#{pane_current_command}` reads the pane process's argv, and Claude Code rewrites
|
|
80
|
+
// its own — measured on this platform's flagship case, tmux answered `2.1.226` (the
|
|
81
|
+
// version) for a pane whose process `comm` was `claude`. So `aiCliDrives` was 0 for a
|
|
82
|
+
// week of driving Claude Code through a tmux Operator. `comm` is the executable name
|
|
83
|
+
// and no title rewrite touches it, so ask the process tree when the argv is not a
|
|
84
|
+
// name (#498). Only then: a pane reporting `zsh` genuinely has nothing in the
|
|
85
|
+
// foreground, and promoting a background child would be an invention.
|
|
86
|
+
return resolvePaneCommand(out, () => paneDescendantComms(t.id));
|
|
80
87
|
}
|
|
81
88
|
case "kitty":
|
|
82
89
|
return kittyForegroundCommand(t.id);
|
|
@@ -90,6 +97,102 @@ export function activeTerminalCommand(target, backend) {
|
|
|
90
97
|
return null;
|
|
91
98
|
}
|
|
92
99
|
}
|
|
100
|
+
/** Shell names that mean "the pane is at a prompt", not "this program is running". */
|
|
101
|
+
const SHELL_NAMES = new Set(["sh", "bash", "zsh", "fish", "dash", "ksh", "tcsh", "csh", "login", "screen", "tmux"]);
|
|
102
|
+
/** A program name is a bare token. A version (`2.1.226`) and a sentence are both NOT one. */
|
|
103
|
+
export function looksLikeCommandName(raw) {
|
|
104
|
+
const value = String(raw ?? "").trim();
|
|
105
|
+
if (!value || value.length > 60)
|
|
106
|
+
return false;
|
|
107
|
+
if (/^v?\d+(\.\d+)+$/.test(value))
|
|
108
|
+
return false; // a version string is not a program name
|
|
109
|
+
return /^[A-Za-z_][\w.@+-]*$/.test(value);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* What the pane is running, given what tmux SAID and a way to ask the process tree (#498).
|
|
113
|
+
*
|
|
114
|
+
* Pure so the measured fixture — tmux says `2.1.226`, the pane's child `comm` is `claude` — is a
|
|
115
|
+
* unit test rather than a story. Keeps tmux's answer whenever it is a name: that is the fast path,
|
|
116
|
+
* it costs no extra exec, and the descendant walk can only ever be a second-best guess.
|
|
117
|
+
*/
|
|
118
|
+
export function resolvePaneCommand(reported, listDescendantComms) {
|
|
119
|
+
const value = String(reported ?? "").trim();
|
|
120
|
+
if (looksLikeCommandName(value))
|
|
121
|
+
return value;
|
|
122
|
+
// The probe is best-effort by construction: `pgrep`/`ps` availability differs by platform, and
|
|
123
|
+
// a missing one must degrade to what tmux said, not lose it.
|
|
124
|
+
let descendants = [];
|
|
125
|
+
try {
|
|
126
|
+
descendants = listDescendantComms();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
descendants = [];
|
|
130
|
+
}
|
|
131
|
+
for (const raw of descendants) {
|
|
132
|
+
const name = (String(raw ?? "").trim().split("/").pop() ?? "").trim();
|
|
133
|
+
if (!looksLikeCommandName(name))
|
|
134
|
+
continue;
|
|
135
|
+
if (SHELL_NAMES.has(name.toLowerCase()))
|
|
136
|
+
continue;
|
|
137
|
+
return name;
|
|
138
|
+
}
|
|
139
|
+
// Nothing better found: hand back exactly what tmux said. The cloud decides what an
|
|
140
|
+
// unreadable value means; this module does not invent a name it did not read.
|
|
141
|
+
return value || null;
|
|
142
|
+
}
|
|
143
|
+
/** Bounded, so a probe on a path a Loop drives continuously can never become the slow part. */
|
|
144
|
+
const PROBE_TIMEOUT_MS = 1_000;
|
|
145
|
+
/** `comm` for the pane's descendant processes, nearest first. `[]` on any failure. */
|
|
146
|
+
function paneDescendantComms(paneId) {
|
|
147
|
+
let pid = "";
|
|
148
|
+
try {
|
|
149
|
+
pid = tmuxExec(["display-message", "-p", "-t", paneId, "#{pane_pid}"], PROBE_TIMEOUT_MS).trim();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
if (!/^\d+$/.test(pid))
|
|
155
|
+
return [];
|
|
156
|
+
const found = [];
|
|
157
|
+
let frontier = [pid];
|
|
158
|
+
// Two levels is enough for `zsh → claude` and for one wrapper in between, and it bounds the
|
|
159
|
+
// work: `pgrep`/`ps` availability differs by platform, so every step degrades to today's
|
|
160
|
+
// answer rather than throwing (the runner is macOS/Linux; kitty and iTerm2 are untouched).
|
|
161
|
+
for (let depth = 0; depth < 2 && frontier.length > 0; depth++) {
|
|
162
|
+
const children = childPids(frontier);
|
|
163
|
+
if (children.length === 0)
|
|
164
|
+
break;
|
|
165
|
+
found.push(...commsFor(children));
|
|
166
|
+
frontier = children;
|
|
167
|
+
}
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
170
|
+
function childPids(parents) {
|
|
171
|
+
try {
|
|
172
|
+
const out = execFileSync("pgrep", ["-P", parents.join(",")], {
|
|
173
|
+
encoding: "utf8",
|
|
174
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
175
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
176
|
+
});
|
|
177
|
+
return out.split("\n").map((s) => s.trim()).filter((s) => /^\d+$/.test(s)).slice(0, 16);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return []; // pgrep exits non-zero when nothing matches — that is an answer, not an error
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function commsFor(pids) {
|
|
184
|
+
try {
|
|
185
|
+
const out = execFileSync("ps", ["-o", "comm=", "-p", pids.join(",")], {
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
188
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
189
|
+
});
|
|
190
|
+
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
93
196
|
/** The foreground process of a kitty window, from `kitty @ ls`. Null if it can't be determined. */
|
|
94
197
|
function kittyForegroundCommand(id) {
|
|
95
198
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1214,11 +1214,44 @@ function diffMembership(attached, eligible, thisNode, blocked = /* @__PURE__ */
|
|
|
1214
1214
|
detach: [...have].filter((id) => !wantIds.has(id))
|
|
1215
1215
|
};
|
|
1216
1216
|
}
|
|
1217
|
+
function shouldRegisterOnOpen(reconnect, alreadyRegistered) {
|
|
1218
|
+
return reconnect || !alreadyRegistered;
|
|
1219
|
+
}
|
|
1217
1220
|
function instanceLabel(inst) {
|
|
1218
1221
|
const short = `${inst.id.slice(0, 8)}\u2026`;
|
|
1219
1222
|
return inst.name ? `${inst.name} (${short})` : short;
|
|
1220
1223
|
}
|
|
1221
1224
|
|
|
1225
|
+
// src/commands/runner/status-line.ts
|
|
1226
|
+
var STATUS_PREFIX = "PAGS-STATUS";
|
|
1227
|
+
function formatStatusLine(status) {
|
|
1228
|
+
const parts = [];
|
|
1229
|
+
if (status.registration) parts.push(`registration=${status.registration}`);
|
|
1230
|
+
if (status.heartbeat) parts.push(`heartbeat=${status.heartbeat}`);
|
|
1231
|
+
if (status.agents) parts.push(`agents=${status.agents}`);
|
|
1232
|
+
if (status.reason) parts.push(`reason=${status.reason.replace(/\s+/g, " ").trim().slice(0, 200)}`);
|
|
1233
|
+
return `${STATUS_PREFIX} ${parts.join(" ")}`;
|
|
1234
|
+
}
|
|
1235
|
+
function parseStatusLine(line) {
|
|
1236
|
+
const trimmed = line.trim();
|
|
1237
|
+
if (!trimmed.startsWith(`${STATUS_PREFIX} `)) return null;
|
|
1238
|
+
const body = trimmed.slice(STATUS_PREFIX.length + 1);
|
|
1239
|
+
const status = {};
|
|
1240
|
+
const reasonAt = body.indexOf("reason=");
|
|
1241
|
+
const head = reasonAt >= 0 ? body.slice(0, reasonAt) : body;
|
|
1242
|
+
if (reasonAt >= 0) {
|
|
1243
|
+
const reason = body.slice(reasonAt + "reason=".length).trim();
|
|
1244
|
+
if (reason) status.reason = reason;
|
|
1245
|
+
}
|
|
1246
|
+
for (const token of head.split(/\s+/).filter(Boolean)) {
|
|
1247
|
+
const [key, value] = token.split("=", 2);
|
|
1248
|
+
if (key === "registration" && (value === "ok" || value === "partial" || value === "fail")) status.registration = value;
|
|
1249
|
+
else if (key === "heartbeat" && (value === "ok" || value === "fail")) status.heartbeat = value;
|
|
1250
|
+
else if (key === "agents" && value) status.agents = value;
|
|
1251
|
+
}
|
|
1252
|
+
return status;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1222
1255
|
// src/commands/runner/relay.ts
|
|
1223
1256
|
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
|
|
1224
1257
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
@@ -1228,7 +1261,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1228
1261
|
const machine = loadMachineIdentity(runnerNode);
|
|
1229
1262
|
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
|
|
1230
1263
|
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
1231
|
-
const
|
|
1264
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1265
|
+
let lastRegisterError = "";
|
|
1266
|
+
const registerRuntime = async (id, forceClaim = force) => {
|
|
1232
1267
|
try {
|
|
1233
1268
|
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
1234
1269
|
endpointUrl: localUrl,
|
|
@@ -1239,13 +1274,23 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1239
1274
|
runnerNode,
|
|
1240
1275
|
machineId: machine.id,
|
|
1241
1276
|
machineNames: machine.names,
|
|
1242
|
-
force
|
|
1277
|
+
force: forceClaim
|
|
1243
1278
|
});
|
|
1279
|
+
registered.add(id);
|
|
1280
|
+
return true;
|
|
1244
1281
|
} catch (e) {
|
|
1245
1282
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1283
|
+
registered.delete(id);
|
|
1284
|
+
lastRegisterError = msg;
|
|
1246
1285
|
writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
|
|
1286
|
+
return false;
|
|
1247
1287
|
}
|
|
1248
1288
|
};
|
|
1289
|
+
const reportRegistration = () => {
|
|
1290
|
+
const agents = `${registered.size}/${instanceIds.length}`;
|
|
1291
|
+
const state = registered.size === instanceIds.length ? "ok" : registered.size === 0 ? "fail" : "partial";
|
|
1292
|
+
writeLine(formatStatusLine({ registration: state, agents, reason: state === "ok" ? void 0 : lastRegisterError }));
|
|
1293
|
+
};
|
|
1249
1294
|
for (const id of instanceIds) await registerRuntime(id);
|
|
1250
1295
|
const attached = /* @__PURE__ */ new Map();
|
|
1251
1296
|
const blocked = /* @__PURE__ */ new Set();
|
|
@@ -1254,10 +1299,31 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1254
1299
|
const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken }, {}).then((r) => r.token);
|
|
1255
1300
|
attached.set(
|
|
1256
1301
|
id,
|
|
1257
|
-
openRelaySocket(
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1302
|
+
openRelaySocket(
|
|
1303
|
+
id,
|
|
1304
|
+
apiBase,
|
|
1305
|
+
mintToken,
|
|
1306
|
+
localUrl,
|
|
1307
|
+
runnerToken,
|
|
1308
|
+
force,
|
|
1309
|
+
(conflicted) => {
|
|
1310
|
+
blocked.add(conflicted);
|
|
1311
|
+
attached.delete(conflicted);
|
|
1312
|
+
},
|
|
1313
|
+
// Registration rides the RECONNECT, which is the whole wake case (#497). The socket
|
|
1314
|
+
// retries with backoff; `POST …/runtime` did not, so after a sleep the machine had a
|
|
1315
|
+
// live relay and no runtime row — and `resumeSessionsForNode`, which lives inside that
|
|
1316
|
+
// route, never ran either, so its own suspended coding sessions stayed suspended. The
|
|
1317
|
+
// upsert is idempotent, so re-registering on every reconnect is safe. The first open
|
|
1318
|
+
// is skipped when the register already succeeded above (or in the discovery pass),
|
|
1319
|
+
// and taken when it did not — which is how a register lost to a boot-time
|
|
1320
|
+
// `fetch failed` finally gets a second chance.
|
|
1321
|
+
async (openedId, reconnect) => {
|
|
1322
|
+
if (!shouldRegisterOnOpen(reconnect, registered.has(openedId))) return;
|
|
1323
|
+
await registerRuntime(openedId, reconnect ? false : force);
|
|
1324
|
+
reportRegistration();
|
|
1325
|
+
}
|
|
1326
|
+
)
|
|
1261
1327
|
);
|
|
1262
1328
|
if (label) writeLine(`Attached agent: ${label}`);
|
|
1263
1329
|
};
|
|
@@ -1269,7 +1335,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1269
1335
|
writeLine(`Detached agent: ${label}`);
|
|
1270
1336
|
};
|
|
1271
1337
|
for (const id of instanceIds) attach(id, "");
|
|
1272
|
-
|
|
1338
|
+
reportRegistration();
|
|
1339
|
+
writeLine(registered.size === instanceIds.length ? `Runtime registered with PAGS \u2713 (${registered.size}/${instanceIds.length} agents)` : `Runtime registration incomplete: ${registered.size}/${instanceIds.length} agents \u2014 retried on each relay (re)connect.`);
|
|
1273
1340
|
writeLine("");
|
|
1274
1341
|
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");
|
|
1275
1342
|
writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname3()}`);
|
|
@@ -1289,9 +1356,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1289
1356
|
}
|
|
1290
1357
|
if (failure && !heartbeatFailing) {
|
|
1291
1358
|
heartbeatFailing = true;
|
|
1359
|
+
writeLine(formatStatusLine({ heartbeat: "fail", reason: failure }));
|
|
1292
1360
|
writeError(`Heartbeat failed: ${failure} \u2014 the console will show this machine as OFFLINE until it recovers. The relay itself is still connected; don't run \`pags up --force\` elsewhere.`);
|
|
1293
1361
|
} else if (!failure && heartbeatFailing) {
|
|
1294
1362
|
heartbeatFailing = false;
|
|
1363
|
+
writeLine(formatStatusLine({ heartbeat: "ok" }));
|
|
1295
1364
|
writeLine("Heartbeat recovered \u2014 this machine reads as online again.");
|
|
1296
1365
|
}
|
|
1297
1366
|
heartbeat();
|
|
@@ -1300,10 +1369,23 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1300
1369
|
};
|
|
1301
1370
|
heartbeat();
|
|
1302
1371
|
if (watchInstances) startDiscovery();
|
|
1372
|
+
async function clearFinishedConflicts() {
|
|
1373
|
+
for (const id of [...blocked]) {
|
|
1374
|
+
const free = await requestPags(
|
|
1375
|
+
"GET",
|
|
1376
|
+
`/v1/relay/${apiPathSegment(id)}/status`,
|
|
1377
|
+
{ ...opts, pagsToken }
|
|
1378
|
+
).then((r) => r.connected === false).catch(() => false);
|
|
1379
|
+
if (!free) continue;
|
|
1380
|
+
blocked.delete(id);
|
|
1381
|
+
writeLine(`Relay conflict cleared: ${id.slice(0, 8)}\u2026 \u2014 the other runner is gone; reattaching.`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1303
1384
|
function startDiscovery() {
|
|
1304
1385
|
const tick = () => {
|
|
1305
1386
|
const timer = setTimeout(async () => {
|
|
1306
1387
|
try {
|
|
1388
|
+
await clearFinishedConflicts();
|
|
1307
1389
|
const res = await requestPags(
|
|
1308
1390
|
"GET",
|
|
1309
1391
|
"/v1/instances/my/instances",
|
|
@@ -1333,10 +1415,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1333
1415
|
tick();
|
|
1334
1416
|
}
|
|
1335
1417
|
}
|
|
1336
|
-
function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict) {
|
|
1418
|
+
function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen) {
|
|
1337
1419
|
let backoffMs = 1e3;
|
|
1338
1420
|
let reconnecting = false;
|
|
1339
1421
|
let closed = false;
|
|
1422
|
+
let opened = false;
|
|
1340
1423
|
let socket = null;
|
|
1341
1424
|
let retryTimer = null;
|
|
1342
1425
|
const connect = async () => {
|
|
@@ -1365,7 +1448,10 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
|
|
|
1365
1448
|
socket = ws;
|
|
1366
1449
|
ws.onopen = () => {
|
|
1367
1450
|
backoffMs = 1e3;
|
|
1451
|
+
const reconnect = opened;
|
|
1452
|
+
opened = true;
|
|
1368
1453
|
writeLine(`Relay connected: ${instanceId.slice(0, 8)}\u2026`);
|
|
1454
|
+
void Promise.resolve(onOpen?.(instanceId, reconnect)).catch(() => void 0);
|
|
1369
1455
|
};
|
|
1370
1456
|
ws.onmessage = async (event) => {
|
|
1371
1457
|
const text = typeof event.data === "string" ? event.data : String(event.data);
|
|
@@ -1652,31 +1738,42 @@ function printLogo(version2) {
|
|
|
1652
1738
|
console.log(pad + d(" Browser runner") + (version2 ? d(` \xB7 v${version2}`) : ""));
|
|
1653
1739
|
console.log("");
|
|
1654
1740
|
}
|
|
1741
|
+
var STATUS_ROWS = {
|
|
1742
|
+
runner: { label: "Browser", ok: "running on your computer", busy: "starting up\u2026", bad: "stopped \u2014 press r to retry" },
|
|
1743
|
+
tunnel: { label: "Secure link", ok: "connected to ProAgentStore", busy: "opening\u2026", bad: "offline" },
|
|
1744
|
+
pags: { label: "ProAgentStore", ok: "connected \u2014 ready for jobs", busy: "registering\u2026", bad: "not registered (retries automatically)" }
|
|
1745
|
+
};
|
|
1746
|
+
var LABEL_WIDTH = Math.max(...Object.values(STATUS_ROWS).map((r) => r.label.length)) + 2;
|
|
1655
1747
|
function describe(kind, s) {
|
|
1656
1748
|
const ok = s === "online" || s === "registered";
|
|
1657
1749
|
const busy = s === "starting" || s === "pending";
|
|
1658
1750
|
const icon = ok ? chalk.green("\u2713") : busy ? chalk.yellow("\u2026") : chalk.red("\u2717");
|
|
1659
|
-
const
|
|
1660
|
-
runner: { label: "Browser", ok: "running on your computer", busy: "starting up\u2026", bad: "stopped \u2014 press r to retry" },
|
|
1661
|
-
tunnel: { label: "Secure link", ok: "connected to ProAgentStore", busy: "opening\u2026", bad: "offline" },
|
|
1662
|
-
pags: { label: "ProAgentStore", ok: "connected \u2014 ready for jobs", busy: "registering\u2026", bad: "not registered (retries automatically)" }
|
|
1663
|
-
};
|
|
1664
|
-
const m = map[kind];
|
|
1751
|
+
const m = STATUS_ROWS[kind];
|
|
1665
1752
|
return { icon, label: m.label, note: ok ? m.ok : busy ? m.busy : m.bad };
|
|
1666
1753
|
}
|
|
1754
|
+
function connectingNote(elapsedMs) {
|
|
1755
|
+
if (elapsedMs < 3e4) return "Setting things up\u2026 this takes a few seconds. Keep this window open.";
|
|
1756
|
+
const mins = Math.floor(elapsedMs / 6e4);
|
|
1757
|
+
const elapsed = mins >= 1 ? `${mins}m` : `${Math.floor(elapsedMs / 1e3)}s`;
|
|
1758
|
+
return `Still connecting \u2014 ${elapsed} elapsed. Press l for logs.`;
|
|
1759
|
+
}
|
|
1760
|
+
function pagsNote(registration, heartbeat) {
|
|
1761
|
+
if (registration !== "registered" || heartbeat !== "failing") return void 0;
|
|
1762
|
+
return "registered \u2014 but the heartbeat is failing, so the website reads this machine as offline";
|
|
1763
|
+
}
|
|
1667
1764
|
function printStatus(state) {
|
|
1668
1765
|
clearScreen();
|
|
1669
1766
|
printLogo(state.version);
|
|
1670
1767
|
const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
|
|
1671
1768
|
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname4()));
|
|
1672
1769
|
console.log("");
|
|
1673
|
-
const row = (kind, s) => {
|
|
1770
|
+
const row = (kind, s, override) => {
|
|
1674
1771
|
const { icon, label, note } = describe(kind, s);
|
|
1675
|
-
console.log(pad + icon + " " + w(label.padEnd(
|
|
1772
|
+
console.log(pad + icon + " " + w(label.padEnd(LABEL_WIDTH)) + d(override ?? note));
|
|
1676
1773
|
};
|
|
1677
1774
|
row("runner", state.runner);
|
|
1678
1775
|
row("tunnel", state.tunnel);
|
|
1679
|
-
row("pags", state.registration);
|
|
1776
|
+
row("pags", state.registration, pagsNote(state.registration, state.heartbeat));
|
|
1680
1777
|
console.log("");
|
|
1681
1778
|
if (connected) {
|
|
1682
1779
|
console.log(pad + chalk.green("\u2713 You're all set!") + d(" Your agent can now act on the web."));
|
|
@@ -1688,7 +1785,7 @@ function printStatus(state) {
|
|
|
1688
1785
|
console.log("");
|
|
1689
1786
|
console.log(pad + d("The website can take a few seconds to show \u201Conline\u201D \u2014 that's normal."));
|
|
1690
1787
|
} else {
|
|
1691
|
-
console.log(pad + d(
|
|
1788
|
+
console.log(pad + d(connectingNote(state.startedAt ? Date.now() - state.startedAt : 0)));
|
|
1692
1789
|
if (state.lastEvent) console.log(pad + d("Status: ") + d(state.lastEvent));
|
|
1693
1790
|
}
|
|
1694
1791
|
console.log("");
|
|
@@ -1761,9 +1858,13 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
1761
1858
|
tunnel: "offline",
|
|
1762
1859
|
tunnelUrl: "",
|
|
1763
1860
|
registration: "pending",
|
|
1861
|
+
heartbeat: "ok",
|
|
1764
1862
|
lastEvent: "Fetching instances...",
|
|
1765
1863
|
taskCount: 0,
|
|
1766
|
-
version: CLI_VERSION2
|
|
1864
|
+
version: CLI_VERSION2,
|
|
1865
|
+
// What "a few seconds" is measured against: a state that never resolves was described
|
|
1866
|
+
// as taking a few seconds, indefinitely, because nothing counted (#497).
|
|
1867
|
+
startedAt: Date.now()
|
|
1767
1868
|
};
|
|
1768
1869
|
clearScreen();
|
|
1769
1870
|
printLogo(CLI_VERSION2);
|
|
@@ -1826,6 +1927,19 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
1826
1927
|
if (!trimmed) continue;
|
|
1827
1928
|
logs.push(trimmed);
|
|
1828
1929
|
if (logs.length > 200) logs.shift();
|
|
1930
|
+
const status = parseStatusLine(trimmed);
|
|
1931
|
+
if (status) {
|
|
1932
|
+
if (status.registration) {
|
|
1933
|
+
state.registration = status.registration === "ok" ? "registered" : "failed";
|
|
1934
|
+
state.lastEvent = status.registration === "ok" ? `Registered with PAGS \u2014 ${status.agents ?? "all"} agents ready` : `PAGS registration ${status.registration}${status.agents ? ` (${status.agents} agents)` : ""}${status.reason ? `: ${status.reason}` : ""}`;
|
|
1935
|
+
}
|
|
1936
|
+
if (status.heartbeat) {
|
|
1937
|
+
state.heartbeat = status.heartbeat === "ok" ? "ok" : "failing";
|
|
1938
|
+
state.lastEvent = status.heartbeat === "ok" ? "Heartbeat recovered \u2014 this machine reads as online again" : `Heartbeat failing${status.reason ? `: ${status.reason}` : ""} \u2014 the console will show this machine offline`;
|
|
1939
|
+
}
|
|
1940
|
+
printStatus(state);
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1829
1943
|
if (trimmed.includes("Relay connected:")) {
|
|
1830
1944
|
state.tunnel = "online";
|
|
1831
1945
|
state.tunnelUrl = "WebSocket relay";
|
|
@@ -1836,7 +1950,6 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
1836
1950
|
if (trimmed.includes("WebSocket relay")) {
|
|
1837
1951
|
state.tunnel = "online";
|
|
1838
1952
|
state.tunnelUrl = "WebSocket relay";
|
|
1839
|
-
state.registration = "registered";
|
|
1840
1953
|
state.lastEvent = "Connected via WebSocket relay";
|
|
1841
1954
|
printStatus(state);
|
|
1842
1955
|
continue;
|
|
@@ -1847,21 +1960,13 @@ var upCommand = new Command8("up").description("Start the browser runner for all
|
|
|
1847
1960
|
printStatus(state);
|
|
1848
1961
|
continue;
|
|
1849
1962
|
}
|
|
1850
|
-
if (trimmed.includes("
|
|
1851
|
-
state.
|
|
1852
|
-
state.lastEvent = "Registered with PAGS \u2014 ready for tasks";
|
|
1853
|
-
printStatus(state);
|
|
1854
|
-
continue;
|
|
1855
|
-
}
|
|
1856
|
-
if (trimmed.includes("Another machine")) {
|
|
1857
|
-
state.registration = "failed";
|
|
1858
|
-
state.lastEvent = trimmed.slice(0, 80);
|
|
1963
|
+
if (trimmed.includes("Relay conflict:")) {
|
|
1964
|
+
state.lastEvent = "Another runner holds this agent \u2014 run `pags up --force` here to take it over";
|
|
1859
1965
|
printStatus(state);
|
|
1860
1966
|
continue;
|
|
1861
1967
|
}
|
|
1862
|
-
if (trimmed.includes("
|
|
1863
|
-
state.
|
|
1864
|
-
state.lastEvent = "PAGS registration failed";
|
|
1968
|
+
if (trimmed.includes("Relay conflict cleared:")) {
|
|
1969
|
+
state.lastEvent = "Relay conflict cleared \u2014 reattaching";
|
|
1865
1970
|
printStatus(state);
|
|
1866
1971
|
continue;
|
|
1867
1972
|
}
|