@tbrandenburg/node-red-cli 0.2.13 → 0.2.14

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/README.md CHANGED
@@ -319,8 +319,11 @@ The same flow runs sandboxed via `--docker <image>` against an image that
319
319
  already ships `opencode` + `node-red-agents`, e.g.
320
320
  [`ghcr.io/tbrandenburg/agentic-workflow-dev-env`](https://github.com/tbrandenburg/agentic-workflow-dev-env)
321
321
  (`--network` is required for network access, since the agent calls out to
322
- its own API the package is already in the image, so `--node-modules` is
323
- not needed here):
322
+ its own API; `--node-modules`/`--user-dir` are still required too, since
323
+ Node-RED only discovers node types from a userDir it actually loaded —
324
+ see [#24](https://github.com/tbrandenburg/node-red-cli/issues/24) for a
325
+ currently-tracked compatibility gap when the image's own default userDir
326
+ already ships the package):
324
327
 
325
328
  ```bash
326
329
  echo '{"payload":"Summarize this repo in one sentence.","cwd":"/repo"}' \
@@ -332,7 +335,8 @@ echo '{"payload":"Summarize this repo in one sentence.","cwd":"/repo"}' \
332
335
  "cwd":"cwd","cwdType":"msg","wires":[["return"],[]]},
333
336
  {"id":"return","type":"link out","z":"tab","name":"return","mode":"return"}
334
337
  ]' ask --docker ghcr.io/tbrandenburg/agentic-workflow-dev-env:latest \
335
- --network --timeout=120000 --format=json
338
+ --node-modules @tbrandenburg/node-red-agents --user-dir --network \
339
+ --timeout=120000 --format=json
336
340
  ```
337
341
 
338
342
  ## Host API 🛠️
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbrandenburg/node-red-cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Call existing Node-RED flows from Node.js and the command line",
5
5
  "main": "src/link-call.js",
6
6
  "bin": {
@@ -74,7 +74,13 @@ function checkNpmAvailable() {
74
74
  });
75
75
  }
76
76
 
77
- /** Runs `npm install <name>[@version]` into `userDir`, returns on success, throws a clear error otherwise. */
77
+ /**
78
+ * Runs `npm install <name>[@version]` into `userDir`, returns on success,
79
+ * throws a clear error otherwise. Passes an explicit `--prefix <userDir>`
80
+ * so npm's own project-root/workspace detection can't walk up to an
81
+ * ancestor directory's `node_modules` when `userDir` is fresh/empty
82
+ * (see issue #24); `cwd` is kept as-is for the npm CLI invocation itself.
83
+ */
78
84
  function npmInstall(userDir, { name, version }, timeoutMs = 5 * 60 * 1000) {
79
85
  const installName = version ? `${name}@${version}` : name;
80
86
  const args = [
@@ -85,6 +91,8 @@ function npmInstall(userDir, { name, version }, timeoutMs = 5 * 60 * 1000) {
85
91
  "--no-fund",
86
92
  "--save",
87
93
  "--omit=dev",
94
+ "--prefix",
95
+ userDir,
88
96
  "--",
89
97
  installName
90
98
  ];
@@ -144,5 +152,6 @@ module.exports = {
144
152
  isModuleInstalled,
145
153
  diffMissingModules,
146
154
  installMissingNodeModules,
147
- checkNpmAvailable
155
+ checkNpmAvailable,
156
+ npmInstall
148
157
  };
@@ -34,6 +34,48 @@ function stderrLogHandler() {
34
34
  };
35
35
  }
36
36
 
37
+ /**
38
+ * Waits for Node-RED to finish attempting to start the deployed flows.
39
+ *
40
+ * `RED.start()` resolves as soon as the runtime itself has booted, but the
41
+ * actual flow deploy happens asynchronously afterward and normally signals
42
+ * completion via a one-off `flows:started` event. However, when the flow
43
+ * references a node type that isn't registered (or another deploy-blocking
44
+ * condition applies, e.g. missing external modules or safe mode), Node-RED's
45
+ * `Flow.start()` logs the problem and returns *without* ever emitting
46
+ * `flows:started` (see `@node-red/runtime/lib/flows/index.js`). Awaiting
47
+ * only `flows:started` would then hang forever; since nothing else keeps
48
+ * the event loop alive, the process exits silently with code 0 once the
49
+ * loop drains, abandoning the pending call.
50
+ *
51
+ * Node-RED does always emit a `runtime-event` with id `runtime-state` in
52
+ * both cases: `payload.state === "start"` on success, and
53
+ * `payload.state === "stop"` / `"safe"` on any of the early-return failure
54
+ * paths. Racing both events lets us return as soon as Node-RED has settled
55
+ * either way; if the flows never actually started, the target/return nodes
56
+ * simply won't be instantiated and the existing preflight validation in
57
+ * `createHostLinkCaller` reports the real, specific error instead.
58
+ */
59
+ function waitForFlowsSettled(RED) {
60
+ return new Promise((resolve) => {
61
+ const onStarted = () => {
62
+ RED.events.removeListener("runtime-event", onRuntimeEvent);
63
+ resolve();
64
+ };
65
+ const onRuntimeEvent = (event) => {
66
+ if (
67
+ event?.id === "runtime-state" &&
68
+ (event.payload?.state === "stop" || event.payload?.state === "safe")
69
+ ) {
70
+ RED.events.removeListener("flows:started", onStarted);
71
+ resolve();
72
+ }
73
+ };
74
+ RED.events.once("flows:started", onStarted);
75
+ RED.events.on("runtime-event", onRuntimeEvent);
76
+ });
77
+ }
78
+
37
79
  /**
38
80
  * Runs a single link-call invocation against a real, freshly booted
39
81
  * Node-RED runtime: installs any missing `--node-modules`, boots RED with
@@ -80,9 +122,9 @@ async function runFlowInvocation({ flow, flowFile, msg, options }) {
80
122
  });
81
123
 
82
124
  try {
83
- const flowsStarted = new Promise((resolve) => RED.events.once("flows:started", resolve));
125
+ const flowsSettled = waitForFlowsSettled(RED);
84
126
  await RED.start();
85
- await flowsStarted;
127
+ await flowsSettled;
86
128
 
87
129
  caller = createHostLinkCaller(RED);
88
130
  const result = await caller.call(target, msg, {