particle-cli
Version:
Simple Node commandline application for working with your Particle devices and using the Particle Cloud
570 lines (512 loc) • 20.1 kB
JavaScript
const os = require('os');
const path = require('path');
const fs = require('fs-extra');
const ParticleAPI = require('./api');
const settings = require('../../settings');
const LogicFunction = require('../lib/logic-function');
const logicFunctionTemplatePath = path.join(__dirname, '/../../assets/logicFunction');
const CLICommandBase = require('./base');
/**
* Commands for managing encryption keys.
* @constructor
*/
module.exports = class LogicFunctionsCommand extends CLICommandBase {
constructor(...args) {
super(...args);
this.api = createAPI();
this.logicFuncList = null;
this.org = null;
}
async list({ org }) {
this._setOrg(org);
const logicFunctions = await this._getLogicFunctionListWithSpinner();
if (!logicFunctions.length) {
this._printListHelperOutput();
} else {
this._printListOutput({ logicFunctionsList: logicFunctions });
}
}
_getLogicFunctionListWithSpinner() {
return this.ui.showBusySpinnerUntilResolved(
`Fetching Logic Functions for ${getOrgName(this.org)}...`
,LogicFunction.listFromCloud({ org: this.org, api: this.api }));
}
_printListHelperOutput({ fromFile } = {}) {
if (fromFile) {
this.ui.stdout.write(`No Logic Functions found in your directory.${os.EOL}`);
} else {
this.ui.stdout.write(`No Logic Functions deployed in ${getOrgName(this.org)}.${os.EOL}`);
}
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`To create a Logic Function, see ${this.ui.chalk.yellow('particle logic-function create')}.${os.EOL}`);
this.ui.stdout.write(`To download an existing Logic Function, see ${this.ui.chalk.yellow('particle logic-function get')}.${os.EOL}`);
}
_printListOutput({ logicFunctionsList }) {
this.ui.stdout.write(`Logic Functions deployed in ${getOrgName(this.org)}:${os.EOL}`);
logicFunctionsList.forEach((item) => {
// We assume at least one trigger
this.ui.stdout.write(`- ${item.name} (${item.enabled ? this.ui.chalk.cyanBright('enabled') : this.ui.chalk.cyan('disabled')})${os.EOL}`);
this.ui.stdout.write(` - ID: ${item.id}${os.EOL}`);
this.ui.stdout.write(` - ${item.triggers[0].type} based trigger ${os.EOL}`);
});
this.ui.stdout.write(`${os.EOL}To view a Logic Function's code, see ${this.ui.chalk.yellow('particle logic-function get')}.${os.EOL}`);
}
async get({ org, name, id, params : { filepath } } = { params: { } }) {
this._setOrg(org);
const logicFunctions = await this._getLogicFunctionListWithSpinner();
if (!name && !id) {
name = await this._selectLogicFunctionName(logicFunctions);
}
const logicFunction = await LogicFunction.getByIdOrName({ org, id, name, list: logicFunctions });
logicFunction.path = filepath;
// check if the files already exists
await this._confirmOverwriteIfNeeded({
filePaths: [logicFunction.configurationPath, logicFunction.sourcePath],
});
await logicFunction.saveToDisk();
this._printGetOutput({
jsonPath: logicFunction.configurationPath,
jsPath: logicFunction.sourcePath
});
this._printGetHelperOutput();
}
_printGetOutput({ jsonPath, jsPath }) {
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`Downloaded:${os.EOL}`);
this.ui.stdout.write(` - ${path.basename(jsonPath)}${os.EOL}`);
this.ui.stdout.write(` - ${path.basename(jsPath)}${os.EOL}`);
this.ui.stdout.write(`${os.EOL}`);
}
_printGetHelperOutput() {
this.ui.stdout.write(`Note that any local modifications to these files need to be deployed to the cloud in order to take effect.${os.EOL}` +
`Refer to ${this.ui.chalk.yellow('particle logic-function execute')} and ${this.ui.chalk.yellow('particle logic-function deploy')} for more information.${os.EOL}`);
}
async create({ org, name, description, force, params : { filepath } } = { params: { } }) {
this._setOrg(org);
const {
name: logicFunctionName,
description: logicFunctionDescription
} = await this._promptLogicFunctionInput({ _name: name, _description: description, force });
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`Creating Logic Function ${this.ui.chalk.cyan(logicFunctionName)} for ${getOrgName(this.org)}...${os.EOL}`);
const logicFunction = new LogicFunction({
org,
name: logicFunctionName,
_path: filepath,
description: logicFunctionDescription,
api: this.api
});
await logicFunction.initFromTemplate({ templatePath: logicFunctionTemplatePath });
await this._confirmOverwriteIfNeeded({
filePaths: [logicFunction.configurationPath, logicFunction.sourcePath],
force
});
await logicFunction.saveToDisk();
// notify files were created
this._printCreateOutput({
logicFunctionName,
basePath: logicFunction.path,
jsonPath: logicFunction.configurationPath,
jsPath: logicFunction.sourcePath
});
this._printCreateHelperOutput();
}
async _promptLogicFunctionInput({ _name, _description, force }) {
let name = _name, description = _description;
if (force) {
return {
name: name? name.trim() : '',
description: description ? description.trim() : ''
};
}
if (!_name) {
const result = await this._prompt({
type: 'input',
name: 'name',
message: 'What would you like to call your Function?'
});
if (!result.name) {
throw new Error('Please provide a name for the Logic Function');
}
name = result.name;
}
if (!_description) {
const result = await this._prompt({
type: 'input',
name: 'description',
message: 'Please provide a short description of your Function:'
});
description = result.description;
}
return {
name: name.trim(),
description: description.trim()
};
}
_printCreateOutput({ logicFunctionName, basePath, jsonPath, jsPath }) {
this.ui.stdout.write(`Successfully created ${this.ui.chalk.cyan(logicFunctionName)} locally in ${this.ui.chalk.bold(basePath)}`);
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`Files created:${os.EOL}`);
this.ui.stdout.write(`- ${path.basename(jsPath)}${os.EOL}`);
this.ui.stdout.write(`- ${path.basename(jsonPath)}${os.EOL}`);
}
_printCreateHelperOutput() {
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`Guidelines for creating your Logic Function can be found here https://docs.particle.io/getting-started/cloud/logic/${os.EOL}`);
this.ui.stdout.write(`Once you have written your Logic Function, run${os.EOL}`);
this.ui.stdout.write('- ' + this.ui.chalk.yellow('particle logic-function execute') + ` to run your Function${os.EOL}`);
this.ui.stdout.write('- ' + this.ui.chalk.yellow('particle logic-function deploy') + ` to deploy your new changes${os.EOL}`);
this.ui.stdout.write(`${os.EOL}`);
}
async _prompt({ type, name, message, choices, nonInteractiveError }) {
const question = {
type,
name,
message,
choices
};
const result = await this.ui.prompt([question], { nonInteractiveError });
return result;
}
async _promptOverwrite({ message }) {
const answer = await this._prompt({
type: 'confirm',
name: 'overwrite',
message,
choices: Boolean
});
return answer.overwrite;
}
async _confirmOverwriteIfNeeded({ force, filePaths, _exit = () => process.exit(0) }) {
if (force) {
return;
}
let exists = false;
const pathsToCheck = filePaths;
for (const p of pathsToCheck) {
if (await fs.pathExists(p)) {
exists = true;
}
}
if (exists) {
const overwrite = await this._promptOverwrite({
message: 'This Logic Function was previously downloaded locally. Overwrite?',
});
if (!overwrite) {
this.ui.stdout.write(`Aborted.${os.EOL}`);
_exit();
}
}
return exists;
}
async execute({ org, name, id, product_id: productId, event_name: eventName, device_id: deviceId, data, payload, params: { filepath } }) {
this._setOrg(org);
const logicFunction = await this._pickLogicFunctionFromDisk({ filepath, name, id });
const eventData = await this._getExecuteData({
productId,
deviceId,
data,
eventName,
payload
});
const { status, logs, error } = await this._executeLogicFunctionWithSpinner(logicFunction, eventData);
this._printExecuteOutput({ logs, error, status, logicFunction });
}
async _executeLogicFunctionWithSpinner(logicFunction, eventData) {
return this.ui.showBusySpinnerUntilResolved(
`Executing Logic Function ${this.ui.chalk.bold(logicFunction.name)} for ${getOrgName(this.org)}...`
,logicFunction.execute(eventData));
}
async _pickLogicFunctionFromDisk({ filepath, name, id, action = 'execute' }) {
let { logicFunctions, malformedLogicFunctions } = await LogicFunction.listFromDisk({ filepath, api: this.api, org: this.org });
if (name || id) {
logicFunctions = logicFunctions.filter(lf => (lf.name === name && name) || (lf.id === id && id));
}
if (logicFunctions.length === 0) {
this._printMalformedLogicFunctionsFromDisk(malformedLogicFunctions);
this._printListHelperOutput({ fromFile: true });
throw new Error('No Logic Functions found');
}
if (logicFunctions.length && !name && !id) {
this._printMalformedLogicFunctionsFromDisk(malformedLogicFunctions);
}
if (logicFunctions.length === 1) {
return logicFunctions[0];
}
const answer = await this._prompt({
type: 'list',
name: 'logicFunction',
message: `Which Logic Function would you like to ${action}?`,
choices : logicFunctions,
});
const logicFunction = logicFunctions.find(lf => lf.name === answer.logicFunction);
logicFunction.path = filepath;
return logicFunction;
}
_printMalformedLogicFunctionsFromDisk(malformedLogicFunctions) {
if (malformedLogicFunctions.length) {
this.ui.stdout.write(this.ui.chalk.red(`The following Logic Functions are not valid:${os.EOL}`));
malformedLogicFunctions.forEach((item) => {
this.ui.stdout.write(`- ${item.name}: ${item.error}${os.EOL}`);
});
this.ui.stdout.write(`${os.EOL}`);
}
}
async _getExecuteData({ productId, deviceId, eventName, data, payload }) {
if (payload) {
return this._getExecuteDataFromPayload(payload);
}
return {
event: {
event_name: eventName || 'test_event',
product_id: productId || 0,
device_id: deviceId || '',
event_data: data || ''
}
};
}
async _getExecuteDataFromPayload(payload) {
const parsedAsJson = await this._parseEventFromPayload(payload);
if (!parsedAsJson.error) {
return parsedAsJson.eventData;
}
const parsedAsFile = await this._parseEventFromFile(payload);
if (parsedAsFile.error) {
throw new Error('Unable to parse payload as JSON or file');
} else {
return parsedAsFile.eventData;
}
}
async _parseEventFromPayload(payload) {
let eventData, error;
try {
eventData = JSON.parse(payload);
} catch (_error) {
error = _error;
}
return { error, eventData };
}
async _parseEventFromFile(payloadPah) {
let eventData, error;
try {
eventData = await fs.readJson(payloadPah);
} catch (_error) {
error = _error;
}
return { error, eventData };
}
_printExecuteOutput({ logs, error, status, logicFunction }) {
this.ui.stdout.write(`${os.EOL}`);
const logicFunctionShowName = logicFunction.id ? `${logicFunction.name}(${logicFunction.id})` : logicFunction.name;
this.ui.stdout.write(`Logic Function ${this.ui.chalk.cyanBright(logicFunctionShowName)} executed in ${getOrgName(this.org)}${os.EOL}`);
if (status === 'Success') {
this.ui.stdout.write(this.ui.chalk.cyanBright(`Execution Status: ${status}${os.EOL}`));
if (logs.length === 0) {
this.ui.stdout.write(`No logs obtained from Execution${os.EOL}`);
this.ui.stdout.write(`${os.EOL}`);
} else {
this.ui.stdout.write(`Logs from Execution:${os.EOL}`);
logs.forEach((log, index) => {
this.ui.stdout.write(` ${index + 1}.- ${JSON.stringify(log)}${os.EOL}`);
});
this.ui.stdout.write(`${os.EOL}`);
}
this.ui.stdout.write(`No errors during Execution.${os.EOL}`);
} else {
this.ui.stdout.write(this.ui.chalk.red(`Execution Status: ${status}${os.EOL}`));
this.ui.stdout.write(this.ui.chalk.red(`Error during Execution:${os.EOL}`));
this.ui.stdout.write(`${error}${os.EOL}`);
if (logs.length > 0) {
this.ui.stdout.write(`Logs from Execution:${os.EOL}`);
logs.forEach((log, index) => {
this.ui.stdout.write(` ${index + 1}.- ${JSON.stringify(log)}${os.EOL}`);
});
this.ui.stdout.write(`${os.EOL}`);
}
}
}
async deploy({ org, name, id, product_id: productId, event_name: eventName, device_id: deviceId, data, payload, force, params: { filepath } }) {
this._setOrg(org);
const logicFunction = await this._pickLogicFunctionFromDisk({ filepath, name, id, action: 'deploy' });
const eventData = await this._getExecuteData({
productId,
deviceId,
data,
eventName,
payload
});
const cloudLogicFunctions = await this._getLogicFunctionListWithSpinner();
const cloudLogicFunction = cloudLogicFunctions.find(lf => lf.name === logicFunction.name);
await this._confirmDeploy(logicFunction, force);
if (cloudLogicFunction) {
await this._promptOverwriteCloudLogicFunction(cloudLogicFunction, force);
logicFunction.id = cloudLogicFunction.id;
}
const { status, logs, error } = await this._executeLogicFunctionWithSpinner(logicFunction, eventData);
this._printExecuteOutput({ logs, error, status, logicFunction });
if (status !== 'Success') {
throw new Error('Unable to deploy Logic Function');
}
// TODO (hmontero): put an spinner
await this._deployLogicFunctionWithSpinner(logicFunction);
await logicFunction.saveToDisk();
this._printDeployOutput(logicFunction);
}
async _deployLogicFunctionWithSpinner(logicFunction) {
const logicFunctionShowName = logicFunction.id ? `${logicFunction.name}(${logicFunction.id})` : logicFunction.name;
return this.ui.showBusySpinnerUntilResolved(
`Deploying Logic Function ${this.ui.chalk.bold(logicFunctionShowName)} for ${getOrgName(this.org)}...`
,logicFunction.deploy());
}
async _confirmDeploy(logicFunction, force) {
const logicFunctionShowName = logicFunction.id ? `${logicFunction.name}(${logicFunction.id})` : logicFunction.name;
if (!force) {
const confirm = await this._prompt({
type: 'confirm',
name: 'proceed',
message: `Executing then deploying ${logicFunctionShowName} to ${getOrgName(this.org)}. Proceed?`,
choices: Boolean
});
if (!confirm.proceed) {
this.ui.stdout.write(`Aborted.${os.EOL}`);
return;
}
}
}
async _promptOverwriteCloudLogicFunction(cloudLogicFunction, force) {
if (!force) {
const confirm = await this._prompt({
type: 'confirm',
name: 'proceed',
message: `A Logic Function with name ${cloudLogicFunction.name} is already available in the cloud ${getOrgName(this.org)}.${os.EOL}Proceed and overwrite with the new content?`,
choices: Boolean
});
if (!confirm.proceed) {
this.ui.stdout.write(`Aborted.${os.EOL}`);
process.exit(0);
}
}
}
_printDeployOutput(logicFunction) {
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`${this.ui.chalk.cyanBright('Success!')}${os.EOL}`);
this.ui.stdout.write(`Logic Function ${this.ui.chalk.cyanBright(logicFunction.name)}(${this.ui.chalk.cyanBright(logicFunction.id)}) deployed to ${getOrgName(this.org)}${os.EOL}`);
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`Visit ${this.ui.chalk.yellow('console.particle.io')} to view results from your device(s)!${os.EOL}`);
this.ui.stdout.write(`${os.EOL}`);
}
async updateStatus({ org, name, id, force, params: { filepath } }, { enable }) {
this._setOrg(org);
const cloudLogicFunctions = await this._getLogicFunctionListWithSpinner();
if (!name && !id) {
const action = enable ? 'enable' : 'disable';
name = await this._selectLogicFunctionName(cloudLogicFunctions, action);
}
const logicFunction = await LogicFunction.getByIdOrName({ org, id, name, list: cloudLogicFunctions });
logicFunction.enabled = enable;
await this._updateLogicFunctionWithSpinner(logicFunction, { enable });
this._printUpdateStatusOutput({ name: logicFunction.name, id: logicFunction.id , enable });
const { logicFunctions: localLogicFunctions } = await LogicFunction.listFromDisk({ filepath, org, api: this.api });
const localLogicFunction = localLogicFunctions.find(lf => lf.name === logicFunction.name);
if (localLogicFunction) {
await this._confirmOverwriteIfNeeded({
filePaths: [localLogicFunction.configurationPath, localLogicFunction.sourcePath],
force
});
// assign cloud values to local Logic Function
localLogicFunction.copyFromOtherLogicFunction(logicFunction);
await localLogicFunction.saveToDisk();
this._printUpdateLocalFilesOutput({
jsonPath: localLogicFunction.configurationPath,
jsPath: localLogicFunction.sourcePath,
enable
});
}
}
_updateLogicFunctionWithSpinner(logicFunction, { enable }) {
return this.ui.showBusySpinnerUntilResolved(
`${enable ? 'Enabling' : 'Disabling'} Logic Function ${this.ui.chalk.bold(logicFunction.name)} for ${getOrgName(this.org)}...`
,logicFunction.deploy());
}
_printUpdateStatusOutput({ name, id, enable }) {
this.ui.stdout.write(`Logic Function ${name} (${id}) is now ${enable ? 'enabled' : 'disabled'}.${os.EOL}`);
}
_printUpdateLocalFilesOutput({ jsonPath, jsPath, enable }) {
this.ui.stdout.write(`${os.EOL}`);
this.ui.stdout.write(`The following files were overwritten after ${enable ? 'enabling': 'disabling'} the Logic Function:${os.EOL}`);
this.ui.stdout.write(` - ${path.basename(jsonPath)}${os.EOL}`);
this.ui.stdout.write(` - ${path.basename(jsPath)}${os.EOL}`);
this.ui.stdout.write(`${os.EOL}`);
}
async delete({ org, name, id, force }) {
this._setOrg(org);
const cloudLogicFunctions = await this._getLogicFunctionListWithSpinner();
if (!name && !id) {
const action = 'delete';
name = await this._selectLogicFunctionName(cloudLogicFunctions, action);
}
const logicFunction = await LogicFunction.getByIdOrName({ org, id, name, list: cloudLogicFunctions });
if (!force) {
const confirm = await this._prompt({
type: 'confirm',
name: 'delete',
message: `Are you sure you want to delete Logic Function ${logicFunction.name}? This action cannot be undone.`,
choices: Boolean
});
if (!confirm.delete) {
this.ui.stdout.write(`Aborted.${os.EOL}`);
return;
}
}
await this._deleteLogicFunctionWithSpinner(logicFunction);
this._printDeleteOutput({ name: logicFunction.name, id: logicFunction.id });
}
async _deleteLogicFunctionWithSpinner(logicFunction) {
return this.ui.showBusySpinnerUntilResolved(
`Deleting Logic Function ${this.ui.chalk.bold(logicFunction.name)} for ${getOrgName(this.org)}...`
,logicFunction.deleteFromCloud());
}
async _printDeleteOutput({ name, id }) {
this.ui.stdout.write(`Logic Function ${name}(${id}) has been successfully deleted.${os.EOL}`);
}
async logs() {
// TODO
this.ui.stdout.write(`Please visit ${this.ui.chalk.yellow('console.particle.io')} to view logs.${os.EOL}`);
}
_setOrg(org) {
if (this.org === null) {
this.org = org;
}
}
_serializeLogicFunction(data) {
const logicFunctionCode = data.logic_function.source.code;
const logicFunctionConfigData = data.logic_function;
delete logicFunctionConfigData.source.code;
return { logicFunctionConfigData: { 'logic_function': logicFunctionConfigData }, logicFunctionCode };
}
async _selectLogicFunctionName(list, action = 'download') {
if (list.length === 0) {
this._printListHelperOutput();
throw new Error('No Logic Functions found');
}
const answer = await this._prompt({
type: 'list',
name: 'logic_function',
message: `Which Logic Function would you like to ${action}?`,
choices : list,
nonInteractiveError: 'Provide name for the Logic Function'
});
return answer.logic_function;
}
};
// UTILS //////////////////////////////////////////////////////////////////////
function createAPI() {
return new ParticleAPI(settings.apiUrl, {
accessToken: settings.access_token
});
}
// get org name from org slug
function getOrgName(org) {
return org || 'your Sandbox';
}
module.exports.createAPI = createAPI;