fix(nodes): allow removing stale paired nodes

This commit is contained in:
Peter Steinberger
2026-04-27 13:20:52 +01:00
parent 400be3b63f
commit 7fb2a356e8
18 changed files with 156 additions and 5 deletions

View File

@@ -71,6 +71,30 @@ export function registerNodesPairingCommands(nodes: Command) {
}),
);
nodesCallOpts(
nodes
.command("remove")
.description("Remove a paired node entry")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.action(async (opts: NodesRpcOpts) => {
await runNodesCommand("remove", async () => {
const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
if (!nodeId) {
defaultRuntime.error("--node required");
defaultRuntime.exit(1);
return;
}
const result = await callGatewayCli("node.pair.remove", opts, { nodeId });
if (opts.json) {
defaultRuntime.writeJson(result);
return;
}
const { warn } = getNodesTheme();
defaultRuntime.log(warn(`Removed paired node ${nodeId}`));
});
}),
);
nodesCallOpts(
nodes
.command("rename")

View File

@@ -22,6 +22,7 @@ export function registerNodesCli(program: Command) {
`\n${theme.heading("Examples:")}\n${formatHelpExamples([
["openclaw nodes status", "List known nodes with live status."],
["openclaw nodes pairing pending", "Show pending node pairing requests."],
["openclaw nodes remove --node <id|name|ip>", "Remove a stale paired node entry."],
[
'openclaw nodes invoke --node <id> --command system.which --params \'{"name":"uname"}\'',
"Invoke a node command directly.",

View File

@@ -424,6 +424,35 @@ describe("cli program (nodes basics)", () => {
);
});
it("runs nodes remove and calls node.pair.remove", async () => {
callGateway.mockImplementation(async (...args: unknown[]) => {
const opts = (args[0] ?? {}) as { method?: string };
if (opts.method === "node.list") {
return {
nodes: [{ nodeId: "ios-node", displayName: "iOS Node", paired: true }],
};
}
if (opts.method === "node.pair.list") {
return {
pending: [],
paired: [{ nodeId: "ios-node", displayName: "iOS Node" }],
};
}
if (opts.method === "node.pair.remove") {
return { nodeId: "ios-node" };
}
return { ok: true };
});
await runProgram(["nodes", "remove", "--node", "iOS Node"]);
expect(callGateway).toHaveBeenCalledWith(
expect.objectContaining({
method: "node.pair.remove",
params: { nodeId: "ios-node" },
}),
);
});
it("runs nodes invoke and calls node.invoke", async () => {
mockGatewayWithIosNodeListAnd("node.invoke", {
ok: true,