# How to Build a Command-Line Application with Typescript and AdonisJS Ace

This article shows you how to build a command-line application using `typescript` and `@adonisjs/ace`. The command-line application accepts user input and makes a request to the numbers API. The request returns an interesting fact about the number.
To begin you need Node.js and npm installed on your machine, npm comes together with Node.js. Basic knowledge of typescript is also advised although not necessary.
The code in use can be found in this  [repo](https://github.com/prince-curie/numb-cli).  

## Installation
1. We run `npm init -y`. It sets up a new Node.js project in the current folder with the default details without asking any questions.
2. We install typescript globally using `npm install -g typescript`.
Next, we set up a new typescript project by running the command `tsc --init`.
The `tsc --init` command creates a `tsconfig.json` file.  
Open the generated `tsconfig.json` file and update the following details
```json
    "target": "es2015",
    "outDir": "./dist",     
    "rootDir": "./src",     
```
The key `rootDir` describes the root directory that typescript will compile from. The key `outDir` describes the directory to save the compiled file. The key `target` describes the lowest level to compile the code to.
3. We install AdonisJs Ace which is a tool for creating command-line applications in Node.js
```bash
npm i --save @adonisjs/ace
```
4. We install type definition for Node.js, that allows you to use global values like process, require etc. in typescript.
```bash
npm i --save @types/node
```

## Creating the Shebang
According to  [Wikipedia] [1]
[1]: https://en.wikipedia.org/wiki/Shebang_(Unix)


> In computing, a shebang is the character sequence consisting of the characters number sign and exclamation mark at the beginning of a script. It is also called sha-bang, hashbang, pound-bang, or hash-pling.

The Shebang tells the operating system what interpreter to use in running the script.
To create the shebang:
1. We create the `src` folder, in the project root folder.
2. We create a folder called `bin` in the `src` folder.
3. We create a file we will call `numb-cli.ts`.
4. We paste the shebang by pasting the line below.

```javascript 
#!/usr/bin/env node

console.log('Welcome')
```
 
5. We update the package.json file by adding the key-value pair.
```json
 "bin": {
    "numb-cli": "dist/bin/numb-cli.js"
  },
```
## Testing the Shebang
To test if the shebang is working,
1. We compile the typescript file into javascript by running the command `tsc` on the terminal.
2. We install the package globally by running the command `npm link`
3. We run the package by running the command `numb-cli`.
4. It should return the response Welcome.
N.b: To uninstall the package, use the command `npm unlink`.

![testing cli 1.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1605162162783/p39Xpk6LC.gif)

## Create index.ts file
After testing the shebang, we add the line `import "..";` in place of `console.log('Welcome')`. The new line will import the `index.ts` file, which we will create in the `src` folder. 
In the `package.json` file we update, the value for the key `"main"` with `"dist/index.js",`. 
So the `package.json` file becomes:
```json
{
  "name": "numb-cli",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "numb-cli": "dist/bin/numb-cli.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@adonisjs/ace": "^5.0.8",
    "@types/node": "^14.14.7"
  }
}
```

And the `src/bin/numb-cli.ts` becomes:
```typescript
#!/usr/bin/env node

import "..";
```
In the `index.ts` file, we import `ace`, and set it up to execute commands
```typescript
import ace from '@adonisjs/ace';

ace.wireUpWithCommander()
ace.invoke()
``` 
This should work when tested, but the `@adonisjs/ace` package has no type definition so it will not compile the typescript to javascript.
Let us create a declaration file for `@adonisjs/ace`.
### Creating declaration file
At the root of the `src` folder:
1. Create a folder called `@adonisjs`
2. Create a file called ace.d.ts
3.  Paste in the line `declare module '@adonisjs/ace';`
On testing, a response like the one below will appear.

![testing cli 2.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1605462031377/WHLOJGtHU.gif)

## Creating commands
To create commands, we extend the ace command class. The new class enables us to overrides the signature, description and the handle method. 
- Signature method: The signature method is a getter that returns the name of the command
- Description method: The description method is a getter that returns a description of the command.
- Handle method: This is a method that is called when we call the command. It is an asynchronous method. It accepts two (2) parameters which are not compulsory, 
- args
- options
We register the new command by passing it as a parameter to the `ace.addCommand` method, which we call in the `src/index.ts` file.
 
The `src/index.ts` file becomes 
```typescript
import ace from '@adonisjs/ace';
import Random from './commands/Random';

ace.addCommand(Random);

ace.wireUpWithCommander();
ace.invoke();
```
We will call the command subclass `Random` and keep it in a `Random.ts` file in the `commands` folder. The `Random.ts` file will contain:
```typescript 
import {Command} from '@adonisjs/ace';

class Random extends Command {
    static get signature ():string {
        return 'random';
    }

    static get description ():string {
        return "Returns a random fact about a number";
    }

    async handle () {
        console.log('Welcome');
    }
}

export default Random;
``` 
On testing the created command we get 
![testing cli 3.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1605472448043/sCVv9xFH_.gif)

## Asking questions
`@adonisjs/ace` provides methods to ensure that interaction with users occurs. To interact with users, we will use the `ask` and the `choice` method amongst others. The ask method accepts the question and a default answer which is optional as parameters. It allows a user to input a response to the question asked. The choice method accepts 3 parameters, a question, an array of options to choose from and an optional default option. Using the two methods mentioned above, we ask the users what number he/she wants a random fact about and what category the random fact will be about.
The code in the `Random.ts` file will look like this:
```typescript
import {Command} from '@adonisjs/ace';

class Random extends Command {
    static get signature ():string {
        return 'random';
    }

    static get description ():string {
        return "Returns a random fact about a number";
    }

    async handle () {
        const number:number = await this
            .ask('Input a number')
        
        const category:string = await this
            .choice('Select a category', [
                'trivia',
                'math',
                'date',
                'year'
            ]);
    }
}

export default Random;
```

Testing the CLI should return:

![testing cli 4.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1605652623773/kHpn_oMqt.gif)

## Making an HTTP request
There is a need to use the details gotten earlier from the user in other get the random fact. `@adonisjs/ace` does not have the capability to make an HTTP request. To make an HTTP request we install the `got` NPM package. 
```bash
npm i got
``` 
The command above installs `got` as a dependency.
Next, we import `got` into the `Random.ts` file.
```typescript
import got from 'got';
```
 In the handle method of the `Random` class, we make an HTTP `get` request to the URL that returns the fact.
 
```typescript
const url:string = `http://numbersapi.com/${number}/${category}`

const response = await got(url);
```

## Displaying response
At this point, the user needs to see the random fact. This is possible with either the methods provided by `@adonisjs/ace` Command class or with the use of methods of the Console class. The `@adonisjs/ace` methods each give the displayed text to a unique colour. With the console methods, the text becomes beautiful using the `this.chalk` method of the `Command` class. `this.chalk` accesses an instance of  [kleur](https://www.npmjs.com/package/kleur) offering flexibility to the user.
We add Icons to the displayed response by using the `this.icon`
method of the `Command` class and passing one of the acceptable parameters which are:
- success
- error
- info
- warn
Each of this parameter represents a particular colour and an icon. 
For the CLI we add the snippet below to display other response as well our icon
```typescript
this.success(`${response.body} -  ${this.icon('success')}`);
```

![testing cli 5.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1605663085874/2fHXdY2sw.gif)

## Handling exceptions
We have built the CLI this far:
- Creating a command;
- Asking questions;
- Accepting responses;
- Making an external HTTP request;
- Getting back results;
- Displaying the results to the user.

But what if there is an exception since we did not confirm user input and other errors that may occur from making an external network call.
To handle this `@adonisjs/ace` provides an event listener `onError`. The `onError` listens for exceptions, notes the command that throws it, and passes the exception and the command to a closure function. The closure function is where we display the error message from and stop the running process.
The onError listener is a method of the ace class and is placed in the `index.ts` file.
Since the use of icons and colourful text displays works as methods on the command class. We create an instance of the `Command` class so we can use its methods.
To throw an exception we check the user input ensuring it is a whole number. So in the handle method of the `Random` class, we add these checks:
```typescript
// check if number is of datatype number
if(isNaN(number)) {
    throw new Error(`${number} is not a number.`)
}

// ensures that number is not a float
if(number % 1 != 0) {
    throw new Error(`${number} should not be a decimal number`)
}
```

The `index.ts` file becomes:
```typescript
import ace, {Command} from '@adonisjs/ace';

interface ErrorMessage {
    message: string;
}

const command = new Command()

ace.onError(function (error:ErrorMessage, commandName:string) {
    command.error(`${commandName} reported ${error.message} - ${command.icon('error')}`)
    
    process.exit(1)
  })
```
The `interface ErrorMessage` used above ensures the datatype of keys within the error object. So it becomes the datatype for the object.

![testing cli 6.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1605769708099/W3A_1EUkn.gif)

## Conclusion
This article explains how to build a Nodejs CLI using typescript and @adonisjs/ace. Always remember to compile to javascript before running and to always unlink before linking back when testing a new update. 
Finally, the code in the index.ts file will be:
```typescript
import ace, {Command} from '@adonisjs/ace';
import Random from './commands/Random';

ace.addCommand(Random);

interface ErrorMessage {
    message: string;
}

const command = new Command()

ace.onError(function (error:ErrorMessage, commandName:string) {
    command.error(`${commandName} reported ${error.message} - ${command.icon('error')}`)
    
    process.exit(1)
  })

ace.wireUpWithCommander();
ace.invoke();
```
and the class in the Random.ts file will be:
```typescript
import { Command } from '@adonisjs/ace';
import got from 'got';

class Random extends Command {
    static get signature ():string {
        return 'random';
    }

    static get description ():string {
        return "Returns a random fact about a number";
    }

    async handle () {
        const number:number = await this
            .ask('Input a number')
        if(isNaN(number)) {
            throw new Error(`${number} is not a number.`)
        }

        if(number % 1 != 0) {
            throw new Error(`${number} should not be a decimal number`)
        }
        
        const category:string = await this
            .choice('Select a category', [
                'trivia',
                'math',
                'date',
                'year'
            ]);
        
        const url:string = `http://numbersapi.com/${number}/${category}`

        const response = await got(url);

        this.success(`${response.body} - ${this.icon('success')}`);
        process.exit(0)
    }
}

export default Random;
```
To learn more about @adonisjs/ace you can check the official documentation  [here](https://adonisjs.com/docs/4.1/ace#_questions).
@adonisjs/ace also has the ability to work with the filesystem and the database. I hope this helps as a starting point in showing how to use the `@adonisjs/ace` and also how to use it with typescript.
If you have come this far thanks for reading. Kindly drop your comments in the comment section. 
