flexmonster-cli
Version:
CLI for Flexmonster Pivot Table & Charts installation
644 lines (618 loc) • 27.1 kB
JavaScript
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'));
}
});
}