var fs = require('fs-extra'); var exec = require('child_process').exec; var execSync = require('child_process').execSync; var inquirer = require('inquirer'); var websocket = require('socket.io-client'); var os = require('os'); // ============================== CREATE PLUGIN =============================== /** * This function starts the creation of a new plugin, it downloads volumio-plugins * repository, then prepares questions for the user */ function init() { var self = this; console.log("Creating a new plugin"); if(!fs.existsSync("/home/volumio/volumio-plugins")){ var question = [ { type: 'input', name: 'user', message: 'volumio plugins folder non existent, please type ' + 'your github username' } ]; inquirer.prompt(question).then(function (answer) { var name = answer.user; console.log("cloning repo:\ngit clone https://github.com/" + name + "/volumio-plugins.git"); try { execSync("/usr/bin/git clone --depth 5 --no-single-branch https://github.com/" + name + "/volumio-plugins.git /home/volumio/volumio-plugins"); console.log("Done, please run command again"); }catch(e){ console.log("Unable to find repo, are you sure you forked it?") process.exitCode = 1; } }); } else { process.chdir("/home/volumio/volumio-plugins"); exec("git config --get remote.origin.url", function (error, stdout, stderr) { if (error) { console.error('exec error: ${error}'); process.exitCode = 1; return; } var url = stdout; if (url == "https://github.com/volumio/volumio-plugins.git\n") { exec("git config user.name", function (error, stdout, stderr) { if (error) { console.error('exec error: ${error}'); process.exitCode = 1; return; } var user = stdout; if (user != 'volumio\n'){ console.log("Error, your repo is the original one, please " + "fork it as suggested in the documentation!"); process.exitCode = 1; return; } else{ ask_category(); } }); } else { ask_category(); } }); } } /** * This function asks the user to specify a category for his plugin, then * proceeds to the one for the name */ function ask_category() { var categories = [ "audio_interface", "miscellanea", "music_service", "system_controller", "user_interface" ]; var questions = [ { type: 'rawlist', name: 'category', message: 'Please select the Plugin Category', choices: categories }]; inquirer.prompt(questions).then(function (answer) { ask_name(categories, answer); }); } /** * This function asks the user to specify name for his plugin, then * calls for the creation * @param categories = list of available categories * @param answer = previous selected category */ function ask_name(categories, answer) { var category = answer.category; var prettyName = ""; questions = [ { type: 'input', name: 'name', message: 'Please insert a name for your plugin', filter: function (name) { prettyName = name; name = name.replace(/ /g, '_'); return name.toLowerCase(); }, validate: function (name) { if(name == "") return "insert a proper name"; for(var i in categories){ if(fs.existsSync("/home/volumio/volumio-plugins/plugins/" + categories[i] + "/" + name) || fs.existsSync("/data/plugins/"+ categories[i] + "/" + name) || fs.existsSync("/volumio/app/plugins/"+ categories[i] + "/" + name)) { return "Error: this plugin already exists"; } } return true; } } ]; inquirer.prompt(questions).then(function (answer) { create_plugin(answer, category, prettyName); }); } /** * This function creates the directories for the custom plugin, using * information provided by the user, then calls for customization of files * @param answer = name of the plugin * @param category = category of the plugin */ function create_plugin(answer, category, prettyName) { var name = {}; name.sysName = answer.name; name.prettyName = prettyName; var path = "/home/volumio/volumio-plugins/plugins/" + category; console.log("NAME: " + name.sysName + " CATEGORY: " + category); if(!fs.existsSync(path)) { fs.mkdirSync(path); } path = path + "/" + name.sysName; fs.mkdirSync(path); console.log("Copying sample files"); execSync("/bin/cp -rp /home/volumio/volumio-plugins/example_plugin/* " + path); fs.readFile(path + '/index.js', 'utf8', function (err, data) { if (err){ console.log("Error reading index.js " + err); } else { customize_index(data, name, path, category); } }); } /** * changes index file, according to the name inserted by the user * @param data = the content of index.js * @param name = name of the plugin * @param path = path of the plugin in volumio-plugin * @param category = category of the plugin */ function customize_index(data, name, path, category) { var splitName = name.sysName.split("_"); var camelName = ""; for (var i in splitName) { if (i == 0) camelName += splitName[i]; else camelName += splitName[i].charAt(0).toUpperCase() + splitName[i].slice(1); } var file = data.replace(/ControllerExamplePlugin/g, camelName); fs.writeFile(path + '/index.js', file, 'utf8', function (err) { if(err) return console.log("Error writing index.js " + err); customize_install(name, path, category); }); } /** * changes install file, according to the name inserted by the user * @param name = name of the plugin * @param path = path of the plugin in volumio-plugin * @param category = category of the plugin */ function customize_install(name, path, category) { fs.readFile(path + '/install.sh', 'utf8', function (err,data) { if(err){ console.log("Error reading install.sh " + err); } else{ var file = data.replace(/Example Plugin/g, name.sysName.replace(/_/g, " ")); fs.writeFile(path + "/install.sh", file, 'utf8', function (err) { if(err) return console.log("Error writing install.sh " + err); customize_package(name, path, category); }); } }); } /** * changes package file, according to the name and category inserted by the * user, asks for additional informations like description and author * @param pluginName = name of the plugin * @param path = path of the plugin in volumio-plugin * @param category = category of the plugin */ function customize_package(pluginName, path, category) { try{ var package = fs.readJsonSync(path + '/package.json'); package.name = pluginName.sysName; package.volumio_info.prettyName = pluginName.prettyName; package.volumio_info.plugin_type = category; questions = [ { type: 'input', name: 'username', message: 'Please insert your name', default: 'Volumio Team', validate: function (name) { if (name.length < 2 || !name.match(/[a-z]/i)){ return "please insert at least a couple letters"; } return true; } }, { type: 'input', name: 'description', message: 'Insert a brief description of your plugin (100 chars)', default: pluginName.sysName, validate: function (desc) { if(desc.length > 100){ return "please be brief"; } return true; } } ]; inquirer.prompt(questions).then(function (answer) { package.author = answer.username; package.description = answer.description; fs.writeJsonSync(path + '/package.json', package, {spaces:'\t'}); finalizing(path, package); }); } catch(e){ console.log("Error reading package.json " + e); } } /** * finalizes the creation, copying the new plugin in data and updating * plugin.json * @param path = path of the plugin * @param package = content of package.json */ function finalizing(path, package) { if(!fs.existsSync("/data/plugins/" + package.volumio_info.plugin_type)){ fs.mkdirSync("/data/plugins/" + package.volumio_info.plugin_type); } if(!fs.existsSync("/data/plugins/" + package.volumio_info.plugin_type + "/" + package.name)) { fs.mkdirSync("/data/plugins/" + package.volumio_info.plugin_type + "/" + package.name); } var pluginName = package.name; var field = { "enabled": { "type": "boolean", "value": true }, "status": { "type": "string", "value": "STARTED" } } try{ var plugins = fs.readJsonSync("/data/configuration/plugins.json"); for(var i in plugins){ if(i == package.volumio_info.plugin_type){ plugins[i][pluginName] = field; } } fs.writeJsonSync("/data/configuration/plugins.json", plugins, {spaces:'\t'}); } catch(e){ console.log("Error, impossible to update plugins.json: " + e); } execSync("/bin/cp -rp /home/volumio/volumio-plugins/plugins/" + package.volumio_info.plugin_type + "/" + package.name + "/* " + "/data/plugins/" + package.volumio_info.plugin_type + "/" + package.name); process.chdir("/data/plugins/" + package.volumio_info.plugin_type + "/" + package.name); console.log("Installing dependencies locally"); if (fs.existsSync(process.cwd + '/package-lock.json')) { execSync("/bin/rm package-lock.json"); } execSync("/usr/local/bin/npm install"); if (fs.existsSync(process.cwd + '/package-lock.json')) { execSync("/bin/rm package-lock.json"); } console.log("\nCongratulation, your plugin has been succesfully created!\n" + "You can find it in: " + path + "\n"); } // ============================= UPDATE LOCALLY =============================== /** * This function copies the content of the current folder in the correspondent * folder in data, according to the information found in package.json, updating * the plugin */ function refresh() { console.log("Updating the plugin in Data"); try { var package = fs.readJsonSync("package.json"); execSync("/bin/cp -rp " + process.cwd() + "/* /data/plugins/" + package.volumio_info.plugin_type+ "/" + package.name); console.log("Plugin succesfully refreshed"); } catch(e){ console.log("Error, impossible to copy the plugin: " + e); } } // ================================ COMPRESS ================================== /** * This function creates an archive with the plugin */ function zip(){ console.log("Compressing the plugin"); try { if(! fs.existsSync("node_modules")) { console.log("No modules found, running \"npm install\""); try{ if (fs.existsSync(process.cwd + '/package-lock.json')) { execSync("/bin/rm package-lock.json"); } execSync("/usr/local/bin/npm install"); if (fs.existsSync(process.cwd + '/package-lock.json')) { execSync("/bin/rm package-lock.json"); } } catch (e){ console.log("Error installing node modules: " + e); process.exitCode = 1; return; } } var package = fs.readJsonSync("package.json"); execSync("IFS=$'\\n'; /usr/bin/minizip -o -9 " + package.name + ".zip $(find -type f -not -name " + package.name + ".zip -printf '%P\\n')", {shell: '/bin/bash'}, {cwd: process.cwd()}); console.log("Plugin succesfully compressed"); } catch (e){ console.log("Error compressing plugin: " + e); process.exitCode = 1; } } // ================================= COMMIT =================================== /** * This function starts to publish the package, it calls zip to create it, if * missing, then switches branch and prepares the folder */ function publish() { console.log("Publishing the plugin"); try { var package = fs.readJsonSync("package.json"); var questions = [ { type: 'input', name: 'version', message: 'do you want to change your version? (leave blank ' + 'for default)', default: package.version, validate: function (value) { var temp = value.split('.'); if (temp.length != 3) { return "Please, insert a version number " + "according to format (example: 1.0.0)"; } for (var i in temp) { if (!temp[i].match(/[0-9]/i)) { return "Please, insert only numbers"; } } return true; } } ]; inquirer.prompt(questions).then(function (answer) { package.version = answer.version; fs.writeJsonSync("package.json", package, {spaces:'\t'}); fs.writeFileSync(".gitignore", ".gitignore" + os.EOL + "node_modules" + os.EOL + "*.zip"); try { execSync("/usr/bin/git add *"); } catch (e){ console.log("Nothing to add"); } try { execSync("/usr/bin/git commit -am \"updating plugin " + package.name + " version " + package.version + "\""); } catch (e){ console.log("Nothing to commit"); } zip(); execSync("/bin/mv " + package.name + ".zip /tmp/"); process.chdir("../../../"); execSync("/usr/bin/git checkout gh-pages"); var arch = ""; exec("cat /etc/os-release | grep ^VOLUMIO_ARCH | tr -d \'VOLUMIO_ARCH=\"\'", function (error, stdout, stderr) { if (error) { console.error('Error, cannot detect system architecture: '+error); return; } else { arch = stdout.replace(/\n$/, ''); if (arch == 'x86') { arch = 'i386'; } else { arch = 'armhf'; } create_folder(package, arch); } }); }); } catch (e) { console.log("Error publishing plugin: " + e); } } /** * This functions creates the appropriate folder path for the package * @param package = package.json * @param arch = architecture */ function create_folder(package, arch) { var path = process.cwd() + "/plugins/volumio/" + arch + "/" + package.volumio_info.plugin_type; if(!fs.existsSync(path + "/" + package.name)){ if(!fs.existsSync(path)){ fs.mkdirSync(path); } fs.mkdirSync(path + "/" + package.name); } execSync("/bin/cp -rp /tmp/" + package.name + ".zip " + path + "/" + package.name); update_plugins(package, arch); } /** * This function updates the plugins.json file, adding the information about * the new plugin, then prepares for the commit * @param package = package.json * @param arch = architecture */ function update_plugins(package, arch) { try { var plugins = fs.readJsonSync(process.cwd() + "/plugins/volumio/" + arch + "/plugins.json"); var i = 0; var catFound = false; var plugFound = false; for (i = 0; i < plugins.categories.length; i++){ if(plugins.categories[i].name == package.volumio_info.plugin_type){ var j = 0; for (j = 0; j < plugins.categories[i].plugins.length; j++){ if(plugins.categories[i].plugins[j].name == package.name){ var today = new Date(); plugins.categories[i].plugins[j].updated = today.getDate() + "-" + (today.getMonth()+1) + "-" + today.getFullYear(); plugins.categories[i].plugins[j].version = package.version; update_desc_details(package, plugins, i, j, arch); plugFound = true; catFound = true; } } if(j == plugins.categories[i].plugins.length && !plugFound && plugins.categories[i].plugins[j-1].name != package.name){ write_new_plugin(package, arch, plugins, i); catFound = true; } } } if(i == plugins.categories.length && plugins.categories[i-1].name != package.volumio_info.plugin_type && !catFound){ write_new_category(package, arch, plugins, i); } } catch(e){ console.log("Error updating plugins.json: " + e) } } /** * This function creates a json containing information about the new plugin * @param package = package.json * @param arch = architecture * @param plugins = plugins.json * @param index = plugin_index */ function write_new_plugin(package, arch, plugins, index) { var data = {}; var question = [ { type: 'input', name: 'details', message: 'Insert some details about your plugin (e.g. features, ' + 'requirements, notes, etc... max 1000 chars)', default: "", validate: function (desc) { if(desc.length > 1000){ return "please be brief"; } return true; } } ]; inquirer.prompt(question).then(function (answer) { var today = new Date(); data.prettyName = package.volumio_info.prettyName; if (package.icon != undefined) { data.icon = package.icon; } else { data.icon = "fa-lightbulb-o"; } data.name = package.name; data.version = package.version; data.url = "http://volumio.github.io/volumio-plugins/" + "plugins/volumio/" + arch + "/" + package.volumio_info.plugin_type + "/" + package.name + "/" + package.name + ".zip"; data.license = package.license; data.description = package.description; data.details = answer.details; data.author = package.author; data.screenshots = [{"image": "", "thumb": ""}]; data.updated = today.getDate() + "-" + (today.getMonth()+1) + "-" + today.getFullYear(); plugins.categories[index].plugins.push(data); fs.writeJsonSync(process.cwd() + "/plugins/volumio/" + arch + "/plugins.json", plugins, {spaces:'\t'}); commit(package, arch); }); } /** * This function creates a json with info about the category in which to put * the plugin, called if the category is missing from plugins.json * @param package = package.json * @param arch = architecture * @param plugins = plugins.json * @param index = plugin_index */ function write_new_category(package, arch, plugins, index){ var data = {}; data.prettyName = package.volumio_info.plugin_type.replace(/_/g, " "); data.name = package.volumio_info.plugin_type; data.id = "cat" + (index+1); data.description = ""; data.plugins = []; plugins.categories.push(data); write_new_plugin(package, arch, plugins, index); } /** * This function updates description and details for an already existing plugin * @param package = package.json * @param plugins = plugins.json * @param catIndex = i * @param plugIndex = j */ function update_desc_details(package, plugins, catIndex, plugIndex, arch) { var descDet = {}; var questions = [ { type: 'input', name: 'details', message: 'Do you want to change the details of your plugin?' + ' (leave blank for default)', default: plugins.categories[catIndex].plugins[plugIndex].details, validate: function (desc) { if(desc.length > 1000){ return "please be brief"; } return true; } }, { type: 'input', name: 'description', message: 'Do you want to change the description of your plugin?' + ' (leave blank for default)', default: package.description, validate: function (desc) { if(desc.length > 100){ return "please be brief"; } return true; } } ]; inquirer.prompt(questions).then(function (answer) { plugins.categories[catIndex].plugins[plugIndex].details = answer.details; plugins.categories[catIndex].plugins[plugIndex].description = answer.description; fs.writeJsonSync(process.cwd() + "/plugins/volumio/" + arch + "/plugins.json", plugins, {spaces:'\t'}); commit(package, arch); }); } /** * This function creates a commit for github, it pushes it if called by volumio * else it notifies that commit is ready * @param package = package.json * @param arch = architecture */ function commit(package, arch) { execSync("/usr/bin/git add " + process.cwd() + "/plugins/volumio/" + arch + "/" + package.volumio_info.plugin_type + "/" + package.name + "/*"); execSync("/usr/bin/git commit -am \"updating plugin " + package.name + " " + package.version + "\""); console.log("updating plugin sources:\n"); execSync("/usr/bin/git push origin master"); console.log("updating plugin packages:\n"); execSync("/usr/bin/git push origin gh-pages"); console.log("Congratulations, your package has been correctly uploaded and" + "is ready for merging!") process.exit(1) } // =============================== INSTALL ==================================== function install(){ if(fs.existsSync("package.json")){ let socket = websocket.connect('http://127.0.0.1:3000', {reconnect: true}); var package = fs.readJsonSync("package.json"); zip(); if(!fs.existsSync("/tmp/plugins")) { execSync("/bin/mkdir /tmp/plugins/") } execSync("/bin/mv *.zip /tmp/plugins/" + package.name + ".zip"); socket.emit('installPlugin', {url: 'http://127.0.0.1:3000/plugin-serve/' + package.name + ".zip"}) socket.on('installPluginStatus', function (data) { console.log("Progress: " + data.progress + "\nStatus :" + data.message) if(data.message == "Plugin Successfully Installed"){ console.log("Done!"); socket.close() } }) } else { console.log("No package found") process.exitCode = 1; } } // ================================ UPDATE ==================================== function update() { if(fs.existsSync("package.json")){ let socket = websocket.connect('http://127.0.0.1:3000', {reconnect: true}); var package = fs.readJsonSync("package.json"); zip(); if(!fs.existsSync("/tmp/plugins")) { execSync("/bin/mkdir /tmp/plugins/") } execSync("/bin/mv *.zip /tmp/plugins/" + package.name + ".zip"); socket.emit('updatePlugin', {url: 'http://127.0.0.1:3000/plugin-serve/' + package.name + ".zip", category: package.category, name: package.name}) socket.on('installPluginStatus', function (data) { console.log("Progress: " + data.progress + "\nStatus :" + data.message) if(data.message == "Plugin Successfully Installed"){ console.log("Done!"); socket.close() } }) } else { console.log("No package found") process.exitCode = 1; } } // ================================ START ===================================== var argument = process.argv[2]; switch (argument){ case "init": init() break; case "refresh": refresh() break; case "package": zip() break; case "publish": publish() break; case "install": install() break; case "update": update() break; }