✨  Nathalie Maes Blog

⚡ Build a command-line app from scratch

April 30, 2024
9 min read
Table of Contents

I created this little app a few years ago as I had the goal to just sit in a coffee shop for a day and make something I could finish in one day. I had always wanted to create a commandline application, which does not require any design or much complexity, so I figured that was ideal.

I always want to make applications that are handy for myself as well, so I decided on a calculator that decides your One Rep Max based on the weight you lifted and the amount of times you lifted it. It then uses a formula to calculate your max and then outputs that result.

Finally, we will make it available to download through NPM as well!

Prerequisites

For this tutorial, you will need:

  • npm (instructions to install it can be found here or through a package manager like Homebrew
  • A code editor (I recommend Visual Studio Code, but anything you have will do)

Setup

First, make a new folder and initialize a Node.js project:

Terminal window
mkdir one-rep-max-calculator
cd one-rep-max-calculator
npm init

The prompt will ask you a series of questions to create your package.json file. You can just press Enter to select the default answer for each one.

On the same level, create a new folder called /bin, and inside that create a file index.js, so your folder structure looks like this:

bin
index.js
package.json

At the top of this index.js file, add this line to make it executable:

#!/usr/bin/env node

This tells your system that this script should be run with Node.js.

Let’s get started

Now time to finally write some code!

Before asking anything, we want to show the title of our application as an introduction to the user. Common libraries to show any kind of colors or decoration are chalk (for colors and styles) and boxen (for, well, boxes and borders).

We can install those via:

Terminal window
npm i chalk boxen

And import them in our index.js file:

const chalk = require('chalk');
const boxen = require('boxen');

Now that we have access to those, let’s create our title.

In Node, if we want to show any kind of text to the user, we do that by logging it via console.log. In boxen, we use a function that accepts the title we want it to show as the first parameter, and the styles we want it to have as an object in the second parameter, like so.

console.log(boxen('ONE REP MAX CALCULATOR', {
borderColor: '#E0115F',
borderStyle: 'classic',
padding: 2,
margin: 1,
color: 'white'
}));

This makes a red bordered box saying “ONE REP MAX CALCULATOR”!

Additionally, I figured it was fun to show some additional ASCII art, but it might definitely be overkill for some. We do this with chalk to give it a nice green color, but you can leave the chalk part out of it.

console.log(chalk.hex('#50C878')(`
(your ASCII art here)
`));

You can already test this out in your terminal by running node ./bin/index.js to see it in action!

Show an explanation to the user

I wanted to show a bit more practical information to the user. This is just a line of text separated by two long lines for aesthetic reasons.

console.log(chalk.hex('#50C878')(`
---------------------------------------------------------------------------------
`));
console.log(`Calculate the ${chalk.hex('#E0115F').bold('max weight you can lift')} for a single repetition of an exercise.`);
console.log(chalk.hex('#50C878')(`
---------------------------------------------------------------------------------
`));

Ask the user for input

Now with the introductions out of the way, let’s go ahead and do the actual interactive part where we ask for input (in this case the weight and the reps) from the user. Technically we could use parameters for this, so that users would have to enter 1rm --weight 100 --reps 5 to do the calculation, which is a good option to have for tools that are automated, but not very user friendly. Instead, we will prompt the user to fill in those numbers. A useful tool for this is a package called ~yargs-interactive. so let’s go ahead and install and import that too.

Terminal window
npm i yargs-interactive

index.js:

const yargsInteractive = require('yargs-interactive');

Let’s go through the options we pass it step by step.

The usage line will show up when the user uses the --help command.

.usage('$0 <command>')
.interactive({
interactive: { default: true },
weight: { type: 'input', describe: 'Enter the weight of the lift (in kg)' },
reps: { type: 'input', describe: 'Enter the amount of reps' },
});
})
yargsInteractive()
.usage('$0 <command>')
.interactive({
interactive: { default: true },
weight: { type: 'input', describe: 'Enter the weight of the lift (in kg)' },
reps: { type: 'input', describe: 'Enter the amount of reps' },
})
.then(result => {
console.log(result);
});

A few improvements

Notice how we reused a few colors throughout the application? We can go ahead and assign them to a variable so we can easily reuse them and change them more easily if we want.

const theme = {
green: '#50C878',
red: '#E0115F',
};

Now you can update every '#50C878' appearance with theme.green and '#E0115F' with theme.red.

Additionally, the object inside interactive looks a bit messy. Let’s go ahead and assign that to a separate variable as well.

const options = {
interactive: { default: true },
weight: { type: 'input', describe: 'Enter the weight of the lift (in kg)' },
reps: { type: 'input', describe: 'Enter the amount of reps' },
};

So now you can update your code like this:

yargsInteractive()
.usage('$0 <command>')
.interactive(options)
.then(result => {
console.log(result);
});

If you test this out by running node ./bin/index.js, you will get back an object showing your answered values.

Handle the user input

Now let’s do something with the user data we have. Inside the .then(result => { ... }), we will calculate the One-Rep Max with the Epley formula (which is the easiest one I’ve found).

let { weight, reps } = result;
const max = (weight / (1.0278 - 0.0278 * reps)).toFixed(2);
console.log(`Your one rep max is ${max}`);

We can even make it prettier and use our beloved boxen and chalk packages for this!

const styledMax = chalk.hex(theme.green).bold(`${max} kg`);
console.log(boxen(`Your one rep max is estimated to be ${styledMax}`, {
padding: 1,
borderStyle: 'double',
borderColor: theme.red
}));

Sanitize the user input

Of course, users can enter anything as an input at the moment. They can enter 100, but they can also enter abc or 100.00,00, which are not all values the application will be able to handle. You can test this out by entering random inputs and see what happens.

We can clean up the inputs by replacing any commas with dots, and removing anything that isn’t a number from both the weight and reps.

weight = weight.replace(/,/g, '.').replace(/[^0-9\.]+/g, '');
reps = reps.replace(/,/g, '.');

The full yargsInteractive code:

yargsInteractive()
.usage('$0 <command> test')
.interactive(options)
.then((result) => {
const { weight, reps } = result;
weight = weight.replace(/,/g, '.').replace(/[^0-9\.]+/g, '');
reps = reps.replace(/,/g, '.');
const max = (weight / (1.0278 - 0.0278 * reps)).toFixed(2);
const styledMax = chalk.hex(theme.green).bold(`${max} kg`);
console.log(boxen(`Your one rep max is estimated to be ${styledMax}`, {
padding: 1,
borderStyle: 'double',
borderColor: theme.red
}));
});

Final code

#!/usr/bin/env node
const yargsInteractive = require('yargs-interactive');
const chalk = require('chalk');
const boxen = require('boxen');
const options = {
interactive: { default: true },
weight: { type: 'input', describe: 'Enter the weight of the lift (in kg)' },
reps: { type: 'input', describe: 'Enter the amount of reps' },
};
const theme = {
green: '#50C878',
red: '#E0115F',
};
console.log(boxen('ONE REP MAX CALCULATOR', { borderColor: theme.red, borderStyle: 'classic', padding: 2, margin: 1, color: 'white' }));
console.log(chalk.hex(theme.green)(
`⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣶⣿⣿⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⠿⡏⢿⡟⠿⠁⢸⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣁⣀⡀⢠⣶⠶⠀⠁⣽⣿⣆⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠁⠀⠀⠀⠀⠀⠘⣿⣿⣷⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣆⠀⠀⠀⠀
⠀⠀⢀⣠⣴⣶⣶⣶⣦⣄⠀⠀⠀⠀⢀⣀⣀⣀⠀⠀⠀⣼⣿⣿⣿⣿⣧⠀⠀⠀
⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣐⢾⣿⣿⣿⣿⣷⣦⣌⡻⣿⣿⣿⣿⣿⣧⠀⠀
⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⡇⠀
⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢋⡉⠛⠿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⡇⠀
⠀⣿⣿⣿⣿⣿⣿⣿⣿⡏⢠⣿⣿⣷⡦⠀⣈⣉⣀⣤⣶⣿⣟⣛⠛⠛⠛⠛⠃⠀
⠀⣿⣿⣿⣿⣿⣿⣿⡿⠀⠾⠛⠋⠁⠐⠺⠿⠿⠿⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀
⠀⣿⣿⣿⣿⣿⣿⠟⢁⣴⣶⣾⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣿⣿⣿⣿⠿⠋⣰⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠛⠛⠋⠁⠐⠛⠛⠛⠛⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀`));
console.log(chalk.hex(theme.green)(`
---------------------------------------------------------------------------------
`));
console.log(`Calculate the ${chalk.hex(theme.red).bold('max weight you can lift')} for a single repetition of an exercise.`);
console.log(chalk.hex(theme.green)(`
---------------------------------------------------------------------------------
`));
yargsInteractive()
.usage('$0 <command>')
.interactive(options)
.then(result => {
let { weight, reps } = result;
weight = weight.replace(/,/g, '.').replace(/[^0-9\.]+/g, '');
reps = reps.replace(/,/g, '.');
const max = (weight / (1.0278 - 0.0278 * reps)).toFixed(2);
const styledMax = chalk.hex(theme.green).bold(`${max} kg`);
console.log(boxen(`Your one rep max is estimated to be ${styledMax}`, { padding: 1, borderStyle: 'double', borderColor: theme.red }));
});

Making it installable

It’s fun having your own little tool on your computer, but what if you wanted others to try it out as well? You could upload it to a public repository and have people install it from there, but an easier way would be to upload it as a public NPM package.

For this, open your package.json file and add a bin section. This will decide the command that the user will have to use to start your application, in this case 1rm, but it could be anything you decide.

"bin": {
"1rm": "./index.js"
},

Publishing it on npm

If you want other people to install it too: First, create an account on npmjs.com, then log in with:

Terminal window
npm login

This will open a browser window where you can enter your username and password.

Now to publish your tool:

Terminal window
npm publish

Then others can install it globally via:

Terminal window
npm install -g one-rep-max-calculator

Small note: If you were to update your application afterwards and republish it, don’t forget to update your version number in your package.json.

Final thoughts

And there you have it, your very own commandline application published on NPM!

Some fun improvements you could do to expand on this application could be:

  • Typescript optimized
  • Ask the user whether they want to enter the weight in pounds or kilos
  • Let users choose between the different formulas outside of Epley
  • Clean up the code - separate into logical files and methods
  • Add tests

Or better yet, check out the full code on GitHub if you would like to fork or contribute! ✨