<file_sep>import app from './app';

/**
 * List available serial ports
 */
app
  .command('ports')
  .description('list available serial ports')
  .action(() => {
    app.listSerialPorts();
  });

/**
 * Get chip id
 */
app
  .command('cid')
  .description('get device chip id')
  .option('-p, --port <port>', 'device port')
  .action((options) => {
    app.getChipId(options.port);
  });

/**
 * Firmware upload
 */
app
  .command('upload')
  .description('compile and upload firmware')
  .option('-p, --port <port>', 'device port')
  .option('-d, --dir <dir>', 'firmware directory')
  .option('-l, --lock <chip_id>', 'lock firmware to specific chip id')
  .option('-s, --slaves <slaves>', 'maximum number of slaves allowed')
  .option('-w --webui', 'upload web-ui after uploading the firmware image')
  .option('-t --trial <time>', 'enable trial mode')
  .option('-a --wizard', 'run firmware upload wizard')
  .action((options) => {
    app.uploadFirmware(options);
  });

/**
 * Flash erase
 */
app
  .command('erase')
  .description('erase device flash')
  .option('-p, --port <port>', 'device port')
  .action((options) => {
    app.eraseFlash(options.port);
  });

app
  .command('config')
  .description('configuration')
  .action(() => {
    app.showConfigMenu();
  });

// Start application. Always run this at the end of this file.
app.start();
<file_sep>import chalk from 'chalk';
import clear from 'clear';
import Table from 'cli-table';
import { Command } from 'commander';
import Configstore from 'configstore';
import figlet from 'figlet';
import inquirer from 'inquirer';
import _ from 'lodash';
import helper, { BuildOptions, GitHubAuth } from 'mbox-builder-helper';
import ora from 'ora';
import readPkgUp from 'read-pkg-up';

import { Port } from './model';
import * as prompts from './prompt';
class App extends Command {
  private static instance: App;
  private pkg: readPkgUp.NormalizedReadResult;
  private config: Configstore;
  /**
   * Class constructor
   * @param appName
   */
  private constructor() {
    // read data from package.json
    const _pkgUp = readPkgUp.sync({
      cwd: __filename,
    });
    // get app's bin name from package.json
    const appBinName = _.keys(_pkgUp.packageJson.bin)[0];
    // call super
    super(appBinName);
    // copy local _pkg to class scoped pkg object
    this.pkg = _pkgUp;
    // create config store
    this.config = new Configstore(this.pkg.packageJson.name);
    // clear console
    clear();
    // show top caption
    console.log(
      chalk.cyanBright(
        figlet.textSync('MBox Builder', { horizontalLayout: 'full' })
      )
    );
  }

  /**
   * Store github credentials
   * @param username
   * @param password
   */
  private storeGithubLogin(username: string, password: string): boolean {
    this.config.set('gh_username', username);
    this.config.set('gh_password', password);
    return true;
  }

  /**
   * Retrieve stored github credentials
   * @param username
   * @param password
   */
  private getGithubLogin(): GitHubAuth {
    const auth = {
      username: this.config.get('gh_username'),
      password: this.config.get('gh_password'),
    };
    return auth;
  }

  private checkGithubLogin(): boolean {
    if (this.config.get('gh_username') || this.config.get('gh_password')) {
      return true;
    }
    return false;
  }

  /**
   * Get class's singleton instance
   * @param appName
   */
  public static getInstance(): App {
    if (!App.instance) {
      App.instance = new App();
    }

    return App.instance;
  }

  /**
   * Start app
   */
  public start() {
    this.version(this.pkg.packageJson.version);
    this.description(this.pkg.packageJson.description);
    this.parse(process.argv);
    if (!process.argv.slice(2).length) {
      this.outputHelp();
    }
  }

  /**
   * list available serial ports
   */
  public async listSerialPorts() {
    const loading = ora('Looking for available serial ports').start();

    // create table
    const table = new Table({
      head: [chalk.blue('Path'), chalk.blue('Manufacturer')],
      colWidths: [25, 25],
    });

    // get serial ports
    helper.getPorts().then(
      (ports: readonly Port[]) => {
        // no serial port found
        if (ports.length <= 0) {
          loading.warn(`No serial port available`);
          return;
        }

        // done loading
        loading.succeed();

        // populate table
        ports.forEach((port) => {
          table.push([port.path, port.manufacturer]);
        });

        // show table
        console.log(table.toString());
      },
      (error) => {
        loading.fail(`Could not retrieve serial ports`);
        // TODO: Only show detailed error message if verbose output enabled.
        console.log(chalk.red(`Error: ${error}`));
      }
    );
  }

  /**
   *  Get chip id
   * @param port
   */
  public async getChipId(port: string) {
    const loading = ora();

    // if no port provided as argument, prompt with available ports.
    if (!port) {
      port = (await prompts.selectPort()).path;
    }

    loading.start('Getting chip id from device');

    helper.getChipId(port).then(
      (chipId) => {
        loading.succeed(`Chip id: ${chalk.green(chipId)}`);
      },
      (error) => {
        loading.fail(`Could not read chip id from device`);
        // TODO: Only show detailed error message if verbose output enabled.
        console.log(chalk.red(`Error: ${error}`));
      }
    );
  }

  /**
   * Upload firmware
   */
  public async uploadFirmware(options: {
    port: string;
    dir: string;
    lock: string;
    slaves: number;
    webui: boolean;
    trial: number;
    wizard: boolean;
  }) {
    const loading = ora();
    // if no port provided, prompt with available ports.
    if (!options.port) {
      options.port = (await prompts.selectPort()).path;
    }

    // if no firmware directory provided, use current working directory.
    if (!options.dir) {
      options.dir = process.cwd();
    }

    loading.start('Compiling and uploading firmware');

    if (!this.checkGithubLogin()) {
      loading.fail(chalk.red('Github credentials not found in config'));
      return;
    }

    const buildOptions: BuildOptions = {
      chipId: options.lock ? options.lock : '',
      maxSlave: options.slaves ? options.slaves : 4,
      trialMode: options.trial ? true : false,
      trialTime: options.trial ? options.trial : 1440,
    };

    helper
      .buildAndUpload(
        options.dir,
        options.port,
        this.getGithubLogin(),
        buildOptions
      )
      .then(
        () => {
          loading.succeed(chalk.greenBright('Firmware successfully uploaded'));
        },
        (error) => {
          loading.fail(
            `An error has ocurred trying to build and upload firmware`
          );
          // TODO: Only show detailed error message if verbose output enabled.
          console.log(chalk.red(`Error: ${error}`));
        }
      );
  }

  /**
   * Erase device flash memory
   * @param port
   */
  public async eraseFlash(port: string) {
    const loading = ora();

    // if no port provided as argument, prompt with available ports.
    if (!port) {
      port = (await prompts.selectPort()).path;
    }

    loading.start(chalk.red('Performing flash erase on device'));

    helper.eraseFlash(port).then(
      () => {
        loading.succeed(
          chalk.greenBright('Device flash has been successfully erased')
        );
      },
      (error) => {
        loading.fail(`An error has ocurred trying to erase device flash`);
        // TODO: Only show detailed error message if verbose output enabled.
        console.log(chalk.red(`Error: ${error}`));
      }
    );
  }

  /**
   * Show config menu
   */
  public async showConfigMenu() {
    prompts.configMenu().then((configMenuSelection: { config: string }) => {
      switch (configMenuSelection.config) {
        case 'github':
          if (this.checkGithubLogin()) {
            console.log(chalk.green('Github credentials already exist.'));
            console.log(chalk.grey(JSON.stringify(this.getGithubLogin())));
            inquirer
              .prompt({
                type: 'confirm',
                name: 'editCredentials',
                message: 'Would you like to modify them?',
                default: false,
              })
              .then((result) => {
                if (result.editCredentials) {
                  prompts.addGithubLogin().then((entry: GitHubAuth) => {
                    this.storeGithubLogin(entry.username, entry.password);
                    console.log(chalk.greenBright('Saved!'));
                  });
                } else {
                  return;
                }
              });
          } else {
            prompts.addGithubLogin().then((entry: GitHubAuth) => {
              this.storeGithubLogin(entry.username, entry.password);
            });
          }
          break;

        default:
          break;
      }
    });
  }
}

const app = App.getInstance();

export default app;
<file_sep># Changelog

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### 1.0.1 (2020-09-25)


### Bug Fixes

* **index:** fix commands options arguments ([3a2022b](https://github.com/iotbits-us/mbox-builder-cli/commit/3a2022b75499ad5220dae2c931108b079ea452f2))
* **index:** fix erase command options ([346baa1](https://github.com/iotbits-us/mbox-builder-cli/commit/346baa1c33e1aa9e7536d83207ebbfd4e72e1538))
<file_sep>export type Port = {
  readonly path: string;
  readonly manufacturer?: string;
};
<file_sep># ModbusBox Firmware Builder CLI

ModbusBox Firmware Builder CLI
<file_sep>import inquirer from 'inquirer';
import helper, { BuildOptions } from 'mbox-builder-helper';
import ora from 'ora';

import { Port } from './model';

/**
 * Prompt available serial ports
 * Show available serial port and return a promise with the one selected.
 */
export const selectPort = (): Promise<Port> => {
  return new Promise((resolve, reject) => {
    const loading = ora('Looking for available serial ports').start();
    helper.getPorts().then(
      (ports: readonly Port[]) => {
        if (ports.length <= 0) {
          loading.warn(`No serial port available`);
          return;
        }
        loading.succeed();
        inquirer
          .prompt([
            {
              type: 'rawlist',
              name: 'port',
              message: 'Please select a port',
              choices: ports.map((port) => {
                return port.path;
              }),
              filter: function (val) {
                return val;
              },
            },
          ])
          .then((answer) => {
            resolve({ path: answer.port });
          });
      },
      (error) => {
        loading.fail(`Could not retrieve serial ports`);
        reject(error);
      }
    );
  });
};

const configMenuQuestions = [
  {
    type: 'list',
    name: 'config',
    message: 'What would you like to configure?',
    choices: [
      { name: 'GitHub Credentials', value: 'github' },
      { name: 'Default Building Options', value: 'building', disabled: true },
      { name: 'Debug Info', value: 'debug', disabled: true },
    ],
    filter: function (val: string) {
      return val.toLowerCase();
    },
  },
];

export const configMenu = () => {
  return new Promise((resolve) => {
    inquirer.prompt(configMenuQuestions).then((mainMenuSelection) => {
      resolve(mainMenuSelection);
    });
  });
};

const addGithubLoginQuestions = [
  {
    type: 'input',
    name: 'username',
    message: 'Enter your Github username:',
    filter: String,
  },
  {
    type: 'password',
    name: 'password',
    message: 'Enter your Github password:',
  },
];

export const addGithubLogin = () => {
  return new Promise((resolve) => {
    inquirer.prompt(addGithubLoginQuestions).then((answers) => {
      resolve(answers);
    });
  });
};

const uploadWizardQuestions = [
  {
    type: 'confirm',
    name: 'lockToDevice',
    message: 'Would you like to lock the firmware to the device on USB port?',
    default: false,
  },
  {
    type: 'input',
    name: 'slaves',
    message: 'Enter the maximum amount of slaves 1-4:',
    validate: function (value: string) {
      const valid =
        !isNaN(parseInt(value)) && parseInt(value) > 0 && parseInt(value) < 5;
      return valid || 'Please enter a number between 1 and 4';
    },
    filter: Number,
  },
  {
    type: 'input',
    name: 'trialTime',
    message: 'Enter the trial time:',
    validate: function (value: string) {
      const valid = !isNaN(parseInt(value));
      return valid || 'Please enter a number';
    },
    filter: Number,
  },
];

export const uploadWizard = (): Promise<BuildOptions> => {
  return new Promise((resolve) => {
    const buildOptions: BuildOptions = {};
    inquirer.prompt(uploadWizardQuestions).then((answers) => {
      console.log(answers);
      resolve(buildOptions);
    });
  });
};
