UNPKG

flexmonster-cli

Version:

CLI for Flexmonster Pivot Table & Charts installation

644 lines (618 loc) 27.1 kB
import chalk from 'chalk'; import escExit from 'esc-exit'; import fs from 'fs'; import { spawn } from 'child_process'; import path from 'path'; import inquirer from 'inquirer'; import Listr from 'listr'; import { sync } from 'rimraf'; import decompress from 'decompress'; import { commandInAction, COMMANDS, program, tempDirURL } from '../cli.js'; import { listChoices, copyFolderSync, getArchiveNameFromURL, isMacOS, isWindows, getOSString, isLinux } from '../utils.js'; import { downloadArchive, deleteDownloadedArchive, installAccelerator, getServerSideToolFolder, isFDS, isFDSExecutable, isFA, isCTB, isOSDependent, isServerSideTool, isClientSideTool, isProjectBasedTool, installClientSideTool, getTool, isValidTool, isValidSubtool, getServerToolByOS, downloadProject, getProjectURL, getProjectFolder, unpackProject, getProjectArchiveName, isForOS, installAndRunServiceTool, installAndRunGUITool, handleServiceInstallationProcess, checkIfServiceInstalled } from './base.js'; import { projectInstall } from 'pkg-install'; import { helpUpdate } from './update.js'; const TOOLS_TO_ADD = [{ name: 'fds', value: 'fds', title: 'Flexmonster Data Server', description: 'Flexmonster Data Server (a server-side tool)', guiToolName: 'Flexmonster Admin Panel', fileToBestGuess: 'flexmonster-config.json', subtool: 'executable', os: [{ name: 'Linux (x64)', url: 'https://dist.flexmonster.com/flexmonster-data-server/2.9/latest/FlexmonsterDataServer-linux-x64.tar.gz', executable: 'flexmonster-data-server', serviceExecutable: 'service-install', serviceUninstaller: 'service-uninstall', serviceStatusCommand: 'systemctl status flexmonster-data-server.service', serviceStopCommand: 'sudo systemctl stop flexmonster-data-server.service', guiToolStopCommand: 'killall flexmonster-admin-panel', guiExecutable: 'Flexmonster-Admin-Panel.AppImage' }, { name: 'Linux (ARM64)', url: 'https://dist.flexmonster.com/flexmonster-data-server/2.9/latest/FlexmonsterDataServer-linux-arm64.tar.gz', executable: 'flexmonster-data-server', serviceExecutable: 'service-install', serviceUninstaller: 'service-uninstall', serviceStatusCommand: 'systemctl status flexmonster-data-server.service', serviceStopCommand: 'sudo systemctl stop flexmonster-data-server.service', guiToolStopCommand: 'killall flexmonster-admin-panel', guiExecutable: 'Flexmonster-Admin-Panel.AppImage' }, { name: 'macOS (x64)', url: 'https://dist.flexmonster.com/flexmonster-data-server/2.9/latest/FlexmonsterDataServer-osx-x64.tar.gz', executable: 'flexmonster-data-server', serviceExecutable: 'service-install.sh', serviceUninstaller: 'service-uninstall.sh', serviceStatusCommand: 'ls ~/Library/LaunchAgents/com.flexmonster.DataServer.plist', serviceStopCommand: 'launchctl unload -w ~/Library/LaunchAgents/com.flexmonster.DataServer.plist', guiToolStopCommand: 'killall "Flexmonster Admin Panel"', guiExecutable: 'Flexmonster-Admin-Panel.dmg' }, { name: 'macOS (ARM64)', url: 'https://dist.flexmonster.com/flexmonster-data-server/2.9/latest/FlexmonsterDataServer-osx-arm64.tar.gz', executable: 'flexmonster-data-server', serviceExecutable: 'service-install.sh', serviceUninstaller: 'service-uninstall.sh', serviceStatusCommand: 'ls ~/Library/LaunchAgents/com.flexmonster.DataServer.plist', serviceStopCommand: 'launchctl unload -w ~/Library/LaunchAgents/com.flexmonster.DataServer.plist', guiToolStopCommand: 'killall "Flexmonster Admin Panel"', guiExecutable: 'Flexmonster-Admin-Panel.dmg' }, { name: 'Windows (x64)', url: 'https://dist.flexmonster.com/flexmonster-data-server/2.9/latest/FlexmonsterDataServer-win-x64.zip', executable: 'flexmonster-data-server', serviceExecutable: 'service-install.bat', serviceUninstaller: 'service-uninstall.bat', serviceStatusCommand: 'sc query FlexmonsterDataServer', serviceStopCommand: 'sc stop FlexmonsterDataServer', guiToolStopCommand: 'taskkill /F /IM "Flexmonster Admin Panel.exe"', guiExecutable: 'Flexmonster-Admin-Panel.exe' }, { name: 'Windows (x86)', url: 'https://dist.flexmonster.com/flexmonster-data-server/2.9/latest/FlexmonsterDataServer-win-x86.zip', executable: 'flexmonster-data-server', serviceExecutable: 'service-install.bat', serviceUninstaller: 'service-uninstall.bat', serviceStatusCommand: 'sc query FlexmonsterDataServer', serviceStopCommand: 'sc stop FlexmonsterDataServer', guiToolStopCommand: 'taskkill /F /IM "Flexmonster Admin Panel.exe"', guiExecutable: 'Flexmonster-Admin-Panel.exe', } ], }, { name: 'accelerator', value: 'accelerator', title: 'Flexmonster Accelerator for SSAS', description: 'Flexmonster Accelerator for SSAS (a server-side tool)', os: [{ name: 'Windows (x64)', url: 'https://dist.flexmonster.com/flexmonster-accelerator/2.9/latest/FlexmonsterAccelerator.zip', dir: '', executable: 'Flexmonster Accelerator.msi', }, { name: 'Windows (x86)', url: 'https://dist.flexmonster.com/flexmonster-accelerator/2.9/latest/FlexmonsterAccelerator.zip', dir: '', executable: 'Flexmonster Accelerator.msi', } ], }, { name: 'theme-builder', value: 'theme-builder', title: 'Theme Builder', description: 'Theme Builder for creating custom Flexmonster themes (a project-based tool)', url: 'https://github.com/flexmonster/custom-theme-builder/archive/master.zip', archiveName: 'custom-theme-builder-master.zip', dir: '/custom-theme-builder-master', hasDependencies: true, runnable: false }, { name: 'ng-flexmonster', value: 'ng-flexmonster', title: 'Flexmonster Pivot wrapper for Angular 15 and older projects (legacy)', description: 'Flexmonster Pivot wrapper for Angular 15 and older projects (a legacy node module)', fileToBestGuess: 'package.json', module: 'ng-flexmonster', }, { name: 'ngx-flexmonster', value: 'ngx-flexmonster', title: 'Flexmonster Pivot wrapper for Angular 14+ projects', description: 'Flexmonster Pivot wrapper for Angular 14+ projects (a node module)', fileToBestGuess: 'package.json', module: 'ngx-flexmonster', }, { name: 'react-flexmonster', value: 'react-flexmonster', title: 'Flexmonster Pivot wrapper for React projects', description: 'Flexmonster Pivot wrapper for React projects (a node module)', fileToBestGuess: 'package.json', module: 'react-flexmonster', }, { name: 'vue-flexmonster', value: 'vue-flexmonster', title: 'Flexmonster Pivot for Vue projects', description: 'Flexmonster Pivot for Vue projects (a node module)', fileToBestGuess: 'package.json', module: 'vue-flexmonster', }, { name: 'js-flexmonster', value: 'flexmonster', title: 'Flexmonster Pivot', description: 'Flexmonster Pivot (a node module)', fileToBestGuess: 'package.json', module: 'flexmonster', }, ]; function isValidToolToInstall(tool) { return isValidServerToolToInstall(tool) || isValidProjectBasedToolToInstall(tool); } function isValidServerToolToInstall(tool) { return isFA(tool); } function isValidProjectBasedToolToInstall(tool) { return isCTB(tool); } function isValidServerToolToRun(tool) { return isFDS(tool); } function isValidServerSubtoolToRun(subtool) { return isFDSExecutable(subtool); } const Q_COMMAND_ADD = [{ type: 'list', name: 'tool', message: 'Choose what should be downloaded and installed:', choices: listChoices(TOOLS_TO_ADD) }, { type: 'confirm', name: 'install', message: 'Should Flexmonster Accelerator for SSAS be installed automatically? (It may take a while)', default: false, when: function (answers) { return isForOS(TOOLS_TO_ADD, answers.tool) && isValidServerToolToInstall(answers.tool); } }, { type: 'confirm', name: 'install', message: 'Should all dependencies be installed automatically for the project? (It may take a while)', default: false, when: function (answers) { return isValidProjectBasedToolToInstall(answers.tool); } }, { type: 'confirm', name: 'run', message: 'Should Flexmonster Data Server service be installed and started automatically?', default: false, when: function (answers) { return isValidServerToolToRun(answers.tool) && !isValidServerSubtoolToRun(answers.subtool); } }, { type: 'confirm', name: 'run', message: 'Should Flexmonster Data Server be started automatically?', default: false, when: function (answers) { return isValidServerToolToRun(answers.tool) && isValidServerSubtoolToRun(answers.subtool); } } ]; function composeCommandAddQuestions(argAnswers) { if (argAnswers != undefined) { if (argAnswers.tool != undefined) { console.log('Tool: %s', argAnswers.tool); Q_COMMAND_ADD[0].when = function (answers) { answers.tool = argAnswers.tool; return false; //do not show this question }; } if (isValidSubtool(TOOLS_TO_ADD, argAnswers.subtool, argAnswers.tool)) { console.log('Subtool: %s', argAnswers.subtool); Q_COMMAND_ADD[1].when = function (answers) { answers.subtool = argAnswers.subtool; return false; //do not show this question }; } if (argAnswers.install) { Q_COMMAND_ADD[2].when = function (answers) { answers.install = argAnswers.install && isValidToolToInstall(answers.tool); return false; //do not show this question }; } if (argAnswers.run) { Q_COMMAND_ADD[3].when = function (answers) { answers.run = argAnswers.run && isValidServerToolToRun(answers.tool); return false; //do not show this question }; } } return Q_COMMAND_ADD; } export function initAddCommand() { program.command('add [tool|module] [subtool]') // sub-command name .alias('a') // alternative sub-command .description(COMMANDS[1].description) // command description .option('-i, --install', "true, install/install dependencies automatically after the download (for Flexmonster Accelerator for SSAS, or for Custom Theme Builder)", false) // false by default .option('-r, --run', "true, run automatically after the download (for Flexmonster Data Server)", false) // false by default .action(function (tool, subtool, args) { // function to execute when command is used commandInAction(); let argAnswers = {}; argAnswers.install = args.install; argAnswers.run = args.run; if (isValidTool(TOOLS_TO_ADD, tool)) { tool = tool.toLowerCase(); const toolObj = getTool(TOOLS_TO_ADD, tool); if (!isForOS(TOOLS_TO_ADD, tool)) { console.log(''); console.log('~ there is no %s for %s operating system', toolObj.title, chalk.blueBright(getOSString())); // if (isMacOS(process.platform)) console.log(' try %s as a possible solution or', chalk.underline('https://www.flexmonster.com/doc/troubleshooting-cli/#no-fds-for-macos')); console.log(' contact us at %s to address any questions', chalk.underline('https://www.flexmonster.com/technical-support/')); console.log(''); } else { argAnswers.tool = tool; if (isValidSubtool(TOOLS_TO_ADD, subtool, tool)) { subtool = subtool.toLowerCase(); argAnswers.subtool = subtool; } helpAdd(argAnswers); } } else { if (tool !== undefined) { console.log(''); console.log('~ add command has an invalid tool name: %s', tool); console.log(''); } helpAdd(argAnswers); } }) .addHelpText('after', ` Examples: flexmonster add accelerator flexmonster add accelerator -i flexmonster add fds flexmonster add fds -r flexmonster add fds executable -r flexmonster add theme-builder' flexmonster add theme-builder -i flexmonster add flexmonster flexmonster add js-flexmonster flexmonster add ng-flexmonster flexmonster add react-flexmonster flexmonster add vue-flexmonster `); } export function helpAdd(argAnswers) { escExit(); inquirer.prompt(composeCommandAddQuestions(argAnswers)) .then(function (answers) { executeAdd(answers["tool"], answers["subtool"], answers); }); } async function executeAdd(tool, subtool, args) { tool = tool.toLowerCase(); const toolObj = getTool(TOOLS_TO_ADD, tool); const osObj = getServerToolByOS(TOOLS_TO_ADD, tool, getOSString()); if (!isForOS(TOOLS_TO_ADD, tool)) { console.log(''); console.log('~ there is no %s for %s operating system', toolObj.title, chalk.blueBright(getOSString())); // if (isMacOS(process.platform)) console.log(' try %s as a possible solution or', chalk.underline('https://www.flexmonster.com/doc/troubleshooting-cli/#no-fds-for-macos')); console.log(' contact us at %s to address any questions', chalk.underline('https://www.flexmonster.com/technical-support/')); console.log(''); } else { if (isFDS(tool)) { const wasServiceInstalled = await checkIfServiceInstalled(osObj); if (wasServiceInstalled === true) { console.log(`~ ${toolObj.title} service is already installed`); // Check if flexmonster-data-server directory exists in cwd if (fs.existsSync(osObj.executable) && fs.lstatSync(osObj.executable).isDirectory()) { try { // Try switching to flexmonster-data-server directory process.chdir(osObj.executable); // console.log("working directory after changing: " + process.cwd()); } catch (err) { } } // Switch to the update command helpUpdate(tool) return; } } console.log(''); console.log('--- ADD ---'); const tasks = new Listr([{ title: 'Download ' + toolObj.title, task: (ctx, task) => downloadServerSideTool(osObj, task) .catch(error => { const url = osObj.url; throw new Error(chalk.redBright.bold("Error:") + " download has failed. \n" + chalk.yellowBright.bold("Recommendation:") + " Try to download it from here: " + chalk.yellowBright.bold.underline(url)); }), enabled: () => isServerSideTool(tool), }, { title: 'Download ' + toolObj.title, task: (ctx, task) => downloadProject(toolObj, undefined, task) .catch(() => { const url = getProjectURL(toolObj); throw new Error(chalk.redBright.bold("Error:") + " download has failed. \n" + chalk.yellowBright.bold("Recommendation:") + " Try to download a sample project from here: " + chalk.yellowBright.bold.underline(url)); }), enabled: () => isProjectBasedTool(tool), }, { title: 'Unpack files to ' + chalk.green.bold(getServerSideToolFolder(tool)), task: () => unpackServerSideTool(tool, osObj) .then(() => { const archiveName = getArchiveNameFromURL(osObj.url); deleteDownloadedArchive(archiveName); }) .catch(() => { const archiveName = getArchiveNameFromURL(osObj.url); throw new Error(chalk.redBright.bold("Error:") + " unpacking has failed. \n" + chalk.yellowBright.bold("Recommendation:") + " Try to unpack it manually: " + chalk.yellowBright.bold(archiveName)); }), enabled: () => isServerSideTool(tool), }, { title: 'Unpack files to ' + chalk.green.bold(getProjectFolder(toolObj)), task: () => unpackProject(toolObj) .then(() => { const archiveName = getProjectArchiveName(toolObj); deleteDownloadedArchive(archiveName); }) .catch(error => { const archiveName = getProjectArchiveName(toolObj); throw new Error(chalk.redBright.bold("Error:") + " unpacking has failed. \n" + chalk.yellowBright.bold("Recommendation:") + " Try to unpack it manually: " + chalk.yellowBright.bold(archiveName)); }), enabled: () => isProjectBasedTool(tool), }, { title: 'Install dependencies (It may take a while)', task: async () => { const url = "." + getProjectFolder(toolObj); await projectInstall({ cwd: url }); return; }, enabled: () => (args.install && isValidProjectBasedToolToInstall(tool)), }, { title: 'Install ' + toolObj.title + ' (It may take a while)', task: () => installServerSideTool(tool, osObj), enabled: () => args.install && isValidServerToolToInstall(tool), }, { title: 'Install ' + toolObj.title + ' (It may take a while)', task: () => installClientSideTool(toolObj), enabled: () => isClientSideTool(tool), }, { title: `Install and start ${toolObj.title} service`, task: () => installAndRunServiceTool(tool, osObj, toolObj) .catch(error => { throw error; }), enabled: () => !isLinux(process.platform) && args.run && isValidServerToolToRun(tool) && !isValidServerSubtoolToRun(subtool), }, { title: 'Install and run ' + toolObj.guiToolName + ' (the service GUI tool)' + (isMacOS(process.platform) ? `\n${chalk.blueBright(`Hint: please use Finder to drag the app to the "Applications" folder`)}` : ""), task: () => installAndRunGUITool(tool, osObj, toolObj) .catch(error => { throw error; }), enabled: () => !isLinux(process.platform) && args.run && isValidServerToolToRun(tool) && !isValidServerSubtoolToRun(subtool), }, { title: (isWindows(process.platform) || isMacOS(process.platform)) ? 'Run ' + toolObj.title : 'Prepare to run ' + toolObj.title, task: () => runServerSideTool(tool, osObj, toolObj) .catch(error => { throw error; }), enabled: () => args.run && isValidServerToolToRun(tool) && isValidServerSubtoolToRun(subtool), } ]); tasks.run() .then(() => { console.log(chalk.green.bold('Ready')); console.log(''); if (isOSDependent(tool)) { if (((args.install && isValidServerToolToInstall(tool)) || (args.run && isValidServerToolToRun(tool))) && isWindows(process.platform)) { console.log('Press Ctrl+C here to enter new command (PS: This will not stop the started project)'); } else if (((args.install && isValidServerToolToInstall(tool)) || (args.run && isValidServerToolToRun(tool))) && !isMacOS(process.platform)) { if (isValidServerSubtoolToRun(subtool)) { console.log(chalk.italic(`Starting to run ${toolObj.title}:`)); } if (args.run && isValidServerToolToRun(tool) && !isValidServerSubtoolToRun(subtool)) { handleServiceInstallationProcess(tool, osObj, toolObj) } } else { console.log('Check the %s folder', chalk.green.bold(getServerSideToolFolder(tool))); } console.log(''); } }) .catch(error => { console.log(''); if (isMacOS(process.platform) && error.message && error.message.indexOf('71:172:') > -1 || error.message.indexOf('Not authorized to send Apple events to Terminal') > -1) { console.error(`Recommendation: Please allow ${chalk.bold('Terminal')} to control other apps in ${chalk.bold('System Preferences > Security & Privacy > Automation')} to be able to run the project using Flexmonster CLI.`); } else { console.error(error.message); } console.log(''); }); } } async function downloadServerSideTool(osObj, task) { const url = osObj.url; const archiveName = getArchiveNameFromURL(osObj.url); return await downloadArchive(url, archiveName, task); } function unpackServerSideTool(tool, osObj) { //create temp directory for unpacking if (!fs.existsSync(tempDirURL)) { fs.mkdirSync(tempDirURL); } try { const archiveName = getArchiveNameFromURL(osObj.url); let file = decompress(archiveName, tempDirURL); return new Promise((resolve, reject) => { file.then((files) => { const projectFolderURL = "." + getServerSideToolFolder(tool); try { // delete for all if (fs.existsSync(projectFolderURL)) { sync(projectFolderURL); } fs.mkdirSync(projectFolderURL); if (osObj.dir !== undefined) { copyFolderSync(tempDirURL + osObj.dir, projectFolderURL); } else { copyFolderSync(tempDirURL, projectFolderURL); // overrides its content, including FDS config } sync(tempDirURL); resolve(); } catch (error) { sync(tempDirURL); reject(error); } }).catch((error) => { sync(tempDirURL); reject(error); }); }); } catch (error) { sync(tempDirURL); return Promise.reject(new Error('Failed to unpack')); } } // for the Accelerator only function installServerSideTool(tool, osObj) { const url = "." + getServerSideToolFolder(tool); return installAccelerator(url, osObj.executable); } function runServerSideTool(tool, osObj, toolObj) { const projectFolder = getServerSideToolFolder(tool); var url = "." + projectFolder; return new Promise((resolve, reject) => { try { var childProcess; var executablePath; if (isWindows(process.platform)) { executablePath = osObj.executable; childProcess = spawn('cmd', ['/c', executablePath], { cwd: url, detached: true, shell: true }); } else if (isMacOS(process.platform)) { url = path.resolve('') + projectFolder; executablePath = "./" + osObj.executable; childProcess = spawn('osascript', [ '-e', 'tell application "Terminal" to activate', // ira: activates "Terminal" even if it was closed before '-e', 'tell application "Terminal" to do script "cd ' + url + '"', // ira: cd to the URL in new "Terminal" window '-e', 'tell application "Terminal" to activate', // ira: activates this new window '-e', 'tell application "Terminal" to do script "' + executablePath + '" in selected tab of the front window' // ira: runs executable ]); } else { executablePath = "./" + osObj.executable; childProcess = spawn(executablePath, { cwd: url, shell: true, stdio: 'inherit' }); resolve(); } if (isWindows(process.platform) || isMacOS(process.platform)) { var stderror = ""; var stderrorTimer; childProcess.stderr.on('data', (data) => { stderror += data.toString() + "\n"; clearTimeout(stderrorTimer); stderrorTimer = setTimeout(() => reject(new Error(stderror)), 50); }); childProcess.on('error', (error) => { reject(new Error(error.toString())); }); if (isWindows(process.platform)) { childProcess.on('close', (code) => { if (code > 0) { reject(new Error(`${toolObj.title} failed with code ${code}`)); } else { resolve(`${toolObj.title} completed successfully`); } }) setTimeout(() => resolve(), 500); // ira: not sure why, but childProcess.stdout.on('data', ...) does not happen on my Windows } else { childProcess.stdout.on('data', (data) => { resolve(data.toString()); }); } } } catch (error) { return reject(new Error('Failed to run')); } }); }