Home Latest News Build a JavaScript Command Line Interface (CLI) with Node.js — SitePoint

Build a JavaScript Command Line Interface (CLI) with Node.js — SitePoint

by Admin

As nice as Node.js is for “traditional” net functions, its potential makes use of are far broader. Microservices, REST APIs, tooling, working with the Web of Issues and even desktop functions: it’s received your again.

One other space the place Node.js is basically helpful is for constructing command-line functions — and that’s what we’re going to be doing on this article. We’re going to begin by plenty of third-party packages designed to assist work with the command line, then construct a real-world instance from scratch.

What we’re going to construct is a software for initializing a Git repository. Positive, it’ll run git init underneath the hood, but it surely’ll do extra than simply that. It can additionally create a distant repository on GitHub proper from the command line, enable the person to interactively create a .gitignore file, and eventually carry out an preliminary commit and push.

As ever, the code accompanying this tutorial might be discovered on our GitHub repo.

Build a Node CLI

Earlier than we dive in and begin constructing, it’s value why we would select Node.js to construct a command-line software.

The obvious benefit is that, in case you’re studying this, you’re in all probability already aware of it — and, certainly, with JavaScript.

One other key benefit, as we’ll see as we go alongside, is that the robust Node.js ecosystem implies that among the many a whole lot of 1000’s of packages out there for all method of functions, there are a selection that are particularly designed to assist construct highly effective command-line instruments.

Lastly, we will use npm to handle any dependencies, quite than have to fret about OS-specific package deal managers equivalent to Aptitude, Yum or Homebrew.

Tip: that isn’t essentially true, in that your command-line software could produce other exterior dependencies.

What We’re Going to Construct: ginit

Ginit, our Node CLI in action

For this tutorial, we’re going to create a command-line utility which I’m calling ginit. It’s git init, however on steroids.

You’re in all probability questioning what on earth meaning.

As you little doubt already know, git init initializes a Git repository within the present folder. Nevertheless, that’s often solely one in every of plenty of repetitive steps concerned within the strategy of hooking up a brand new or present mission to Git. For instance, as a part of a typical workflow, you could properly:

  1. initialize the native repository by working git init
  2. create a distant repository, for instance on GitHub or Bitbucket — sometimes by leaving the command line and firing up an online browser
  3. add the distant
  4. create a .gitignore file
  5. add your mission recordsdata
  6. commit the preliminary set of recordsdata
  7. push as much as the distant repository.

There are sometimes extra steps concerned, however we’ll follow these for the needs of our app. Nonetheless, these steps are fairly repetitive. Wouldn’t it’s higher if we might do all this from the command line, with no copy-pasting of Git URLs and such like?

So what ginit will do is create a Git repository within the present folder, create a distant repository — we’ll be utilizing GitHub for this — after which add it as a distant. Then it’s going to present a easy interactive “wizard” for making a .gitignore file, add the contents of the folder and push it as much as the distant repository. It won’t prevent hours, but it surely’ll take away a number of the preliminary friction when beginning a brand new mission.

With that in thoughts, let’s get began.

The Software Dependencies

One factor is for sure: when it comes to look, the console won’t ever have the sophistication of a graphical person interface. Nonetheless, that doesn’t imply it must be plain, ugly, monochrome textual content. You is likely to be shocked by simply how a lot you are able to do visually, whereas on the similar time retaining it purposeful. We’ll be a few libraries for enhancing the show: chalk for colorizing the output and clui so as to add some further visible elements. Only for enjoyable, we’ll use figlet to create a elaborate ASCII-based banner, and we’ll additionally use clear to clear the console.

When it comes to enter and output, the low-level Readline Node.js module could possibly be used to immediate the person and request enter, and in easy circumstances is greater than ample. However we’re going to benefit from a third-party package deal which provides a better diploma of sophistication — Inquirer. In addition to offering a mechanism for asking questions, it additionally implements easy enter controls: suppose radio buttons and checkboxes, however within the console.

We’ll even be utilizing minimist to parse command-line arguments.

Right here’s an entire checklist of the packages we’ll use particularly for growing on the command line:

  • chalk — colorizes the output
  • clear — clears the terminal display screen
  • clui — attracts command-line tables, gauges and spinners
  • figlet — creates ASCII artwork from textual content
  • inquirer — creates interactive command-line person interface
  • minimist — parses argument choices
  • configstore — simply masses and saves config with out you having to consider the place and the way.

Moreover, we’ll even be utilizing the next:

Getting Began

Though we’re going to create the appliance from scratch, don’t neglect you can additionally seize a duplicate of the code from the repository which accompanies this text.

Create a brand new listing for the mission. You don’t should name it ginit, after all:

mkdir ginit
cd ginit

Create a brand new package deal.json file:

npm init -y

And edit it to seem like so:

{
“name”: “ginit”,
“version”: “1.0.0”,
“description”: “‘git init’ on steroids”,
“main”: “index.js”,
“scripts”: {
“test”: “echo “Error: no test specified” && exit 1″
},
“keywords”: [
“Git”,
“CLI”
],
“author”: ““,
“license”: “ISC”
}

Now set up the dependencies:

npm set up chalk clear clui figlet inquirer minimist configstore @octokit/relaxation @octokit/auth-basic lodash simple-git contact

Now create an index.js file in the identical folder and require the next dependencies:

const chalk = require(‘chalk’);
const clear = require(‘clear’);
const figlet = require(‘figlet’);

Including Some Helper Strategies

We’re going to create a lib folder the place we’ll cut up our helper code into modules:

  • recordsdata.js — primary file administration
  • inquirer.js — command-line person interplay
  • github.js — entry token administration
  • repo.js — Git repository administration.

Let’s begin with lib/recordsdata.js. Right here, we have to:

  • get the present listing (to get a default repo title)
  • test whether or not a listing exists (to find out whether or not the present folder is already a Git repository by on the lookout for a folder named .git).

This sounds simple, however there are a few gotchas to take into accounts.

Firstly, you is likely to be tempted to make use of the fs module’s realpathSync technique to get the present listing:

path.basename(path.dirname(fs.realpathSync(__filename)));

This can work after we’re calling the appliance from the identical listing (for instance, utilizing node index.js), however keep in mind that we’re going to be making our console software out there globally. This implies we’ll need the title of the listing we’re working in, not the listing the place the appliance resides. For this objective, it’s higher to make use of course of.cwd:

path.basename(course of.cwd());

Secondly, the popular technique of checking whether or not a file or listing exists retains altering. The present means is to make use of existsSync. This returns true if the trail exists, false in any other case.

Lastly, it’s value noting that if you’re writing a command-line software, utilizing the synchronous model of those types of strategies is simply fantastic.

Placing that every one collectively, let’s create a utility package deal in lib/recordsdata.js:

const fs = require(‘fs’);
const path = require(‘path’);

module.exports = {
getCurrentDirectoryBase: () => {
return path.basename(course of.cwd());
},

directoryExists: (filePath) => {
return fs.existsSync(filePath);
}
};

Return to index.js and make sure you require the brand new file:

const recordsdata = require(‘./lib/recordsdata’);

With this in place, we will begin growing the appliance.

Initializing the Node CLI

Now let’s implement the start-up section of our console software.

To be able to exhibit a number of the packages we’ve put in to boost the console output, let’s clear the display screen after which show a banner:

// index.js

clear();

console.log(
chalk.yellow(
figlet.textSync(‘Ginit’, { horizontalLayout: ‘full’ })
)
);

You’ll be able to run the appliance utilizing node index.js. The output from that is proven beneath.

The welcome banner on our Node CLI, created using Chalk and Figlet

Subsequent up, let’s run a easy test to make sure that the present folder isn’t already a Git repository. That’s simple: we simply test for the existence of a .git folder utilizing the utility technique we simply created:

//index.js

if (recordsdata.directoryExists(‘.git’)) {
console.log(chalk.purple(‘Already a Git repository!’));
course of.exit();
}

Tip: discover we’re utilizing the chalk module to indicate a red-colored message.

Prompting the Consumer for Enter

The following factor we have to do is create a operate that can immediate the person for his or her GitHub credentials.

We are able to use Inquirer for this. The module contains plenty of strategies for varied kinds of prompts, that are roughly analogous to HTML type controls. To be able to gather the person’s GitHub username and password, we’re going to make use of the enter and password sorts respectively.

First, create lib/inquirer.js and insert this code:

const inquirer = require(‘inquirer’);

module.exports = {
askGithubCredentials: () => {
const questions = [
{
title: ‘username’,
sort: ‘enter’,
message: ‘Enter your GitHub username or e-mail handle:’,
validate: operate( worth ) {
if (worth.size) {
return true;
} else {
return ‘Please enter your username or e-mail handle.’;
}
}
},
{
title: ‘password’,
sort: ‘password’,
message: ‘Enter your password:’,
validate: operate(worth) {
if (worth.size) {
return true;
} else {
return ‘Please enter your password.’;
}
}
}
];
return inquirer.immediate(questions);
},
};

As you possibly can see, inquirer.immediate() asks the person a collection of questions, offered within the type of an array as the primary argument. Every query is made up of an object which defines the title of the sector, the sort (we’re simply utilizing enter and password respectively right here, however later we’ll have a look at a extra superior instance), and the immediate (message) to show.

The enter the person offers might be handed in to the calling operate as a Promise. If profitable, we’ll find yourself with a easy object with two properties — username and password.

You’ll be able to check all of this out by including the next to index.js:

const inquirer = require(‘./lib/inquirer’);

const run = async () => {
const credentials = await inquirer.askGithubCredentials();
console.log(credentials);
};

run();

Then run the script utilizing node index.js.

Getting user input with Inquirer

Tip: if you’re performed testing, don’t neglect to take away the road const inquirer = require(‘./lib/inquirer’); from index.js, as we gained’t really want it on this file.

Dealing With GitHub Authentication

The following step is to create a operate to retrieve an OAuth token for the GitHub API. Basically, we’re going to “exchange” the username and password for a token.

After all, we don’t need customers to should enter their credentials each time they use the software. As a substitute, we’ll retailer the OAuth token for subsequent requests. That is the place the configstore package deal is available in.

Storing Config

Storing config is outwardly fairly simple: you possibly can merely learn and write to/from a JSON file with out the necessity for a third-party package deal. Nevertheless, the configstore package deal offers just a few key benefits:

  1. It determines probably the most acceptable location for the file for you, bearing in mind your working system and the present person.
  2. There’s no must explicitly learn or write to the file. You merely modify a configstore object and that’s taken care of for you within the background.

To make use of it, merely create an occasion, passing it an software identifier. For instance:

const Configstore = require(‘configstore’);
const conf = new Configstore(‘ginit’);

If the configstore file doesn’t exist, it’ll return an empty object and create the file within the background. If there’s already a configstore file, the contents might be made out there to your software. Now you can use conf as a easy object, getting or setting properties as required. As talked about above, you don’t want to fret about saving it afterwards. That will get taken care of for you.

Tip: on macOS, you’ll discover the file in /Customers/[YOUR-USERNME]/.config/configstore/ginit.json. On Linux, it’s in /dwelling/[YOUR-USERNME]/.config/configstore/ginit.json.

Speaking with the GitHub API

Let’s create a library for dealing with the GitHub token. Create the file lib/github.js and place the next code inside it:

const CLI = require(‘clui’);
const Configstore = require(‘configstore’);
const Octokit = require(‘@octokit/relaxation’);
const Spinner = CLI.Spinner;
const { createBasicAuth } = require(“@octokit/auth-basic”);

const inquirer = require(‘./inquirer’);
const pkg = require(‘../package deal.json’);

const conf = new Configstore(pkg.title);

Now let’s add the operate that checks whether or not we’ve already received an entry token. We’ll additionally add a operate that permits different libs to entry octokit(GitHub) capabilities:

let octokit;

module.exports = {
getInstance: () => {
return octokit;
},

getStoredGithubToken: () => {
return conf.get(‘github.token’);
},
};

If a conf object exists and it has github.token property, which means that there’s already a token in storage. On this case, we return the token worth again to the invoking operate. We’ll get to that afterward.

If no token is detected, we have to fetch one. After all, getting an OAuth token includes a community request, which implies a brief look forward to the person. This provides us a chance to take a look at the clui package deal which offers some enhancements for console-based functions, amongst them an animated spinner.

Making a spinner is straightforward:

const standing = new Spinner(‘Authenticating you, please wait…’);
standing.begin();

When you’re performed, merely cease it and it’ll disappear from the display screen:

standing.cease();

Tip: you can too set the caption dynamically utilizing the replace technique. This could possibly be helpful you probably have some indication of progress — for instance, displaying the proportion full.

Right here’s the code to authenticate with GitHub:

module.exports = {
getInstance: () => { … },
getStoredGithubToken: () => { … },

getPersonalAccesToken: async () => {
const credentials = await inquirer.askGithubCredentials();
const standing = new Spinner(‘Authenticating you, please wait…’);

standing.begin();

const auth = createBasicAuth({
username: credentials.username,
password: credentials.password,
async on2Fa() {
// TBD
},
token: {
scopes: [‘user’, ‘public_repo’, ‘repo’, ‘repo:status’],
be aware: ‘ginit, the command-line software for initalizing Git repos’
}
});

strive {
const res = await auth();

if(res.token) {
conf.set(‘github.token’, res.token);
return res.token;
} else {
throw new Error(“GitHub token was not found in the response”);
}
} lastly {
standing.cease();
}
},
};

Let’s step via this:

  1. We immediate the person for his or her credentials utilizing the askGithubCredentials technique we outlined earlier.
  2. We use the createBasicAuth technique to create an auth operate, which we’ll name within the subsequent step. We cross the person’s username and password to this technique, in addition to a token object with two properties:
    • be aware — a be aware to remind us what the OAuth token is for.
    • scopes — an inventory of scopes that this authorization is in. You’ll be able to learn extra about out there scopes in GitHub’s documentation.
  3. We then await the results of calling the auth operate inside a strive block.
  4. If authentication is profitable and a token is current within the response, we set it within the configstore for subsequent time and return the token.
  5. If the token is lacking, or authentication doesn’t succeed for no matter cause, the error will bubble on up the stack in order that we will catch it in index.js. We’ll implement this performance later.

Any entry tokens you create, whether or not manually or through the API as we’re doing right here, you’ll be capable to see them right here. Through the course of growth, you could discover you could delete ginit’s entry token — identifiable by the be aware parameter provided above — so as to re-generate it.

If you happen to’ve been following alongside and want to check out what we have now up to now, you possibly can replace index.js as follows:

const github = require(‘./lib/github’);

const run = async () => {
let token = github.getStoredGithubToken();
if(!token) {
token = await github.getPersonalAccesToken();
}
console.log(token);
};

The primary time you run it, you need to be prompted in your username and GitHub password. The app ought to then create a private entry token on GitHub and save the token to the configstore, earlier than logging it to the console. Each time you run the app after that, the app will pull the token straight from the configstore and log that to the display screen.

Coping with Two-factor Authentication

Hopefully you seen the on2Fa technique within the code above. This might be referred to as when a person has two-factor authentication enabled on their GitHub account. Let’s fill that out now:

// inquirer.js

const inquirer = require(‘inquirer’);

module.exports = {
askGithubCredentials: () => { … },

getTwoFactorAuthenticationCode: () => {
return inquirer.immediate({
title: ‘twoFactorAuthenticationCode’,
sort: ‘enter’,
message: ‘Enter your two-factor authentication code:’,
validate: operate(worth) {
if (worth.size) {
return true;
} else {
return ‘Please enter your two-factor authentication code.’;
}
}
});
},
};

We are able to name the getTwoFactorAuthenticationCode technique from throughout the on2Fa technique, like so:

// github.js

async on2Fa() {
standing.cease();
const res = await inquirer.getTwoFactorAuthenticationCode();
standing.begin();
return res.twoFactorAuthenticationCode;
},

And now our app can deal with GitHub accounts with two-factor authentication enabled.

Making a Repository

As soon as we’ve received an OAuth token, we will use it to create a distant repository with GitHub.

Once more, we will use Inquirer to ask a collection of questions. We want a reputation for the repo, we’ll ask for an elective description, and we additionally must know whether or not it needs to be public or non-public.

We’ll use minimist to seize defaults for the title and outline from elective command-line arguments. For instance:

ginit my-repo “just a test repository”

This can set the default title to my-repo and the outline to only a check repository.

The next line will place the arguments in an array listed by an underscore:

const argv = require(‘minimist’)(course of.argv.slice(2));
// { _: [ ‘my-repo’, ‘just a test repository’ ] }

Tip: this solely actually scratches the floor of the minimist package deal. You can too use it to interpret flags, switches and title/worth pairs. Take a look at the documentation for extra info.

We’ll write code to parse the command-line arguments and ask a collection of questions. First, replace lib/inquirer.js as follows:

const inquirer = require(‘inquirer’);
const recordsdata = require(‘./recordsdata’);

module.exports = {
askGithubCredentials: () => { … },
getTwoFactorAuthenticationCode: () => { … },

askRepoDetails: () => {
const argv = require(‘minimist’)(course of.argv.slice(2));

const questions = [
{
sort: ‘enter’,
title: ‘title’,
message: ‘Enter a reputation for the repository:’,
default: argv._[0] || recordsdata.getCurrentDirectoryBase(),
validate: operate( worth ) {
if (worth.size) {
return true;
} else {
return ‘Please enter a reputation for the repository.’;
}
}
},
null,
message: ‘Optionally enter an outline of the repository:’
,
{
sort: ‘checklist’,
title: ‘visibility’,
message: ‘Public or non-public:’,
decisions: [ ‘public’, ‘private’ ],
default: ‘public’
}
];
return inquirer.immediate(questions);
},
};

Subsequent, create the file lib/repo.js and add this code:

const CLI = require(‘clui’);
const fs = require(‘fs’);
const git = require(‘simple-git/promise’)();
const Spinner = CLI.Spinner;
const contact = require(“touch”);
const _ = require(‘lodash’);

const inquirer = require(‘./inquirer’);
const gh = require(‘./github’);

module.exports = {
createRemoteRepo: async () => {
const github = gh.getInstance();
const solutions = await inquirer.askRepoDetails();

const knowledge = {
title: solutions.title,
description: solutions.description,
non-public: (solutions.visibility === ‘non-public’)
};

const standing = new Spinner(‘Creating distant repository…’);
standing.begin();

strive {
const response = await github.repos.createForAuthenticatedUser(knowledge);
return response.knowledge.ssh_url;
} lastly {
standing.cease();
}
},
};

As soon as we have now that info, we will merely use the GitHub package deal to create a repo, which is able to give us a URL for the newly created repository. We are able to then set that up as a distant in our native Git repository. First, nonetheless, let’s interactively create a .gitignore file.

Making a .gitignore File

For the following step, we’ll create a easy command-line “wizard” to generate a .gitignore file. If the person is working our software in an present mission listing, let’s present them an inventory of recordsdata and directories already within the present working listing, and permit them to pick out which of them to disregard.

The Inquirer package deal offers a checkbox enter sort for simply that.

Inquirer’s checkboxes in action

The very first thing we have to do is scan the present listing, ignoring the .git folder and any present .gitignore file (we do that by making use of lodash’s with out technique):

const filelist = _.with out(fs.readdirSync(‘.’), ‘.git’, ‘.gitignore’);

If there’s nothing so as to add, there’s no level in persevering with, so let’s merely contact the present .gitignore file and bail out of the operate:

if (filelist.size) {

} else {
contact(‘.gitignore’);
}

Lastly, let’s make the most of Inquirer’s checkbox “widget” to checklist the recordsdata. Insert the next code in lib/inquirer.js:

askIgnoreFiles: (filelist) => {
const questions = [
{
sort: ‘checkbox’,
title: ‘ignore’,
message: ‘Choose the recordsdata and/or folders you want to ignore:’,
decisions: filelist,
default: [‘node_modules’, ‘bower_components’]
}
];
return inquirer.immediate(questions);
},

Discover that we will additionally present an inventory of defaults. On this case, we’re pre-selecting node_modules and bower_components, ought to they exist.

With the Inquirer code in place, we will now assemble the createGitignore() operate. Insert this code in lib/repo.js:

createGitignore: async () => {
const filelist = _.with out(fs.readdirSync(‘.’), ‘.git’, ‘.gitignore’);

if (filelist.size) {
const solutions = await inquirer.askIgnoreFiles(filelist);

if (solutions.ignore.size) {
fs.writeFileSync( ‘.gitignore’, solutions.ignore.be a part of( ‘n’ ) );
} else {
contact( ‘.gitignore’ );
}
} else {
contact(‘.gitignore’);
}
},

As soon as “submitted”, we then generate a .gitignore by becoming a member of up the chosen checklist of recordsdata, separated with a newline. Our operate now just about ensures we’ve received a .gitignore file, so we will proceed with initializing a Git repository.

Interacting with Git from throughout the App

There are a variety of the way to work together with Git, however maybe the best is to make use of the simple-git package deal. This offers a set of chainable strategies which, behind the scenes, run the Git executable.

These are the repetitive duties we’ll use it to automate:

  1. run git init
  2. add the .gitignore file
  3. add the remaining contents of the working listing
  4. carry out an preliminary commit
  5. add the newly created distant repository
  6. push the working listing as much as the distant.

Insert the next code in lib/repo.js:

setupRepo: async (url) => {
const standing = new Spinner(‘Initializing native repository and pushing to distant…’);
standing.begin();

strive {
git.init()
.then(git.add(‘.gitignore’))
.then(git.add(‘./*’))
.then(git.commit(‘Preliminary commit’))
.then(git.addRemote(‘origin’, url))
.then(git.push(‘origin’, ‘grasp’));
} lastly {
standing.cease();
}
},

Placing It All Collectively

First, let’s set a helper operate in lib/github.js for establishing an oauth authentication:

githubAuth: (token) => {
octokit = new Octokit({
auth: token
});
},

Subsequent, we create a operate in index.js for dealing with the logic of buying the token. Place this code earlier than the run() operate:

const getGithubToken = async () => {
// Fetch token from config retailer
let token = github.getStoredGithubToken();
if(token) {
return token;
}

// No token discovered, use credentials to entry GitHub account
token = await github.getPersonalAccesToken();

return token;
};

Lastly, we replace the run() operate by writing code that can deal with the principle logic of the app:

const repo = require(‘./lib/repo’);

const run = async () => {
strive {
// Retrieve & Set Authentication Token
const token = await getGithubToken();
github.githubAuth(token);

// Create distant repository
const url = await repo.createRemoteRepo();

// Create .gitignore file
await repo.createGitignore();

// Arrange native repository and push to distant
await repo.setupRepo(url);

console.log(chalk.inexperienced(‘All performed!’));
} catch(err) {
if (err) {
swap (err.standing) {
case 401:
console.log(chalk.purple(‘Couldn’t log you in. Please present appropriate credentials/token.’));
break;
case 422:
console.log(chalk.purple(‘There’s already a distant repository or token with the identical title’));
break;
default:
console.log(chalk.purple(err));
}
}
}
};

As you possibly can see, we make sure the person is authenticated earlier than calling all of our different capabilities (createRemoteRepo(), createGitignore(), setupRepo()) sequentially. The code additionally handles any errors and gives the person acceptable suggestions.

You’ll be able to try the finished index.js file on our GitHub repo.

At this level it is best to have a working app. Give it a try to fulfill your self that it really works as anticipated.

Making the ginit Command Accessible Globally

The one remaining factor to do is to make our command out there globally. To do that, we’ll want so as to add a shebang line to the highest of index.js:

#!/usr/bin/env node

Subsequent, we have to add a bin property to our package deal.json file. This maps the command title (ginit) to the title of the file to be executed (relative to package deal.json):

“bin”: {
“ginit”: “./index.js”
}

After that, set up the module globally and also you’ll have a working shell command:

npm set up -g

Tip: this may even work on Home windows, as npm will helpfully set up a cmd wrapper alongside your script.

If you wish to verify the set up labored, you possibly can checklist your globally put in Node modules utilizing this:

npm ls -g –depth=0

Taking it Additional

We’ve received a reasonably nifty, albeit easy command-line app for initializing Git repositories. However there’s lots extra you can do to boost it additional.

If you happen to’re a Bitbucket person, you can adapt this system to make use of the Bitbucket API to create a repository. There’s a Node.js API wrapper out there that can assist you get began. You might want to add an extra command-line choice or immediate to ask the person whether or not they wish to use GitHub or Bitbucket (Inquirer can be excellent for simply that) or merely exchange the GitHub-specific code with a Bitbucket various.

You may additionally present the ability to specify your individual set of defaults for the .gitgnore file, as an alternative of a hardcoded checklist. The preferences package deal is likely to be appropriate right here, or you can present a set of “templates” — maybe prompting the person for the kind of mission. You may also wish to have a look at integrating it with the .gitignore.io command-line software/API.

Past all that, you might also wish to add further validation, present the flexibility to skip sure sections, and extra.

Related Posts