@tbrandenburg/node-red-agents 0.1.3 → 0.1.4

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.
@@ -1,48 +1,48 @@
1
1
  [
2
- {
3
- "id": "gh-example-inject",
4
- "type": "inject",
5
- "name": "",
6
- "props": [{ "p": "payload" }],
7
- "repeat": "",
8
- "crontab": "",
9
- "once": false,
10
- "onceDelay": 0.1,
11
- "topic": "",
12
- "payload": "",
13
- "payloadType": "date",
14
- "x": 150,
15
- "y": 120,
16
- "wires": [["gh-example-node"]]
17
- },
18
- {
19
- "id": "gh-example-node",
20
- "type": "gh",
21
- "name": "Open PRs",
22
- "command": "pr",
23
- "commandType": "str",
24
- "args": "list --state open --json number,title,author,url",
25
- "argsType": "str",
26
- "repo": "owner/repository",
27
- "repoType": "str",
28
- "host": "",
29
- "timeoutMs": 60000,
30
- "x": 350,
31
- "y": 120,
32
- "wires": [["gh-example-debug"]]
33
- },
34
- {
35
- "id": "gh-example-debug",
36
- "type": "debug",
37
- "name": "PRs",
38
- "active": true,
39
- "tosidebar": true,
40
- "console": false,
41
- "tostatus": false,
42
- "complete": "payload",
43
- "targetType": "msg",
44
- "x": 550,
45
- "y": 120,
46
- "wires": []
47
- }
2
+ {
3
+ "id": "gh-example-inject",
4
+ "type": "inject",
5
+ "name": "",
6
+ "props": [{ "p": "payload" }],
7
+ "repeat": "",
8
+ "crontab": "",
9
+ "once": false,
10
+ "onceDelay": 0.1,
11
+ "topic": "",
12
+ "payload": "",
13
+ "payloadType": "date",
14
+ "x": 150,
15
+ "y": 120,
16
+ "wires": [["gh-example-node"]]
17
+ },
18
+ {
19
+ "id": "gh-example-node",
20
+ "type": "gh",
21
+ "name": "Open PRs",
22
+ "command": "pr",
23
+ "commandType": "str",
24
+ "args": "list --state open --json number,title,author,url",
25
+ "argsType": "str",
26
+ "repo": "owner/repository",
27
+ "repoType": "str",
28
+ "host": "",
29
+ "timeoutMs": 60000,
30
+ "x": 350,
31
+ "y": 120,
32
+ "wires": [["gh-example-debug"]]
33
+ },
34
+ {
35
+ "id": "gh-example-debug",
36
+ "type": "debug",
37
+ "name": "PRs",
38
+ "active": true,
39
+ "tosidebar": true,
40
+ "console": false,
41
+ "tostatus": false,
42
+ "complete": "payload",
43
+ "targetType": "msg",
44
+ "x": 550,
45
+ "y": 120,
46
+ "wires": []
47
+ }
48
48
  ]
@@ -1,42 +1,42 @@
1
1
  [
2
- {
3
- "id": "gh-wf-example-function",
4
- "type": "function",
5
- "name": "Build gh args",
6
- "func": "msg.gh = {\n command: 'workflow',\n args: ['run', 'deploy.yml', '--ref', 'main', '-f', 'environment=staging'],\n repo: 'owner/repository'\n};\nreturn msg;",
7
- "outputs": 1,
8
- "x": 200,
9
- "y": 220,
10
- "wires": [["gh-wf-example-node"]]
11
- },
12
- {
13
- "id": "gh-wf-example-node",
14
- "type": "gh",
15
- "name": "Trigger workflow",
16
- "command": "",
17
- "commandType": "str",
18
- "args": "",
19
- "argsType": "str",
20
- "repo": "",
21
- "repoType": "str",
22
- "host": "",
23
- "timeoutMs": 60000,
24
- "x": 420,
25
- "y": 220,
26
- "wires": [["gh-wf-example-debug"]]
27
- },
28
- {
29
- "id": "gh-wf-example-debug",
30
- "type": "debug",
31
- "name": "Result",
32
- "active": true,
33
- "tosidebar": true,
34
- "console": false,
35
- "tostatus": false,
36
- "complete": "true",
37
- "targetType": "full",
38
- "x": 620,
39
- "y": 220,
40
- "wires": []
41
- }
2
+ {
3
+ "id": "gh-wf-example-function",
4
+ "type": "function",
5
+ "name": "Build gh args",
6
+ "func": "msg.gh = {\n command: 'workflow',\n args: ['run', 'deploy.yml', '--ref', 'main', '-f', 'environment=staging'],\n repo: 'owner/repository'\n};\nreturn msg;",
7
+ "outputs": 1,
8
+ "x": 200,
9
+ "y": 220,
10
+ "wires": [["gh-wf-example-node"]]
11
+ },
12
+ {
13
+ "id": "gh-wf-example-node",
14
+ "type": "gh",
15
+ "name": "Trigger workflow",
16
+ "command": "",
17
+ "commandType": "str",
18
+ "args": "",
19
+ "argsType": "str",
20
+ "repo": "",
21
+ "repoType": "str",
22
+ "host": "",
23
+ "timeoutMs": 60000,
24
+ "x": 420,
25
+ "y": 220,
26
+ "wires": [["gh-wf-example-debug"]]
27
+ },
28
+ {
29
+ "id": "gh-wf-example-debug",
30
+ "type": "debug",
31
+ "name": "Result",
32
+ "active": true,
33
+ "tosidebar": true,
34
+ "console": false,
35
+ "tostatus": false,
36
+ "complete": "true",
37
+ "targetType": "full",
38
+ "x": 620,
39
+ "y": 220,
40
+ "wires": []
41
+ }
42
42
  ]
package/nodes/gh/gh.js CHANGED
@@ -1,9 +1,9 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const { spawn } = require('child_process');
4
- const { parseArgs } = require('./lib/parse-args');
3
+ const { spawn } = require("child_process");
4
+ const { parseArgs } = require("./lib/parse-args");
5
5
 
6
- const GH_BINARY = 'gh';
6
+ const GH_BINARY = "gh";
7
7
  const DEFAULT_TIMEOUT_MS = 60000;
8
8
 
9
9
  // Resolves a typed-input style config field (str/msg/flow/global), honoring
@@ -11,227 +11,236 @@ const DEFAULT_TIMEOUT_MS = 60000;
11
11
  // opencode-run.js's prompt/cwd handling). An empty 'str' path is treated as
12
12
  // "not configured" rather than an evaluation error.
13
13
  function evalField(RED, node, msg, value, type) {
14
- if (type === 'str' && (value === undefined || value === null || value === '')) {
15
- return undefined;
16
- }
17
- return RED.util.evaluateNodeProperty(value, type, node, msg);
14
+ if (type === "str" && (value === undefined || value === null || value === "")) {
15
+ return undefined;
16
+ }
17
+ return RED.util.evaluateNodeProperty(value, type, node, msg);
18
18
  }
19
19
 
20
20
  // A valid gh top-level command is a single token (e.g. "pr", "issue",
21
21
  // "api") -- never the literal "gh" itself and never something containing
22
22
  // whitespace (which would indicate a whole command line was pasted in).
23
23
  function validateCommand(command) {
24
- if (typeof command !== 'string' || command.trim() === '') {
25
- return 'no command configured (set the Command field or msg.gh.command)';
26
- }
27
- const trimmed = command.trim();
28
- if (/\s/.test(trimmed)) {
29
- return 'Command must be a single gh subcommand (e.g. "pr"), not a full command line: "' + trimmed + '"';
30
- }
31
- if (trimmed.toLowerCase() === 'gh') {
32
- return '"gh" is the executable itself, not a command -- use e.g. "pr" with gh already implied';
33
- }
34
- return null;
24
+ if (typeof command !== "string" || command.trim() === "") {
25
+ return "no command configured (set the Command field or msg.gh.command)";
26
+ }
27
+ const trimmed = command.trim();
28
+ if (/\s/.test(trimmed)) {
29
+ return (
30
+ 'Command must be a single gh subcommand (e.g. "pr"), not a full command line: "' +
31
+ trimmed +
32
+ '"'
33
+ );
34
+ }
35
+ if (trimmed.toLowerCase() === "gh") {
36
+ return '"gh" is the executable itself, not a command -- use e.g. "pr" with gh already implied';
37
+ }
38
+ return null;
35
39
  }
36
40
 
37
41
  // Resolves an "args" value (from config or msg.gh.args) into a string[].
38
42
  // Strings are tokenized (quote-aware, no shell evaluation); arrays are used
39
43
  // as-is (each element coerced to a string); anything else is rejected.
40
44
  function resolveArgs(value) {
41
- if (value === undefined || value === null) return [];
42
- if (Array.isArray(value)) return value.map(String);
43
- if (typeof value === 'string') return parseArgs(value);
44
- throw new Error('args must be a string or an array of strings, got ' + typeof value);
45
+ if (value === undefined || value === null) return [];
46
+ if (Array.isArray(value)) return value.map(String);
47
+ if (typeof value === "string") return parseArgs(value);
48
+ throw new Error("args must be a string or an array of strings, got " + typeof value);
45
49
  }
46
50
 
47
51
  module.exports = function (RED) {
48
- function GhNode(config) {
49
- RED.nodes.createNode(this, config);
50
- const node = this;
51
-
52
- node.command = config.command || '';
53
- node.commandType = config.commandType || 'str';
54
- node.args = config.args || '';
55
- node.argsType = config.argsType || 'str';
56
- node.repo = config.repo || '';
57
- node.repoType = config.repoType || 'str';
58
- node.host = config.host || '';
59
- node.timeoutMs = Number(config.timeoutMs) || DEFAULT_TIMEOUT_MS;
60
-
61
- node.on('input', function (msg, send, done) {
62
- const ghOverride = (msg.gh && typeof msg.gh === 'object') ? msg.gh : {};
63
-
64
- // --- command ---
65
- let command;
66
- try {
67
- command = Object.prototype.hasOwnProperty.call(ghOverride, 'command')
68
- ? ghOverride.command
69
- : evalField(RED, node, msg, node.command, node.commandType);
70
- } catch (err) {
71
- node.status({ fill: 'red', shape: 'ring', text: 'bad command config' });
72
- done(err);
73
- return;
74
- }
75
-
76
- const commandError = validateCommand(command);
77
- if (commandError) {
78
- node.status({ fill: 'red', shape: 'ring', text: 'bad command' });
79
- done(new Error('gh: ' + commandError));
80
- return;
81
- }
82
- command = command.trim();
83
-
84
- // --- args ---
85
- let rawArgs;
86
- try {
87
- rawArgs = Object.prototype.hasOwnProperty.call(ghOverride, 'args')
88
- ? ghOverride.args
89
- : evalField(RED, node, msg, node.args, node.argsType);
90
- } catch (err) {
91
- node.status({ fill: 'red', shape: 'ring', text: 'bad args config' });
92
- done(err);
93
- return;
94
- }
95
-
96
- let args;
97
- try {
98
- args = resolveArgs(rawArgs);
99
- } catch (err) {
100
- node.status({ fill: 'red', shape: 'ring', text: 'bad args' });
101
- done(new Error('gh: ' + err.message));
102
- return;
103
- }
104
-
105
- // --- repo ---
106
- let repo;
107
- try {
108
- repo = Object.prototype.hasOwnProperty.call(ghOverride, 'repo')
109
- ? ghOverride.repo
110
- : evalField(RED, node, msg, node.repo, node.repoType);
111
- } catch (err) {
112
- node.status({ fill: 'red', shape: 'ring', text: 'bad repo config' });
113
- done(err);
114
- return;
115
- }
116
- repo = (repo === undefined || repo === null || repo === '') ? undefined : String(repo);
117
-
118
- // --- host (Advanced; simple str field, no typed input) ---
119
- const host = ghOverride.host || node.host || undefined;
120
-
121
- node.status({ fill: 'blue', shape: 'dot', text: 'running ' + command });
122
-
123
- const env = Object.assign({}, process.env);
124
- if (repo) env.GH_REPO = repo;
125
- if (host) env.GH_HOST = host;
126
-
127
- let stdout = '';
128
- let stderr = '';
129
- let child;
130
- try {
131
- child = spawn(GH_BINARY, [command, ...args], {
132
- env,
133
- stdio: ['ignore', 'pipe', 'pipe'],
134
- shell: false
135
- });
136
- } catch (err) {
137
- node.status({ fill: 'red', shape: 'ring', text: 'spawn failed' });
138
- done(err);
139
- return;
140
- }
141
-
142
- // Own explicit timeout instead of spawn()'s built-in timeout
143
- // option: it's simpler to reason about (we control exactly when
144
- // the timer is armed/cleared) and sidesteps that option's timer
145
- // not always being cleared promptly when the child never
146
- // actually starts (e.g. ENOENT).
147
- let timedOut = false;
148
- const timer = setTimeout(() => {
149
- timedOut = true;
150
- child.kill('SIGTERM');
151
- }, node.timeoutMs);
152
- if (typeof timer.unref === 'function') timer.unref();
153
-
154
- child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
155
- child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
156
-
157
- child.on('error', (err) => {
158
- clearTimeout(timer);
159
- node.status({ fill: 'red', shape: 'ring', text: 'error' });
160
- const message = (err && err.code === 'ENOENT')
161
- ? 'gh executable not found on PATH'
162
- : 'failed to run gh: ' + err.message;
163
- const wrapped = new Error('gh: ' + message);
164
- wrapped.command = command;
165
- wrapped.args = args;
166
- done(wrapped);
167
- });
168
-
169
- child.on('close', (code, signal) => {
170
- clearTimeout(timer);
171
- if (signal) {
172
- node.status({ fill: 'red', shape: 'ring', text: timedOut ? 'timeout' : 'killed' });
173
- const err = new Error(
174
- timedOut
175
- ? 'gh: timed out after ' + node.timeoutMs + 'ms and was killed'
176
- : 'gh: process killed by signal ' + signal
177
- );
178
- err.command = command;
179
- err.args = args;
180
- err.signal = signal;
181
- err.timedOut = timedOut;
182
- done(err);
183
- return;
184
- }
185
-
186
- if (code !== 0) {
187
- node.status({ fill: 'red', shape: 'dot', text: 'exit ' + code });
188
- const err = new Error(
189
- 'gh: command failed (exit ' + code + ')' + (stderr.trim() ? ': ' + stderr.trim() : '')
190
- );
191
- err.command = command;
192
- err.args = args;
193
- err.exitCode = code;
194
- err.stderr = stderr.trim();
195
- done(err);
196
- return;
197
- }
198
-
199
- const text = stdout.replace(/\r?\n$/, '');
200
- let payload;
201
- try {
202
- payload = JSON.parse(text.trim());
203
- } catch (e) {
204
- payload = text;
205
- }
206
-
207
- msg.payload = payload;
208
- msg.gh = {
209
- command,
210
- args,
211
- repo: repo || '',
212
- host: host || '',
213
- exitCode: 0,
214
- stderr: stderr.trim()
215
- };
216
-
217
- node.status({ fill: 'green', shape: 'dot', text: 'done' });
218
- const clearStatus = setTimeout(() => node.status({}), 2000);
219
- if (typeof clearStatus.unref === 'function') clearStatus.unref();
220
- send(msg);
221
- done();
222
- });
223
-
224
- node._child = child;
52
+ function GhNode(config) {
53
+ RED.nodes.createNode(this, config);
54
+ const node = this;
55
+
56
+ node.command = config.command || "";
57
+ node.commandType = config.commandType || "str";
58
+ node.args = config.args || "";
59
+ node.argsType = config.argsType || "str";
60
+ node.repo = config.repo || "";
61
+ node.repoType = config.repoType || "str";
62
+ node.host = config.host || "";
63
+ node.timeoutMs = Number(config.timeoutMs) || DEFAULT_TIMEOUT_MS;
64
+
65
+ node.on("input", function (msg, send, done) {
66
+ const ghOverride = msg.gh && typeof msg.gh === "object" ? msg.gh : {};
67
+
68
+ // --- command ---
69
+ let command;
70
+ try {
71
+ command = Object.prototype.hasOwnProperty.call(ghOverride, "command")
72
+ ? ghOverride.command
73
+ : evalField(RED, node, msg, node.command, node.commandType);
74
+ } catch (err) {
75
+ node.status({ fill: "red", shape: "ring", text: "bad command config" });
76
+ done(err);
77
+ return;
78
+ }
79
+
80
+ const commandError = validateCommand(command);
81
+ if (commandError) {
82
+ node.status({ fill: "red", shape: "ring", text: "bad command" });
83
+ done(new Error("gh: " + commandError));
84
+ return;
85
+ }
86
+ command = command.trim();
87
+
88
+ // --- args ---
89
+ let rawArgs;
90
+ try {
91
+ rawArgs = Object.prototype.hasOwnProperty.call(ghOverride, "args")
92
+ ? ghOverride.args
93
+ : evalField(RED, node, msg, node.args, node.argsType);
94
+ } catch (err) {
95
+ node.status({ fill: "red", shape: "ring", text: "bad args config" });
96
+ done(err);
97
+ return;
98
+ }
99
+
100
+ let args;
101
+ try {
102
+ args = resolveArgs(rawArgs);
103
+ } catch (err) {
104
+ node.status({ fill: "red", shape: "ring", text: "bad args" });
105
+ done(new Error("gh: " + err.message));
106
+ return;
107
+ }
108
+
109
+ // --- repo ---
110
+ let repo;
111
+ try {
112
+ repo = Object.prototype.hasOwnProperty.call(ghOverride, "repo")
113
+ ? ghOverride.repo
114
+ : evalField(RED, node, msg, node.repo, node.repoType);
115
+ } catch (err) {
116
+ node.status({ fill: "red", shape: "ring", text: "bad repo config" });
117
+ done(err);
118
+ return;
119
+ }
120
+ repo = repo === undefined || repo === null || repo === "" ? undefined : String(repo);
121
+
122
+ // --- host (Advanced; simple str field, no typed input) ---
123
+ const host = ghOverride.host || node.host || undefined;
124
+
125
+ node.status({ fill: "blue", shape: "dot", text: "running " + command });
126
+
127
+ const env = Object.assign({}, process.env);
128
+ if (repo) env.GH_REPO = repo;
129
+ if (host) env.GH_HOST = host;
130
+
131
+ let stdout = "";
132
+ let stderr = "";
133
+ let child;
134
+ try {
135
+ child = spawn(GH_BINARY, [command, ...args], {
136
+ env,
137
+ stdio: ["ignore", "pipe", "pipe"],
138
+ shell: false,
225
139
  });
226
-
227
- node.on('close', function (done) {
228
- if (node._child && !node._child.killed) {
229
- node._child.kill();
230
- }
231
- node.status({});
232
- done();
233
- });
234
- }
235
-
236
- RED.nodes.registerType('gh', GhNode);
140
+ } catch (err) {
141
+ node.status({ fill: "red", shape: "ring", text: "spawn failed" });
142
+ done(err);
143
+ return;
144
+ }
145
+
146
+ // Own explicit timeout instead of spawn()'s built-in timeout
147
+ // option: it's simpler to reason about (we control exactly when
148
+ // the timer is armed/cleared) and sidesteps that option's timer
149
+ // not always being cleared promptly when the child never
150
+ // actually starts (e.g. ENOENT).
151
+ let timedOut = false;
152
+ const timer = setTimeout(() => {
153
+ timedOut = true;
154
+ child.kill("SIGTERM");
155
+ }, node.timeoutMs);
156
+ if (typeof timer.unref === "function") timer.unref();
157
+
158
+ child.stdout.on("data", (chunk) => {
159
+ stdout += chunk.toString();
160
+ });
161
+ child.stderr.on("data", (chunk) => {
162
+ stderr += chunk.toString();
163
+ });
164
+
165
+ child.on("error", (err) => {
166
+ clearTimeout(timer);
167
+ node.status({ fill: "red", shape: "ring", text: "error" });
168
+ const message =
169
+ err && err.code === "ENOENT"
170
+ ? "gh executable not found on PATH"
171
+ : "failed to run gh: " + err.message;
172
+ const wrapped = new Error("gh: " + message);
173
+ wrapped.command = command;
174
+ wrapped.args = args;
175
+ done(wrapped);
176
+ });
177
+
178
+ child.on("close", (code, signal) => {
179
+ clearTimeout(timer);
180
+ if (signal) {
181
+ node.status({ fill: "red", shape: "ring", text: timedOut ? "timeout" : "killed" });
182
+ const err = new Error(
183
+ timedOut
184
+ ? "gh: timed out after " + node.timeoutMs + "ms and was killed"
185
+ : "gh: process killed by signal " + signal,
186
+ );
187
+ err.command = command;
188
+ err.args = args;
189
+ err.signal = signal;
190
+ err.timedOut = timedOut;
191
+ done(err);
192
+ return;
193
+ }
194
+
195
+ if (code !== 0) {
196
+ node.status({ fill: "red", shape: "dot", text: "exit " + code });
197
+ const err = new Error(
198
+ "gh: command failed (exit " + code + ")" + (stderr.trim() ? ": " + stderr.trim() : ""),
199
+ );
200
+ err.command = command;
201
+ err.args = args;
202
+ err.exitCode = code;
203
+ err.stderr = stderr.trim();
204
+ done(err);
205
+ return;
206
+ }
207
+
208
+ const text = stdout.replace(/\r?\n$/, "");
209
+ let payload;
210
+ try {
211
+ payload = JSON.parse(text.trim());
212
+ } catch (e) {
213
+ payload = text;
214
+ }
215
+
216
+ msg.payload = payload;
217
+ msg.gh = {
218
+ command,
219
+ args,
220
+ repo: repo || "",
221
+ host: host || "",
222
+ exitCode: 0,
223
+ stderr: stderr.trim(),
224
+ };
225
+
226
+ node.status({ fill: "green", shape: "dot", text: "done" });
227
+ const clearStatus = setTimeout(() => node.status({}), 2000);
228
+ if (typeof clearStatus.unref === "function") clearStatus.unref();
229
+ send(msg);
230
+ done();
231
+ });
232
+
233
+ node._child = child;
234
+ });
235
+
236
+ node.on("close", function (done) {
237
+ if (node._child && !node._child.killed) {
238
+ node._child.kill();
239
+ }
240
+ node.status({});
241
+ done();
242
+ });
243
+ }
244
+
245
+ RED.nodes.registerType("gh", GhNode);
237
246
  };