Gateway: add APNs push test pipeline (#20307)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 6a1c4422079b075fb7900890fa09819f41aee8b1
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-18 19:32:42 +00:00
committed by GitHub
parent 1f5cd65d60
commit 99d099aa84
14 changed files with 839 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
import type { Command } from "commander";
import { defaultRuntime } from "../../runtime.js";
import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js";
type NodesPushOpts = NodesRpcOpts & {
node?: string;
title?: string;
body?: string;
environment?: string;
};
function normalizeEnvironment(value: unknown): "sandbox" | "production" | null {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim().toLowerCase();
if (normalized === "sandbox" || normalized === "production") {
return normalized;
}
return null;
}
export function registerNodesPushCommand(nodes: Command) {
nodesCallOpts(
nodes
.command("push")
.description("Send an APNs test push to an iOS node")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--title <text>", "Push title", "OpenClaw")
.option("--body <text>", "Push body")
.option("--environment <sandbox|production>", "Override APNs environment")
.action(async (opts: NodesPushOpts) => {
await runNodesCommand("push", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const title = String(opts.title ?? "").trim() || "OpenClaw";
const body = String(opts.body ?? "").trim() || `Push test for node ${nodeId}`;
const environment = normalizeEnvironment(opts.environment);
if (opts.environment && !environment) {
throw new Error("invalid --environment (use sandbox|production)");
}
const params: Record<string, unknown> = {
nodeId,
title,
body,
};
if (environment) {
params.environment = environment;
}
const result = await callGatewayCli("push.test", opts, params);
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
const parsed =
typeof result === "object" && result !== null
? (result as {
ok?: unknown;
status?: unknown;
reason?: unknown;
environment?: unknown;
})
: {};
const ok = parsed.ok === true;
const status = typeof parsed.status === "number" ? parsed.status : 0;
const reason =
typeof parsed.reason === "string" && parsed.reason.trim().length > 0
? parsed.reason.trim()
: undefined;
const env =
typeof parsed.environment === "string" && parsed.environment.trim().length > 0
? parsed.environment.trim()
: "unknown";
const { ok: okLabel, error: errorLabel } = getNodesTheme();
const label = ok ? okLabel : errorLabel;
defaultRuntime.log(label(`push.test status=${status} ok=${ok} env=${env}`));
if (reason) {
defaultRuntime.log(`reason: ${reason}`);
}
});
}),
{ timeoutMs: 25_000 },
);
}

View File

@@ -8,6 +8,7 @@ import { registerNodesInvokeCommands } from "./register.invoke.js";
import { registerNodesLocationCommands } from "./register.location.js";
import { registerNodesNotifyCommand } from "./register.notify.js";
import { registerNodesPairingCommands } from "./register.pairing.js";
import { registerNodesPushCommand } from "./register.push.js";
import { registerNodesScreenCommands } from "./register.screen.js";
import { registerNodesStatusCommands } from "./register.status.js";
@@ -30,6 +31,7 @@ export function registerNodesCli(program: Command) {
registerNodesPairingCommands(nodes);
registerNodesInvokeCommands(nodes);
registerNodesNotifyCommand(nodes);
registerNodesPushCommand(nodes);
registerNodesCanvasCommands(nodes);
registerNodesCameraCommands(nodes);
registerNodesScreenCommands(nodes);