@proagentstore/cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +123 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -472,6 +472,7 @@ var publishCommand = new Command4("publish").description("Publish an agent to Pr
|
|
|
472
472
|
|
|
473
473
|
// src/commands/runner.ts
|
|
474
474
|
import { spawn as spawn2 } from "child_process";
|
|
475
|
+
import { randomUUID } from "crypto";
|
|
475
476
|
import { existsSync as existsSync4 } from "fs";
|
|
476
477
|
import { resolve as resolve4 } from "path";
|
|
477
478
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -507,6 +508,12 @@ function buildRunnerArgs(opts) {
|
|
|
507
508
|
if (opts.headless) args.push("--headless");
|
|
508
509
|
return args;
|
|
509
510
|
}
|
|
511
|
+
function buildCloudflaredArgs(localUrl) {
|
|
512
|
+
return ["tunnel", "--url", localUrl];
|
|
513
|
+
}
|
|
514
|
+
function parseCloudflaredTunnelUrl(text) {
|
|
515
|
+
return text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i)?.[0] || null;
|
|
516
|
+
}
|
|
510
517
|
function buildRuntimeRegistrationBody(opts, capabilities = []) {
|
|
511
518
|
return {
|
|
512
519
|
endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
|
|
@@ -533,7 +540,7 @@ function findWorkspaceRoot() {
|
|
|
533
540
|
function bundledRunnerPath() {
|
|
534
541
|
return fileURLToPath2(new URL("./browser-runner/index.js", import.meta.url));
|
|
535
542
|
}
|
|
536
|
-
function
|
|
543
|
+
function runnerSpawnSpec(opts) {
|
|
537
544
|
const root = findWorkspaceRoot();
|
|
538
545
|
const localPackage = resolve4(root, "packages", "browser-runner", "src", "index.ts");
|
|
539
546
|
const bundledPackage = bundledRunnerPath();
|
|
@@ -549,9 +556,13 @@ function startRunnerForeground(opts) {
|
|
|
549
556
|
command = process.execPath;
|
|
550
557
|
args = [bundledPackage, ...runnerArgs];
|
|
551
558
|
}
|
|
559
|
+
return { command, args, cwd };
|
|
560
|
+
}
|
|
561
|
+
function startRunnerForeground(opts) {
|
|
562
|
+
const spec = runnerSpawnSpec(opts);
|
|
552
563
|
return new Promise((resolvePromise, reject) => {
|
|
553
|
-
const child = spawn2(command, args, {
|
|
554
|
-
cwd,
|
|
564
|
+
const child = spawn2(spec.command, spec.args, {
|
|
565
|
+
cwd: spec.cwd,
|
|
555
566
|
stdio: "inherit",
|
|
556
567
|
shell: process.platform === "win32"
|
|
557
568
|
});
|
|
@@ -562,6 +573,20 @@ function startRunnerForeground(opts) {
|
|
|
562
573
|
});
|
|
563
574
|
});
|
|
564
575
|
}
|
|
576
|
+
async function waitForLocalRunner(opts, timeoutMs = 15e3) {
|
|
577
|
+
const started = Date.now();
|
|
578
|
+
let lastError;
|
|
579
|
+
while (Date.now() - started < timeoutMs) {
|
|
580
|
+
try {
|
|
581
|
+
await requestRunner("GET", "/health", opts);
|
|
582
|
+
return;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
lastError = error;
|
|
585
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
throw new Error(`runner did not become healthy: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
589
|
+
}
|
|
565
590
|
async function requestRunner(method, path, opts, body) {
|
|
566
591
|
const headers = {
|
|
567
592
|
...runnerRequestHeaders(opts)
|
|
@@ -621,6 +646,101 @@ function createRunnerCommand() {
|
|
|
621
646
|
command.command("start").description("Start the local browser runner 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) => {
|
|
622
647
|
await startRunnerForeground(opts);
|
|
623
648
|
});
|
|
649
|
+
command.command("connect <instanceId>").description("Start a local runner, open a Cloudflare quick tunnel, and register it with PAGS").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", "cloudflared").option("--runner-version <version>", "Runner version").option("--skip-probe", "Skip PAGS runtime probe after registration").action(async (instanceId, opts) => {
|
|
650
|
+
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
|
|
651
|
+
const host = clean(opts.host) || "127.0.0.1";
|
|
652
|
+
const port = clean(opts.port) || "49171";
|
|
653
|
+
const localUrl = `http://${host}:${port}`;
|
|
654
|
+
const runnerOpts = {
|
|
655
|
+
...opts,
|
|
656
|
+
host,
|
|
657
|
+
port,
|
|
658
|
+
token: runnerToken,
|
|
659
|
+
instanceId
|
|
660
|
+
};
|
|
661
|
+
const spec = runnerSpawnSpec(runnerOpts);
|
|
662
|
+
const runner = spawn2(spec.command, spec.args, {
|
|
663
|
+
cwd: spec.cwd,
|
|
664
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
665
|
+
shell: process.platform === "win32"
|
|
666
|
+
});
|
|
667
|
+
runner.stdout?.on("data", (data) => process.stdout.write(data));
|
|
668
|
+
runner.stderr?.on("data", (data) => process.stderr.write(data));
|
|
669
|
+
runner.on("exit", (code) => {
|
|
670
|
+
if (code && code !== 0) writeError(`runner exited with code ${code}`);
|
|
671
|
+
});
|
|
672
|
+
let tunnel = null;
|
|
673
|
+
const shutdown = () => {
|
|
674
|
+
if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
|
|
675
|
+
if (!runner.killed) runner.kill("SIGTERM");
|
|
676
|
+
};
|
|
677
|
+
process.once("SIGINT", () => {
|
|
678
|
+
shutdown();
|
|
679
|
+
process.exit(0);
|
|
680
|
+
});
|
|
681
|
+
process.once("SIGTERM", () => {
|
|
682
|
+
shutdown();
|
|
683
|
+
process.exit(0);
|
|
684
|
+
});
|
|
685
|
+
try {
|
|
686
|
+
await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId });
|
|
687
|
+
writeLine(`Local runner healthy at ${localUrl}`);
|
|
688
|
+
const tunnelUrl = await new Promise((resolveTunnel, reject) => {
|
|
689
|
+
let output = "";
|
|
690
|
+
const cloudflared = clean(opts.cloudflared) || "cloudflared";
|
|
691
|
+
tunnel = spawn2(cloudflared, buildCloudflaredArgs(localUrl), {
|
|
692
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
693
|
+
shell: process.platform === "win32"
|
|
694
|
+
});
|
|
695
|
+
const onData = (data) => {
|
|
696
|
+
const text = data.toString("utf-8");
|
|
697
|
+
output += text;
|
|
698
|
+
process.stderr.write(text);
|
|
699
|
+
const parsed = parseCloudflaredTunnelUrl(output);
|
|
700
|
+
if (parsed) resolveTunnel(parsed);
|
|
701
|
+
};
|
|
702
|
+
tunnel.stdout?.on("data", onData);
|
|
703
|
+
tunnel.stderr?.on("data", onData);
|
|
704
|
+
tunnel.on("error", reject);
|
|
705
|
+
tunnel.on("exit", (code) => {
|
|
706
|
+
if (code && code !== 0) reject(new Error(`cloudflared exited with code ${code}`));
|
|
707
|
+
});
|
|
708
|
+
setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
|
|
709
|
+
});
|
|
710
|
+
writeLine(`Tunnel ready: ${tunnelUrl}`);
|
|
711
|
+
await waitForLocalRunner({ url: tunnelUrl, token: runnerToken, instanceId }, 3e4);
|
|
712
|
+
writeLine("Tunnel health check passed");
|
|
713
|
+
const capabilities = await requestRunner("GET", "/capabilities", {
|
|
714
|
+
url: tunnelUrl,
|
|
715
|
+
token: runnerToken,
|
|
716
|
+
instanceId
|
|
717
|
+
});
|
|
718
|
+
const result = await requestPags("POST", `/v1/instances/${apiPathSegment(instanceId)}/runtime`, opts, {
|
|
719
|
+
endpointUrl: tunnelUrl,
|
|
720
|
+
token: runnerToken,
|
|
721
|
+
placement: "local",
|
|
722
|
+
capabilities: Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [],
|
|
723
|
+
runnerVersion: clean(opts.runnerVersion) || ""
|
|
724
|
+
});
|
|
725
|
+
writeLine(JSON.stringify(result, null, 2));
|
|
726
|
+
if (!opts.skipProbe) {
|
|
727
|
+
const status = await requestPags(
|
|
728
|
+
"GET",
|
|
729
|
+
`/v1/instances/${apiPathSegment(instanceId)}/runtime/status`,
|
|
730
|
+
opts
|
|
731
|
+
);
|
|
732
|
+
writeLine(JSON.stringify(status, null, 2));
|
|
733
|
+
}
|
|
734
|
+
writeLine("Connected. Keep this process running while the agent uses your local browser.");
|
|
735
|
+
await new Promise((resolvePromise) => {
|
|
736
|
+
runner.on("exit", () => resolvePromise());
|
|
737
|
+
tunnel?.on("exit", () => resolvePromise());
|
|
738
|
+
});
|
|
739
|
+
} catch (error) {
|
|
740
|
+
shutdown();
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
624
744
|
command.command("status").description("Check local runner health and capabilities").option("--url <url>", "Runner URL").option("--token <token>", "Runner bearer token").option("--instance-id <id>", "PAGS instance id header").action(async (opts) => {
|
|
625
745
|
const health = await requestRunner("GET", "/health", opts);
|
|
626
746
|
const capabilities = await requestRunner("GET", "/capabilities", opts);
|