Oclif - Delegate / Wrap Existing CLI Commands

05/07/2021, Fri
Categories: #shell #typescript
Tags: #NodeJs

CLI Wrap with Spawn and Programmatic Access

Oclif is a Nodejs command line generator tool that grants you the ability to conveniently create a CLI with the following

npx oclif single mycoollistofthings

Supposedly you have your own custom CLI, but you wish to delegate some of these commands to an existing CLI tool. You are then wrapping another CLI command for the intent of adding additional functionality with mycoollistofthings. Here is the new command's mapping behavior of the ls -r command.

npx mycoollistofthings -r

Every command created by oclif will take its command line name as its corresponding file name inside the src/commands/ folder of the oclif project. This file will be responsible for running the ls command underneath it.

src/
├── commands
│   └── mycoollistofthings.ts
...

One way to delegate the command for ls to mycoollistofthings is to spawn the process underneath it when the command is running

// Choosen to create the CLI with the TypeScript option
const { spawn } = require('child_process');
import { Command, flags } from '@oclif/command'

class MyCoolListOfThings extends Command {
  static description = 'describe the command here'

  static flags = {
    // add --version flag to show CLI version
    version: flags.version({ char: 'v' }),
    help: flags.help({ char: 'h' }),
    // flag with a value (-n, --name=VALUE)
    name: flags.string({ char: 'n', description: 'name to print' }),
    // flag with no value (-f, --force)
    force: flags.boolean({ char: 'f' })
  }

  static args = [{ name: 'file' }]

  async run() {
    const { args, flags } = this.parse(MyCoolListOfThings)

    let ls = spawn('ls', ['-r']);

    ls.stdout
      .on('data', (data: any) => {
        console.log(`${data}`);
      });
  }
}

export = MyCoolListOfThings

This is not only good if you wish to have sensible defaults, but spawning the command gives you dynamic access for adding extra flag arguments when needed.

const { spawn } = require('child_process');
import { Command, flags } from '@oclif/command'

class MyCoolListOfThings extends Command {
  static description = 'describe the command here'

  static flags = {
    // add --version flag to show CLI version
    version: flags.version({ char: 'v' }),
    help: flags.help({ char: 'h' }),
    // flag with a value (-n, --name=VALUE)
    name: flags.string({ char: 'n', description: 'name to print' }),
    // flag with no value (-f, --force)
    force: flags.boolean({ char: 'f' }),
    all: flags.boolean({ char: 'a' }),
  }

  static args = [{ name: 'file' }]

  async run() {
    const { args, flags } = this.parse(MyCoolListOfThings)
    const spawnArgs = flags.all ? ['--r', '--a'] : ['--r']

    let ls = spawn('ls', spawnArgs);

    ls.stdout
      .on('data', (data: any) => {
        console.log(`${data}`);
      });
  }
}

export = MyCoolListOfThings

However, there will be times when attempting to "wrap" another CLI will not work as expected. Such as the case when attempting to wrap the snowpack CLI because the HMR will not work. The saving grace of using snowpack is that it has programmatic access.

So when "spawn wrapping" a CLI does not work, see if the programmatic access is available for the CLI execution.