Automate GitHub: Build a CLI App with Node.js #1

Automate GitHub: Build a CLI App with Node.js #1

Let's build a CLI app with Node.js to automate GitHub workflow in this step-by-step tutorial! (Part 1)

At the end of this 2-part tutorial, you will have a simple CLI tool that can automate the process of pushing a local git repository to your GitHub.

As you all know, creating a new remote repository on GitHub requires the following steps:

  1. Login to your Github account
  2. Click on New Repository and enter the details
  3. git init in your local repo
  4. Optional: Create .gitignore file and add the files to ignore
  5. git add <file>
  6. git commit -m "Initial commit"
  7. git remote add origin <url>
  8. git push -u origin master

Instead of having to go through these 7-8 steps every time you want to push a new project on GitHub, let's build a tool to automate this process.

Prerequisites/ Tools Needed

  • Beginner experience with Node.js and JavaScript
  • Node and npm installed
  • For Windows: Git, Git Bash installed
  • Code Editor software installed

Step 1: Set up a Node.js project

Initialize and create package.json file by running:

mkdir cli-app
cd cli-app
npm init

Then run the code below to install the dependencies we will need for this project:

npm install figlet chalk clear @octokit/rest commander inquirer configstore simple-git glob

Brief explanation of the dependencies:

  • figlet: allow writing big text on terminal
  • chalk: allow styling terminal words with colours
  • clear: to clear terminal screen via JavaScript
  • @octokit/rest: GitHub REST API client for JavaScript
  • commander: allows execution of commands to terminal
  • inquirer: more flexible option than readline module, enables terminal to ask questions and accept answers
  • configstore: store token securely for reuse
  • simple-git: simplify running git commands in Node.js
  • glob: easily retrieve all files in a directory as an array

Step 2: Create files

Build our app directory by creating these files in the root directory:

  1. index.js: Where app initializes and listens for input in the terminal
  2. creds.js: GitHub authentication functions
  3. new_repo.js: To create a new repo functions

At this point, your app directory and package.json should look like: image.png

Step 3: Index.js

This is where our app will start. First, let's import the dependencies we need here:

const app = require('commander');
const chalk = require('chalk');
const clear = require('clear');
const figlet = require('figlet');
const inquirer = require('inquirer');

Now let's create an init command, which means our app will start when the user types node index.js init on the terminal. This is where our dependency commander comes in.

We can use the command() method to create the init command. Then add a description to our command with description() and finally, the action() method takes care of what this command will do (i.e. show a welcome message).

In code:

app.command('init')
  .description('Run CLI tool')
  .action(async() => {
      //show welcome message
     console.log("Welcome to the GitHub initializer tool");
})

app.parse(process.argv); //get the arg (i.e. init)

//show help if no arg is passed
if (!app.args.length) {
    app.help(); 
}

Let's run the node index.js init and see the result.

init.gif

Great! That's worked perfectly. But isn't the UI quite boring? Let's use figlet and chalk to beautify it. Here's what I have inside actions() now.

clear(); //clears the terminal

//display app title
console.log(chalk.magentaBright(
figlet.textSync('CLI App Tutorial', { horizontalLayout: 'full' })));

//show welcome message
console.log("Welcome to the GitHub initializer tool.\nThis tool is built for a tutorial at "
    + chalk.yellow("https://lo-victoria.com")+ ". Do check out her blog! ^^");

Let's run node index.js init again. It looks way better now, isn't it?

init-2.gif

There's another thing that we can improve on. We can add a start script in package.json so that we don't have to type node index.js init on the terminal every time we want to start our app. We can type npm start instead.

In package.json under scripts:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js init" //add this line
}

Back to index.js, after showing our welcome message to the user, we should prompt if they want to proceed to create a remote repo.

To do that, we must first create a question array with the an object like this:

const question = [{
    name: 'proceed',
    type: 'input',
    message: 'Proceed to push this project to a Github remote repo?',
    choices: ['Yes', 'No'],
    default: 'Yes'
}];

As you can see above, the object has self-explanatory properties such as name, type and message. Since this is a yes/no question, we also add a choices property and default the answer as 'Yes'.

Now we shall use our inquirer dependency to prompt the question like so:

const answer = await inquirer.prompt(question);

if(answer.proceed == "Yes"){
   //proceed with Github authentication, creating the repo, etc.
    console.log(chalk.gray("Authenticating..."))
}else{
    //show exit message
    console.log(chalk.gray("Okay, bye."))
}

And this is what our app look like so far.

init-3.gif

If the user's answer is 'Yes', the next step is to allow the user to authenticate their GitHub from this tool. Let's write the authentication logic in creds.js.

Step 4: Generate a Token

In creds.js, we will prompt the user to enter their personal access token to authenticate their GitHub.

To learn more about different authentication strategies, please read their documentation.

To generate a token, login to your GitHub account. Go to Settings > Developer Settings > Personal Access Token. Click on 'Generate new token'.

generate new token.PNG

Then enter the description of the token and check the 'repo' checkbox under Scopes as shown below.

new token.PNG

Scroll down and click 'Generate token'. You will be redirected to this page below.

generated.PNG

Make sure you copy and save this token somewhere safe because once you leave the page, you will not be able to retrieve your token again. If you forgot to save it somewhere and lose it, you'll have to delete and re-generate a new token.

Step 5: Creds.js

Now that we have our token, let's head to creds.js to prompt and authenticate a user in the terminal. We shall use inquirer and octokit, which we installed earlier. Let's import it at the top in creds.js.

const inquirer = require('inquirer');
const { Octokit } = require("@octokit/rest");

Now let's create a function called authenticate to prompt the user's token and authenticate them.

async function authenticate(){
  //1. create question
   const question = [{
       name: 'token',
       type: 'input',
       message: 'Enter your Github personal access token.',
       validate: function(value) {
          if (value.length == 40) {
              return true;
           } else return 'Please enter a valid token.';
        }
   }];
   //2. prompt question
   const answer = await inquirer.prompt(question);

   //3. try authenticating with user's answer
   try{
      const octokit = new Octokit({
        auth: answer.token,
      }); 
      return octokit;
   //4. print error if any
   }catch (error){
      console.log(error);
   }
}

//5. export for use in index.js
module.exports = {authenticate}

Let's test our function. First, import it into index.js so we can run it there.

//Add this line at the top of index.js
const auth = require('./creds');

Then, call the authenticate() function in the init command.

app.command('init')
    .description('Run CLI tool')
    .action(async() => {
        //welcome message - see step3
        //prompt question - see step3

        if(answer.proceed == "Yes"){
            console.log(chalk.gray("Authenticating..."))
            const octokit = await auth.authenticate(); //add this line
        }else{
            console.log(chalk.gray("Okay, bye."))
        }
    })

Let's run npm start to test this quickly. Paste the token you generated in Step 4 when prompted. You can test with random values to see if the inquirer validation works.

auth.gif

Great, we have created a simple authentication prompt for our app. However, having to copy the token and paste it every time the app prompts for it can be tedious. Also, what if we lose or misplace our token after some time?

Let's use the configstore dependency to persist our token and enable our app to not prompt for a token if it is stored in config.

Step 6: Configstore

First, import and initialize it at the top of creds.js.

const Configstore = require('configstore');
const packageJson = require('./package.json');

// Create a Configstore instance
const config = new Configstore(packageJson.name);

Now let's revise our authenticate function to check if we have stored a token in our config.

If it exists, we simply authenticate with that token. If not, we will prompt the user for the token then authenticate with the user's answer and store it in config for next time.

Here's our revised authenticate function.

async function authenticate(){
   //1. try getting a token
   let token = config.get('github_token'); 
   //2. if it exists, authenticate with it
   if(token){
     console.log("Token is found in config. Skipping prompt.")
      try{
         const octokit = new Octokit({
           auth: token,
          }); 
          return octokit;
      }catch (error){
        throw error;
      }
   }else{
  //3. if no token is stored, prompt user for one
   const question = [{
       name: 'token',
       type: 'input',
       message: 'Enter your Github personal access token.',
       validate: function(value) {
          if (value.length == 40) {
              return true;
           } else return 'Please enter a valid token.';
        }
   }];
   const answer = await inquirer.prompt(question);

   //4. authenticate with user's answer
   try{
      const octokit = new Octokit({
        auth: answer.token,
      }); 
      //5. store the token for next time
      config.set('github_token', answer.token);
      return octokit;
   }catch (error){
      console.log(error);
   }
}

As shown in the clip below, upon the 2nd time we run the app, it won't ask for the token anymore because it is now stored in config. tokenized.gif

Conclusion

Today we have learnt how to built a simple CLI tool that:

  • displays a fancy welcome message
  • ask the user questions
  • gets the user's answers
  • authenticates a user via their GitHub personal access token
  • stores the token for future use

In the next part of the article, we shall complete this app by adding the following capabilities:

  • create a new remote repo for the authenticated user
  • create a .gitignore file
  • allows user to select files to ignore
  • initializes and commits the rest of the files
  • pushes the local repo to GitHub
  • displays a success message

Thanks for reading this long tutorial. I hope you find it both interesting and helpful. Do like and share the article around, and feel free to ask any questions in the comments below. Please refer to the Resources section to learn more about the different dependencies we have used for the app. Stay tuned for the next part! Cheers!


Resources

Did you find this article valuable?

Support Victoria Lo by becoming a sponsor. Any amount is appreciated!

ย