Automate GitHub: Build a CLI App with Node.js #2
Let's build a CLI app with Node.js to automate GitHub workflow in this step-by-step tutorial! (Part 2 Final)
Welcome back to the 2nd and final part of the tutorial on how to build a simple CLI tool that can automate the process of pushing a local git repository to your GitHub.
If you haven't read the first part, please do so here so you can follow along.
Step 7: New_repo.js (newRepo)
Let's continue where we left off. After we authenticate the user with the authenticate
function in creds.js
, we can now create a remote repo using octokit
's repos.createForAuthenticatedUser(data)
.
We can supply information about the repo such as its name, description and visibility in data
as an argument.
For more information on the parameters, please see the documentation here.
The repos.createForAuthenticatedUser(data)
is a request that on success, will return data about the repo that was created. All we need from that response data is the clone_url
.
This url is in the format:
https://github.com/[username]/[repo-name].git
We need this url when we push files from our local repo to the remote repo with:
git.push(url, 'master');
So let's create the newRepo
function in new_repo.js
where we will do the following:
- Ask the user for repo information (i.e. name, description and visibility)
- Pass this information as
data
inrepos.createForAuthenticatedUser(data)
- Return the
clone_url
of the response.data object
As usual, we create a questions
array with objects representing each question. Then use inquirer.prompt(questions)
to ask the user in the terminal.
So first, import inquirer
at the top of new_repo.js
. We also need to import a built-in Node.js module called path
to get the base name of our current working directory.
For example, my current working directory is: C:\Users\victo\Desktop\Projects\cli-app. We can get this value using process.cwd()
.
To get the base name, we can use path.basename(process.cwd())
. Thus, the base name is cli-app, which will be the default name of our repo (unless the user specifies something else).
Feel free to read more about the Node.js path module in their documentation.
const path = require('path');
const inquirer = require('inquirer');
Then we write our newRepo
function below:
async function newRepo(octokit){
//create questions array, ask for name, description and visibility
const questions = [
{
name: 'name',
type: 'input',
message: 'Enter new repo name.',
default: path.basename(process.cwd()), //set default to basename
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter a valid input.';
}
}
},
{
name: 'description',
type: 'input',
message: 'Enter new repo description (optional).',
default: null
},
{
name: 'visibility',
type: 'input',
message: 'Set repo to public or private?',
choices: ['public', 'private'],
default: 'private'
}
];
//prompt the questions
const answers = await inquirer.prompt(questions);
//create the data argument object from user's answers
const data = {
name: answers.name,
description: answers.description,
private: (answers.visibility === 'private')
};
try {
//create the remote repo and return the clone_url
const response = await octokit.repos.createForAuthenticatedUser(data)
return response.data.clone_url;
} catch (error) {
console.log(error)
}
}
//final step, export function to use in index.js
module.exports = {newRepo}
Our function is complete. Let's call it in index.js
, after the authenticate
function. Import it first at the top of index.js
.
const repo = require('./new_repo');
Then add these lines below the auth.authenticate()
line, inside the init
command.
app.command('init')
.description('Run CLI tool')
.action(async() => {
//welcome message - see part1
//prompt question - see part1
if(answer.proceed == "Yes"){
console.log(chalk.gray("Authenticating...")) //from part1
const octokit = await auth.authenticate(); //from part1
//add these 2 lines below
console.log(chalk.gray("Initializing new remote repo..."));
const url = await repo.newRepo(octokit);
}else{
console.log(chalk.gray("Okay, bye."))
}
})
Let's test if it works! Run npm start
on the terminal.
As shown in the clip above, our remote repo has been successfully created with the parameters the user provided in the 3 questions.
Notice it is now just an empty repo. The next step is to create a .gitignore
file, ask the user which files they want to ignore, then commit and push the rest to the remote repo.
Step 8: New_repo.js (ignoreFiles)
In our new_repo.js
file, let's create a function called ignoreFiles
. This function will do 2 things:
- List all the files in the project directory
- Prompt the user to select the file(s) they want to add to
.gitignore
from the list. By default,node_modules
would be included.
If the user selects none, an empty .gitignore
file will be created. Else, we use Node.js built-in module fs
(File System) to write the selected ignored files to .gitignore
using fs.writeFileSync()
.
Feel free to check out Node.js File System module documentation here.
In order to create a list of all the files in our project directory, including nested ones, we use glob
, a dependency we installed earlier in Part 1 of this tutorial. glob
allows us to iterate through our directory and return all the files as an array of file names.
We will then pass this array of file names in the prompt, so the user can select the ones they want to ignore from the terminal.
So let's first import the dependencies we need: fs
and glob
at the top of new_repo.js
.
const fs = require("fs");
const glob = require("glob");
Then we write the ignoreFiles
function below:
async function ignoreFiles(){
//get array of files in the project, ignore node_modules folder
const files = glob.sync("**/*",{"ignore":'**/node_modules/**'});
//add any node_modules to gitignore by default
const filesToIgnore = glob.sync('{*/node_modules/,node_modules/}');
if(filesToIgnore.length){
fs.writeFileSync('.gitignore', filesToIgnore.join('\n')+'\n');
}else {
//if no files are chosen to be ignored, create an empty .gitignore
fs.closeSync(fs.openSync('.gitignore', 'w'));
}
//create question and pass files as the choices
const question = [
{
name: 'ignore',
type: 'checkbox',
message: 'Select the file and/or folders you wish to ignore:',
choices: files
}
];
//prompt the question
const answers = await inquirer.prompt(question);
//if user selects some files/folders, write them into .gitignore
if (answers.ignore.length) {
fs.writeFileSync('.gitignore', answers.ignore.join('\n'));
}
}
//export ignoreFiles to use in index.js
module.exports = {newRepo, ignoreFiles}
Now we can call ignoreFiles
in index.js
after calling newRepo
.
//Some code above
if(answer.proceed == "Yes"){
console.log(chalk.gray("Authenticating...")) //from part1
const octokit = await auth.authenticate(); //from part1
console.log(chalk.gray("Initializing new remote repo...")); //step7
const url = await repo.newRepo(octokit); //step7
//add these 2 lines below
console.log(chalk.gray("Remote repo created. Choose files to ignore."));
await repo.ignoreFiles();
}
//Some code below
Here's what our app should be able to do at this point:
- Display a welcome message
- Ask user if they want to continue with this tool
- If yes, ask for token. If token already saved, skip to next step.
- Ask repo name, description and visibility. Then create it on GitHub.
- Ask which files to ignore. Then create
.gitignore
file with the selected files.
Let's finish up this app. All there's left to do is to commit and push the files that are not in .gitignore
from local to the remote repo.
Step 9: New_repo.js (initialCommit)
The initialCommit
function uses the simple-git
dependency we installed in Part 1 to easily make git commands in JavaScript. So first, import it at the top of new_repo.js
.
const git = require('simple-git')();
The function will take the clone_url
from Step 7 as an argument to do the following:
git init
git add .gitignore
git add ./*
git commit -m "Initial commit"
git remote add origin <clone_url>
git push <clone_url> master
If there's no error, the function returns true. Let's see its code below:
async function initialCommit(url) {
try {
await git
.init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', url)
.push(url, 'master', ['--set-upstream']);
return true;
} catch (error) {
console.log(error)
}
}
//don't forget to export it
module.exports = {newRepo, ignoreFiles, initialCommit}
Finally, let's add the last lines of code in our index.js
to complete the app.
//Some code above
if(answer.proceed == "Yes"){
console.log(chalk.gray("Authenticating...")) //from part1
const octokit = await auth.authenticate(); //from part1
console.log(chalk.gray("Initializing new remote repo...")); //step7
const url = await repo.newRepo(octokit); //step7
console.log(chalk.gray("Remote repo created. Choose files to ignore.")); //step8
await repo.ignoreFiles(); //step8
//add these lines
console.log(chalk.gray("Committing files to GitHub at: " + chalk.yellow(url)));
const commit = await repo.initialCommit(url);
//if initialCommit returns true, show success message
if(commit){
console.log(chalk.green("Your project has been successfully
committed to Github!"));
}
}
//Some code below
Final Product
Congratulations! We have completed the app. There's a couple of optional polishing actions such as:
- Create another command to allow user to delete their stored token
- Publish the tool to npm for reusability
Since this tutorial has been quite long, I'll just quickly show you the 2nd optional action on how to publish the tool to npm.
Optional Step: Publishing
In package.json, add the bin
property and remove the start
script under scripts
.
"bin": "index.js"
Then add this line below at the top on index.js
. This tells the terminal to use Node.js when executing the file.
#! /usr/bin/env node
Finally, to test locally, run npm link
to install the cli-app
. Then run:
cli-app init
The app should start and work the same as npm start
. After you tested the app and it works correctly, run npm unlink
then login to your npm account with:
npm login
Enter your credentials and then run:
npm publish
Note: I ended up renaming the project to
repo-create
because the namecli-app
was taken on npm.
Installation
Now that it's published on npm, we can install the tool with the command:
npm install --global @victoria-lo/repo-create
To use the tool, simply run the repo-create init
command on any project you want to create and commit a remote repo for.
Conclusion
We've reached the end of this 2-part tutorial series on how to build a simple CLI tool with Node.js. Thank you for reading! I hope this has been as enjoyable to read for you as much as I've enjoyed writing this.
Please refer to the links below for the tool on npm and the GitHub repo for the code.
Please like and share this article, and ask any questions you may have in the comments below. Cheers!