@proagentstore/cli 0.4.152 → 0.4.154
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/engine-check.js +41 -1
- package/dist/browser-runner/coding/engine-spawn.js +20 -1
- package/dist/browser-runner/coding/grok-continuity.js +53 -0
- package/dist/browser-runner/coding/headless-state.js +1 -1
- package/dist/browser-runner/coding/headless.js +27 -1
- package/dist/index.js +153 -37
- package/package.json +1 -1
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { unwrapLauncher } from "./launcher-wrapper.js";
|
|
3
|
-
import { statSync } from "node:fs";
|
|
3
|
+
import { realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { handlerFor } from "./handlers.js";
|
|
7
7
|
import { parseCommand } from "./headless.js";
|
|
8
8
|
import { resolveEngineSpawn } from "./engine-spawn.js";
|
|
9
|
+
function safeRealpath(p) {
|
|
10
|
+
try {
|
|
11
|
+
return realpathSync(p);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function defaultVersionText(launch, env) {
|
|
18
|
+
// The file the check FOUND, not the bare name: off Windows `command` is the name as given, which spawnSync would
|
|
19
|
+
// resolve against this process's PATH rather than the one being checked — and report another binary's version.
|
|
20
|
+
const command = launch.via === "direct" && launch.binaryPath ? launch.binaryPath : launch.command;
|
|
21
|
+
const r = spawnSync(command, launch.args, { encoding: "utf8", env, timeout: 5_000, windowsHide: true, windowsVerbatimArguments: launch.windowsVerbatimArguments });
|
|
22
|
+
return r.error || r.status !== 0 ? null : `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
23
|
+
}
|
|
24
|
+
/** `codex-cli 0.146.0`, `2.1.3 (Claude Code)`, `grok 1.0.62 (1474f9cbdd91)` → the version. Null when there is none. */
|
|
25
|
+
export function parseEngineVersion(text) {
|
|
26
|
+
return text?.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0] ?? null;
|
|
27
|
+
}
|
|
28
|
+
/** Whose `grok` answered: xAI's names itself; the community npm one prints a bare version from a node_modules script. */
|
|
29
|
+
export function grokVendor(text, binaryPath) {
|
|
30
|
+
if (text && /^\s*grok \d+\.\d+/m.test(text))
|
|
31
|
+
return "xai";
|
|
32
|
+
if (/[\\/]node_modules[\\/]|@vibe-kit/.test(binaryPath ?? ""))
|
|
33
|
+
return "community";
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
9
36
|
function defaultExists(p) {
|
|
10
37
|
try {
|
|
11
38
|
statSync(p);
|
|
@@ -52,6 +79,14 @@ export function detectEngineLogin(engine, deps = {}) {
|
|
|
52
79
|
}
|
|
53
80
|
return { login: "missing", where: file };
|
|
54
81
|
}
|
|
82
|
+
if (engine === "grok") {
|
|
83
|
+
// xAI's Grok CLI keeps its subscription login (grok.com / accounts.x.ai) in auth.json under GROK_HOME, default
|
|
84
|
+
// ~/.grok. An external auth provider or OIDC setup signs in another way, so absence is not proof there.
|
|
85
|
+
if (env.GROK_AUTH_PROVIDER_COMMAND?.trim() || env.GROK_OIDC_ISSUER?.trim() || env.GROK_DEPLOYMENT_KEY?.trim())
|
|
86
|
+
return { login: "unknown" };
|
|
87
|
+
const file = join(env.GROK_HOME?.trim() || join(home, ".grok"), "auth.json");
|
|
88
|
+
return { login: exists(file) ? "found" : "missing", where: file };
|
|
89
|
+
}
|
|
55
90
|
if (engine === "gemini") {
|
|
56
91
|
// Google sign-in lands here; Gemini also accepts other auth (gcloud, Vertex), so absence is
|
|
57
92
|
// not proof of "signed out".
|
|
@@ -73,6 +108,9 @@ export function checkEngine(input, deps = {}) {
|
|
|
73
108
|
const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
|
|
74
109
|
const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
|
|
75
110
|
const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
|
|
111
|
+
// Grok's version is what tells xAI's CLI from a same-named npm package, so it is always read for Grok (a native
|
|
112
|
+
// binary, milliseconds); the others only when asked (coding_engine_get), because each is a Node startup.
|
|
113
|
+
const versionText = binaryPath && (engine === "grok" || input.withVersion) ? (deps.versionText ?? defaultVersionText)(resolveEngineSpawn(bin, ["--version"], deps), deps.env ?? process.env) : null;
|
|
76
114
|
return {
|
|
77
115
|
checked: true,
|
|
78
116
|
engine,
|
|
@@ -81,5 +119,7 @@ export function checkEngine(input, deps = {}) {
|
|
|
81
119
|
...(binaryPath ? { binaryPath, binaryVia: via } : {}),
|
|
82
120
|
login: login.login,
|
|
83
121
|
...(login.where ? { loginWhere: login.where } : {}),
|
|
122
|
+
...(binaryPath && (engine === "grok" || input.withVersion) ? { version: parseEngineVersion(versionText) } : {}),
|
|
123
|
+
...(engine === "grok" && binaryPath ? { vendor: grokVendor(versionText, (deps.realpath ?? safeRealpath)(binaryPath)) } : {}),
|
|
84
124
|
};
|
|
85
125
|
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { posix, win32 } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* xAI's Grok CLI for a bare `grok`, wherever PATH points (#1156). Its installer (x.ai/cli/install.sh, install.ps1) always
|
|
6
|
+
* puts it in `~/.grok/bin` (`GROK_BIN_DIR` overrides), and adds that to PATH only in the SHELL's profile — so a
|
|
7
|
+
* `pags up` started by launchd, systemd, a Scheduled Task or an older terminal does not see it. And an npm package of
|
|
8
|
+
* the same name, the community `@vibe-kit/grok-cli` (API-key billed, different flags), can sit earlier on PATH: on the
|
|
9
|
+
* Mac mini it did, in nvm's bin. The preset means xAI's CLI, so its own location wins when it is there. An explicit path
|
|
10
|
+
* in a preset is the owner's choice and is never rewritten.
|
|
11
|
+
*/
|
|
12
|
+
export function officialEngineBinary(bin, deps = {}) {
|
|
13
|
+
if (bin !== "grok")
|
|
14
|
+
return null;
|
|
15
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const dir = envVar(env, "GROK_BIN_DIR", windows)?.trim() || (windows ? win32 : posix).join(deps.home ?? homedir(), ".grok", "bin");
|
|
18
|
+
const file = (windows ? win32 : posix).join(dir, windows ? "grok.exe" : "grok");
|
|
19
|
+
return (deps.isExecutable ?? defaultIsExecutable)(file) ? file : null;
|
|
20
|
+
}
|
|
3
21
|
function defaultIsExecutable(p) {
|
|
4
22
|
try {
|
|
5
23
|
if (!statSync(p).isFile())
|
|
@@ -67,7 +85,8 @@ export function quoteCmdArg(arg) {
|
|
|
67
85
|
a = `"${a}"`;
|
|
68
86
|
return a.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
|
|
69
87
|
}
|
|
70
|
-
export function resolveEngineSpawn(
|
|
88
|
+
export function resolveEngineSpawn(requested, args, deps = {}) {
|
|
89
|
+
const bin = officialEngineBinary(requested, deps) ?? requested;
|
|
71
90
|
const windows = (deps.platform ?? process.platform) === "win32";
|
|
72
91
|
if (!windows)
|
|
73
92
|
return { command: bin, args, binaryPath: resolveBinary(bin, deps), via: "direct" };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok keeps ITS OWN conversation across one-shot turns (#1156) — by an id the runner chooses.
|
|
3
|
+
*
|
|
4
|
+
* Every Grok turn is a new `grok … -p "<turn>"` process, so without this each turn started with no memory of the last
|
|
5
|
+
* one but the platform's replay. `-c/--continue` was the CLI's only offer when the preset was written, and it picks the
|
|
6
|
+
* MOST RECENT session in the directory — another `grok` run in the same checkout (the owner's own) would answer instead
|
|
7
|
+
* (`coding-engines.ts`, "NO PRESET RESUMES"). xAI's Grok CLI now takes an explicit id, so continuity is exact:
|
|
8
|
+
*
|
|
9
|
+
* first turn `-s <uuid>` a NEW conversation under an id the runner made up (1.x: must be a UUID, must not exist);
|
|
10
|
+
* later turns `-r <uuid>` resume exactly that one (errors if it does not exist — never "the most recent").
|
|
11
|
+
*
|
|
12
|
+
* Checked against `grok --help` of xAI's Grok CLI 1.0.62 (2026-10-18). 0.2.x also accepts both: there `-s` creates-or-
|
|
13
|
+
* resumes and `-r` errors on an unknown id. NOT yet exercised end to end — the CLI refuses every turn until it is signed
|
|
14
|
+
* in, and it was not on the machine this was written on — so the runner treats it as unproven: a turn whose flags are
|
|
15
|
+
* rejected is retried once without them and the session runs without continuity, and the platform's replay is still
|
|
16
|
+
* sent with every turn (`holdsConversationThisTurn` stays false for Grok) until a signed-in run proves it.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
const UUID_RE = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
|
|
20
|
+
/** Only a UUID ever reaches argv — anything else read back from the state file is discarded. */
|
|
21
|
+
export function isGrokConversationId(value) {
|
|
22
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
23
|
+
}
|
|
24
|
+
export function newGrokConversation(makeId = randomUUID) {
|
|
25
|
+
return { id: makeId(), started: false };
|
|
26
|
+
}
|
|
27
|
+
/** The turn's argv: the continuity flag first (any order parses), then the preset, then the turn as `-p`'s value. */
|
|
28
|
+
export function grokTurnArgs(userArgs, turnText, conversation) {
|
|
29
|
+
if (!conversation)
|
|
30
|
+
return [...userArgs, turnText];
|
|
31
|
+
return [conversation.started ? "-r" : "-s", conversation.id, ...userArgs, turnText];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The CLI refused the continuity flags themselves — an argument error, printed before it signs in or runs anything (the
|
|
35
|
+
* argument parser runs first). Narrow on purpose: retrying any other failure could run a real instruction twice.
|
|
36
|
+
*/
|
|
37
|
+
// Only a line that BEGINS with the CLI's own `error:` (clap's argument errors), and only about these flags or the id —
|
|
38
|
+
// a tool's output in a failed turn that merely mentions a session must never cause a same-turn retry.
|
|
39
|
+
const REJECTED_RE = /^\s*error:\s(?:.*unexpected argument '?-(?:s|r)\b|.*(?:--session-id|--resume)\b|.*must be a valid uuid|.*session\b.*\b(?:not found|already exists|does not exist))/i;
|
|
40
|
+
export function grokRejectedContinuity(lines) {
|
|
41
|
+
return lines.some((l) => REJECTED_RE.test(l));
|
|
42
|
+
}
|
|
43
|
+
/** What a finished turn means for the conversation. Pure. */
|
|
44
|
+
export function afterGrokTurn(conversation, outcome) {
|
|
45
|
+
// The flags themselves were refused before anything ran: run without continuity from now on.
|
|
46
|
+
if (outcome.code && outcome.rejected)
|
|
47
|
+
return { next: null, persist: null, disable: true };
|
|
48
|
+
// Done: the CLI holds the conversation now. Remember it, so the next turn — and the next runner start — resume it.
|
|
49
|
+
if (outcome.code === 0)
|
|
50
|
+
return { next: { ...conversation, started: true }, persist: conversation.id, disable: false };
|
|
51
|
+
// Any other failure: the conversation may be half-made or unknown. Forget it, so the next turn starts a clean one.
|
|
52
|
+
return { next: null, persist: null, disable: false };
|
|
53
|
+
}
|
|
@@ -5,7 +5,7 @@ export function isCodexThreadId(value) {
|
|
|
5
5
|
return typeof value === "string" && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value);
|
|
6
6
|
}
|
|
7
7
|
function stateKey(id, engine) {
|
|
8
|
-
return engine === "
|
|
8
|
+
return engine === "claude" ? id : `${engine}:${id}`;
|
|
9
9
|
}
|
|
10
10
|
function loadFile(path) {
|
|
11
11
|
if (!path || !existsSync(path))
|
|
@@ -14,6 +14,7 @@ import { handlerFor } from "./handlers.js";
|
|
|
14
14
|
import { resolveEngineAuth } from "./engine-auth.js";
|
|
15
15
|
import { buildCodexResumeArgs, engineAdapterFor, engineInvocationModeFromAdapter, engineInvocationWarning, genericRawEngineAdapter } from "./engine-adapter.js";
|
|
16
16
|
import { isCodexThreadId, readCodexState, readState, writeState } from "./headless-state.js";
|
|
17
|
+
import { afterGrokTurn, grokRejectedContinuity, grokTurnArgs, isGrokConversationId, newGrokConversation } from "./grok-continuity.js";
|
|
17
18
|
export { buildClaudeArgs } from "./engine-adapter.js";
|
|
18
19
|
export { defaultStatePath } from "./headless-state.js";
|
|
19
20
|
/**
|
|
@@ -70,6 +71,10 @@ export class HeadlessSession {
|
|
|
70
71
|
claudeSessionId = null;
|
|
71
72
|
/** Codex's thread id from `thread.started` — used only by the explicit-ID resume path (#848). */
|
|
72
73
|
codexThreadId = null;
|
|
74
|
+
/** Grok's own conversation across one-shot turns (#1156, grok-continuity.ts); null = the next turn starts one. */
|
|
75
|
+
grokConversation = null;
|
|
76
|
+
/** Off once this machine's CLI refused the continuity flags: the session then runs as before, turn by turn. */
|
|
77
|
+
grokContinuity = true;
|
|
73
78
|
/**
|
|
74
79
|
* The cloud's context brief, until the first turn spends it (ADR 0005, #693). Null once
|
|
75
80
|
* delivered, and null from the start when the engine resumed its own conversation.
|
|
@@ -270,6 +275,8 @@ export class HeadlessSession {
|
|
|
270
275
|
config.clientType === "codex" && this.mode === "stream-json" && !config.fresh
|
|
271
276
|
? readCodexState(config.statePath, config.id) ?? (config.resumeFrom ? readCodexState(config.statePath, config.resumeFrom) : null)
|
|
272
277
|
: null;
|
|
278
|
+
const storedGrok = config.clientType === "grok" && !config.fresh ? (readState(config.statePath, config.id, "grok") ?? (config.resumeFrom ? readState(config.statePath, config.resumeFrom, "grok") : null)) : null;
|
|
279
|
+
this.grokConversation = isGrokConversationId(storedGrok) ? { id: storedGrok, started: true } : null;
|
|
273
280
|
// AFTER both lines above, because `resumedConversation` reads them: the brief is the fallback,
|
|
274
281
|
// so an engine that found its own conversation drops it unread rather than being handed a
|
|
275
282
|
// summary of the conversation it is already in.
|
|
@@ -590,9 +597,15 @@ export class HeadlessSession {
|
|
|
590
597
|
this.structuredOutputRejected = false;
|
|
591
598
|
const resumedCodexThreadId = this.codexResumeThreadId;
|
|
592
599
|
const inputId = this.turnInputId;
|
|
600
|
+
// #1156: Grok carries its conversation by an id this runner chose — `-s` to make it, `-r` to resume it.
|
|
601
|
+
if (this.config.clientType === "grok" && this.grokContinuity && !this.grokConversation)
|
|
602
|
+
this.grokConversation = newGrokConversation();
|
|
603
|
+
const grokConversation = this.config.clientType === "grok" && this.grokContinuity ? this.grokConversation : null;
|
|
593
604
|
const turnArgs = resumedCodexThreadId
|
|
594
605
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
595
|
-
:
|
|
606
|
+
: grokConversation
|
|
607
|
+
? grokTurnArgs(this.cmdArgs, text, grokConversation)
|
|
608
|
+
: this.adapter.buildTurnArgs(this.cmdArgs, text);
|
|
596
609
|
const env = this.spawnEnv;
|
|
597
610
|
const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
|
|
598
611
|
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
@@ -687,6 +700,19 @@ export class HeadlessSession {
|
|
|
687
700
|
this.runOneShot(text);
|
|
688
701
|
return;
|
|
689
702
|
}
|
|
703
|
+
if (grokConversation) {
|
|
704
|
+
const after = afterGrokTurn(grokConversation, { code, rejected: grokRejectedContinuity(this.turnTail) });
|
|
705
|
+
this.grokConversation = after.next;
|
|
706
|
+
writeState(this.config.statePath, this.config.id, after.persist, "grok");
|
|
707
|
+
// The flags were refused before anything ran (an argument error): the same turn again, without them.
|
|
708
|
+
if (after.disable) {
|
|
709
|
+
this.grokContinuity = false;
|
|
710
|
+
this.proc = null;
|
|
711
|
+
this.push("[grok CLI rejected its session flags; retrying this turn without conversation continuity]");
|
|
712
|
+
this.runOneShot(text);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
690
716
|
// A rejected/expired explicit ID is not retried as the same turn: the CLI may have started
|
|
691
717
|
// work before failing. Drop only that known-bad local key so the NEXT turn is safely fresh.
|
|
692
718
|
if (code && resumedCodexThreadId && this.codexThreadId === resumedCodexThreadId) {
|
package/dist/index.js
CHANGED
|
@@ -384,9 +384,58 @@ jobs:
|
|
|
384
384
|
import { spawn } from "child_process";
|
|
385
385
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
386
386
|
import { createServer } from "http";
|
|
387
|
-
import { homedir } from "os";
|
|
387
|
+
import { homedir, hostname } from "os";
|
|
388
388
|
import { join as join3 } from "path";
|
|
389
389
|
import { Command as Command3 } from "commander";
|
|
390
|
+
|
|
391
|
+
// src/commands/login-device.ts
|
|
392
|
+
function chooseLoginFlow(opts, env = process.env, platform = process.platform) {
|
|
393
|
+
if (opts.device) return { flow: "device", why: "--device" };
|
|
394
|
+
if (opts.browser) return { flow: "browser", why: "--browser" };
|
|
395
|
+
if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) return { flow: "device", why: "this is an SSH session" };
|
|
396
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return { flow: "device", why: "there is no display here" };
|
|
397
|
+
return { flow: "browser", why: "" };
|
|
398
|
+
}
|
|
399
|
+
function sessionExpiresAt(token) {
|
|
400
|
+
try {
|
|
401
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[0] ?? "", "base64url").toString("utf8"));
|
|
402
|
+
return typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp * 1e3 : null;
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async function deviceLogin(deps) {
|
|
408
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
409
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
410
|
+
const now = deps.now ?? Date.now;
|
|
411
|
+
const post = (path, body) => fetchFn(`${deps.apiBase}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
412
|
+
const res = await post("/v1/auth/device/start", { node: deps.node });
|
|
413
|
+
if (!res.ok) throw new Error(`Could not start a device sign-in (HTTP ${res.status}).`);
|
|
414
|
+
const started = await res.json();
|
|
415
|
+
deps.log("To sign this machine in, open this link on your phone or any device where you are signed in to ProAgentStore:");
|
|
416
|
+
deps.log(` ${started.verification_uri_complete}`);
|
|
417
|
+
deps.log(`or go to ${started.verification_uri} and enter the code: ${started.user_code}`);
|
|
418
|
+
deps.log(`Check that the page shows this machine (${deps.node}) and this code. Waiting for approval \u2014 the code lasts ${Math.round(started.expires_in / 60)} minutes\u2026`);
|
|
419
|
+
let interval = Math.max(1, started.interval) * 1e3;
|
|
420
|
+
const deadline = now() + started.expires_in * 1e3;
|
|
421
|
+
while (now() < deadline) {
|
|
422
|
+
await sleep2(interval);
|
|
423
|
+
let answer;
|
|
424
|
+
try {
|
|
425
|
+
const r = await post("/v1/auth/device/token", { device_code: started.device_code });
|
|
426
|
+
answer = await r.json().catch(() => ({}));
|
|
427
|
+
if (r.ok && answer.session) return answer.session;
|
|
428
|
+
} catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (answer.error === "slow_down") interval += 5e3;
|
|
432
|
+
else if (answer.error === "access_denied") throw new Error("The sign-in was denied on the approval page.");
|
|
433
|
+
else if (answer.error === "expired_token") break;
|
|
434
|
+
}
|
|
435
|
+
throw new Error("The code expired before it was approved. Run `pags login --device` again for a new one.");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/commands/login.ts
|
|
390
439
|
var CONFIG_DIR = join3(homedir(), ".config", "proagentstore");
|
|
391
440
|
var TOKEN_FILE = join3(CONFIG_DIR, "session.json");
|
|
392
441
|
var API_BASE = "https://api.proagentstore.online";
|
|
@@ -407,6 +456,14 @@ function saveSession(session) {
|
|
|
407
456
|
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
408
457
|
writeFileSync2(TOKEN_FILE, JSON.stringify(session, null, 2));
|
|
409
458
|
}
|
|
459
|
+
async function storeSession(token) {
|
|
460
|
+
const meRes = await fetch(`${API_BASE}/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` } });
|
|
461
|
+
if (!meRes.ok) throw new Error("Session validation failed");
|
|
462
|
+
const me = await meRes.json();
|
|
463
|
+
const session = { token, user: { id: me.id, login: me.login, avatar: me.avatar }, expiresAt: sessionExpiresAt(token) ?? Date.now() + 29 * 24 * 36e5 };
|
|
464
|
+
saveSession(session);
|
|
465
|
+
return session;
|
|
466
|
+
}
|
|
410
467
|
function requireSession() {
|
|
411
468
|
const session = loadSession();
|
|
412
469
|
if (!session) {
|
|
@@ -428,7 +485,19 @@ function openBrowser(url) {
|
|
|
428
485
|
} catch {
|
|
429
486
|
}
|
|
430
487
|
}
|
|
431
|
-
var loginCommand = new Command3("login").description("Sign in with Google or GitHub
|
|
488
|
+
var loginCommand = new Command3("login").description("Sign in with Google or GitHub \u2014 in a browser here, or approved from another device over SSH/headless").option("--github", "Use GitHub instead of Google").option("--device", "Sign in from another device: show a link and a code to approve on your phone (SSH, no display)").option("--browser", "Open a browser on this machine, even over SSH").action(async (opts) => {
|
|
489
|
+
const choice = chooseLoginFlow(opts);
|
|
490
|
+
if (choice.flow === "device") {
|
|
491
|
+
if (choice.why !== "--device") writeLine(`Signing in from another device, because ${choice.why} (use --browser to open one here instead).`);
|
|
492
|
+
try {
|
|
493
|
+
const session = await storeSession(await deviceLogin({ apiBase: API_BASE, node: hostname(), log: writeLine }));
|
|
494
|
+
writeLine(`Signed in as ${session.user.login}. Token stored at ${TOKEN_FILE}`);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
432
501
|
const provider = opts.github ? "github" : "google";
|
|
433
502
|
const port = await findFreePort();
|
|
434
503
|
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
@@ -454,17 +523,7 @@ var loginCommand = new Command3("login").description("Sign in with Google or Git
|
|
|
454
523
|
return;
|
|
455
524
|
}
|
|
456
525
|
try {
|
|
457
|
-
const
|
|
458
|
-
headers: { Authorization: `Bearer ${session}` }
|
|
459
|
-
});
|
|
460
|
-
if (!meRes.ok) throw new Error("Session validation failed");
|
|
461
|
-
const me = await meRes.json();
|
|
462
|
-
saveSession({
|
|
463
|
-
token: session,
|
|
464
|
-
user: { id: me.id, login: me.login, avatar: me.avatar },
|
|
465
|
-
expiresAt: Date.now() + 29 * 24 * 60 * 60 * 1e3
|
|
466
|
-
// ~29 days
|
|
467
|
-
});
|
|
526
|
+
const me = (await storeSession(session)).user;
|
|
468
527
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
469
528
|
res.end(`<h1>Signed in as ${me.login}</h1><p>You can close this tab and return to the terminal.</p><script>window.close()</script>`);
|
|
470
529
|
clearTimeout(timeout);
|
|
@@ -532,7 +591,7 @@ import { Command as Command4 } from "commander";
|
|
|
532
591
|
// src/machine.ts
|
|
533
592
|
import { randomUUID } from "crypto";
|
|
534
593
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
535
|
-
import { homedir as homedir2, hostname } from "os";
|
|
594
|
+
import { homedir as homedir2, hostname as hostname2 } from "os";
|
|
536
595
|
import { join as join4 } from "path";
|
|
537
596
|
var CONFIG_DIR2 = join4(homedir2(), ".config", "proagentstore");
|
|
538
597
|
var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
|
|
@@ -607,7 +666,7 @@ function saveMachineIdentity(identity2) {
|
|
|
607
666
|
function sameIdentity(a, b) {
|
|
608
667
|
return a.id === b.id && a.names.join("\0") === b.names.join("\0") && (a.declined ?? []).join("\0") === (b.declined ?? []).join("\0");
|
|
609
668
|
}
|
|
610
|
-
function loadMachineIdentity(now =
|
|
669
|
+
function loadMachineIdentity(now = hostname2()) {
|
|
611
670
|
let stored = null;
|
|
612
671
|
try {
|
|
613
672
|
if (existsSync4(MACHINE_FILE)) stored = parseMachineFile(readFileSync3(MACHINE_FILE, "utf-8"));
|
|
@@ -815,7 +874,7 @@ async function defaultAsk(question) {
|
|
|
815
874
|
}
|
|
816
875
|
|
|
817
876
|
// src/commands/runner/http.ts
|
|
818
|
-
import { hostname as
|
|
877
|
+
import { hostname as hostname3 } from "os";
|
|
819
878
|
function clean(value) {
|
|
820
879
|
const trimmed = value?.trim();
|
|
821
880
|
return trimmed || void 0;
|
|
@@ -841,7 +900,7 @@ function apiPathSegment(value) {
|
|
|
841
900
|
return encodeURIComponent(value);
|
|
842
901
|
}
|
|
843
902
|
function buildRuntimeRegistrationBody(opts, capabilities = []) {
|
|
844
|
-
const node =
|
|
903
|
+
const node = hostname3();
|
|
845
904
|
const machine = loadMachineIdentity(node);
|
|
846
905
|
return {
|
|
847
906
|
endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
|
|
@@ -1028,7 +1087,7 @@ var mcpCommand = new Command5("mcp").description("Run a local stdio proxy for th
|
|
|
1028
1087
|
|
|
1029
1088
|
// src/commands/remote-auth.ts
|
|
1030
1089
|
import { Command as Command6 } from "commander";
|
|
1031
|
-
import { hostname as
|
|
1090
|
+
import { hostname as hostname4 } from "os";
|
|
1032
1091
|
var CONSOLE = "https://proagentstore.online/console";
|
|
1033
1092
|
var FIELDS = ["request_id", "agent", "machine", "resource", "scopes", "approval_url", "expires_at"];
|
|
1034
1093
|
var SECRET_KEY = /verifier|proof|private|secret|token|password|credential|ciphertext|session|cookie|^code$/i;
|
|
@@ -1054,7 +1113,7 @@ function buildRelayBody(input, identity2) {
|
|
|
1054
1113
|
}
|
|
1055
1114
|
function identity() {
|
|
1056
1115
|
const id = loadMachineIdentity();
|
|
1057
|
-
return { id: id.id, node: id.names[0] ??
|
|
1116
|
+
return { id: id.id, node: id.names[0] ?? hostname4() };
|
|
1058
1117
|
}
|
|
1059
1118
|
async function readStdin() {
|
|
1060
1119
|
const chunks = [];
|
|
@@ -1301,7 +1360,7 @@ async function waitForLocalRunner(opts, timeoutMs = 15e3) {
|
|
|
1301
1360
|
}
|
|
1302
1361
|
|
|
1303
1362
|
// src/commands/runner/relay.ts
|
|
1304
|
-
import { hostname as
|
|
1363
|
+
import { hostname as hostname5 } from "os";
|
|
1305
1364
|
|
|
1306
1365
|
// src/commands/runner/resources.ts
|
|
1307
1366
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, statfsSync, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -2508,9 +2567,9 @@ var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
|
2508
2567
|
var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
|
|
2509
2568
|
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
|
|
2510
2569
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
2511
|
-
const pagsToken = clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
|
|
2512
|
-
if (!pagsToken) throw new Error("PAGS token required for WebSocket relay");
|
|
2513
|
-
const runnerNode =
|
|
2570
|
+
const pagsToken = () => clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
|
|
2571
|
+
if (!pagsToken()) throw new Error("PAGS token required for WebSocket relay");
|
|
2572
|
+
const runnerNode = hostname5();
|
|
2514
2573
|
const machine = loadMachineIdentity(runnerNode);
|
|
2515
2574
|
const autoUpdateKey = machine.id ? autoUpdatePolicyKey(machine.id) : "";
|
|
2516
2575
|
const cell = new PolicyCell(autoUpdateKey ? loadAutoUpdatePolicy(autoUpdateKey) ?? { autoUpdate: false } : { autoUpdate: false }, (p) => saveAutoUpdatePolicy(autoUpdateKey, p), !!autoUpdateKey);
|
|
@@ -2706,7 +2765,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2706
2765
|
const plan = reattachPlan(request, { held: attached.has(named), blocked: blocked.has(named), watching: watchInstances, scope: instanceIds });
|
|
2707
2766
|
if (plan.refuse) return { status: 409, result: { error: plan.refuse } };
|
|
2708
2767
|
if (plan.release) {
|
|
2709
|
-
const res = await requestPags("GET", "/v1/instances/my/instances", { ...opts, pagsToken });
|
|
2768
|
+
const res = await requestPags("GET", "/v1/instances/my/instances", { ...opts, pagsToken: pagsToken() });
|
|
2710
2769
|
const released = pinnedAway(attached.keys(), res.instances ?? [], runnerNode, machine.names);
|
|
2711
2770
|
for (const id of released) detach(id, `${id.slice(0, 8)}\u2026 (pinned to another machine now)`);
|
|
2712
2771
|
if (released.length && attached.size === 0) writeLine("This `pags up --instance` holds no agent now \u2014 close it, or restart it without --instance.");
|
|
@@ -2727,7 +2786,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2727
2786
|
};
|
|
2728
2787
|
const attach = (id, label = `${id.slice(0, 8)}\u2026`) => {
|
|
2729
2788
|
if (attached.has(id)) return;
|
|
2730
|
-
const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken }, {}).then((r) => r.token);
|
|
2789
|
+
const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken: pagsToken() }, {}).then((r) => r.token);
|
|
2731
2790
|
attached.set(
|
|
2732
2791
|
id,
|
|
2733
2792
|
openRelaySocket(
|
|
@@ -2778,7 +2837,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2778
2837
|
writeLine(startup.state === "ok" ? `Runtime registered with PAGS \u2713 (${startup.agents} agents)` : `Runtime registration incomplete: ${startup.agents} agents \u2014 retried on each relay (re)connect${watchInstances ? " and every 20s while this runs" : ""}.`);
|
|
2779
2838
|
writeLine("");
|
|
2780
2839
|
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");
|
|
2781
|
-
writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${
|
|
2840
|
+
writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname5()}`);
|
|
2782
2841
|
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
|
|
2783
2842
|
writeLine(" No cloudflared needed. Ctrl+C to disconnect.");
|
|
2784
2843
|
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");
|
|
@@ -2799,8 +2858,10 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2799
2858
|
...process.env.PAGS_RUNNER_RSID ? { rsid: process.env.PAGS_RUNNER_RSID } : {},
|
|
2800
2859
|
pid: process.pid,
|
|
2801
2860
|
...process.env.PAGS_RUNNER_LAUNCH ? { launch: process.env.PAGS_RUNNER_LAUNCH } : {},
|
|
2802
|
-
supervisor: restartOwner()
|
|
2861
|
+
supervisor: restartOwner(),
|
|
2803
2862
|
// #1154: what starts it again if it dies
|
|
2863
|
+
sessionExpiresAt: sessionExpiresAt(pagsToken() ?? "") ?? void 0
|
|
2864
|
+
// #1157: when this runner signs out
|
|
2804
2865
|
},
|
|
2805
2866
|
relayRttMs,
|
|
2806
2867
|
sessions: sessions ?? void 0
|
|
@@ -2839,7 +2900,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2839
2900
|
const free = await requestPags(
|
|
2840
2901
|
"GET",
|
|
2841
2902
|
`/v1/relay/${apiPathSegment(id)}/status`,
|
|
2842
|
-
{ ...opts, pagsToken }
|
|
2903
|
+
{ ...opts, pagsToken: pagsToken() }
|
|
2843
2904
|
).then((r) => r.connected === false).catch(() => false);
|
|
2844
2905
|
if (!free) continue;
|
|
2845
2906
|
blocked.delete(id);
|
|
@@ -2852,7 +2913,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2852
2913
|
const res = await requestPags(
|
|
2853
2914
|
"GET",
|
|
2854
2915
|
"/v1/instances/my/instances",
|
|
2855
|
-
{ ...opts, pagsToken }
|
|
2916
|
+
{ ...opts, pagsToken: pagsToken() }
|
|
2856
2917
|
);
|
|
2857
2918
|
const { attach: toAttach, detach: toDetach } = diffMembership(
|
|
2858
2919
|
attached.keys(),
|
|
@@ -2903,7 +2964,7 @@ async function readSessionResources(localUrl, runnerToken) {
|
|
|
2903
2964
|
}
|
|
2904
2965
|
var RTT_PROBE_PREFIX = "rtt:";
|
|
2905
2966
|
var RTT_ECHO_PREFIX = "rtt-echo:";
|
|
2906
|
-
function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode =
|
|
2967
|
+
function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname5(), beginMutation = () => () => void 0) {
|
|
2907
2968
|
let backoffMs = 1e3;
|
|
2908
2969
|
let reconnecting = false;
|
|
2909
2970
|
let closed = false;
|
|
@@ -3342,8 +3403,50 @@ function recordChildExit(o) {
|
|
|
3342
3403
|
return report;
|
|
3343
3404
|
}
|
|
3344
3405
|
|
|
3406
|
+
// src/session-refresh.ts
|
|
3407
|
+
var REFRESH_WITHIN_MS = 7 * 24 * 36e5;
|
|
3408
|
+
var WARN_WITHIN_MS = 3 * 24 * 36e5;
|
|
3409
|
+
var CHECK_EVERY_MS = 6 * 36e5;
|
|
3410
|
+
function refreshDecision(expiresAt, now) {
|
|
3411
|
+
if (expiresAt === null) return "unreadable";
|
|
3412
|
+
if (expiresAt <= now) return "expired";
|
|
3413
|
+
return expiresAt - now <= REFRESH_WITHIN_MS ? "refresh" : "fresh";
|
|
3414
|
+
}
|
|
3415
|
+
var hoursLeft = (ms) => ms >= 48 * 36e5 ? `${Math.round(ms / (24 * 36e5))} days` : `${Math.max(1, Math.round(ms / 36e5))} hours`;
|
|
3416
|
+
async function checkSession(deps) {
|
|
3417
|
+
const now = (deps.now ?? Date.now)();
|
|
3418
|
+
const token = deps.token();
|
|
3419
|
+
if (!token) return { decision: "unreadable" };
|
|
3420
|
+
const expiresAt = sessionExpiresAt(token);
|
|
3421
|
+
const decision = refreshDecision(expiresAt, now);
|
|
3422
|
+
if (decision === "expired") {
|
|
3423
|
+
deps.warn("Session expired: this runner is signed out. Sign in again with `pags login` (`pags login --device` over SSH).");
|
|
3424
|
+
return { decision };
|
|
3425
|
+
}
|
|
3426
|
+
if (decision !== "refresh") return { decision };
|
|
3427
|
+
const left = expiresAt - now;
|
|
3428
|
+
try {
|
|
3429
|
+
const res = await (deps.fetch ?? fetch)(`${deps.apiBase}/v1/auth/session/refresh`, { method: "POST", headers: { Authorization: `Bearer ${token}` } });
|
|
3430
|
+
const body = await res.json().catch(() => ({}));
|
|
3431
|
+
if (!res.ok || !body.session) throw new Error(body.error || `HTTP ${res.status}`);
|
|
3432
|
+
deps.onRefreshed(body.session, token);
|
|
3433
|
+
const until = sessionExpiresAt(body.session);
|
|
3434
|
+
deps.log(`Session refreshed \u2014 signed in until ${until ? new Date(until).toISOString().slice(0, 10) : "the next 30 days"}.`);
|
|
3435
|
+
return { decision, refreshed: true };
|
|
3436
|
+
} catch (e) {
|
|
3437
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
3438
|
+
if (left <= WARN_WITHIN_MS) deps.warn(`Session refresh failed (${why}): this runner signs out in ${hoursLeft(left)}. Sign in again with \`pags login\` (\`pags login --device\` over SSH).`);
|
|
3439
|
+
return { decision, refreshed: false };
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function startSessionRefresh(deps, every = setInterval) {
|
|
3443
|
+
const run = () => void checkSession(deps).catch(() => void 0);
|
|
3444
|
+
run();
|
|
3445
|
+
every(run, CHECK_EVERY_MS).unref?.();
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3345
3448
|
// src/commands/runner/command.ts
|
|
3346
|
-
import { hostname as
|
|
3449
|
+
import { hostname as hostname6 } from "os";
|
|
3347
3450
|
function collectCapability(value, previous = []) {
|
|
3348
3451
|
return [...previous, value];
|
|
3349
3452
|
}
|
|
@@ -3361,7 +3464,7 @@ function createRunnerCommand() {
|
|
|
3361
3464
|
const lockAccount = clean(process.env.PAGS_LOCK_ACCOUNT) || "";
|
|
3362
3465
|
let lockHeld = null;
|
|
3363
3466
|
if (lockAccount) {
|
|
3364
|
-
const taken = await acquireLock({ account: lockAccount, node:
|
|
3467
|
+
const taken = await acquireLock({ account: lockAccount, node: hostname6(), machineId: loadMachineIdentity().id || null, version: CLI_VERSION, port: Number(port), runnerToken });
|
|
3365
3468
|
if (!taken.ok) {
|
|
3366
3469
|
for (const line of taken.lock ? holderMessage(taken.lock, taken.verdict) : ["Another `pags up` holds this machine for this account."]) writeError(line);
|
|
3367
3470
|
process.exit(1);
|
|
@@ -3440,6 +3543,19 @@ function createRunnerCommand() {
|
|
|
3440
3543
|
await connectViaRelay(instanceIds, localUrl, runnerToken, opts, Boolean(opts.force), Boolean(opts.watchInstances));
|
|
3441
3544
|
void deliver().catch(() => void 0);
|
|
3442
3545
|
setInterval(() => void deliver().catch(() => void 0), 5 * 6e4).unref();
|
|
3546
|
+
if (!clean(opts.pagsToken)) {
|
|
3547
|
+
startSessionRefresh({
|
|
3548
|
+
apiBase: pagsApiBase(opts.apiBase),
|
|
3549
|
+
token: () => clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token) || null,
|
|
3550
|
+
onRefreshed: (token, previous) => {
|
|
3551
|
+
process.env.PAGS_TOKEN = token;
|
|
3552
|
+
const stored = loadSession();
|
|
3553
|
+
if (stored?.token === previous) saveSession({ ...stored, token, expiresAt: sessionExpiresAt(token) ?? stored.expiresAt });
|
|
3554
|
+
},
|
|
3555
|
+
log: writeLine,
|
|
3556
|
+
warn: writeError
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3443
3559
|
await new Promise((resolvePromise) => {
|
|
3444
3560
|
runner.on("exit", () => resolvePromise());
|
|
3445
3561
|
});
|
|
@@ -3702,11 +3818,11 @@ var logsCommand = new Command9("logs").description("Show the background runner's
|
|
|
3702
3818
|
// src/commands/up.ts
|
|
3703
3819
|
import { createRequire as createRequire2 } from "module";
|
|
3704
3820
|
import { Command as Command10 } from "commander";
|
|
3705
|
-
import { homedir as homedir5, hostname as
|
|
3821
|
+
import { homedir as homedir5, hostname as hostname8, userInfo } from "os";
|
|
3706
3822
|
|
|
3707
3823
|
// src/tui.ts
|
|
3708
3824
|
import chalk from "chalk";
|
|
3709
|
-
import { hostname as
|
|
3825
|
+
import { hostname as hostname7 } from "os";
|
|
3710
3826
|
import readline from "readline";
|
|
3711
3827
|
var ACCENT = "#7c3aed";
|
|
3712
3828
|
var c = chalk.hex(ACCENT);
|
|
@@ -3759,7 +3875,7 @@ function printStatus(state) {
|
|
|
3759
3875
|
clearScreen();
|
|
3760
3876
|
printLogo(state.version);
|
|
3761
3877
|
const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
|
|
3762
|
-
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(
|
|
3878
|
+
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname7()));
|
|
3763
3879
|
console.log("");
|
|
3764
3880
|
const row = (kind, s, override) => {
|
|
3765
3881
|
const { icon, label, note } = describe(kind, s);
|
|
@@ -4288,7 +4404,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
|
|
|
4288
4404
|
headless: opts.headless
|
|
4289
4405
|
}).catch(() => void 0);
|
|
4290
4406
|
if (!opts.instance) {
|
|
4291
|
-
const split = partitionByPin(instances,
|
|
4407
|
+
const split = partitionByPin(instances, hostname8(), loadMachineIdentity(hostname8()).names);
|
|
4292
4408
|
instances = split.here;
|
|
4293
4409
|
elsewhere = split.elsewhere;
|
|
4294
4410
|
}
|
|
@@ -4401,7 +4517,7 @@ var upCommand = new Command10("up").description("Start the browser runner for al
|
|
|
4401
4517
|
printStatus(state);
|
|
4402
4518
|
continue;
|
|
4403
4519
|
}
|
|
4404
|
-
if (/error|Error|EADDRINUSE|ECONNREFUSED|failed/i.test(trimmed)) {
|
|
4520
|
+
if (/error|Error|EADDRINUSE|ECONNREFUSED|failed|expired/i.test(trimmed)) {
|
|
4405
4521
|
state.lastEvent = trimmed.slice(0, 80);
|
|
4406
4522
|
printStatus(state);
|
|
4407
4523
|
}
|