├── .gitignore ├── test ├── node-red-admin_spec.js └── lib │ ├── commands │ ├── result_helper.js │ ├── list_spec.js │ ├── remove_spec.js │ ├── enable_spec.js │ ├── install_spec.js │ ├── disable_spec.js │ ├── hash_spec.js │ ├── info_spec.js │ ├── target_spec.js │ ├── search_spec.js │ └── login_spec.js │ ├── result_spec.js │ ├── config_spec.js │ └── request_spec.js ├── node-red-admin.js ├── lib ├── prompt.js ├── commands │ ├── list.js │ ├── projects.js │ ├── info.js │ ├── enable.js │ ├── disable.js │ ├── hash.js │ ├── remove.js │ ├── install.js │ ├── target.js │ ├── login.js │ ├── search.js │ └── init │ │ ├── index.js │ │ └── resources │ │ └── settings.js.mustache ├── config.js ├── request.js ├── index.js └── result.js ├── .github ├── ISSUE_TEMPLATE │ ├── -anything-else.md │ └── --bug_report.md ├── workflows │ └── tests.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── CHANGELOG.md ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .nyc_output 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | node_modules 30 | -------------------------------------------------------------------------------- /test/node-red-admin_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | //require("../node-red-admin"); 18 | -------------------------------------------------------------------------------- /node-red-admin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | **/ 17 | 18 | require("./lib/index.js")(process.argv.slice(2)).catch(err => { 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/prompt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var { read } = require("read"); 18 | 19 | // This exists purely to provide a place to hook in a unit test mock of the 20 | // read module 21 | 22 | module.exports = { 23 | read: read 24 | }; 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-anything-else.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Anything Else 3 | about: Something that is not a bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please DO NOT raise an issue. 11 | 12 | We DO NOT use the issue tracker for general support or feature requests. Only bug reports should be raised here using the 'Bug report' template. 13 | 14 | For general support, please use the [Node-RED Forum](https://discourse.nodered.org) or [slack team](https://nodered.org/slack). You could also consider asking a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/node-red) and tag it `node-red`. 15 | That way the whole Node-RED user community can help, rather than rely on the core development team. 16 | 17 | For feature requests, please use the Node-RED Forum](https://discourse.nodered.org). Many ideas have already been discussed there and you should search that for your request before starting a new discussion. 18 | -------------------------------------------------------------------------------- /lib/commands/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | return request.request('/nodes', {}).then(result.logNodeList); 21 | } 22 | command.alias = "list"; 23 | command.usage = command.alias; 24 | command.description = "List all of the installed nodes"; 25 | 26 | 27 | module.exports = command; 28 | -------------------------------------------------------------------------------- /lib/commands/projects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | return request.request('/projects', {}).then(result.logProjectList); 21 | } 22 | command.alias = "projects"; 23 | command.usage = command.alias; 24 | command.description = "List available projects"; 25 | 26 | module.exports = command; 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Run tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [18, 20, 22, 24] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install Dependencies 25 | run: npm install 26 | - name: Run tests 27 | run: | 28 | npm run test 29 | npm run coverage 30 | - name: Publish to coveralls.io 31 | if: ${{ matrix.node-version == 20 }} 32 | uses: coverallsapp/github-action@v2 33 | with: 34 | github-token: ${{ github.token }} 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 21 | 22 | ### What are the steps to reproduce? 23 | 24 | ### What happens? 25 | 26 | ### What do you expect to happen? 27 | 28 | ### Please tell us about your environment: 29 | 30 | - [ ] Node-RED Admin version: 31 | - [ ] Node-RED version: 32 | - [ ] Node.js version: 33 | - [ ] npm version: 34 | - [ ] Platform/OS: 35 | -------------------------------------------------------------------------------- /lib/commands/info.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | var node = argv._[1]; 21 | if (!node) { 22 | return result.help(command); 23 | } 24 | return request.request('/nodes/' + node, {}).then(result.logDetails); 25 | } 26 | command.alias = "info"; 27 | command.usage = command.alias+" {module|node}"; 28 | command.description = "Display more information about the module or node"; 29 | 30 | module.exports = command; 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Reproducible software issues in the core of Node-RED 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 26 | 27 | ### What are the steps to reproduce? 28 | 29 | ### What happens? 30 | 31 | ### What do you expect to happen? 32 | 33 | ### Please tell us about your environment: 34 | 35 | - [ ] Node-RED version: 36 | - [ ] Node.js version: 37 | - [ ] npm version: 38 | - [ ] Platform/OS: 39 | - [ ] Browser: 40 | -------------------------------------------------------------------------------- /lib/commands/enable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | var node = argv._[1]; 21 | if (!node) { 22 | return result.help(command); 23 | } 24 | return request.request('/nodes/' + node, { 25 | method: "PUT", 26 | data: { 27 | enabled: true 28 | } 29 | }).then(result.logList); 30 | } 31 | command.alias = "enable"; 32 | command.usage = command.alias+" {module|id}"; 33 | command.description = "Enable the specified module or node set"; 34 | 35 | 36 | module.exports = command; 37 | -------------------------------------------------------------------------------- /lib/commands/disable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | var node = argv._[1]; 21 | if (!node) { 22 | return result.help(command); 23 | } 24 | return request.request('/nodes/' + node, { 25 | method: "PUT", 26 | data: { 27 | enabled: false 28 | } 29 | }).then(result.logList); 30 | } 31 | command.alias = "disable"; 32 | command.usage = command.alias+" {module|id}"; 33 | command.description = "Disable the specified module or node set"; 34 | 35 | 36 | module.exports = command; 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | 16 | 23 | 24 | ## Proposed changes 25 | 26 | 27 | 28 | ## Checklist 29 | 30 | 31 | - [ ] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md) 32 | - [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team. 33 | - [ ] I have run `npm run test` to verify the unit tests pass 34 | - [ ] I have added suitable unit tests to cover the new/changed functionality 35 | -------------------------------------------------------------------------------- /test/lib/commands/result_helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | var sinon = require("sinon"); 17 | var result = require("../../../lib/result"); 18 | 19 | module.exports = { 20 | log: sinon.spy(), 21 | warn: sinon.spy(), 22 | help: sinon.spy(), 23 | logList: sinon.spy(), 24 | logNodeList: sinon.spy(), 25 | logDetails: sinon.spy(), 26 | 27 | reset: function() { 28 | module.exports.log.resetHistory(); 29 | module.exports.warn.resetHistory(); 30 | module.exports.help.resetHistory(); 31 | module.exports.logList.resetHistory(); 32 | module.exports.logNodeList.resetHistory(); 33 | module.exports.logDetails.resetHistory(); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/commands/hash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var prompt = require("../prompt"); 18 | try { bcrypt = require('@node-rs/bcrypt'); } 19 | catch(e) { bcrypt = require('bcryptjs'); } 20 | 21 | async function command(argv,result) { 22 | if (argv.json) { 23 | console.warn("hash-pw command does not support json format output"); 24 | } 25 | const password = await prompt.read({prompt:"Password:",silent: true}) 26 | if (password) { 27 | result.log(bcrypt.hashSync(password, 8)); 28 | } 29 | } 30 | command.alias = "hash-pw"; 31 | command.usage = command.alias; 32 | command.description = "Creates a password hash suitable for use with adminAuth or httpNodeAuth"; 33 | 34 | module.exports = command; 35 | -------------------------------------------------------------------------------- /lib/commands/remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | var module = argv._[1]; 21 | if (!module) { 22 | return result.help(command); 23 | } 24 | return request.request('/nodes/' + module, { 25 | method: "DELETE" 26 | }).then(function() { 27 | if (argv.json) { 28 | result.log(JSON.stringify({message: "Uninstalled " + module}, null, 4)); 29 | } else { 30 | result.log("Uninstalled " + module); 31 | } 32 | }); 33 | } 34 | command.alias = "remove"; 35 | command.usage = command.alias+" "; 36 | command.description = "Remove the NPM module"; 37 | 38 | 39 | module.exports = command; 40 | -------------------------------------------------------------------------------- /lib/commands/install.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | 19 | function command(argv,result) { 20 | var module = argv._[1]; 21 | var url = argv._[2]; 22 | if (!module) { 23 | return result.help(command); 24 | } 25 | 26 | var data = {}; 27 | var m = /^(.+)@(.+)$/.exec(module); 28 | if (m) { 29 | data.module = m[1]; 30 | data.version = m[2]; 31 | } else { 32 | data.module = module; 33 | } 34 | if (url) { 35 | data.url = url; 36 | } 37 | return request.request('/nodes', { 38 | method: "POST", 39 | data: data 40 | }).then(result.logDetails); 41 | } 42 | command.alias = "install"; 43 | command.usage = command.alias+" []"; 44 | command.description = "Install a module."; 45 | 46 | 47 | module.exports = command; 48 | -------------------------------------------------------------------------------- /lib/commands/target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var config = require("../config"); 18 | 19 | function command(argv,result) { 20 | return new Promise((resolve,reject) => { 21 | var target = argv._[1]; 22 | if (target) { 23 | if (!/^https?:\/\/.+/.test(target)) { 24 | reject("Invalid target URL: " + target); 25 | return; 26 | } 27 | if (target.slice(-1) == "/") { 28 | target = target.slice(0, target.length - 1); 29 | } 30 | config.target(target); 31 | } 32 | if (argv.json) { 33 | result.log(JSON.stringify({target: config.target()}, null, 4)); 34 | } else { 35 | result.log("Target: " + config.target()); 36 | } 37 | resolve(); 38 | }); 39 | } 40 | 41 | command.alias = "target"; 42 | command.usage = command.alias+" [url]"; 43 | command.description = "Set or view the target URL"; 44 | 45 | 46 | module.exports = command; 47 | -------------------------------------------------------------------------------- /test/lib/commands/list_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/list"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | var request = require("../../../lib/request"); 22 | var result = require("./result_helper"); 23 | 24 | describe("commands/list", function() { 25 | afterEach(function() { 26 | request.request.restore(); 27 | result.reset(); 28 | }); 29 | 30 | it('lists all nodes', function(done) { 31 | var error; 32 | sinon.stub(request,"request").callsFake(function(path,opts) { 33 | try { 34 | should(path).be.eql("/nodes"); 35 | opts.should.eql({}); 36 | } catch(err) { 37 | error = err; 38 | } 39 | return Promise.resolve([]); 40 | }); 41 | command({},result).then(function() { 42 | if (error) { 43 | throw error; 44 | } 45 | result.logNodeList.called.should.be.true(); 46 | done(); 47 | }).catch(done); 48 | }); 49 | 50 | }); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 4.1.2 2 | 3 | - chore: update axios to 1.12.2 (#53) @bryopsida 4 | 5 | ### 4.1.1 6 | 7 | - Update dependencies @knolleary 8 | - Add new themes introduced in @node-red-contrib-themes/theme-collection v4.1 (#50) @bonanitech 9 | 10 | ### 4.1.0 11 | 12 | - Add telemetry consent to init command @knolleary 13 | 14 | ### 4.0.2 15 | 16 | - Update dependencies @knolleary 17 | 18 | ### 4.0.1 19 | 20 | - Updated axios and mocha dependencies @knolleary 21 | 22 | ### 4.0.0 23 | 24 | - Update template settings for 4.0 25 | - Update dependencies 26 | - Replace bcrypt with @node-rs/bcrypt 27 | - Set minimum node.js version to 18 28 | 29 | ### 3.1.3 30 | 31 | - Bump axios to 1.6.8 @knolleary 32 | 33 | ### 3.1.2 34 | 35 | - Bump axios to 1.6.7 @knolleary 36 | 37 | ### 3.1.1 38 | 39 | - Bump axios to 1.6.1 (#31) @hardillb 40 | 41 | ### 3.1.0 42 | 43 | - Remove temporary dev comments (#22) @bonanitech 44 | - Leave Monaco theme commented out by default (#23) @bonanitech 45 | - Add new themes introduced in @node-red-contrib-themes/theme-collection v3.0 (#24) @bonanitech 46 | - Update dependencies (#27) @knolleary 47 | - Update template settings for 3.1 (#26) @knolleary 48 | - Update monaco link (#25) @knolleary 49 | 50 | ### 3.0.0 51 | 52 | - Update httpStatic and add httpStaticRoot for changes in Node-RED V3 (#20) @Steve-Mcl 53 | - Update dependencies 54 | - Drop node 12 support 55 | 56 | ### 2.2.4 57 | 58 | - Add Dracula theme (#17) @bonanitech 59 | - Default to monaco editor for V3 (#19) @Steve-Mcl 60 | - Update dependencies 61 | 62 | ### 2.2.3 63 | 64 | - Update dependencies 65 | 66 | ### 2.2.2 67 | 68 | - Update dependencies 69 | 70 | ### 2.2.1 71 | 72 | - Update dependencies 73 | 74 | ### 2.2.0 75 | 76 | - Default admin init to enable `functionExternalModules` 77 | 78 | ### 2.1.0 79 | 80 | - Add `node-red-admin init` command to help create settings file 81 | 82 | ### 2.0.0 83 | 84 | - Drop support for old Node versions. Now requires at least Node 12. 85 | - Update dependencies to latest 86 | -------------------------------------------------------------------------------- /lib/commands/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var request = require("../request"); 18 | var config = require("../config"); 19 | var prompt = require("../prompt"); 20 | 21 | async function command(argv,result) { 22 | config.tokens(null); 23 | const resp = await request.request('/auth/login',{}) 24 | if (resp.type) { 25 | if (resp.type == "credentials") { 26 | const username = await prompt.read({prompt:"Username:"}) 27 | const password = await prompt.read({prompt:"Password:",silent: true}) 28 | const loginResp = await request.request('/auth/token', { 29 | method: "POST", 30 | data: { 31 | client_id: 'node-red-admin', 32 | grant_type: 'password', 33 | scope: '*', 34 | username: username, 35 | password: password 36 | } 37 | }).catch(resp => { 38 | throw new Error("Login failed"); 39 | }) 40 | config.tokens(loginResp); 41 | result.log("Logged in"); 42 | } else { 43 | throw new Error("Unsupported login type"); 44 | } 45 | } 46 | } 47 | 48 | command.alias = "login"; 49 | command.usage = command.alias+""; 50 | command.description = "Log in to the targeted Node-RED admin api"; 51 | 52 | module.exports = command; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-admin", 3 | "version": "4.1.2", 4 | "description": "The Node-RED admin command line interface", 5 | "homepage": "https://nodered.org", 6 | "bugs": { 7 | "url": "https://github.com/node-red/node-red-admin/issues/" 8 | }, 9 | "license": "Apache-2.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/node-red/node-red-admin.git" 13 | }, 14 | "main": "lib/index.js", 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "contributors": [ 19 | { 20 | "name": "Nick O'Leary" 21 | }, 22 | { 23 | "name": "Anna Thomas" 24 | } 25 | ], 26 | "scripts": { 27 | "test": "nyc mocha", 28 | "coverage": "nyc report --reporter=lcov" 29 | }, 30 | "dependencies": { 31 | "ansi-colors": "^4.1.3", 32 | "axios": "1.12.2", 33 | "bcryptjs": "3.0.2", 34 | "cli-table": "^0.3.11", 35 | "enquirer": "^2.3.6", 36 | "minimist": "^1.2.8", 37 | "mustache": "^4.2.0", 38 | "read": "^3.0.1" 39 | }, 40 | "devDependencies": { 41 | "mocha": "^11.1.0", 42 | "nyc": "^17.1.0", 43 | "should": "^13.2.3", 44 | "sinon": "^20.0.0", 45 | "sinon-test": "^3.1.6" 46 | }, 47 | "optionalDependencies": { 48 | "@node-rs/bcrypt": "1.10.7" 49 | }, 50 | "bin": { 51 | "node-red-admin": "node-red-admin.js" 52 | }, 53 | "mocha": { 54 | "spec:": "test/**/*.spec.js", 55 | "diff": true, 56 | "extension": [ 57 | "js" 58 | ], 59 | "opts": false, 60 | "package": "./package.json", 61 | "reporter": "spec", 62 | "slow": 75, 63 | "timeout": 2000, 64 | "ui": "bdd", 65 | "recursive": "true", 66 | "watch-files": [ 67 | "lib/**/*.js", 68 | "test/**/*.js" 69 | ], 70 | "watch-ignore": [ 71 | "lib/vendor" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-RED Admin CLI 2 | 3 | The Node-RED admin command line interface. 4 | 5 | [![Tests](https://github.com/node-red/node-red-admin/actions/workflows/tests.yml/badge.svg)](https://github.com/node-red/node-red-admin/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/node-red/node-red-admin/badge.svg?branch=master)](https://coveralls.io/r/node-red/node-red-admin?branch=master) 6 | 7 | 8 | A command line tool for remotely administering Node-RED. 9 | 10 | It is built into `node-red` and can be run as: 11 | 12 | node-red admin .... 13 | 14 | 15 | ## Standalone install 16 | 17 | Install this globally to make the `node-red-admin` command available on your path: 18 | 19 | npm install -g node-red-admin 20 | 21 | Note: you may need to run this with `sudo`, or from within an Administrator command shell. 22 | 23 | You may also need to add `--unsafe-perm` to the command if you hit permissions errors during installation. 24 | 25 | ## Usage 26 | 27 | Usage: 28 | node-red-admin [args] [--help] [--userDir DIR] [--json] 29 | 30 | Description: 31 | Node-RED command-line client 32 | 33 | Commands: 34 | target - Set or view the target URL and port like http://localhost:1880 35 | login - Log user in to the target of the Node-RED admin API 36 | list - List all of the installed nodes 37 | info - Display more information about the module or node 38 | enable - Enable the specified module or node set 39 | disable - Disable the specified module or node set 40 | search - Search for Node-RED modules to install 41 | install - Install the module from NPM to Node-RED 42 | remove - Remove the NPM module from Node-RED 43 | hash-pw - Creates a hash to use for Node-RED settings like "adminAuth" 44 | 45 | By default, the tool stores its configuration in `~/.node-red/.cli-config.json`. You 46 | can specify a different directory for the config file using the `--userDir` argument. 47 | 48 | The `--json` option causes the tool to format its output as JSON making it suitable 49 | for scripting. 50 | -------------------------------------------------------------------------------- /test/lib/commands/remove_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/remove"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var request = require("../../../lib/request"); 23 | var result = require("./result_helper"); 24 | 25 | describe("commands/remove", function() { 26 | afterEach(function() { 27 | if (request.request.restore) { 28 | request.request.restore(); 29 | } 30 | result.reset(); 31 | }); 32 | 33 | it('removes a node', function(done) { 34 | var error; 35 | sinon.stub(request,"request").callsFake(function(path,opts) { 36 | try { 37 | should(path).be.eql("/nodes/testnode"); 38 | opts.should.eql({ 39 | method:"DELETE" 40 | }); 41 | } catch(err) { 42 | error = err; 43 | } 44 | return Promise.resolve([]); 45 | }); 46 | command({_:[null,"testnode"]},result).then(function() { 47 | if (error) { 48 | throw error; 49 | } 50 | result.log.called.should.be.true(); 51 | done(); 52 | }).catch(done); 53 | }); 54 | 55 | it('displays command help if node not specified', function(done) { 56 | command({_:{}},result); 57 | result.help.called.should.be.true(); 58 | done(); 59 | }); 60 | }); -------------------------------------------------------------------------------- /test/lib/commands/enable_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/enable"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var request = require("../../../lib/request"); 23 | var result = require("./result_helper"); 24 | 25 | describe("commands/enable", function() { 26 | afterEach(function() { 27 | if (request.request.restore) { 28 | request.request.restore(); 29 | } 30 | result.reset(); 31 | }); 32 | 33 | it('enables a node', function(done) { 34 | var error; 35 | sinon.stub(request,"request").callsFake(function(path,opts) { 36 | try { 37 | should(path).be.eql("/nodes/testnode"); 38 | opts.should.eql({ 39 | method:"PUT", 40 | data:{"enabled":true} 41 | }); 42 | } catch(err) { 43 | error = err; 44 | } 45 | return Promise.resolve([]); 46 | }); 47 | command({_:[null,"testnode"]},result).then(function() { 48 | if (error) { 49 | throw error; 50 | } 51 | result.logList.called.should.be.true(); 52 | done(); 53 | }).catch(done); 54 | }); 55 | 56 | it('displays command help if node not specified', function(done) { 57 | command({_:{}},result); 58 | result.help.called.should.be.true(); 59 | done(); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/lib/commands/install_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/install"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var request = require("../../../lib/request"); 23 | var result = require("./result_helper"); 24 | 25 | describe("commands/install", function() { 26 | afterEach(function() { 27 | if (request.request.restore) { 28 | request.request.restore(); 29 | } 30 | result.reset(); 31 | }); 32 | 33 | it('installs a node', function(done) { 34 | var error; 35 | sinon.stub(request,"request").callsFake(function(path,opts) { 36 | try { 37 | should(path).be.eql("/nodes"); 38 | opts.should.eql({ 39 | method:"POST", 40 | data:{"module":"testnode"} 41 | }); 42 | } catch(err) { 43 | error = err; 44 | } 45 | return Promise.resolve([]); 46 | }); 47 | command({_:[null,"testnode"]},result).then(function() { 48 | if (error) { 49 | throw error; 50 | } 51 | result.logDetails.called.should.be.true(); 52 | done(); 53 | }).catch(done); 54 | }); 55 | 56 | it('displays command help if node not specified', function(done) { 57 | command({_:{}},result); 58 | result.help.called.should.be.true(); 59 | done(); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/lib/commands/disable_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/disable"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var request = require("../../../lib/request"); 23 | var result = require("./result_helper"); 24 | 25 | describe("commands/disable", function() { 26 | afterEach(function() { 27 | if (request.request.restore) { 28 | request.request.restore(); 29 | } 30 | result.reset(); 31 | }); 32 | 33 | it('disables a node', function(done) { 34 | var error; 35 | sinon.stub(request,"request").callsFake(function(path,opts) { 36 | try { 37 | should(path).be.eql("/nodes/testnode"); 38 | opts.should.eql({ 39 | method:"PUT", 40 | data:{"enabled":false} 41 | }); 42 | } catch(err) { 43 | error = err; 44 | } 45 | return Promise.resolve([]); 46 | }); 47 | command({_:[null,"testnode"]},result).then(function() { 48 | if (error) { 49 | throw error; 50 | } 51 | result.logList.called.should.be.true(); 52 | done(); 53 | }).catch(done); 54 | }); 55 | 56 | it('displays command help if node not specified', function(done) { 57 | command({_:{}},result); 58 | result.help.called.should.be.true(); 59 | done(); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/lib/commands/hash_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/hash"); 18 | 19 | var prompt = require("../../../lib/prompt"); 20 | 21 | var should = require("should"); 22 | var sinon = require("sinon"); 23 | 24 | 25 | var request = require("../../../lib/request"); 26 | try { bcrypt = require('bcrypt'); } 27 | catch(e) { bcrypt = require('bcryptjs'); } 28 | 29 | var result = require("./result_helper"); 30 | 31 | describe("commands/hash-pw", function() { 32 | afterEach(function() { 33 | result.reset(); 34 | prompt.read.restore(); 35 | }); 36 | it('generates a bcrypt hash of provided password',function(done) { 37 | sinon.stub(prompt,"read").resolves("a-test-password"); 38 | 39 | command({},result).then(function() { 40 | result.log.calledOnce.should.be.true(); 41 | var hash = result.log.firstCall.args[0]; 42 | bcrypt.compare("a-test-password",hash,function(err,match) { 43 | match.should.be.true 44 | done(); 45 | }); 46 | }); 47 | }); 48 | it('ignores blank password',function(done) { 49 | sinon.stub(prompt,"read").resolves("") 50 | 51 | command({},result).then(function() { 52 | result.log.called.should.be.false(); 53 | done(); 54 | }); 55 | }); 56 | it('ignores null password',function(done) { 57 | sinon.stub(prompt,"read").resolves(null) 58 | 59 | command({},result).then(function() { 60 | result.log.called.should.be.false(); 61 | done(); 62 | }); 63 | }); 64 | 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var path = require("path"); 18 | var fs = require("fs"); 19 | 20 | var userHome = process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME; 21 | var configDir = path.join(userHome, ".node-red"); 22 | var configFile = path.join(configDir, ".cli-config.json"); 23 | 24 | var config; 25 | 26 | function load() { 27 | if (config === null || typeof config === "undefined") { 28 | try { 29 | config = JSON.parse(fs.readFileSync(configFile)); 30 | } catch (err) { 31 | config = {}; 32 | } 33 | } 34 | } 35 | 36 | function save() { 37 | try { 38 | fs.mkdirSync(configDir); 39 | } catch (err) { 40 | if (err.code != "EEXIST") { 41 | throw err; 42 | } 43 | } 44 | fs.writeFileSync(configFile, JSON.stringify(config, null, 4)); 45 | } 46 | module.exports = { 47 | init: function(userDir) { 48 | configDir = userDir; 49 | configFile = path.join(configDir, ".cli-config.json"); 50 | }, 51 | unload: function() { 52 | config = null; 53 | } 54 | }; 55 | 56 | var properties = [ 57 | {name:"target",default:"http://localhost:1880"}, 58 | {name:"tokens"} 59 | ]; 60 | 61 | properties.forEach(function(prop) { 62 | module.exports[prop.name] = function(arg) { 63 | load(); 64 | if (arg === undefined) { 65 | return config[prop.name] || prop.default; 66 | } else if (arg === null) { 67 | delete config[prop.name]; 68 | } else { 69 | config[prop.name] = arg; 70 | } 71 | save(); 72 | }; 73 | }); 74 | 75 | -------------------------------------------------------------------------------- /test/lib/result_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var result = require("../../lib/result"); 18 | 19 | /** 20 | * This is a very lazy set of tests. They simply invoke the api with no checking 21 | * of the results. 22 | * Individual commands already verify they return the correct data to this module. 23 | * The exact format of the command output is not finalised. 24 | */ 25 | 26 | describe("lib/result", function() { 27 | it("log",function() { 28 | result.log("msg"); 29 | }); 30 | it("warn",function() { 31 | result.warn("msg"); 32 | result.warn("401"); 33 | }); 34 | it("help",function() { 35 | result.help({usage:"usage",description:"description",options:"options"}); 36 | }); 37 | it("logList",function() { 38 | result.logList([]); 39 | result.logList({nodes:[]}); 40 | }); 41 | it("logNodeList",function() { 42 | result.logNodeList([ 43 | {id:"nodeId1",types:["a","b"],enabled:true}, 44 | {id:"nodeId2",types:["c"],enabled:false}, 45 | {id:"nodeId4",types:["d","e"],enabled:false,err:"error"}, 46 | {id:"nodeId3",types:[],enabled:true,err:"error"}, 47 | {id:"nodeId3",types:[],enabled:true,err:"error"} 48 | ]); 49 | }); 50 | it("logDetails",function() { 51 | result.logDetails({id:"testId",module:"testModule",version:"testVersion",types:["a"],enabled:true}); 52 | result.logDetails({id:"testId",module:"testModule",version:"testVersion",types:["a"],enabled:false}); 53 | result.logDetails({id:"testId",module:"testModule",version:"testVersion",types:["a"],err:"error",enabled:true}); 54 | result.logDetails({id:"testId",module:"testModule",version:"testVersion",types:["a"],err:"error",enabled:false}); 55 | result.logDetails({name:"testModule",version:"testVersion",nodes:[{id:"nodeId",types:["a","b"],enabled:true}]}); 56 | }); 57 | }); -------------------------------------------------------------------------------- /test/lib/commands/info_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/info"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var request = require("../../../lib/request"); 23 | var result = require("./result_helper"); 24 | 25 | describe("commands/info", function() { 26 | afterEach(function() { 27 | if (request.request.restore) { 28 | request.request.restore(); 29 | } 30 | result.reset(); 31 | }); 32 | 33 | it('displays information on a module', function(done) { 34 | var error; 35 | sinon.stub(request,"request").callsFake(function(path,opts) { 36 | try { 37 | should(path).be.eql("/nodes/testnode"); 38 | opts.should.eql({}); 39 | } catch(err) { 40 | error = err; 41 | } 42 | return Promise.resolve([]); 43 | }); 44 | command({_:[null,"testnode"]},result).then(function() { 45 | if (error) { 46 | throw error; 47 | } 48 | result.logDetails.called.should.be.true(); 49 | done(); 50 | }).catch(done); 51 | }); 52 | 53 | it('reports error', function(done) { 54 | var error; 55 | sinon.stub(request,"request").callsFake(function(path,opts) { 56 | try { 57 | should(path).be.eql("/nodes/testnode"); 58 | opts.should.eql({}); 59 | } catch(err) { 60 | error = err; 61 | } 62 | return Promise.reject("error"); 63 | }); 64 | command({_:[null,"testnode"]},result).then(function() { 65 | done("Should have returned the error"); 66 | }).catch(err => { done() }); 67 | }); 68 | 69 | it('displays command help if node not specified', function(done) { 70 | command({_:{}},result); 71 | result.help.called.should.be.true(); 72 | done(); 73 | }); 74 | 75 | }); -------------------------------------------------------------------------------- /test/lib/commands/target_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/target"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var config = require("../../../lib/config"); 23 | 24 | var result = require("./result_helper"); 25 | 26 | describe("commands/target", function() { 27 | var target; 28 | beforeEach(function() { 29 | target = "http://test.example.com"; 30 | sinon.stub(config,"target").callsFake(function(arg) { 31 | if (arg) { target = arg } else { return target;} 32 | }); 33 | }); 34 | afterEach(function() { 35 | result.reset(); 36 | config.target.restore(); 37 | }); 38 | 39 | it('queries the target', function(done) { 40 | command({_:[]},result).then(() => { 41 | config.target.called.should.be.true(); 42 | config.target.args[0].should.have.lengthOf(0); 43 | result.log.called.should.be.true(); 44 | /http\:\/\/test\.example\.com/.test(result.log.args[0][0]).should.be.true(); 45 | done(); 46 | }).catch(done); 47 | }); 48 | 49 | it('sets the target', function(done) { 50 | command({_:[null,"http://newtarget.example.com"]},result).then(() => { 51 | config.target.called.should.be.true(); 52 | config.target.args[0][0].should.eql("http://newtarget.example.com"); 53 | result.log.called.should.be.true(); 54 | /http\:\/\/newtarget\.example\.com/.test(result.log.args[0][0]).should.be.true(); 55 | done(); 56 | }).catch(done); 57 | }); 58 | 59 | it('rejects non http targets', function(done) { 60 | command({_:[null,"ftp://newtarget.example.com"]},result).then(() => { 61 | done("Should not have accepted non http target") 62 | }).catch(err => { 63 | config.target.called.should.be.false(); 64 | done(); 65 | }).catch(done); 66 | }); 67 | it('strips trailing slash from target', function(done) { 68 | command({_:[null,"http://newtarget.example.com/"]},result).then(() => { 69 | config.target.called.should.be.true(); 70 | config.target.args[0][0].should.eql("http://newtarget.example.com"); 71 | done(); 72 | }).catch(done); 73 | }); 74 | 75 | 76 | 77 | 78 | }); -------------------------------------------------------------------------------- /lib/commands/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var httpRequest = require("axios"); 18 | 19 | function command(argv,result) { 20 | var module = argv._[1]; 21 | if (!module) { 22 | return result.help(command); 23 | } 24 | 25 | var options = { 26 | method: "GET", 27 | headers: { 28 | 'Accept': 'application/json', 29 | } 30 | }; 31 | return httpRequest.get('https://flows.nodered.org/things?format=json&per_page=50&type=node&term='+module, options).then(response => { 32 | if (response.status == 200) { 33 | var info = response.data; 34 | var matches = []; 35 | if (info.data && info.data.length > 0) { 36 | for (var i = 0; i < info.data.length; i++) { 37 | var n = info.data[i]; 38 | var label = info.data[i].name + " - " + info.data[i].description; 39 | var index = label.indexOf(module); 40 | matches.push({ 41 | label: label, 42 | index: index===-1?1000:index, 43 | n:n 44 | }); 45 | } 46 | matches.sort(function(A,B) { return A.index - B.index; }); 47 | if (argv.json) { 48 | result.log(JSON.stringify(matches.map(function(m) { return { 49 | name: m.n.name, 50 | description: m.n.description, 51 | version: (m.n['dist-tags']&& m.n['dist-tags'].latest)?m.n['dist-tags'].latest:undefined, 52 | updated_at: m.n.updated_at 53 | };}), null, 4)); 54 | } else { 55 | matches.forEach(function(m) { 56 | result.log(m.label); 57 | }); 58 | } 59 | 60 | } else { 61 | if (argv.json) { 62 | result.log("[]"); 63 | } else { 64 | result.log("No results found"); 65 | } 66 | } 67 | } else { 68 | throw new Error(response.status + ": " + response.data); 69 | } 70 | }); 71 | 72 | } 73 | command.alias = "search"; 74 | command.usage = command.alias+" "; 75 | command.description = "Search for Node-RED modules to install"; 76 | 77 | 78 | module.exports = command; 79 | -------------------------------------------------------------------------------- /test/lib/commands/search_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/search"); 18 | 19 | var should = require("should"); 20 | var sinon = require("sinon"); 21 | 22 | var httpRequest = require("axios"); 23 | var result = require("./result_helper"); 24 | 25 | describe("commands/install", function() { 26 | afterEach(function() { 27 | if (httpRequest.get.restore) { 28 | httpRequest.get.restore(); 29 | } 30 | result.reset(); 31 | }); 32 | 33 | it('reports no results when none match',function(done) { 34 | sinon.stub(httpRequest,"get").returns(Promise.resolve({status:200,data:{"data":[]}})); 35 | 36 | command({_:[null,"testnode"]},result).then(function() { 37 | result.log.called.should.be.true(); 38 | result.log.args[0][0].should.eql("No results found"); 39 | done(); 40 | }).catch(done); 41 | 42 | }); 43 | it('lists results ordered by relevance',function(done) { 44 | sinon.stub(httpRequest,"get").returns(Promise.resolve({status:200,data:{ 45 | "data":[ 46 | { "name":"another-node", "description":"a testnode - THREE" }, 47 | { "name":"testnode", "description":"a test node - ONE" }, 48 | { "name":"@scoped/testnode", "description":"once more - TWO" } 49 | ] 50 | }})); 51 | 52 | command({_:[null,"testnode"]},result).then(function() { 53 | result.log.args.length.should.equal(3); 54 | /ONE/.test(result.log.args[0][0]).should.be.true(); 55 | /TWO/.test(result.log.args[1][0]).should.be.true(); 56 | /THREE/.test(result.log.args[2][0]).should.be.true(); 57 | done(); 58 | }).catch(done); 59 | 60 | }); 61 | 62 | it('reports unexpected http response',function(done) { 63 | sinon.stub(httpRequest,"get").returns(Promise.resolve({status:101,data:"testError"})); 64 | 65 | command({_:[null,"testnode"]},result).then(function() { 66 | done("Should not have resolved") 67 | }).catch(err => { 68 | result.log.called.should.be.false(); 69 | /101: testError/.test(err).should.be.true(); 70 | done(); 71 | }).catch(done); 72 | }); 73 | 74 | it('displays command help if node not specified', function(done) { 75 | command({_:{}},result); 76 | result.help.called.should.be.true(); 77 | done(); 78 | }); 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | const request = require('axios'); 18 | const config = require("./config"); 19 | 20 | module.exports = { 21 | request: function(path, options) { 22 | var basePath = config.target(); 23 | options = options || {}; 24 | options.headers = options.headers || {}; 25 | options.headers['accept'] = 'application/json'; 26 | if (options.method === 'PUT' || options.method === 'POST') { 27 | options.headers['content-type'] = 'application/json'; 28 | } 29 | const url = basePath + path; 30 | 31 | if (config.tokens()) { 32 | options.headers['Authorization'] = "Bearer "+config.tokens().access_token; 33 | } 34 | 35 | // Pull out the request function so we can stub it in the tests 36 | var requestFunc = request.get; 37 | 38 | if (options.method === 'PUT') { 39 | requestFunc = request.put; 40 | } else if (options.method === 'POST') { 41 | requestFunc = request.post; 42 | } else if (options.method === 'DELETE') { 43 | requestFunc = request.delete; 44 | } 45 | if (process.env.NR_TRACE) { 46 | console.log(options); 47 | } 48 | // GET takes two args - url/options 49 | // PUT/POST take three - utl/data/options 50 | return requestFunc(url,options.data || options, options).then(response => { 51 | if (process.env.NR_TRACE) { 52 | console.log(response.data); 53 | } 54 | if (response.status === 200) { // OK 55 | return response.data; 56 | } else if (response.status === 204) { // No content 57 | return; 58 | } else { 59 | var message = response.status; 60 | if (response.data) { 61 | message += ": "+(response.data.message||response.data); 62 | } 63 | var err = new Error(message); 64 | throw err; 65 | } 66 | }).catch(err => { 67 | if (process.env.NR_TRACE) { 68 | if (err.response) { 69 | console.log("Response"); 70 | console.log("Status: "+err.response.status); 71 | console.log(err.response.data); 72 | } else { 73 | console.log("No response"); 74 | } 75 | } 76 | throw err; 77 | }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var config = require("./config"); 18 | 19 | var commands = { 20 | "target": require("./commands/target"), 21 | "list": require("./commands/list"), 22 | "init": require("./commands/init"), 23 | "info": require("./commands/info"), 24 | "enable": require("./commands/enable"), 25 | "disable": require("./commands/disable"), 26 | "search": require("./commands/search"), 27 | "install": require("./commands/install"), 28 | "remove": require("./commands/remove"), 29 | "login": require("./commands/login"), 30 | "hash-pw": require("./commands/hash"), 31 | // Leave 'projects' undocumented for now - needs more work to be useful 32 | "projects": require("./commands/projects") 33 | }; 34 | 35 | function help() { 36 | var helpText = "Usage:" + "\n" + 37 | " node-red admin [args] [--help] [--userDir DIR] [--json]\n\n" + 38 | "Description:" + "\n" + 39 | " Node-RED command-line client\n\n" + 40 | "Commands:\n" + 41 | " init - Interactively generate a Node-RED settings file\n" + 42 | " hash-pw - Creates a hash to use for Node-RED settings like \"adminAuth\"\n" + 43 | " target - Set or view the target URL and port like http://localhost:1880\n" + 44 | " login - Log user in to the target of the Node-RED admin API\n" + 45 | " list - List all of the installed nodes\n" + 46 | " info - Display more information about the module or node\n" + 47 | " enable - Enable the specified module or node set\n" + 48 | " disable - Disable the specified module or node set\n" + 49 | " search - Search for Node-RED modules to install\n" + 50 | " install - Install the module from NPM to Node-RED\n" + 51 | " remove - Remove the NPM module from Node-RED\n" 52 | ; 53 | console.log(helpText); 54 | return Promise.resolve(); 55 | } 56 | 57 | module.exports = function(args) { 58 | var argv = require('minimist')(args); 59 | var command = argv._[0]; 60 | if (commands[command]) { 61 | var result = require("./result"); 62 | if (argv.json) { 63 | result.format("json"); 64 | } 65 | if (argv.u || argv.userDir) { 66 | config.init(argv.u||argv.userDir); 67 | } 68 | if (argv.h || argv.help || argv['?']) { 69 | return result.help(commands[command]); 70 | } else { 71 | return commands[command](argv,result).catch(err => { 72 | result.warn(err); 73 | throw err; 74 | }); 75 | } 76 | } else { 77 | return help(); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /test/lib/config_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var should = require("should"); 18 | var sinon = require("sinon"); 19 | sinon.test = require("sinon-test")(sinon); 20 | var fs = require("fs"); 21 | 22 | var config = require("../../lib/config"); 23 | 24 | describe("lib/config", function() { 25 | "use strict"; 26 | afterEach(function() { 27 | config.unload(); 28 | }); 29 | it('loads preferences when target referenced', sinon.test(function() { 30 | this.stub(fs,"readFileSync").callsFake(function() { 31 | return '{"target":"http://example.com:1880"}'; 32 | }); 33 | config.target().should.eql("http://example.com:1880"); 34 | })); 35 | it('provide default value for target', sinon.test(function() { 36 | this.stub(fs,"readFileSync").callsFake(function() { 37 | return '{}'; 38 | }); 39 | config.target().should.eql("http://localhost:1880"); 40 | })); 41 | 42 | it('saves preferences when target set', sinon.test(function() { 43 | this.stub(fs,"readFileSync").callsFake(function() { 44 | return '{"target":"http://another.example.com:1880"}'; 45 | }); 46 | this.stub(fs,"writeFileSync").callsFake(function() {}); 47 | 48 | config.target().should.eql("http://another.example.com:1880"); 49 | config.target("http://final.example.com:1880"); 50 | config.target().should.eql("http://final.example.com:1880"); 51 | 52 | fs.readFileSync.calledOnce.should.be.true; 53 | fs.writeFileSync.calledOnce.should.be.true; 54 | 55 | })); 56 | 57 | it('provide default value for tokens', sinon.test(function() { 58 | this.stub(fs,"readFileSync").callsFake(function() { 59 | return '{}'; 60 | }); 61 | should.not.exist(config.tokens()); 62 | })); 63 | it('saves preferences when tokens set', sinon.test(function() { 64 | this.stub(fs,"readFileSync").callsFake(function() { 65 | return '{}'; 66 | }); 67 | this.stub(fs,"writeFileSync").callsFake(function() {}); 68 | 69 | should.not.exist(config.tokens()); 70 | config.tokens({access_token:"123"}); 71 | config.tokens().should.eql({access_token:"123"}); 72 | 73 | fs.readFileSync.calledOnce.should.be.true; 74 | fs.writeFileSync.calledOnce.should.be.true; 75 | })); 76 | 77 | it('setting preference to null removes it', sinon.test(function() { 78 | this.stub(fs,"readFileSync").callsFake(function() { 79 | return '{"tokens":{"access_token":"123"}}'; 80 | }); 81 | this.stub(fs,"writeFileSync").callsFake(function() {}); 82 | 83 | config.tokens().should.eql({access_token:"123"}); 84 | config.tokens(null); 85 | should.not.exist(config.tokens()); 86 | 87 | fs.readFileSync.calledOnce.should.be.true; 88 | fs.writeFileSync.calledOnce.should.be.true; 89 | })); 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /test/lib/commands/login_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var command = require("../../../lib/commands/login"); 18 | 19 | var prompt = require("../../../lib/prompt"); 20 | 21 | var should = require("should"); 22 | var sinon = require("sinon"); 23 | 24 | var request = require("../../../lib/request"); 25 | var config = require("../../../lib/config"); 26 | var result = require("./result_helper"); 27 | 28 | describe("commands/login", function() { 29 | beforeEach(function() { 30 | sinon.stub(config,"tokens").callsFake(function(token) {}); 31 | sinon.stub(prompt,"read").callsFake(function(opts) { 32 | if (/Username/.test(opts.prompt)) { 33 | return Promise.resolve("username") 34 | } else if (/Password/.test(opts.prompt)) { 35 | return Promise.resolve("password") 36 | } 37 | }); 38 | }); 39 | afterEach(function() { 40 | config.tokens.restore(); 41 | prompt.read.restore(); 42 | result.reset(); 43 | if (request.request.restore) { 44 | request.request.restore(); 45 | } 46 | }); 47 | 48 | it('logs in user', function(done) { 49 | var requestStub = sinon.stub(request,"request"); 50 | requestStub.onCall(0).returns(Promise.resolve({type:"credentials"})); 51 | requestStub.onCall(1).returns(Promise.resolve({access_token:"12345"})); 52 | 53 | 54 | command({},result).then(() => { 55 | requestStub.calledTwice.should.be.true(); 56 | requestStub.args[0][0].should.eql("/auth/login"); 57 | requestStub.args[1][0].should.eql("/auth/token"); 58 | requestStub.args[1][1].should.eql({ 59 | method:"POST", 60 | data:{"client_id":"node-red-admin","grant_type":"password","scope":"*","username":"username","password":"password"} 61 | }); 62 | 63 | 64 | config.tokens.calledTwice.should.be.true(); 65 | should.not.exist(config.tokens.args[0][0]); 66 | config.tokens.args[1][0].should.eql({access_token:"12345"}); 67 | 68 | /Logged in/.test(result.log.args[0][0]).should.be.true(); 69 | 70 | done(); 71 | }).catch(err=> { 72 | console.log("CAUGH", err); 73 | done(err) 74 | }); 75 | }); 76 | 77 | it('handles unsupported login type', function(done) { 78 | var requestStub = sinon.stub(request,"request"); 79 | requestStub.onCall(0).returns(Promise.resolve({type:"unknown"})); 80 | requestStub.onCall(1).returns(Promise.resolve({access_token:"12345"})); 81 | command({},result).then(() => { 82 | done("Should not have resolved with unsupported login type") 83 | }).catch(err => { 84 | /Unsupported login type/.test(err).should.be.true(); 85 | done(); 86 | }).catch(done); 87 | }); 88 | it('handles no authentication', function(done) { 89 | var requestStub = sinon.stub(request,"request"); 90 | requestStub.onCall(0).returns(Promise.resolve({})); 91 | command({},result).then(() => { 92 | requestStub.calledOnce.should.be.true(); 93 | requestStub.args[0][0].should.eql("/auth/login"); 94 | result.log.called.should.be.false(); 95 | result.warn.called.should.be.false(); 96 | done(); 97 | }).catch(done); 98 | }); 99 | it('handles login failure', function(done) { 100 | var requestStub = sinon.stub(request,"request"); 101 | requestStub.onCall(0).returns(Promise.resolve({type:"credentials"})); 102 | requestStub.onCall(1).returns(Promise.reject()); 103 | command({},result).then(() => { 104 | done("Should not have resolved with login failure"); 105 | }).catch(err => { 106 | config.tokens.calledOnce.should.be.true(); 107 | should.not.exist(config.tokens.args[0][0]); 108 | /Login failed/.test(err).should.be.true(); 109 | done(); 110 | }).catch(done); 111 | }); 112 | 113 | it('handles unexpected error', function(done) { 114 | var requestStub = sinon.stub(request,"request"); 115 | requestStub.onCall(0).returns(Promise.reject("fail")); 116 | command({},result).then(() => { 117 | done("Should not have resolved with login failure"); 118 | }).catch(err => { 119 | config.tokens.calledOnce.should.be.true(); 120 | should.not.exist(config.tokens.args[0][0]); 121 | /fail/.test(err).should.be.true(); 122 | done(); 123 | }).catch(done); 124 | }); 125 | 126 | 127 | }); 128 | -------------------------------------------------------------------------------- /lib/result.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var Table = require('cli-table'); 18 | var util = require("util"); 19 | const config = require("./config"); 20 | 21 | var outputFormat = "text"; 22 | 23 | function logDetails(result) { 24 | if (result.nodes) { // summary of node-module 25 | logModule(result); 26 | } else { // detailed node-set 27 | logNodeSet(result); 28 | } 29 | } 30 | 31 | function logModule(result) { 32 | if (outputFormat === "json") { 33 | console.log(JSON.stringify(result, null, 4)); 34 | return; 35 | } 36 | var table = plainTable({plain:true}); 37 | table.push(["Module:",result.name]); 38 | table.push(["Version:",result.version]); 39 | console.log(table.toString()); 40 | console.log(); 41 | logNodeList(result.nodes); 42 | } 43 | 44 | function logList(result) { 45 | if (outputFormat === "json") { 46 | console.log(JSON.stringify(result, null, 4)); 47 | return; 48 | } 49 | if (result.nodes) { // summary of node-module 50 | logNodeList(result.nodes); 51 | } else { // summary of node-set 52 | logNodeList(result); 53 | } 54 | } 55 | 56 | function logNodeSet(node) { 57 | if (outputFormat === "json") { 58 | console.log(JSON.stringify(node, null, 4)); 59 | return; 60 | } 61 | if (Array.isArray(node)) { 62 | if (node.length > 0) { 63 | node = node[0]; 64 | } 65 | } 66 | var table = plainTable({plain:true}); 67 | table.push(["Name:", node.id]); 68 | table.push(["Module:",node.module]); 69 | table.push(["Version:",node.version]); 70 | 71 | table.push(["Types:", node.types.join(", ")]); 72 | table.push(["State:", (node.err?node.err:(node.enabled?"enabled":"disabled"))]); 73 | 74 | console.log(table.toString()); 75 | } 76 | 77 | function logNodeList(nodes) { 78 | if (outputFormat === "json") { 79 | console.log(JSON.stringify(nodes, null, 4)); 80 | return; 81 | } 82 | if (!Array.isArray(nodes)) { 83 | nodes = [nodes]; 84 | } 85 | nodes.sort(function(n1,n2) { 86 | var id1 = n1.id.toLowerCase(); 87 | var id2 = n2.id.toLowerCase(); 88 | if (id1id2) { 92 | return 1; 93 | } 94 | return 0; 95 | }); 96 | var nodeTable = plainTable(); 97 | nodeTable.push(["Nodes","Types","State"]); 98 | 99 | for(var i=0;i0?"\n":"")+(enabled?node.types[j]:node.types[j]); 108 | } 109 | } 110 | if (types.length === 0) { 111 | types = "none"; 112 | } 113 | 114 | nodeTable.push([enabled?node.id:node.id, 115 | enabled?types:types, 116 | state]); 117 | } 118 | console.log(nodeTable.toString()); 119 | } 120 | 121 | function logProjectList(projects) { 122 | if (outputFormat === "json") { 123 | console.log(JSON.stringify(projects, null, 4)); 124 | return; 125 | } 126 | var projectList = projects.projects || []; 127 | if (projectList.length === 0) { 128 | console.log("No projects available"); 129 | return; 130 | } 131 | projectList.sort(); 132 | projectList.forEach(proj => { 133 | console.log((projects.active === proj ? "*":" ")+" "+proj); 134 | }); 135 | } 136 | 137 | 138 | function plainTable(opts) { 139 | opts = opts||{}; 140 | opts.chars = { 141 | 'top': '' , 'top-mid': '' , 'top-left': '' , 'top-right': '', 142 | 'bottom': '' , 'bottom-mid': '' , 'bottom-left': '' , 143 | 'bottom-right': '', 'left': '' , 'left-mid': '' , 'mid': '' , 144 | 'mid-mid': '', 'right': '' , 'right-mid': '' , 'middle': ' ' }; 145 | opts.style = { 'padding-left': 0, 'padding-right': 0 }; 146 | return new Table(opts); 147 | } 148 | module.exports = { 149 | log:function(msg) { 150 | console.log(msg); 151 | }, 152 | warn:function(msg) { 153 | if (process.env.NR_TRACE && msg.stack) { 154 | console.warn(msg.stack); 155 | } 156 | if (msg.response) { 157 | if (msg.response.status === 401) { 158 | if (outputFormat === "json") { 159 | console.log(JSON.stringify({error:"Not logged in. Use 'login' to log in.", status: 401}, null, 4)); 160 | } else { 161 | console.warn("Not logged in. Use 'login' to log in."); 162 | } 163 | } else if (msg.response.data) { 164 | if (msg.response.status === 404 && !msg.response.data.message) { 165 | if (outputFormat === "json") { 166 | console.log(JSON.stringify({error:"Node-RED Admin API not found. Use 'target' to set API location", status: 404}, null, 4)); 167 | } else { 168 | console.warn("Node-RED Admin API not found. Use 'target' to set API location"); 169 | } 170 | } else { 171 | if (outputFormat === "json") { 172 | console.log(JSON.stringify({error:msg.response.data.message, status: msg.response.status}, null, 4)); 173 | } else { 174 | console.warn(msg.response.status+": "+msg.response.data.message); 175 | } 176 | } 177 | } else { 178 | if (outputFormat === "json") { 179 | console.log(JSON.stringify({error:msg.toString(), status: msg.response.status}, null, 4)); 180 | } else { 181 | console.warn(msg.response.status+": "+msg.toString()); 182 | } 183 | } 184 | } else { 185 | var text = msg.toString(); 186 | if (/ECONNREFUSED/.test(text)) { 187 | text = "Failed to connect to "+config.target(); 188 | } 189 | if (outputFormat === "json") { 190 | console.log(JSON.stringify({error:text})); 191 | } else { 192 | console.warn(text); 193 | } 194 | } 195 | }, 196 | help: function(command) { 197 | var helpText = "Usage:" + "\n" + 198 | " node-red admin " + command.usage + "\n\n" + 199 | "Description:" + "\n" + 200 | " " + command.description + "\n\n" + 201 | "Options:" + "\n" + 202 | (command.options ? " " + command.options + "\n" : "") + 203 | " -h|? --help display this help text and exit"; 204 | console.log(helpText); 205 | return Promise.resolve(); 206 | }, 207 | logList:logList, 208 | logNodeList:logNodeList, 209 | logDetails:logDetails, 210 | logProjectList:logProjectList, 211 | format: function(format) { 212 | outputFormat = format; 213 | } 214 | }; 215 | -------------------------------------------------------------------------------- /test/lib/request_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var should = require("should"); 18 | var sinon = require("sinon"); 19 | var fs = require("fs"); 20 | var request = require("axios"); 21 | 22 | var api = require("../../lib/request"); 23 | var config = require("../../lib/config"); 24 | 25 | describe("lib/request", function() { 26 | "use strict"; 27 | before(function() { 28 | sinon.stub(config,"target").returns("http://example.com/target"); 29 | sinon.stub(config,"tokens").returns(null); 30 | }); 31 | after(function() { 32 | config.target.restore(); 33 | config.tokens.restore(); 34 | }); 35 | 36 | it('uses config.target for base path', function(done) { 37 | sinon.stub(request, 'get').returns(Promise.resolve({status: 200})); 38 | 39 | api.request("/foo",{}).then(function(res) { 40 | try { 41 | request.get.args[0][0].should.eql("http://example.com/target/foo"); 42 | request.get.args[0][1].headers.should.not.have.a.property("Authorization"); 43 | done(); 44 | } catch(err) { 45 | done(err); 46 | } finally { 47 | request.get.restore(); 48 | } 49 | }).catch(function(err) { 50 | request.get.restore(); 51 | done(err); 52 | }); 53 | }); 54 | 55 | it('returns the json response to a get', function(done) { 56 | sinon.stub(request, 'get').returns(Promise.resolve({status:200, data: {a:"b"}})) 57 | 58 | api.request("/foo",{}).then(function(res) { 59 | try { 60 | res.should.eql({a:"b"}); 61 | done(); 62 | } catch(err) { 63 | done(err); 64 | } finally { 65 | request.get.restore(); 66 | } 67 | }).catch(function(err) { 68 | request.get.restore(); 69 | done(err); 70 | }); 71 | }); 72 | 73 | it('returns the json response to a put', function(done) { 74 | sinon.stub(request, 'put').returns(Promise.resolve({status:200, data: {a:"b"}})) 75 | 76 | api.request("/nodes/node",{method: "PUT"}).then(function(res) { 77 | try { 78 | res.should.eql({a:"b"}); 79 | done(); 80 | } catch(err) { 81 | done(err); 82 | } finally { 83 | request.put.restore(); 84 | } 85 | }).catch(function(err) { 86 | request.put.restore(); 87 | done(err); 88 | }); 89 | }); 90 | 91 | it('returns the json response to a post', function(done) { 92 | sinon.stub(request, 'post').returns(Promise.resolve({status:200, data: {a:"b"}})) 93 | 94 | api.request("/nodes",{method: "POST"}).then(function(res) { 95 | try { 96 | res.should.eql({a:"b"}); 97 | done(); 98 | } catch(err) { 99 | done(err); 100 | } finally { 101 | request.post.restore(); 102 | } 103 | }).catch(function(err) { 104 | request.post.restore(); 105 | done(err); 106 | }); 107 | }); 108 | 109 | it('returns to a delete', function(done) { 110 | sinon.stub(request, 'delete').returns(Promise.resolve({status:200, data: {a:"b"}})) 111 | 112 | api.request("/nodes/plugin",{method: "DELETE"}).then(function() { 113 | request.delete.restore(); 114 | done(); 115 | }).catch(function(err) { 116 | request.delete.restore(); 117 | done(err); 118 | }); 119 | }); 120 | 121 | 122 | it('rejects unauthorised', function(done) { 123 | var rejection = Promise.reject({status:401}); 124 | rejection.catch(()=>{}); 125 | sinon.stub(request, 'get').returns(rejection) 126 | 127 | api.request("/nodes/plugin",{}).then(function() { 128 | request.get.restore(); 129 | done(new Error("Unauthorised response not rejected")); 130 | }).catch(function(err) { 131 | try { 132 | err.status.should.eql(401); 133 | done(); 134 | } catch(err) { 135 | done(err); 136 | } finally { 137 | request.get.restore(); 138 | } 139 | }); 140 | }); 141 | 142 | it('rejects error', function(done) { 143 | var rejection = Promise.reject({status:400,data:{message:"test error"}}); 144 | rejection.catch(()=>{}); 145 | sinon.stub(request, 'get').returns(rejection) 146 | 147 | api.request("/nodes/plugin",{}).then(function() { 148 | request.get.restore(); 149 | done(new Error("Unauthorised response not rejected")); 150 | }).catch(function(err) { 151 | try { 152 | err.status.should.eql(400); 153 | err.data.message.should.eql("test error") 154 | done(); 155 | } catch(err) { 156 | done(err); 157 | } finally { 158 | request.get.restore(); 159 | } 160 | }); 161 | }); 162 | 163 | 164 | it('returns unexpected status', function(done) { 165 | sinon.stub(request, 'get').returns(Promise.resolve({status:101, data: "response"})) 166 | 167 | api.request("/nodes/plugin",{}).then(function() { 168 | request.get.restore(); 169 | done(new Error("Unexpected status not logged")); 170 | }).catch(function(err) { 171 | try { 172 | err.message.should.eql("101: response"); 173 | done(); 174 | } catch(err) { 175 | done(err); 176 | } finally { 177 | request.get.restore(); 178 | } 179 | }); 180 | }); 181 | 182 | it('returns server message', function(done) { 183 | sinon.stub(request, 'get').returns(Promise.resolve({status:101, data: {"message":"server response"}})) 184 | 185 | api.request("/nodes/plugin",{}).then(function() { 186 | request.get.restore(); 187 | done(new Error("Unexpected status not logged")); 188 | }).catch(function(err) { 189 | try { 190 | err.message.should.eql("101: server response"); 191 | done(); 192 | } catch(err) { 193 | done(err); 194 | } finally { 195 | request.get.restore(); 196 | } 197 | }); 198 | }); 199 | 200 | it('attaches authorization header if token available', function(done) { 201 | sinon.stub(request, 'get').returns(Promise.resolve({status: 200,data:{a:"b"}})); 202 | config.tokens.restore(); 203 | sinon.stub(config,"tokens").returns({access_token:"123456"}); 204 | 205 | api.request("/foo",{}).then(function(res) { 206 | try { 207 | res.should.eql({a:"b"}); 208 | request.get.args[0][1].headers.should.have.a.property("Authorization","Bearer 123456"); 209 | done(); 210 | } catch(err) { 211 | done(err); 212 | } finally { 213 | request.get.restore(); 214 | } 215 | }).catch(function(err) { 216 | request.get.restore(); 217 | done(err); 218 | }); 219 | 220 | }); 221 | 222 | 223 | it('logs output if NR_TRACE is set', function(done) { 224 | sinon.stub(request, 'get').returns(Promise.resolve({status: 200,data:{a:"b"}})); 225 | sinon.stub(console, 'log'); 226 | process.env.NR_TRACE = true; 227 | api.request("/foo",{}).then(function(res) { 228 | try{ 229 | var wasCalled = console.log.called; 230 | console.log.restore(); 231 | wasCalled.should.be.true; 232 | done(); 233 | } catch(err) { 234 | console.log.restore(); 235 | done(err); 236 | } finally { 237 | delete process.env.NR_TRACE; 238 | request.get.restore(); 239 | } 240 | }).catch(function(err) { 241 | delete process.env.NR_TRACE; 242 | console.log.restore(); 243 | request.get.restore(); 244 | done(err); 245 | }); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /lib/commands/init/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | const Enquirer = require('enquirer'); 18 | const color = require('ansi-colors'); 19 | 20 | const mustache = require("mustache"); 21 | const fs = require("fs"); 22 | const path = require("path"); 23 | 24 | let bcrypt; 25 | try { bcrypt = require('@node-rs/bcrypt'); } 26 | catch(e) { bcrypt = require('bcryptjs'); } 27 | 28 | function prompt(opts) { 29 | const enq = new Enquirer(); 30 | return enq.prompt(opts); 31 | } 32 | 33 | /** 34 | * 1. identify userdir 35 | * 2. check if settings file already exists 36 | * 3. Enable projects feature with version control? 37 | 38 | * 3. get flowFile name 39 | * 3. get credentialSecret 40 | * 4. ask to setup adminAuth 41 | * - prompt for username 42 | * - prompt for password 43 | */ 44 | 45 | async function loadTemplateSettings() { 46 | const templateFile = path.join(__dirname,"resources/settings.js.mustache"); 47 | return fs.promises.readFile(templateFile,"utf8"); 48 | } 49 | 50 | async function fillTemplate(template, context) { 51 | return mustache.render(template,context); 52 | } 53 | 54 | function heading(str) { 55 | console.log("\n"+color.cyan(str)); 56 | console.log(color.cyan(Array(str.length).fill("=").join(""))); 57 | } 58 | function message(str) { 59 | console.log(color.bold(str)+"\n"); 60 | } 61 | 62 | async function promptSettingsFile(opts) { 63 | const defaultSettingsFile = path.join(opts.userDir,"settings.js"); 64 | const responses = await prompt([ 65 | { 66 | type: 'input', 67 | name: 'settingsFile', 68 | initial: defaultSettingsFile, 69 | message: "Settings file", 70 | onSubmit(key, value, p) { 71 | p.state.answers.exists = fs.existsSync(value); 72 | } 73 | }, 74 | { 75 | type: 'select', 76 | name: 'confirmOverwrite', 77 | initial: "No", 78 | message: 'That file already exists. Are you sure you want to overwrite it?', 79 | choices: ['Yes', 'No'], 80 | result(value) { 81 | return value === "Yes" 82 | }, 83 | skip() { 84 | return !this.state.answers.exists 85 | } 86 | } 87 | ]); 88 | 89 | if (responses.exists && !responses.confirmOverwrite) { 90 | return promptSettingsFile(opts); 91 | } 92 | // const alreadyExists = await fs.exists(responses.settingsFile); 93 | // responses.exists = 94 | return responses; 95 | } 96 | 97 | async function promptTelemetry() { 98 | heading("Share Anonymouse Usage Information"); 99 | 100 | const responses = await prompt([ 101 | { 102 | type: 'select', 103 | name: 'telemetryEnabled', 104 | // initial: "Yes", 105 | message: `Node-RED can notify you when there is a new version available. 106 | This ensures you keep up to date with the latest features and fixes. 107 | This requires sending anonymised data back to the Node-RED team. 108 | It does not include any details of your flows or users. 109 | For full information on what information is collected and how it is used, 110 | please see https://nodered.org/docs/telemetry 111 | `, 112 | choices: ['Yes, send my usage data', 'No, do not send my usage data'], 113 | result(value) { 114 | return /Yes/.test(value) 115 | } 116 | } 117 | ]) 118 | return responses 119 | } 120 | 121 | async function promptUser() { 122 | const responses = await prompt([ 123 | { 124 | type: 'input', 125 | name: 'username', 126 | message: "Username", 127 | validate(val) { return !!val.trim() ? true: "Invalid username"} 128 | }, 129 | { 130 | type: 'password', 131 | name: 'password', 132 | message: "Password", 133 | validate(val) { 134 | if (val.length < 8) { 135 | return "Password too short. Must be at least 8 characters" 136 | } 137 | return true 138 | } 139 | }, 140 | { 141 | type: 'select', 142 | name: 'permissions', 143 | message: "User permissions", 144 | choices: [ {name:"full access", value:"*"}, {name:"read-only access", value:"read"}], 145 | result(value) { 146 | return this.find(value).value; 147 | } 148 | } 149 | ]) 150 | responses.password = bcrypt.hashSync(responses.password, 8); 151 | return responses; 152 | } 153 | 154 | async function promptSecurity() { 155 | heading("User Security"); 156 | 157 | const responses = await prompt([ 158 | { 159 | type: 'select', 160 | name: 'adminAuth', 161 | initial: "Yes", 162 | message: 'Do you want to setup user security?', 163 | choices: ['Yes', 'No'], 164 | result(value) { 165 | return value === "Yes" 166 | } 167 | } 168 | ]) 169 | if (responses.adminAuth) { 170 | responses.users = []; 171 | while(true) { 172 | responses.users.push(await promptUser()); 173 | const resp = await prompt({ 174 | type: 'select', 175 | name: 'addMore', 176 | initial: "No", 177 | message: 'Add another user?', 178 | choices: ['Yes', 'No'], 179 | result(value) { 180 | return value === "Yes" 181 | } 182 | }) 183 | if (!resp.addMore) { 184 | break; 185 | } 186 | } 187 | } 188 | return responses; 189 | } 190 | 191 | async function promptProjects() { 192 | heading("Projects"); 193 | message("The Projects feature allows you to version control your flow using a local git repository."); 194 | const responses = await prompt([ 195 | { 196 | type: 'select', 197 | name: 'enabled', 198 | initial: "No", 199 | message: 'Do you want to enable the Projects feature?', 200 | choices: ['Yes', 'No'], 201 | result(value) { 202 | return value === "Yes"; 203 | } 204 | }, 205 | // { 206 | // type: 'select', 207 | // name: '_continue', 208 | // message: 'Node-RED will help you create your project the first time you access the editor.', 209 | // choices: ['Continue'], 210 | // skip() { 211 | // return !this.state.answers.enabled; 212 | // } 213 | // }, 214 | { 215 | type: 'select', 216 | name: 'workflow', 217 | message: 'What project workflow do you want to use?', 218 | choices: [ 219 | {value: 'manual', name: 'manual - you must manually commit changes'}, 220 | {value: 'auto', name: 'auto - changes are automatically committed'} 221 | ], 222 | skip() { 223 | return !this.state.answers.enabled; 224 | }, 225 | result(value) { 226 | return this.find(value).value; 227 | } 228 | } 229 | ]) 230 | // delete responses._continue; 231 | return responses 232 | } 233 | 234 | async function promptFlowFileSettings() { 235 | heading("Flow File settings"); 236 | const responses = await prompt([ 237 | { 238 | type: 'input', 239 | name: 'flowFile', 240 | message: 'Enter a name for your flows file', 241 | default: 'flows.json' 242 | }, 243 | { 244 | type: 'password', 245 | name: 'credentialSecret', 246 | message: 'Provide a passphrase to encrypt your credentials file' 247 | } 248 | ]) 249 | return responses 250 | } 251 | 252 | async function promptNodeSettings() { 253 | heading("Node settings"); 254 | const responses = await prompt([ 255 | { 256 | type: 'select', 257 | name: 'functionExternalModules', 258 | message: 'Allow Function nodes to load external modules? (functionExternalModules)', 259 | initial: 'Yes', 260 | choices: ['Yes', 'No'], 261 | result(value) { 262 | return value === "Yes" 263 | }, 264 | } 265 | ]); 266 | return responses; 267 | } 268 | async function promptEditorSettings() { 269 | heading("Editor settings"); 270 | const responses = await prompt([ 271 | { 272 | type: 'select', 273 | name: 'theme', 274 | message: 'Select a theme for the editor. To use any theme other than "default", you will need to install @node-red-contrib-themes/theme-collection in your Node-RED user directory.', 275 | initial: 'default', 276 | choices: [ "default", "aurora", "cobalt2", "dark", "dracula", "espresso-libre", "github-dark", "github-dark-default", "github-dark-dimmed", "midnight-red", "monoindustrial", "monokai", "monokai-dimmed", "night-owl", "noctis", "noctis-azureus", "noctis-bordo", "noctis-minimus", "noctis-obscuro", "noctis-sereno", "noctis-uva", "noctis-viola", "oceanic-next", "oled", "one-dark-pro", "one-dark-pro-darker", "railscasts-extended", "selenized-dark", "selenized-light", "solarized-dark", "solarized-light", "tokyo-night", "tokyo-night-light", "tokyo-night-storm", "totallyinformation", "zenburn", "zendesk-garden"], 277 | }, 278 | { 279 | type: 'select', 280 | name: 'codeEditor', 281 | message: 'Select the text editor component to use in the Node-RED Editor', 282 | initial: 'monaco', 283 | choices: [ {name:"monaco (default)", value:"monaco"}, {name:"ace", value:"ace"} ], 284 | result(value) { 285 | return this.find(value).value; 286 | } 287 | } 288 | ]); 289 | return responses; 290 | } 291 | async function command(argv, result) { 292 | const config = { 293 | intro: `Node-RED Settings created at ${new Date().toUTCString()}`, 294 | flowFile: "flows.json", 295 | editorTheme: "" 296 | }; 297 | 298 | heading("Node-RED Settings File initialisation"); 299 | message(`This tool will help you create a Node-RED settings file.`); 300 | 301 | 302 | const userDir = argv["u"] || argv["userDir"] || path.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME,".node-red"); 303 | 304 | const fileSettings = await promptSettingsFile({userDir}); 305 | 306 | const telemetryResponses = await promptTelemetry() 307 | config.telemetryEnabled = telemetryResponses.telemetryEnabled ? 'true' : 'false' 308 | 309 | const securityResponses = await promptSecurity(); 310 | if (securityResponses.adminAuth) { 311 | let adminAuth = { 312 | type: "credentials", 313 | users: securityResponses.users 314 | }; 315 | config.adminAuth = JSON.stringify(adminAuth, null, 4).replace(/\n/g,"\n "); 316 | } 317 | 318 | const projectsResponses = await promptProjects(); 319 | let flowFileSettings = {}; 320 | if (!projectsResponses.enabled) { 321 | flowFileSettings = await promptFlowFileSettings(); 322 | config.flowFile = flowFileSettings.flowFile; 323 | if (flowFileSettings.hasOwnProperty("credentialSecret")) { 324 | config.credentialSecret = flowFileSettings.credentialSecret?`"${flowFileSettings.credentialSecret}"`:"false" 325 | } 326 | config.projects = { 327 | enabled: false, 328 | workflow: "manual" 329 | } 330 | } else { 331 | config.projects = projectsResponses 332 | } 333 | const editorSettings = await promptEditorSettings(); 334 | config.codeEditor = editorSettings.codeEditor; 335 | if (editorSettings.theme !== "default") { 336 | config.editorTheme = editorSettings.theme 337 | } 338 | const nodeSettings = await promptNodeSettings(); 339 | config.functionExternalModules = nodeSettings.functionExternalModules; 340 | 341 | 342 | const template = await loadTemplateSettings(); 343 | const settings = await fillTemplate(template, config) 344 | 345 | const settingsDir = path.dirname(fileSettings.settingsFile); 346 | await fs.promises.mkdir(settingsDir,{recursive: true}); 347 | await fs.promises.writeFile(fileSettings.settingsFile, settings, "utf-8"); 348 | 349 | 350 | console.log(color.yellow(`\n\nSettings file written to ${fileSettings.settingsFile}`)); 351 | 352 | if (config.editorTheme) { 353 | console.log(color.yellow(`To use the '${config.editorTheme}' editor theme, remember to install @node-red-contrib-themes/theme-collection in your Node-RED user directory`)) 354 | } 355 | } 356 | command.alias = "init"; 357 | command.usage = command.alias; 358 | command.description = "Initialise a Node-RED settings file"; 359 | 360 | module.exports = command; 361 | -------------------------------------------------------------------------------- /lib/commands/init/resources/settings.js.mustache: -------------------------------------------------------------------------------- 1 | /** 2 | * {{intro}} 3 | * 4 | * It can contain any valid JavaScript code that will get run when Node-RED 5 | * is started. 6 | * 7 | * Lines that start with // are commented out. 8 | * Each entry should be separated from the entries above and below by a comma ',' 9 | * 10 | * For more information about individual settings, refer to the documentation: 11 | * https://nodered.org/docs/user-guide/runtime/configuration 12 | * 13 | * The settings are split into the following sections: 14 | * - Flow File and User Directory Settings 15 | * - Security 16 | * - Server Settings 17 | * - Runtime Settings 18 | * - Editor Settings 19 | * - Node Settings 20 | * 21 | **/ 22 | 23 | module.exports = { 24 | 25 | /******************************************************************************* 26 | * Flow File and User Directory Settings 27 | * - flowFile 28 | * - credentialSecret 29 | * - flowFilePretty 30 | * - userDir 31 | * - nodesDir 32 | ******************************************************************************/ 33 | 34 | /** The file containing the flows. If not set, defaults to flows_.json **/ 35 | flowFile: "{{flowFile}}", 36 | 37 | /** By default, credentials are encrypted in storage using a generated key. To 38 | * specify your own secret, set the following property. 39 | * If you want to disable encryption of credentials, set this property to false. 40 | * Note: once you set this property, do not change it - doing so will prevent 41 | * node-red from being able to decrypt your existing credentials and they will be 42 | * lost. 43 | */ 44 | {{^credentialSecret}}//{{/credentialSecret}}credentialSecret: {{^credentialSecret}}""{{/credentialSecret}}{{{credentialSecret}}}, 45 | 46 | /** By default, the flow JSON will be formatted over multiple lines making 47 | * it easier to compare changes when using version control. 48 | * To disable pretty-printing of the JSON set the following property to false. 49 | */ 50 | flowFilePretty: true, 51 | 52 | /** By default, all user data is stored in a directory called `.node-red` under 53 | * the user's home directory. To use a different location, the following 54 | * property can be used 55 | */ 56 | //userDir: '/home/nol/.node-red/', 57 | 58 | /** Node-RED scans the `nodes` directory in the userDir to find local node files. 59 | * The following property can be used to specify an additional directory to scan. 60 | */ 61 | //nodesDir: '/home/nol/.node-red/nodes', 62 | 63 | /******************************************************************************* 64 | * Security 65 | * - adminAuth 66 | * - https 67 | * - httpsRefreshInterval 68 | * - requireHttps 69 | * - httpNodeAuth 70 | * - httpStaticAuth 71 | ******************************************************************************/ 72 | 73 | /** To password protect the Node-RED editor and admin API, the following 74 | * property can be used. See https://nodered.org/docs/security.html for details. 75 | */ 76 | {{^adminAuth}} 77 | //adminAuth: { 78 | // type: "credentials", 79 | // users: [{ 80 | // username: "admin", 81 | // password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.", 82 | // permissions: "*" 83 | // }] 84 | //},{{/adminAuth}} 85 | {{#adminAuth}}adminAuth: {{{adminAuth}}}, 86 | {{/adminAuth}} 87 | 88 | /** The following property can be used to enable HTTPS 89 | * This property can be either an object, containing both a (private) key 90 | * and a (public) certificate, or a function that returns such an object. 91 | * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener 92 | * for details of its contents. 93 | */ 94 | 95 | /** Option 1: static object */ 96 | //https: { 97 | // key: require("fs").readFileSync('privkey.pem'), 98 | // cert: require("fs").readFileSync('cert.pem') 99 | //}, 100 | 101 | /** Option 2: function that returns the HTTP configuration object */ 102 | // https: function() { 103 | // // This function should return the options object, or a Promise 104 | // // that resolves to the options object 105 | // return { 106 | // key: require("fs").readFileSync('privkey.pem'), 107 | // cert: require("fs").readFileSync('cert.pem') 108 | // } 109 | // }, 110 | 111 | /** If the `https` setting is a function, the following setting can be used 112 | * to set how often, in hours, the function will be called. That can be used 113 | * to refresh any certificates. 114 | */ 115 | //httpsRefreshInterval : 12, 116 | 117 | /** The following property can be used to cause insecure HTTP connections to 118 | * be redirected to HTTPS. 119 | */ 120 | //requireHttps: true, 121 | 122 | /** To password protect the node-defined HTTP endpoints (httpNodeRoot), 123 | * including node-red-dashboard, or the static content (httpStatic), the 124 | * following properties can be used. 125 | * The `pass` field is a bcrypt hash of the password. 126 | * See https://nodered.org/docs/security.html#generating-the-password-hash 127 | */ 128 | //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, 129 | //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, 130 | 131 | /******************************************************************************* 132 | * Server Settings 133 | * - uiPort 134 | * - uiHost 135 | * - apiMaxLength 136 | * - httpServerOptions 137 | * - httpAdminRoot 138 | * - httpAdminMiddleware 139 | * - httpAdminCookieOptions 140 | * - httpNodeRoot 141 | * - httpNodeCors 142 | * - httpNodeMiddleware 143 | * - httpStatic 144 | * - httpStaticRoot 145 | * - httpStaticCors 146 | ******************************************************************************/ 147 | 148 | /** the tcp port that the Node-RED web server is listening on */ 149 | uiPort: process.env.PORT || 1880, 150 | 151 | /** By default, the Node-RED UI accepts connections on all IPv4 interfaces. 152 | * To listen on all IPv6 addresses, set uiHost to "::", 153 | * The following property can be used to listen on a specific interface. For 154 | * example, the following would only allow connections from the local machine. 155 | */ 156 | //uiHost: "127.0.0.1", 157 | 158 | /** The maximum size of HTTP request that will be accepted by the runtime api. 159 | * Default: 5mb 160 | */ 161 | //apiMaxLength: '5mb', 162 | 163 | /** The following property can be used to pass custom options to the Express.js 164 | * server used by Node-RED. For a full list of available options, refer 165 | * to http://expressjs.com/en/api.html#app.settings.table 166 | */ 167 | //httpServerOptions: { }, 168 | 169 | /** By default, the Node-RED UI is available at http://localhost:1880/ 170 | * The following property can be used to specify a different root path. 171 | * If set to false, this is disabled. 172 | */ 173 | //httpAdminRoot: '/admin', 174 | 175 | /** The following property can be used to add a custom middleware function 176 | * in front of all admin http routes. For example, to set custom http 177 | * headers. It can be a single function or an array of middleware functions. 178 | */ 179 | // httpAdminMiddleware: function(req,res,next) { 180 | // // Set the X-Frame-Options header to limit where the editor 181 | // // can be embedded 182 | // //res.set('X-Frame-Options', 'sameorigin'); 183 | // next(); 184 | // }, 185 | 186 | /** The following property can be used to set addition options on the session 187 | * cookie used as part of adminAuth authentication system 188 | * Available options are documented here: https://www.npmjs.com/package/express-session#cookie 189 | */ 190 | // httpAdminCookieOptions: { }, 191 | 192 | /** Some nodes, such as HTTP In, can be used to listen for incoming http requests. 193 | * By default, these are served relative to '/'. The following property 194 | * can be used to specify a different root path. If set to false, this is 195 | * disabled. 196 | */ 197 | //httpNodeRoot: '/red-nodes', 198 | 199 | /** The following property can be used to configure cross-origin resource sharing 200 | * in the HTTP nodes. 201 | * See https://github.com/troygoode/node-cors#configuration-options for 202 | * details on its contents. The following is a basic permissive set of options: 203 | */ 204 | //httpNodeCors: { 205 | // origin: "*", 206 | // methods: "GET,PUT,POST,DELETE" 207 | //}, 208 | 209 | /** If you need to set an http proxy please set an environment variable 210 | * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. 211 | * For example - http_proxy=http://myproxy.com:8080 212 | * (Setting it here will have no effect) 213 | * You may also specify no_proxy (or NO_PROXY) to supply a comma separated 214 | * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk 215 | */ 216 | 217 | /** The following property can be used to add a custom middleware function 218 | * in front of all http in nodes. This allows custom authentication to be 219 | * applied to all http in nodes, or any other sort of common request processing. 220 | * It can be a single function or an array of middleware functions. 221 | */ 222 | //httpNodeMiddleware: function(req,res,next) { 223 | // // Handle/reject the request, or pass it on to the http in node by calling next(); 224 | // // Optionally skip our rawBodyParser by setting this to true; 225 | // //req.skipRawBodyParser = true; 226 | // next(); 227 | //}, 228 | 229 | /** When httpAdminRoot is used to move the UI to a different root path, the 230 | * following property can be used to identify a directory of static content 231 | * that should be served at http://localhost:1880/. 232 | * When httpStaticRoot is set differently to httpAdminRoot, there is no need 233 | * to move httpAdminRoot 234 | */ 235 | //httpStatic: '/home/nol/node-red-static/', //single static source 236 | /** 237 | * OR multiple static sources can be created using an array of objects... 238 | * Each object can also contain an options object for further configuration. 239 | * See https://expressjs.com/en/api.html#express.static for available options. 240 | * They can also contain an option `cors` object to set specific Cross-Origin 241 | * Resource Sharing rules for the source. `httpStaticCors` can be used to 242 | * set a default cors policy across all static routes. 243 | */ 244 | //httpStatic: [ 245 | // {path: '/home/nol/pics/', root: "/img/"}, 246 | // {path: '/home/nol/reports/', root: "/doc/"}, 247 | // {path: '/home/nol/videos/', root: "/vid/", options: {maxAge: '1d'}} 248 | //], 249 | 250 | /** 251 | * All static routes will be appended to httpStaticRoot 252 | * e.g. if httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/" 253 | * then "/home/nol/docs" will be served at "/static/" 254 | * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}] 255 | * and httpStaticRoot = "/static/" 256 | * then "/home/nol/pics/" will be served at "/static/img/" 257 | */ 258 | //httpStaticRoot: '/static/', 259 | 260 | /** The following property can be used to configure cross-origin resource sharing 261 | * in the http static routes. 262 | * See https://github.com/troygoode/node-cors#configuration-options for 263 | * details on its contents. The following is a basic permissive set of options: 264 | */ 265 | //httpStaticCors: { 266 | // origin: "*", 267 | // methods: "GET,PUT,POST,DELETE" 268 | //}, 269 | 270 | /** The following property can be used to modify proxy options */ 271 | // proxyOptions: { 272 | // mode: "legacy", // legacy mode is for non-strict previous proxy determination logic (node-red < v4 compatible) 273 | // }, 274 | 275 | /******************************************************************************* 276 | * Runtime Settings 277 | * - lang 278 | * - runtimeState 279 | * - telemetry 280 | * - diagnostics 281 | * - logging 282 | * - contextStorage 283 | * - exportGlobalContextKeys 284 | * - externalModules 285 | ******************************************************************************/ 286 | 287 | /** Uncomment the following to run node-red in your preferred language. 288 | * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko 289 | * Some languages are more complete than others. 290 | */ 291 | // lang: "de", 292 | 293 | /** Configure diagnostics options 294 | * - enabled: When `enabled` is `true` (or unset), diagnostics data will 295 | * be available at http://localhost:1880/diagnostics 296 | * - ui: When `ui` is `true` (or unset), the action `show-system-info` will 297 | * be available to logged in users of node-red editor 298 | */ 299 | diagnostics: { 300 | /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ 301 | enabled: true, 302 | /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ 303 | ui: true, 304 | }, 305 | /** Configure runtimeState options 306 | * - enabled: When `enabled` is `true` flows runtime can be Started/Stopped 307 | * by POSTing to available at http://localhost:1880/flows/state 308 | * - ui: When `ui` is `true`, the action `core:start-flows` and 309 | * `core:stop-flows` will be available to logged in users of node-red editor 310 | * Also, the deploy menu (when set to default) will show a stop or start button 311 | */ 312 | runtimeState: { 313 | /** enable or disable flows/state endpoint. Must be set to `false` to disable */ 314 | enabled: false, 315 | /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ 316 | ui: false, 317 | }, 318 | telemetry: { 319 | /** 320 | * By default, telemetry is disabled until the user provides consent the first 321 | * time they open the editor. 322 | * 323 | * The following property can be uncommented and set to true/false to enable/disable 324 | * telemetry without seeking further consent in the editor. 325 | * The user can override this setting via the user settings dialog within the editor 326 | */ 327 | {{^telemetryEnabled}}//enabled: true,{{/telemetryEnabled}} 328 | {{#telemetryEnabled}}enabled: {{telemetryEnabled}},{{/telemetryEnabled}} 329 | /** 330 | * If telemetry is enabled, the editor will notify the user if a new version of Node-RED 331 | * is available. Set the following property to false to disable this notification. 332 | */ 333 | // updateNotification: true 334 | }, 335 | /** Configure the logging output */ 336 | logging: { 337 | /** Only console logging is currently supported */ 338 | console: { 339 | /** Level of logging to be recorded. Options are: 340 | * fatal - only those errors which make the application unusable should be recorded 341 | * error - record errors which are deemed fatal for a particular request + fatal errors 342 | * warn - record problems which are non fatal + errors + fatal errors 343 | * info - record information about the general running of the application + warn + error + fatal errors 344 | * debug - record information which is more verbose than info + info + warn + error + fatal errors 345 | * trace - record very detailed logging + debug + info + warn + error + fatal errors 346 | * off - turn off all logging (doesn't affect metrics or audit) 347 | */ 348 | level: "info", 349 | /** Whether or not to include metric events in the log output */ 350 | metrics: false, 351 | /** Whether or not to include audit events in the log output */ 352 | audit: false 353 | } 354 | }, 355 | 356 | /** Context Storage 357 | * The following property can be used to enable context storage. The configuration 358 | * provided here will enable file-based context that flushes to disk every 30 seconds. 359 | * Refer to the documentation for further options: https://nodered.org/docs/api/context/ 360 | */ 361 | //contextStorage: { 362 | // default: { 363 | // module:"localfilesystem" 364 | // }, 365 | //}, 366 | 367 | /** `global.keys()` returns a list of all properties set in global context. 368 | * This allows them to be displayed in the Context Sidebar within the editor. 369 | * In some circumstances it is not desirable to expose them to the editor. The 370 | * following property can be used to hide any property set in `functionGlobalContext` 371 | * from being list by `global.keys()`. 372 | * By default, the property is set to false to avoid accidental exposure of 373 | * their values. Setting this to true will cause the keys to be listed. 374 | */ 375 | exportGlobalContextKeys: false, 376 | 377 | /** Configure how the runtime will handle external npm modules. 378 | * This covers: 379 | * - whether the editor will allow new node modules to be installed 380 | * - whether nodes, such as the Function node are allowed to have their 381 | * own dynamically configured dependencies. 382 | * The allow/denyList options can be used to limit what modules the runtime 383 | * will install/load. It can use '*' as a wildcard that matches anything. 384 | */ 385 | externalModules: { 386 | // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ 387 | // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ 388 | // palette: { /** Configuration for the Palette Manager */ 389 | // allowInstall: true, /** Enable the Palette Manager in the editor */ 390 | // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ 391 | // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ 392 | // allowList: ['*'], 393 | // denyList: [], 394 | // allowUpdateList: ['*'], 395 | // denyUpdateList: [] 396 | // }, 397 | // modules: { /** Configuration for node-specified modules */ 398 | // allowInstall: true, 399 | // allowList: [], 400 | // denyList: [] 401 | // } 402 | }, 403 | 404 | 405 | /******************************************************************************* 406 | * Editor Settings 407 | * - disableEditor 408 | * - editorTheme 409 | ******************************************************************************/ 410 | 411 | /** The following property can be used to disable the editor. The admin API 412 | * is not affected by this option. To disable both the editor and the admin 413 | * API, use either the httpRoot or httpAdminRoot properties 414 | */ 415 | //disableEditor: false, 416 | 417 | /** Customising the editor 418 | * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes 419 | * for all available options. 420 | */ 421 | editorTheme: { 422 | /** The following property can be used to set a custom theme for the editor. 423 | * See https://github.com/node-red-contrib-themes/theme-collection for 424 | * a collection of themes to chose from. 425 | */ 426 | {{^editorTheme}}//{{/editorTheme}}theme: "{{editorTheme}}", 427 | 428 | /** To disable the 'Welcome to Node-RED' tour that is displayed the first 429 | * time you access the editor for each release of Node-RED, set this to false 430 | */ 431 | //tours: false, 432 | 433 | palette: { 434 | /** The following property can be used to order the categories in the editor 435 | * palette. If a node's category is not in the list, the category will get 436 | * added to the end of the palette. 437 | * If not set, the following default order is used: 438 | */ 439 | //categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], 440 | }, 441 | projects: { 442 | /** To enable the Projects feature, set this value to true */ 443 | enabled: {{projects.enabled}}, 444 | workflow: { 445 | /** Set the default projects workflow mode. 446 | * - manual - you must manually commit changes 447 | * - auto - changes are automatically committed 448 | * This can be overridden per-user from the 'Git config' 449 | * section of 'User Settings' within the editor 450 | */ 451 | mode: "{{projects.workflow}}" 452 | } 453 | }, 454 | 455 | codeEditor: { 456 | /** Select the text editor component used by the editor. 457 | * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired 458 | */ 459 | lib: "{{codeEditor}}", 460 | options: { 461 | /** The follow options only apply if the editor is set to "monaco" 462 | * 463 | * theme - must match the file name of a theme in 464 | * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme 465 | * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme" 466 | */ 467 | //theme: "vs", 468 | /** other overrides can be set e.g. fontSize, fontFamily, fontLigatures etc. 469 | * for the full list, see https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html 470 | */ 471 | //fontSize: 14, 472 | //fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", 473 | //fontLigatures: true, 474 | } 475 | }, 476 | markdownEditor: { 477 | mermaid: { 478 | /** enable or disable mermaid diagram in markdown document 479 | */ 480 | enabled: true 481 | } 482 | }, 483 | 484 | multiplayer: { 485 | /** To enable the Multiplayer feature, set this value to true */ 486 | enabled: false 487 | }, 488 | }, 489 | 490 | /******************************************************************************* 491 | * Node Settings 492 | * - fileWorkingDirectory 493 | * - functionGlobalContext 494 | * - functionExternalModules 495 | * - functionTimeout 496 | * - nodeMessageBufferMaxLength 497 | * - ui (for use with Node-RED Dashboard) 498 | * - debugUseColors 499 | * - debugMaxLength 500 | * - debugStatusLength 501 | * - execMaxBufferSize 502 | * - httpRequestTimeout 503 | * - mqttReconnectTime 504 | * - serialReconnectTime 505 | * - socketReconnectTime 506 | * - socketTimeout 507 | * - tcpMsgQueueSize 508 | * - inboundWebSocketTimeout 509 | * - tlsConfigDisableLocalFiles 510 | * - webSocketNodeVerifyClient 511 | ******************************************************************************/ 512 | 513 | /** The working directory to handle relative file paths from within the File nodes 514 | * defaults to the working directory of the Node-RED process. 515 | */ 516 | //fileWorkingDirectory: "", 517 | 518 | /** Allow the Function node to load additional npm modules directly */ 519 | functionExternalModules: {{functionExternalModules}}, 520 | 521 | /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ 522 | functionTimeout: 0, 523 | 524 | /** The following property can be used to set predefined values in Global Context. 525 | * This allows extra node modules to be made available with in Function node. 526 | * For example, the following: 527 | * functionGlobalContext: { os:require('os') } 528 | * will allow the `os` module to be accessed in a Function node using: 529 | * global.get("os") 530 | */ 531 | functionGlobalContext: { 532 | // os:require('os'), 533 | }, 534 | 535 | /** The maximum number of messages nodes will buffer internally as part of their 536 | * operation. This applies across a range of nodes that operate on message sequences. 537 | * defaults to no limit. A value of 0 also means no limit is applied. 538 | */ 539 | //nodeMessageBufferMaxLength: 0, 540 | 541 | /** If you installed the optional node-red-dashboard you can set it's path 542 | * relative to httpNodeRoot 543 | * Other optional properties include 544 | * readOnly:{boolean}, 545 | * middleware:{function or array}, (req,res,next) - http middleware 546 | * ioMiddleware:{function or array}, (socket,next) - socket.io middleware 547 | */ 548 | //ui: { path: "ui" }, 549 | 550 | /** Colourise the console output of the debug node */ 551 | //debugUseColors: true, 552 | 553 | /** The maximum length, in characters, of any message sent to the debug sidebar tab */ 554 | debugMaxLength: 1000, 555 | 556 | /** The maximum length, in characters, of status messages under the debug node */ 557 | //debugStatusLength: 32, 558 | 559 | /** Maximum buffer size for the exec node. Defaults to 10Mb */ 560 | //execMaxBufferSize: 10000000, 561 | 562 | /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ 563 | //httpRequestTimeout: 120000, 564 | 565 | /** Retry time in milliseconds for MQTT connections */ 566 | mqttReconnectTime: 15000, 567 | 568 | /** Retry time in milliseconds for Serial port connections */ 569 | serialReconnectTime: 15000, 570 | 571 | /** Retry time in milliseconds for TCP socket connections */ 572 | //socketReconnectTime: 10000, 573 | 574 | /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ 575 | //socketTimeout: 120000, 576 | 577 | /** Maximum number of messages to wait in queue while attempting to connect to TCP socket 578 | * defaults to 1000 579 | */ 580 | //tcpMsgQueueSize: 2000, 581 | 582 | /** Timeout in milliseconds for inbound WebSocket connections that do not 583 | * match any configured node. Defaults to 5000 584 | */ 585 | //inboundWebSocketTimeout: 5000, 586 | 587 | /** To disable the option for using local files for storing keys and 588 | * certificates in the TLS configuration node, set this to true. 589 | */ 590 | //tlsConfigDisableLocalFiles: true, 591 | 592 | /** The following property can be used to verify WebSocket connection attempts. 593 | * This allows, for example, the HTTP request headers to be checked to ensure 594 | * they include valid authentication information. 595 | */ 596 | //webSocketNodeVerifyClient: function(info) { 597 | // /** 'info' has three properties: 598 | // * - origin : the value in the Origin header 599 | // * - req : the HTTP request 600 | // * - secure : true if req.connection.authorized or req.connection.encrypted is set 601 | // * 602 | // * The function should return true if the connection should be accepted, false otherwise. 603 | // * 604 | // * Alternatively, if this function is defined to accept a second argument, callback, 605 | // * it can be used to verify the client asynchronously. 606 | // * The callback takes three arguments: 607 | // * - result : boolean, whether to accept the connection or not 608 | // * - code : if result is false, the HTTP error status to return 609 | // * - reason: if result is false, the HTTP reason string to return 610 | // */ 611 | //}, 612 | } 613 | --------------------------------------------------------------------------------