# Building a CLI in Node for DevPail

> This is the second in a [series of articles](https://toddesposito.com/series/simplify-your-tooling) about building **DevPail**, my general-purpose development environment tooling tool. You don't have to read the previous article to learn something here, but the context may help you understand what I'm talking about.

## The story so far...

In the [first article](https://toddesposito.com/devpail-1-introduction), I introduced my solution to tooling churn, which is to keep all the tooling for each of my projects into an isolated, transportable environment I'm calling a **DevPail**. It's a [Docker](https://docker.com) *container* where the tooling runs, coupled with a *volume* which holds all the tooling and development artifacts. However, the commands needed to use **DevPail** are fairly involved, so let's solve that issue today by creating a Command Line Interface (CLI) to handle all these tedious details.

## Adding a CLI to the project

**DevPail**'s project directory so far is pretty bare:

```text
devpail/
  - imagesrc/
    - homedir/
    - Dockerfile
```

### Node configuration

Since we've chosen to build or CLI in Node, we need to initialize a Node package:

```bash
$ cd devpail
$ mkdir src
$ npm init -y
```

Here we're creating the `src` directory to hold our CLI source code, and asked `npm` to create a `package.json`.

### Coding from 30,000 feet

My habit is to build "kinda" code before actual code. It's real code, but at about 30,000 feet, with zero hope of running. This gives me the basic structure of the code, and a framework I can fill in with the gritty-kitty details as I go.

For **DevPail**, I'll need to be able to:

* Build the **DevPail** *image*; let's call this `devpail --build`
* Run a shell in the **DevPail** *container* for any project; let's call this `devpail --shell`
* Run the **DevPail** tooling inside the *container* for any project; let's call this `devpail`

## Let's start coding

Let's create a file called `src/cli.js` to hold this code, and start building that framework. We know that the first thing we have to do is process those command line options, if any.

```javascript
const status = processOptions()
if (status) {
    console.log(`\nDevPail: ${status}\n`)
}
```

As promised, this code totally won't work yet. Let's move down one level:

```javascript
function processOptions() {
    var args = process.argv.slice(2)
    if (args && args[0] === '--build') {
        return buildImage()
    } else if (args && args[0] === '--shell') {
        return runShell()
    } else {
        return runContainer()
    }
}
```

`process.argv` contains the entire command line, including the command used to start **DevPail**, so we slice it out, and check what's left for command line arguments. We always check that there are any arguments at all, and then if the first one is something we want to act on.

> In our very simple case, the above works well enough. However, for anything even slightly more complex, you'll want to look into a package such as `yargs` to handle your command line arguments.

Now that we have our basic flow in place, we can flesh out the details.

## Fleshing out the details

### Building `--build`

We've already know how to build the **DevPail** image from the command line:

```console
docker build -t devpail imagesrc
```

Let's adapt that to Node. To run a `docker`, a shell command, we'll use the `child_process` module. And to tell `docker` where the `Dockerfile` is, we'll need to use the `path` module. Let's start by including those at the top of our script:

```javascript
const { spawnSync } = require('child_process')
const path = require('path')
```

And then we'll Node-ify that command line:

```javascript
function buildImage() {
    console.log('DevPail: Building image "devpail:default"...')
    spawnSync(
        'docker',
        [
            'build',
            '-t',
            'devpail:default',
            path.resolve(__dirname, '..', 'imagesrc')
        ],
        {
            shell: true,
            stdio: 'inherit'
        }
    )

```

That `path.resolve(...)` call uses the inbuilt `__dirname` constant to get the location of the running script (`src/cli.js`), and then step up one level, and give us the `imagesrc` directory in which our `Dockerfile` is housed.

`--build`: ✔

### Running **DevPail** with no arguments

I'm going to skip ahead to the third point in the list, for reasons which will become apparent.

You'll recall that rather lengthy command line which runs the container:

```console
docker run -it --rm -v myproject-tooling:/home/pn/app -v ~/myproject:/home/pn/app/src devpail
```

#### First Pass

Let's make this easier and a bit smarter. 

We'll require that you run the `devpail` command (the one we're building now) from the root of the project you want to "DevPail-ize", as it were. This means that the current working directory should be the target project's name.

The solution will look much like that for `--build`, but with a few tweaks. Our first pass will look like:

```javascript
function runContainer() {
    var project = path.basename(process.cwd())
    spawnSync(
        'docker',
        [
            'run',
            '-it',
            '--rm',
            '-v', `${project}-DevPail-Tooling:/home/pn/app`,
            '-v', `${process.cwd()}:/home/pn/app/src`,
            `devpail:default`
        ],
        {
            shell: true,
            stdio: 'inherit'
        }
}
```

#### Handling duplicate project folder names

Of course, you might use the same name for more than one project, but in different sub-trees, so we'll handle that case by adding a hash of the FULL path to the project's directory:

```javascript
function createProjectName() {
    return path.basename(process.cwd()) 
        + '-' 
        + require('crypto')
            .createHash('md5')
            .update(process.cwd())
            .digest('hex')
            .slice(0, 8)
}

function runContainer() {
    var project = createProjectName()
...
```

#### Naming the container

Docker will create a name for each running *container* from a couple random words which bear no relation to the purpose of the *container*. Let's make that name meaningful. And while we're at it, let's do the same for the *container*'s hostname, which will be useful later on.

```javascript
function runContainer() {
    var project = createProjectName()
    spawnSync(
        'docker',
        [
...
            '--name', `${project}-DevPail`,
            '--hostname', project,
            `devpail:default`
        ],
...
}
```

`devpail` (no arguments): ✔

### Implementing `--shell`

We can run a shell in our *container* by way of the `--entrypoint` argument to the `docker` command, as in:

`docker -it --rm --entrypoint /bin/bash devpail`

Of course, the above command line will also need all the other parameters we provided in the section above, and that sounds like `--shell` is just a special case of the no-arguments version of the `devpail` command.

Let's revisit `processOptions()` first:

```javascript
function processOptions() {
...
    } else if (args && args[0] === '--shell') {
        return runContainer(['--entrypoint', '/bin/bash'])
    } else {
        return runContainer()
    }
}
```

Now let's handle those arguments in `runContainer()`:

```javascript
function runContainer(opts = []) {
    var project = createProjectName()
    spawnSync(
        'docker',
        [
...
            ...opts,
            `devpail:default`
        ],
...
}
```

`--shell`: ✔

## Next Up: making Gulp work

At this point, we can easily build and run **DevPail** for a project, but it doesn't really accomplish anything just yet.

In the next installment of this series, we'll build out a pluggable GulpJS system that will actually do something useful.

I've made huge updates to the publicly available **DevPail** project, on both [GitHub](https://github.com/tdesposito/DevPail) and [NPM](https://npmjs.org/package/devpail). It's evolved considerably since the first article, and I'm using it in production for several projects. So please feel to take a look at it. And then tune back in here to read more about the process of creating this tool!






