Files
mesh-catalog/modules/nodered/tools/index.ts
T
jschoubben 7b06a7a408 Event names are local now, in the manifests and in the code
Every module named its events the way the old bus spelled a routing key —
`module.<module>.<verb>`. Design 29 says a module names an event locally and the
mesh works out where it lands, so all 37 were stale against a rule already
decided. On the new bus that derives into a namespace belonging to a module
called "module", so no cross-module subscription in the mesh matched anything:
nothing failed, nothing reacted (novox/hq 04-ISSUES/127).

36 manifests converted, and 43 files of module code with them. The code mattered
as much as the manifests: the runtime builds the subject from what `emit()` is
handed, so a converted manifest with unconverted code would have had the
permission and the subject disagree.

Three things the new check found on the way:

- `photos` emitted an event its manifest never declared, which the new bus refuses
  outright. Declared.
- `showcase` waited for an event nothing emits, so its demo could never be
  triggered — only `showcase` may publish under its own name. It emits both halves
  now.
- `distribution` declared an event named after a different module. It emits
  `image.pushed` under its own name. An event about a *role* belongs on the seat,
  where the name outlives whoever holds it, but the sdk has no way to publish on a
  seat yet, so that stays recorded rather than declared.

The audit logger's "everything" pattern is `**` rather than the old bus's `#`.
2026-09-27 14:42:28 +02:00

65 lines
2.7 KiB
TypeScript

// nodered's tools — importing nodered's own admin-API client (novox/hq ADR 0039). They return
// structured data; the mesh serves them through the sdk's tool harness.
//
// The deploy tool is nodered's one event source (novox/hq ADR 0041/0042): a successful deploy
// emits module.nodered.flows.deployed. nodered has nothing to observe on a timer, so there is no
// separate events entrypoint — the emit rides the action that causes it. The emit is best-effort:
// if no broker is bound, the deploy still succeeds and the announcement is simply skipped.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { emit } from "@novox/mesh-sdk/events";
import { NodeRedClient } from "../client.js";
export function getNodeRedTools(nodered: NodeRedClient): ToolDefinition[] {
return [
{
name: "nodered_list_flows",
description: "List Node-RED flows (tabs) — id, label, disabled state — and the total node count.",
input: {},
run: async () => nodered.listFlows(),
},
{
name: "nodered_list_nodes",
description: "List the Node-RED node modules installed in the runtime and their versions.",
input: {},
run: async () => {
const nodes = await nodered.listNodes();
return { count: nodes.length, nodes };
},
},
{
name: "nodered_deploy",
description:
"Replace the whole Node-RED flow configuration and deploy it. `flows` is the full node " +
"array (as GET /flows returns). Emits module.nodered.flows.deployed on success.",
input: {
flows: { type: "array", description: "the full flow configuration — every node across every tab" },
type: { type: "string", description: "deployment type: full (default), nodes, or flows" },
},
run: async (args) => {
const flows = args.flows as unknown[];
if (!Array.isArray(flows)) throw new Error("flows must be an array of Node-RED nodes");
const type = args.type ? String(args.type) : "full";
const result = await nodered.deployFlows(flows, type);
// Best-effort announcement — a deploy must not fail because the broker is unbound here.
try {
await emit("flows.deployed", { rev: result.rev, nodeCount: result.nodeCount, type });
} catch (err) {
console.error(`[nodered] deployed but could not emit: ${err}`);
}
return result;
},
},
];
}
// The tools exist only when a Node-RED URL is configured; without one, nodered contributes none
// rather than failing the whole tool runtime.
registerModuleTools("nodered", (env) => {
try {
return getNodeRedTools(NodeRedClient.fromEnv(env));
} catch {
return [];
}
});