Skip to content

Commit 3ad0e6b

Browse files
authored
Init command (#15)
* Initial init code * Add init command
1 parent 24f18de commit 3ad0e6b

File tree

4 files changed

+840
-3
lines changed

4 files changed

+840
-3
lines changed

lib/commands/init/index.js

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
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('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 promptUser() {
98+
const responses = await prompt([
99+
{
100+
type: 'input',
101+
name: 'username',
102+
message: "Username",
103+
validate(val) { return !!val.trim() ? true: "Invalid username"}
104+
},
105+
{
106+
type: 'password',
107+
name: 'password',
108+
message: "Password",
109+
validate(val) {
110+
if (val.length < 8) {
111+
return "Password too short. Must be at least 8 characters"
112+
}
113+
return true
114+
}
115+
},
116+
{
117+
type: 'select',
118+
name: 'permission',
119+
message: "User permissions",
120+
choices: [ {name:"full access", value:"*"}, {name:"read-only access", value:"read"}],
121+
result(value) {
122+
return this.find(value).value;
123+
}
124+
}
125+
])
126+
responses.password = bcrypt.hashSync(responses.password, 8);
127+
return responses;
128+
}
129+
130+
async function promptSecurity() {
131+
heading("User Security");
132+
133+
const responses = await prompt([
134+
{
135+
type: 'select',
136+
name: 'adminAuth',
137+
initial: "Yes",
138+
message: 'Do you want to setup user security?',
139+
choices: ['Yes', 'No'],
140+
result(value) {
141+
return value === "Yes"
142+
}
143+
}
144+
])
145+
if (responses.adminAuth) {
146+
responses.users = [];
147+
while(true) {
148+
responses.users.push(await promptUser());
149+
const resp = await prompt({
150+
type: 'select',
151+
name: 'addMore',
152+
initial: "No",
153+
message: 'Add another user?',
154+
choices: ['Yes', 'No'],
155+
result(value) {
156+
return value === "Yes"
157+
}
158+
})
159+
if (!resp.addMore) {
160+
break;
161+
}
162+
}
163+
}
164+
return responses;
165+
}
166+
167+
async function promptProjects() {
168+
heading("Projects");
169+
message("The Projects feature allows you to version control your flow using a local git repository.");
170+
const responses = await prompt([
171+
{
172+
type: 'select',
173+
name: 'enabled',
174+
initial: "No",
175+
message: 'Do you want to enable the Projects feature?',
176+
choices: ['Yes', 'No'],
177+
result(value) {
178+
return value === "Yes";
179+
}
180+
},
181+
// {
182+
// type: 'select',
183+
// name: '_continue',
184+
// message: 'Node-RED will help you create your project the first time you access the editor.',
185+
// choices: ['Continue'],
186+
// skip() {
187+
// return !this.state.answers.enabled;
188+
// }
189+
// },
190+
{
191+
type: 'select',
192+
name: 'workflow',
193+
message: 'What project workflow do you want to use?',
194+
choices: [
195+
{value: 'manual', name: 'manual - you must manually commit changes'},
196+
{value: 'auto', name: 'auto - changes are automatically committed'}
197+
],
198+
skip() {
199+
return !this.state.answers.enabled;
200+
},
201+
result(value) {
202+
return this.find(value).value;
203+
}
204+
}
205+
])
206+
// delete responses._continue;
207+
return responses
208+
}
209+
210+
async function promptFlowFileSettings() {
211+
heading("Flow File settings");
212+
const responses = await prompt([
213+
{
214+
type: 'input',
215+
name: 'flowFile',
216+
message: 'Enter a name for your flows file',
217+
default: 'flows.json'
218+
},
219+
{
220+
type: 'password',
221+
name: 'credentialSecret',
222+
message: 'Provide a passphrase to encrypt your credentials file'
223+
}
224+
])
225+
return responses
226+
}
227+
228+
229+
//
230+
// dark
231+
// midnight-red
232+
// oled
233+
// solarized-dark
234+
// solarized-light
235+
//
236+
237+
238+
async function promptNodeSettings() {
239+
heading("Node settings");
240+
const responses = await prompt([
241+
{
242+
type: 'select',
243+
name: 'functionExternalModules',
244+
message: 'Allow Function nodes to load external modules? (functionExternalModules)',
245+
initial: 'No',
246+
choices: ['Yes', 'No'],
247+
result(value) {
248+
return value === "Yes"
249+
},
250+
}
251+
]);
252+
return responses;
253+
}
254+
async function promptEditorSettings() {
255+
heading("Editor settings");
256+
const responses = await prompt([
257+
{
258+
type: 'select',
259+
name: 'theme',
260+
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.',
261+
initial: 'default',
262+
choices: [ "default", "dark", "midnight-red", "oled", "solarized-dark", "solarized-light"],
263+
},
264+
{
265+
type: 'select',
266+
name: 'codeEditor',
267+
message: 'Select the text editor component to use in the Node-RED Editor',
268+
initial: 'ace',
269+
choices: [ {name:"ace (default)", value:"ace"}, {name:"monaco (new for 2.0)", value:"monaco"}],
270+
result(value) {
271+
return this.find(value).value;
272+
}
273+
}
274+
]);
275+
return responses;
276+
}
277+
async function command(argv, result) {
278+
const config = {
279+
intro: `Node-RED Settings created at ${new Date().toUTCString()}`,
280+
flowFile: "flows.json",
281+
editorTheme: ""
282+
};
283+
284+
heading("Node-RED Settings File initialisation");
285+
message(`This tool will help you create a Node-RED settings file.`);
286+
287+
288+
const userDir = argv["u"] || argv["userDir"] || path.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME,".node-red");
289+
290+
const fileSettings = await promptSettingsFile({userDir});
291+
292+
const securityResponses = await promptSecurity();
293+
if (securityResponses.adminAuth) {
294+
let adminAuth = {
295+
type: "credentials",
296+
users: securityResponses.users
297+
};
298+
config.adminAuth = JSON.stringify(adminAuth,"",4).replace(/\n/g,"\n ");
299+
}
300+
301+
const projectsResponses = await promptProjects();
302+
let flowFileSettings = {};
303+
if (!projectsResponses.enabled) {
304+
flowFileSettings = await promptFlowFileSettings();
305+
config.flowFile = flowFileSettings.flowFile;
306+
if (flowFileSettings.hasOwnProperty("credentialSecret")) {
307+
config.credentialSecret = flowFileSettings.credentialSecret?`"${flowFileSettings.credentialSecret}"`:"false"
308+
}
309+
config.projects = {
310+
enabled: false,
311+
workflow: "manual"
312+
}
313+
} else {
314+
config.projects = projectsResponses
315+
}
316+
const editorSettings = await promptEditorSettings();
317+
config.codeEditor = editorSettings.codeEditor;
318+
if (editorSettings.theme !== "default") {
319+
config.editorTheme = editorSettings.theme
320+
}
321+
const nodeSettings = await promptNodeSettings();
322+
config.functionExternalModules = nodeSettings.functionExternalModules;
323+
324+
325+
const template = await loadTemplateSettings();
326+
const settings = await fillTemplate(template, config)
327+
328+
const settingsDir = path.dirname(fileSettings.settingsFile);
329+
await fs.promises.mkdir(settingsDir,{recursive: true});
330+
await fs.promises.writeFile(fileSettings.settingsFile, settings, "utf-8");
331+
332+
333+
console.log(color.yellow(`\n\nSettings file written to ${fileSettings.settingsFile}`));
334+
335+
if (config.editorTheme) {
336+
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`))
337+
}
338+
}
339+
command.alias = "init";
340+
command.usage = command.alias;
341+
command.description = "Initialise a Node-RED settings file";
342+
343+
module.exports = command;

0 commit comments

Comments
 (0)