Как создать инструмент CLI в NodeJS

Как разработчики, мы как бы живем с инструментами CLI. Начиная от git и до cloud shells - мы используем эти инструменты везде. Итак, пришло время сделать свой собственный. В этом процессе мы будем использовать великолепную среду oclif от Heroku.
Что такое Oclif?
Это открытая среда для быстрого создания инструментов CLI, предоставляемая хорошо известным Heroku .
Что будет в результате?
Мы сделаем команду todo list, которая может иметь четыре действия:
- Добавить новое задание
- Посмотреть все задачи
- Обновить задачу
- Удалить задачу
Инициализировать наш проект
Oclif может генерировать два типа проектов -
- Проекты, которые имеют одну команду.
- Проекты, которые могут иметь несколько команд, включая вложенные.
В этой статье нам понадобится проект с несколькими командами, поэтому давайте сгенерируем его:
npx oclif multi todocli
Выполнение этой команды и следование инструкциям инициализирует новый проект с именем todocli в текущем каталоге.
Теперь давайте перейдем в каталог и запустим help:
cd todocli # А затем ./bin/run --help
Получим следующий результат:
> USAGE
$ todocli [COMMAND]
COMMANDS
hello
help display help for todocliЭто показывает доступные команды и их документацию.

Структура проекта
Внутри src каталога мы можем найти каталог с именем commands. Этот каталог содержит все команды с их относительными именами файлов. Например, если у нас есть команда, hello в каталоге будет файл с именем hello.js, и команда будет работать без какой-либо дополнительной настройки. Давайте удалим, hello.js поскольку нам это не понадобится.
Настройка базы данных
Для хранения наших задач нам нужна система хранения. Для простоты мы будем использовать lowdb - довольно простую систему хранения файлов в виде json.
Давайте установим ее:
npm install lowdb --save
После установки lowdb создадим файл db.js со следующим содержимым:
// db.js
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const adapter = new FileSync('db.json');
const db = low(adapter);
// Установим занчения по умолчанию и запишим их в нашу базу
db.defaults({todos: []}).write();
const Todo = db.get('todos');
module.exports = {
db,
Todo
}Здесь мы просто загружаем необходимую библиотеку и файл db.json (пустой файл), а затем определяем пустой массив todos в качестве нашей базовой коллекции (это похоже на таблицу, если вы знакомы с SQL базами данных).
Добавление задач
oclif предоставляет нам простую функциональность для генерации команд. Давайте запустим следующее:
oclif command add
Она создаст файл с именем add.js внутри src/commands каталога. Давайте заменим содержимое этого файла на код ниже:
// add.js
const { Command, flags } = require('@oclif/command');
const { Todo } = require('../db');
class AddCommand extends Command {
async run() {
const {
flags: { task }
} = this.parse(AddCommand);
const res = await = Todo.push({
task,
id: Todo.value().length,
done: false,
}).write(); this.log(res);
}
}
AddCommand.description = `Add a new Todo
...
Adds a new todo to the existing list
`
AddCommand.flags = {
task: flags.string({
char: 'n',
description: 'task'
}),
};
module.exports = AddCommand;Файл имеет несколько ключевых компонентов:
- функция run, которая выполняет основные функции этой команды,
- descriptions, это документация для команды,
- flags, которые описывают флаги, передаваемые команде.
Здесь у нас есть флаг с именем, task который имеет тип string. Мы можем запустить команду следующим образом:
./bin/run add --task="welcome task"
Эта команда добавит задачу в нашу базу данных и напечатает ответ этой операции.
Показать список задач
// show.js
const { Command } = require('@oclif/command');
const chalk = require('chalk');
const { Todo } = require('../db');
class ShowCommand extends Command {
async run() {
const res = await Todo.sortBy('id').value();
res.forEach(({id, task, done}) => {
this.log(
`${chalk.magenta(id)} ${
done ? chalk.green('DONE') : chalk.yellowBright('NOT DONE')
} : ${task}`
);
});
}
}
ShowCommand.description = `Shows existing task
...
Show all the task sorted by their ids
`
module.exports = ShowCommand;Здесь внутри show.js мы показываем все задачи в порядке возрастания. Мы добавили немного цвета, с помощью chalkjs чтобы результаты нашей команды выглядели лучше.

Обновление задач
// update.js
const { Command, flags } = require('@oclif/command');
const { Todo } = require('../db');
class UpdateCommand extends Command {
async run() {
const {
flags: { id },
} = this.parse(UpdateCommand);
const res = await Todo.find({
id: parseInt(id, 10)
}).assign({
done: true
}).write(); this.log(res)
}
}
UpdateCommand.description = `Marks a task as done
...
Marks a task as done
`
UpdateCommand.flags = {
id: flags.string({
char: 'n',
description: 'task id'
}),
};
module.exports = UpdateCommand;Для простоты мы сейчас просто маркируем задачи, как done при обновлении. Мы просто передали id задачи как flag.
./bin/run update --id=1
Эта команда установит done = true для задачи с id = 1.
Удаление задач
// remove.js
const { Command, flags } = require('@oclif/command');
const { Todo } = require('../db');
class RemoveCommand extends Command {
async run() {
const {
flags: { id },
} = this.parse(RemoveCommand);
const res = await Todo.remove({
id: parseInt(id, 10)
}).write(); this.log(res)
}
}
RemoveCommand.description = `Removes a task by id
...
Removes a task permanently from database by id
`
RemoveCommand.flags = {
id: flags.string({
char: 'n',
description: 'task id'
}),
};
module.exports = RemoveCommand;Удаление довольно просто: мы передаем id как флаг, а затем удаляем соответствующую задачу из нашей базы данных.
В заключение
Для того чтобы нашим todo лист могли пользоваться не только вы но и ваши друзья, его можно опубликовать в npm, как это сделать, мы рассматривали в предыдущей статье: Как публиковать пакеты в npm