pochade-node-red 0.1.0

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 ADDED
@@ -0,0 +1,71 @@
1
+ ![Pochade Node-RED Splash Image](./pochade-js-logo.jpg)
2
+
3
+ # Pochade Node-RED
4
+ ## For Writing Node-RED Plugins with Passion
5
+
6
+ Pochade Node-RED is a lightweight, opinionated generator for building Node-RED nodes (plugins) with modern standards but without the bloat. It sets up a clean, vanilla JavaScript environment so you can focus on writing the logic for your node.
7
+
8
+ ## Features
9
+
10
+ - **Instant Scaffolding** - Generates a complete, ready-to-deploy Node-RED node project.
11
+ - **Auto Reload and Install** - Work in the nodes home directory, our watch script auto-installs and auto-reloads node-red when you make changes in the folder.
12
+ - **Interactive Setup** - easy-to-use CLI questionnaire to configure your node.
13
+ - **Vanilla JS** - No complex build pipelines by default, just standard Node.js and HTML.
14
+ - **Developer Experience** - Includes helper scripts to easily install your plugin into your local Node-RED instance for testing.
15
+ - **AI Ready** - Includes documentation to help AI assistants (`LLM.md`, `AGENTS.md`) understand your project structure.
16
+
17
+ ---
18
+
19
+ ## Quick Start
20
+
21
+ Create a new Node-RED node project with a single command:
22
+
23
+ ```sh
24
+ npx pochade-node-red my-node-red-plugin
25
+ ```
26
+
27
+ cd into the new directory.
28
+
29
+ ### Installing to Local Node-RED
30
+
31
+ To test your node, you need to install it into your local Node-RED environment. We've included a handy script for this:
32
+
33
+ ```sh
34
+ npm run install-plugin
35
+ ```
36
+
37
+ ### Watch Mode
38
+
39
+ If you are making frequent changes, you can use:
40
+
41
+ ```sh
42
+ npm run watch
43
+ ```
44
+
45
+ This will start node-red and on every text change will re-install the plugin to the ~/.node-red node_modules directory and restart node-red.
46
+
47
+ ---
48
+
49
+ ## Project Structure
50
+
51
+ ```
52
+ my-node-red-plugin/
53
+ ├── src/
54
+ │ ├── my-node.js # The Node.js runtime logic
55
+ │ └── my-node.html # The Editor UI and help text
56
+ ├── bin/ # Utility scripts
57
+ ├── package.json # Node-RED module metadata
58
+ └── README.md # Documentation for your user
59
+ ```
60
+
61
+ ---
62
+
63
+ ## License
64
+
65
+ Unlicense (Public Domain)
66
+
67
+ ---
68
+
69
+ ## Contributing
70
+
71
+ Issues and pull requests welcome at [github.com/lnsy-dev/pochade-node-red](https://github.com/lnsy-dev/pochade-node-red)
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * pochade-node-red
5
+ *
6
+ * Creates a new Pochade Node-RED project from the template.
7
+ *
8
+ * Usage: npx pochade-node-red my-node
9
+ *
10
+ * @module pochade-node-red
11
+ */
12
+
13
+ const spawn = require('cross-spawn');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const readline = require('readline');
17
+
18
+ /**
19
+ * Creates a readline interface for user input
20
+ *
21
+ * @returns {readline.Interface} The readline interface
22
+ */
23
+ function createReadlineInterface() {
24
+ return readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Prompts the user with a question and returns their answer
32
+ *
33
+ * @param {readline.Interface} rl - The readline interface
34
+ * @param {string} question - The question to ask
35
+ * @param {string} defaultValue - Optional default value
36
+ * @returns {Promise<string>} The user's answer
37
+ */
38
+ function ask(rl, question, defaultValue = '') {
39
+ return new Promise((resolve) => {
40
+ const prompt = defaultValue
41
+ ? `${question} (${defaultValue}): `
42
+ : `${question}: `;
43
+
44
+ rl.question(prompt, (answer) => {
45
+ resolve(answer.trim() || defaultValue);
46
+ });
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Prompts the user with a question and validates the answer
52
+ *
53
+ * @param {readline.Interface} rl - The readline interface
54
+ * @param {string} question - The question to ask
55
+ * @param {function} validator - Function that returns true if valid, or a string error message
56
+ * @param {string} defaultValue - Optional default value
57
+ * @returns {Promise<string>} The user's answer
58
+ */
59
+ async function askWithValidation(rl, question, validator, defaultValue = '') {
60
+ while (true) {
61
+ const answer = await ask(rl, question, defaultValue);
62
+ const validation = validator(answer);
63
+ if (validation === true) {
64
+ return answer;
65
+ }
66
+ console.log(`❌ ${validation}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Validates a package/project name (URL-friendly, no spaces)
72
+ * @param {string} name
73
+ * @returns {boolean|string}
74
+ */
75
+ function validatePackageName(name) {
76
+ if (!name) return "Name cannot be empty";
77
+ if (!/^[a-z0-9-_]+$/.test(name)) {
78
+ return "Name must only contain lowercase letters, numbers, hyphens, and underscores";
79
+ }
80
+ return true;
81
+ }
82
+
83
+ /**
84
+ * Validates a node name (safe for filenames)
85
+ * @param {string} name
86
+ * @returns {boolean|string}
87
+ */
88
+ function validateNodeName(name) {
89
+ if (!name) return "Name cannot be empty";
90
+ if (!/^[a-z0-9-]+$/.test(name)) {
91
+ return "Name must only contain lowercase letters, numbers, and hyphens";
92
+ }
93
+ return true;
94
+ }
95
+
96
+
97
+
98
+ /**
99
+ * Collects project configuration from user input
100
+ *
101
+ * @param {string} projectName - The project name
102
+ * @returns {Promise<object>} Configuration object with all project details
103
+ */
104
+ async function collectProjectInfo(projectName) {
105
+ const rl = createReadlineInterface();
106
+
107
+ const logo = ".-. .-. .-. . . .-. .-. .-. . .-.\r\n|-\' | | | |-| |-| | )|- | `-.\r\n\' `-\' `-\' \' ` ` \' `-\' `-\' `-\' `-\'\r\n Node-RED Plugins with Passion\r\n By LNSY\r\n"
108
+ console.log(logo);
109
+
110
+ console.log('\n📝 Let\'s set up your Node-RED Plugin project!\n');
111
+
112
+ // Default node name derived from project name
113
+ const defaultNodeName = projectName.replace(/^node-red-contrib-/, '');
114
+
115
+ const config = {
116
+ project_name: projectName,
117
+ project_description: await ask(rl, 'Project description', 'A Node-RED node'),
118
+ node_sidebar_title: await ask(rl, 'Sidebar title (category)', 'function'),
119
+ node_name: await askWithValidation(rl, 'Node name', validateNodeName, defaultNodeName),
120
+ node_purpose: await ask(rl, 'Node purpose', 'Processes messages'),
121
+ author_name: await ask(rl, 'Author name', ''),
122
+ license: await ask(rl, 'License', 'MIT')
123
+ };
124
+
125
+
126
+
127
+ rl.close();
128
+
129
+ return config;
130
+ }
131
+
132
+ /**
133
+ * Replaces template variables in a file content
134
+ *
135
+ * @param {string} content - The content with template variables
136
+ * @param {object} config - Configuration object with values
137
+ * @returns {string} Content with variables replaced
138
+ */
139
+ function replaceTemplateVariables(content, config) {
140
+ return content
141
+ .replace(/\$\{\s*project_description\s*\}/g, config.project_description)
142
+ .replace(/\$\{\s*node_sidebar_title\s*\}/g, config.node_sidebar_title)
143
+ .replace(/\$\{\s*node_name\s*\}/g, config.node_name)
144
+ .replace(/\$\{\s*node_purpose\s*\}/g, config.node_purpose);
145
+ }
146
+
147
+ /**
148
+ * Recursively updates files in the project directory
149
+ *
150
+ * @param {string} dir - Directory to process
151
+ * @param {object} config - Configuration object
152
+ */
153
+ function updateFiles(dir, config) {
154
+ const files = fs.readdirSync(dir);
155
+
156
+ for (const file of files) {
157
+ const filePath = path.join(dir, file);
158
+ const stats = fs.statSync(filePath);
159
+
160
+ if (stats.isDirectory()) {
161
+ updateFiles(filePath, config);
162
+ } else {
163
+ // Only process text files
164
+ if (['.js', '.html', '.json', '.md'].includes(path.extname(file))) {
165
+ let content = fs.readFileSync(filePath, 'utf-8');
166
+ content = replaceTemplateVariables(content, config);
167
+ fs.writeFileSync(filePath, content, 'utf-8');
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Main function to create a new Pochade Node-RED project
175
+ *
176
+ * @returns {Promise<void>}
177
+ */
178
+ async function createProject() {
179
+ const projectName = process.argv[2];
180
+
181
+ // Validate project name
182
+ if (!projectName) {
183
+ console.error('Error: Please specify the project name.');
184
+ console.log('Usage: npx pochade-node-red <project-name>');
185
+ process.exit(1);
186
+ }
187
+
188
+ const nameValidation = validatePackageName(projectName);
189
+ if (nameValidation !== true) {
190
+ console.error(`Error: Invalid project name "${projectName}".`);
191
+ console.error(nameValidation);
192
+ process.exit(1);
193
+ }
194
+
195
+ const currentDir = process.cwd();
196
+ const projectDir = path.resolve(currentDir, projectName);
197
+
198
+ if (fs.existsSync(projectDir)) {
199
+ console.error(`Error: Directory "${projectName}" already exists.`);
200
+ process.exit(1);
201
+ }
202
+
203
+ const config = await collectProjectInfo(projectName);
204
+
205
+ console.log(`\n🚀 Creating a new Node-RED node in ${projectDir}...`);
206
+
207
+ fs.mkdirSync(projectDir, { recursive: true });
208
+
209
+ const templateDir = path.resolve(__dirname, '..', 'template');
210
+
211
+ if (!fs.existsSync(templateDir)) {
212
+ console.error('Error: Template directory not found.');
213
+ process.exit(1);
214
+ }
215
+
216
+ // Copy template
217
+ fs.cpSync(templateDir, projectDir, { recursive: true });
218
+
219
+ // Rename dotfiles
220
+ const dotfiles = [
221
+ { from: 'gitignore', to: '.gitignore' },
222
+ { from: 'npmignore', to: '.npmignore' }
223
+ ];
224
+
225
+ dotfiles.forEach(({ from, to }) => {
226
+ const fromPath = path.join(projectDir, from);
227
+ const toPath = path.join(projectDir, to);
228
+ if (fs.existsSync(fromPath)) {
229
+ fs.renameSync(fromPath, toPath);
230
+ }
231
+ });
232
+
233
+ // Rename node files
234
+ const srcDir = path.join(projectDir, 'src');
235
+ if (fs.existsSync(srcDir)) {
236
+ const nodeJsPath = path.join(srcDir, 'sample.js');
237
+ const nodeHtmlPath = path.join(srcDir, 'sample.html');
238
+
239
+ if (fs.existsSync(nodeJsPath)) {
240
+ fs.renameSync(nodeJsPath, path.join(srcDir, `${config.node_name}.js`));
241
+ }
242
+ if (fs.existsSync(nodeHtmlPath)) {
243
+ fs.renameSync(nodeHtmlPath, path.join(srcDir, `${config.node_name}.html`));
244
+ }
245
+ }
246
+
247
+ // Update file contents
248
+ updateFiles(projectDir, config);
249
+
250
+ // Update package.json specifically
251
+ const packageJsonPath = path.join(projectDir, 'package.json');
252
+ const projectPackageJson = require(packageJsonPath);
253
+ projectPackageJson.name = config.project_name;
254
+ projectPackageJson.author = config.author_name;
255
+ projectPackageJson.license = config.license;
256
+
257
+ // Ensure the node-red section points to the correct file
258
+ if (projectPackageJson['node-red'] && projectPackageJson['node-red'].nodes) {
259
+ // Reconstruct the nodes object with the new key and value
260
+ projectPackageJson['node-red'].nodes = {};
261
+ projectPackageJson['node-red'].nodes[config.node_name] = `src/${config.node_name}.js`;
262
+ }
263
+
264
+ fs.writeFileSync(packageJsonPath, JSON.stringify(projectPackageJson, null, 2));
265
+
266
+ console.log('\n📦 Installing dependencies...');
267
+
268
+ const installResult = spawn.sync('npm', ['install'], {
269
+ cwd: projectDir,
270
+ stdio: 'inherit'
271
+ });
272
+
273
+ if (installResult.status !== 0) {
274
+ console.error('\n❌ Error: npm install failed.');
275
+ process.exit(1);
276
+ }
277
+
278
+ console.log('\n✨ Success!');
279
+ console.log(`\nTo get started:\n`);
280
+ console.log(` cd ${projectName}`);
281
+ console.log(` npm run install-plugin # Installs this node to your local Node-RED`);
282
+ console.log(` npm run watch # Develop with auto-restart`);
283
+ console.log('\n🎨 Happy coding!');
284
+ }
285
+
286
+ createProject();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "pochade-node-red",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight generator for Node-RED nodes and plugins.",
5
+ "bin": {
6
+ "pochade-node-red": "bin/create-pochade-js.js"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lnsy-dev/pochade-node-red.git"
11
+ },
12
+ "keywords": [
13
+ "node-red",
14
+ "template",
15
+ "starter",
16
+ "vanilla-js",
17
+ "rspack",
18
+ "dataroom-js",
19
+ "custom-elements"
20
+ ],
21
+ "author": "LNSY",
22
+ "license": "Unlicense",
23
+ "bugs": {
24
+ "url": "https://github.com/lnsy-dev/pochade-node-red/issues"
25
+ },
26
+ "homepage": "https://github.com/lnsy-dev/pochade-node-red#readme",
27
+ "dependencies": {
28
+ "cross-spawn": "^7.0.6"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "template/"
33
+ ]
34
+ }
@@ -0,0 +1,140 @@
1
+ # Node-RED Plugin Implementation Guide for AI Agents
2
+
3
+ This document provides a comprehensive guide for Large Language Models (LLMs) on how to implement Node-RED plugins (nodes).
4
+
5
+ ## Project Structure
6
+
7
+ A Node-RED node module typically has the following structure:
8
+
9
+ ```
10
+ package.json
11
+ nodes/
12
+ <node-name>.js # Runtime behavior
13
+ <node-name>.html # Editor UI and configuration
14
+ ```
15
+
16
+ ### package.json
17
+
18
+ The `package.json` must include a `node-red` section that maps node names to their JavaScript files.
19
+
20
+ ```json
21
+ {
22
+ "name": "node-red-contrib-example",
23
+ "version": "1.0.0",
24
+ "node-red": {
25
+ "nodes": {
26
+ "example-node": "nodes/example-node.js"
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## JavaScript File (.js)
33
+
34
+ The JavaScript file defines the runtime behavior of the node. It must export a function that registers the node with the Node-RED runtime.
35
+
36
+ ### Template
37
+
38
+ ```javascript
39
+ module.exports = function(RED) {
40
+ function ExampleNode(config) {
41
+ RED.nodes.createNode(this, config);
42
+ var node = this;
43
+
44
+ // Retrieve configuration values
45
+ node.name = config.name;
46
+
47
+ // Handle incoming messages
48
+ node.on('input', function(msg, send, done) {
49
+ // Process the message
50
+ msg.payload = msg.payload.toLowerCase();
51
+
52
+ // Send the message
53
+ send(msg);
54
+
55
+ // Signal completion
56
+ if (done) {
57
+ done();
58
+ }
59
+ });
60
+
61
+ // Clean up on close
62
+ node.on('close', function() {
63
+ // Cleanup logic
64
+ });
65
+ }
66
+
67
+ // Register the node type
68
+ RED.nodes.registerType("example-node", ExampleNode);
69
+ }
70
+ ```
71
+
72
+ ### Key Concepts
73
+
74
+ - **RED.nodes.createNode(this, config)**: Initializes the node instance.
75
+ - **this.on('input', ...)**: Listener for incoming messages.
76
+ - **send(msg)**: Sends a message to the next node(s). Configurable to support multiple outputs.
77
+ - **done()**: Callback to signal that processing is complete (for pre-1.0 compatibility loops).
78
+
79
+ ## HTML File (.html)
80
+
81
+ The HTML file defines the node's appearance in the Node-RED editor, its configuration dialog, and its help text. It contains three script blocks.
82
+
83
+ ### Template
84
+
85
+ ```html
86
+ <!-- Registration and Configuration -->
87
+ <script type="text/javascript">
88
+ RED.nodes.registerType('example-node', {
89
+ category: 'function',
90
+ color: '#a6bbcf',
91
+ defaults: {
92
+ name: { value: "" },
93
+ property: { value: "payload", required: true }
94
+ },
95
+ inputs: 1,
96
+ outputs: 1,
97
+ icon: "file.png",
98
+ label: function() {
99
+ return this.name || "example node";
100
+ }
101
+ });
102
+ </script>
103
+
104
+ <!-- Edit Dialog -->
105
+ <script type="text/html" data-template-name="example-node">
106
+ <div class="form-row">
107
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
108
+ <input type="text" id="node-input-name" placeholder="Name">
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-input-property"><i class="fa fa-edit"></i> Property</label>
112
+ <input type="text" id="node-input-property">
113
+ </div>
114
+ </script>
115
+
116
+ <!-- Help Text -->
117
+ <script type="text/html" data-help-name="example-node">
118
+ <p>A simple node that converts payload to lowercase.</p>
119
+ </script>
120
+ ```
121
+
122
+ ### Key Concepts
123
+
124
+ - **category**: The palette category (e.g., 'input', 'output', 'function', or custom).
125
+ - **defaults**: Defines the configuration properties that are saved and restored.
126
+ - **data-template-name**: Must match the registered type name.
127
+ - **data-help-name**: Must match the registered type name.
128
+
129
+ ## Best Practices
130
+
131
+ 1. **Error Handling**: Use `node.error("message", msg)` to log errors.
132
+ 2. **Status**: Use `node.status({fill:"red",shape:"ring",text:"disconnected"})` to show status in the editor.
133
+ 3. **Context**: Use `node.context()`, `flow.context()`, or `global.context()` to store state.
134
+ 4. **Dependencies**: Declare any external libraries in `package.json`.
135
+
136
+ ## Development Cycle
137
+
138
+ 1. **Install**: `npm install` in your node directory.
139
+ 2. **Link**: `npm link` (or `npm install . -g`) to make it available to Node-RED.
140
+ 3. **Restart**: Restart Node-RED to load changes.
@@ -0,0 +1,39 @@
1
+ # ${node_name}
2
+
3
+ ${node_purpose}
4
+
5
+ A Node-RED node component.
6
+
7
+ ## Installation
8
+
9
+ You can install this node directly from your Node-RED user directory (typically `~/.node-red`):
10
+
11
+ ```bash
12
+ cd ~/.node-red
13
+ npm install ${project_name}
14
+ ```
15
+
16
+ Or if you are developing locally, run `npm run install-plugin` from the project directory.
17
+
18
+ ## Usage
19
+
20
+ After installing and restarting Node-RED, you will find the **${node_name}** node in the **${node_sidebar_title}** category.
21
+
22
+ Drag the node onto the canvas and configure it.
23
+
24
+
25
+ ### Watch Mode
26
+
27
+ If you are making frequent changes, you can use:
28
+
29
+ ```sh
30
+ npm run watch
31
+ ```
32
+
33
+ This will start node-red and on every text change will re-install the plugin to the ~/.node-red node_modules directory and restart node-red.
34
+
35
+
36
+
37
+ ## License
38
+
39
+ ${license}
@@ -0,0 +1,4 @@
1
+ # Cursor Rules
2
+
3
+ You are an expert Node-RED developer.
4
+ Please refer to [AGENTS.md](./AGENTS.md) for specific guidelines on implementing Node-RED nodes in this project.
@@ -0,0 +1,4 @@
1
+ # Gemini Context
2
+
3
+ You are assisting with a Node-RED plugin project.
4
+ Consult [AGENTS.md](./AGENTS.md) for the architectural patterns and implementation details required for this codebase.