Home » Интерактивные командные приглашения в Node.js — как использовать
Интерактивные командные приглашения в Node.js — как использовать

Интерактивные командные приглашения в Node.js — как использовать

Когда деплоишь очередной проект на своём сервере, частенько возникают ситуации, когда нужно быстро настроить что-то “на лету” или собрать информацию от пользователя прямо в консоли. И вот тут-то обычные console.log и process.argv уже не катят — нужно что-то более интерактивное. Сегодня разберём, как сделать ваши Node.js скрипты максимально user-friendly с помощью интерактивных командных приглашений.

Эта штука особенно полезна для системных администраторов и девопсов, которые пишут скрипты автоматизации, установщики приложений или утилиты для настройки серверов. Представьте — один скрипт, который может интерактивно настроить nginx, спросить у вас пароли, выбрать нужные модули и всё это с валидацией и красивым интерфейсом прямо в терминале.

Как это работает под капотом?

Интерактивные командные приглашения в Node.js работают через стандартные потоки ввода/вывода (stdin/stdout). Основная магия происходит в модуле readline, который входит в стандартную библиотеку Node.js, но для продвинутых задач лучше использовать специализированные пакеты.

Принцип работы довольно простой:

  • Скрипт отображает вопрос в терминале
  • Приостанавливает выполнение и ждёт ввода пользователя
  • Получает введённые данные, валидирует их
  • Продолжает выполнение с полученными данными

Самые популярные библиотеки для этого дела:

  • inquirer — самая мощная и функциональная
  • prompts — лёгкая альтернатива с современным API
  • readline-sync — простая синхронная библиотека
  • enquirer — быстрая и стильная альтернатива inquirer

Пошаговая настройка с Inquirer

Начнём с самой популярной библиотеки — inquirer. Она проверена временем и используется в куче известных проектов типа Angular CLI, Vue CLI и других.

Устанавливаем пакет:

npm install inquirer
# или
yarn add inquirer

Базовый пример использования:

const inquirer = require('inquirer');

async function setupServer() {
  const answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'serverName',
      message: 'Введите имя сервера:',
      default: 'web-server-01'
    },
    {
      type: 'list',
      name: 'webServer',
      message: 'Выберите веб-сервер:',
      choices: ['nginx', 'apache', 'lighttpd'],
      default: 'nginx'
    },
    {
      type: 'checkbox',
      name: 'modules',
      message: 'Выберите модули для установки:',
      choices: [
        'SSL/TLS',
        'Gzip compression',
        'Rate limiting',
        'Load balancing'
      ]
    },
    {
      type: 'confirm',
      name: 'autoStart',
      message: 'Запускать сервер автоматически?',
      default: true
    }
  ]);

  console.log('Настройки сервера:', answers);
}

setupServer();

Типы вопросов и их применение

Inquirer поддерживает множество типов вопросов, каждый из которых подходит для разных сценариев:

Тип Описание Когда использовать
input Обычный текстовый ввод Имена серверов, пути к файлам
password Скрытый ввод пароля Пароли, API ключи
list Выбор из списка Выбор версии ПО, типа сервера
checkbox Множественный выбор Выбор модулей, плагинов
confirm Да/Нет вопрос Подтверждение действий
number Числовой ввод Порты, лимиты, таймауты

Пример продвинутого использования с валидацией:

const questions = [
  {
    type: 'input',
    name: 'port',
    message: 'Введите порт для веб-сервера:',
    default: '80',
    validate: function(value) {
      const port = parseInt(value);
      if (isNaN(port) || port < 1 || port > 65535) {
        return 'Порт должен быть числом от 1 до 65535';
      }
      if (port < 1024 && process.getuid() !== 0) {
        return 'Для портов < 1024 нужны права root';
      }
      return true;
    }
  },
  {
    type: 'password',
    name: 'dbPassword',
    message: 'Введите пароль для базы данных:',
    validate: function(value) {
      if (value.length < 8) {
        return 'Пароль должен быть минимум 8 символов';
      }
      return true;
    }
  },
  {
    type: 'list',
    name: 'environment',
    message: 'Выберите окружение:',
    choices: ['production', 'staging', 'development'],
    when: function(answers) {
      // Показываем этот вопрос только если порт не 80
      return answers.port !== '80';
    }
  }
];

Альтернативы: Prompts и Enquirer

Если inquirer кажется слишком тяжёлым (а он действительно довольно увесистый), есть более лёгкие альтернативы.

Prompts — современная библиотека с Promise-based API:

npm install prompts

const prompts = require('prompts');

const questions = [
  {
    type: 'text',
    name: 'hostname',
    message: 'Hostname сервера:'
  },
  {
    type: 'select',
    name: 'os',
    message: 'Операционная система:',
    choices: [
      { title: 'Ubuntu 20.04', value: 'ubuntu20' },
      { title: 'CentOS 8', value: 'centos8' },
      { title: 'Debian 11', value: 'debian11' }
    ]
  }
];

(async () => {
  const response = await prompts(questions);
  console.log(response);
})();

Enquirer — быстрая альтернатива с красивым интерфейсом:

npm install enquirer

const { prompt } = require('enquirer');

const questions = [
  {
    type: 'input',
    name: 'name',
    message: 'Название проекта:'
  },
  {
    type: 'multiselect',
    name: 'services',
    message: 'Выберите сервисы:',
    choices: ['Redis', 'MongoDB', 'PostgreSQL', 'Elasticsearch']
  }
];

prompt(questions)
  .then(answers => console.log(answers))
  .catch(console.error);

Практические кейсы использования

Вот несколько реальных примеров, где интерактивные приглашения просто незаменимы:

1. Установщик приложения

const inquirer = require('inquirer');
const fs = require('fs');
const path = require('path');

async function installApp() {
  const config = await inquirer.prompt([
    {
      type: 'input',
      name: 'appName',
      message: 'Название приложения:',
      validate: input => input.length > 0 || 'Название не может быть пустым'
    },
    {
      type: 'input',
      name: 'domain',
      message: 'Домен:',
      validate: input => {
        const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/;
        return domainRegex.test(input) || 'Введите корректный домен';
      }
    },
    {
      type: 'checkbox',
      name: 'features',
      message: 'Дополнительные возможности:',
      choices: [
        'SSL сертификат (Let\'s Encrypt)',
        'Backup настройка',
        'Monitoring (Prometheus)',
        'Логирование (ELK Stack)'
      ]
    }
  ]);

  // Генерируем конфиг файл
  const configContent = `
module.exports = {
  appName: '${config.appName}',
  domain: '${config.domain}',
  features: ${JSON.stringify(config.features, null, 2)}
};
`;

  fs.writeFileSync('./app.config.js', configContent);
  console.log('✅ Конфигурация сохранена!');
}

installApp();

2. Генератор nginx конфигурации

async function generateNginxConfig() {
  const answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'serverName',
      message: 'Server name (домен):',
      default: 'example.com'
    },
    {
      type: 'input',
      name: 'root',
      message: 'Document root:',
      default: '/var/www/html'
    },
    {
      type: 'input',
      name: 'upstreamPort',
      message: 'Upstream порт:',
      default: '3000'
    },
    {
      type: 'confirm',
      name: 'ssl',
      message: 'Включить SSL?',
      default: true
    },
    {
      type: 'confirm',
      name: 'gzip',
      message: 'Включить Gzip?',
      default: true
    }
  ]);

  const nginxConfig = `
server {
    listen 80;
    server_name ${answers.serverName};
    root ${answers.root};
    index index.html index.htm;

    ${answers.ssl ? `
    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/${answers.serverName}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${answers.serverName}/privkey.pem;
    ` : ''}

    ${answers.gzip ? `
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
    ` : ''}

    location / {
        proxy_pass http://127.0.0.1:${answers.upstreamPort};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
`;

  fs.writeFileSync(`/etc/nginx/sites-available/${answers.serverName}`, nginxConfig);
  console.log(`✅ Конфигурация nginx создана для ${answers.serverName}`);
}

generateNginxConfig();

Продвинутые возможности

Самое интересное начинается, когда комбинируешь интерактивные приглашения с другими инструментами:

Интеграция с Docker

const inquirer = require('inquirer');
const { exec } = require('child_process');
const util = require('util');

const execAsync = util.promisify(exec);

async function deployWithDocker() {
  const config = await inquirer.prompt([
    {
      type: 'input',
      name: 'imageName',
      message: 'Название Docker образа:'
    },
    {
      type: 'input',
      name: 'containerName',
      message: 'Название контейнера:'
    },
    {
      type: 'input',
      name: 'port',
      message: 'Порт для проброса:',
      default: '80'
    },
    {
      type: 'editor',
      name: 'envVars',
      message: 'Environment переменные (каждая с новой строки):',
      default: 'NODE_ENV=production\nPORT=3000'
    }
  ]);

  // Парсим переменные окружения
  const envVars = config.envVars.split('\n')
    .filter(line => line.trim())
    .map(line => `-e ${line.trim()}`)
    .join(' ');

  const dockerCommand = `docker run -d --name ${config.containerName} -p ${config.port}:${config.port} ${envVars} ${config.imageName}`;

  console.log('Выполняем команду:', dockerCommand);
  
  try {
    const { stdout } = await execAsync(dockerCommand);
    console.log('✅ Контейнер запущен:', stdout);
  } catch (error) {
    console.error('❌ Ошибка при запуске:', error.message);
  }
}

deployWithDocker();

Создание CLI инструмента

#!/usr/bin/env node

const inquirer = require('inquirer');
const commander = require('commander');
const program = new commander.Command();

program
  .name('server-setup')
  .description('Утилита для настройки сервера')
  .version('1.0.0');

program
  .command('init')
  .description('Интерактивная настройка сервера')
  .action(async () => {
    const answers = await inquirer.prompt([
      {
        type: 'list',
        name: 'action',
        message: 'Что настраиваем?',
        choices: [
          'Новый веб-сервер',
          'База данных',
          'Load balancer',
          'Monitoring'
        ]
      }
    ]);

    switch (answers.action) {
      case 'Новый веб-сервер':
        await setupWebServer();
        break;
      case 'База данных':
        await setupDatabase();
        break;
      // ... другие случаи
    }
  });

program.parse();

Сравнение популярных библиотек

Библиотека Размер Типы вопросов Плюсы Минусы
inquirer ~500KB 8 типов Много возможностей, большое сообщество Тяжёлая, медленная загрузка
prompts ~50KB 15 типов Лёгкая, современный API Меньше документации
enquirer ~100KB 20+ типов Быстрая, красивая Иногда глючит в разных терминалах
readline-sync ~30KB 5 типов Очень простая Синхронная, блокирует поток

Интересные факты и нестандартные применения

Вот несколько креативных способов использования интерактивных приглашений:

  • Интерактивное логирование — можно создать скрипт, который при ошибке спрашивает у разработчика дополнительную информацию и автоматически создаёт тикет в Jira
  • Динамические конфигурации — скрипт может подключаться к серверу, анализировать его характеристики и предлагать оптимальные настройки
  • Мониторинг в реальном времени — создайте интерактивный dashboard прямо в терминале с возможностью управления сервисами
  • Автоматизация деплоя — скрипт может спрашивать у вас, какую версию деплоить, на какие сервера, и выполнять все необходимые проверки

Пример интерактивного мониторинга

const inquirer = require('inquirer');
const { exec } = require('child_process');

async function serverMonitoring() {
  while (true) {
    const action = await inquirer.prompt([
      {
        type: 'list',
        name: 'command',
        message: 'Выберите действие:',
        choices: [
          'Показать нагрузку CPU',
          'Показать использование памяти',
          'Показать активные процессы',
          'Перезапустить сервис',
          'Выход'
        ]
      }
    ]);

    switch (action.command) {
      case 'Показать нагрузку CPU':
        exec('top -bn1 | grep "Cpu(s)" | awk \'{print $2}\' | cut -d\'%\' -f1', (err, stdout) => {
          console.log(`CPU загрузка: ${stdout.trim()}%`);
        });
        break;
      case 'Перезапустить сервис':
        const service = await inquirer.prompt([
          {
            type: 'input',
            name: 'serviceName',
            message: 'Название сервиса:'
          }
        ]);
        exec(`sudo systemctl restart ${service.serviceName}`, (err, stdout) => {
          if (err) {
            console.error('Ошибка:', err.message);
          } else {
            console.log(`✅ Сервис ${service.serviceName} перезапущен`);
          }
        });
        break;
      case 'Выход':
        return;
    }
  }
}

serverMonitoring();

Автоматизация и интеграция с другими инструментами

Интерактивные приглашения открывают массу возможностей для автоматизации рутинных задач. Вот как можно интегрировать их с популярными инструментами:

Интеграция с PM2

async function pm2Setup() {
  const config = await inquirer.prompt([
    {
      type: 'input',
      name: 'appName',
      message: 'Название приложения:'
    },
    {
      type: 'input',
      name: 'script',
      message: 'Путь к скрипту:',
      default: 'app.js'
    },
    {
      type: 'number',
      name: 'instances',
      message: 'Количество инстансов:',
      default: 1
    },
    {
      type: 'list',
      name: 'mode',
      message: 'Режим запуска:',
      choices: ['fork', 'cluster']
    }
  ]);

  const pm2Config = {
    name: config.appName,
    script: config.script,
    instances: config.instances,
    exec_mode: config.mode,
    env: {
      NODE_ENV: 'production'
    }
  };

  fs.writeFileSync('ecosystem.config.js', `module.exports = { apps: [${JSON.stringify(pm2Config, null, 2)}] };`);
  console.log('PM2 конфигурация создана!');
}

pm2Setup();

Создание SSL сертификатов

async function setupSSL() {
  const answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'domain',
      message: 'Домен для SSL сертификата:'
    },
    {
      type: 'input',
      name: 'email',
      message: 'Email для Let\'s Encrypt:'
    },
    {
      type: 'confirm',
      name: 'staging',
      message: 'Использовать staging сервер? (для тестов)',
      default: false
    }
  ]);

  const certbotCommand = `certbot certonly --nginx -d ${answers.domain} --email ${answers.email} --agree-tos --non-interactive ${answers.staging ? '--staging' : ''}`;
  
  console.log('Получаем SSL сертификат...');
  exec(certbotCommand, (error, stdout, stderr) => {
    if (error) {
      console.error('Ошибка получения сертификата:', error.message);
      return;
    }
    console.log('✅ SSL сертификат получен!');
    console.log(stdout);
  });
}

setupSSL();

Если планируете активно использовать такие скрипты на продакшене, рекомендую взять себе нормальный VPS или даже выделенный сервер — на shared хостинге особо не поразвернёшься с Node.js скриптами.

Полезные ссылки

Заключение и рекомендации

Интерактивные командные приглашения — это мощный инструмент для создания user-friendly скриптов автоматизации. Они особенно полезны в следующих случаях:

  • Для системных администраторов — создание установщиков, конфигураторов, утилит мониторинга
  • Для DevOps — автоматизация деплоя, настройка CI/CD пайплайнов
  • Для разработчиков — создание CLI инструментов, генераторов кода

Мои рекомендации по выбору библиотеки:

  • Используйте inquirer, если нужна максимальная функциональность и стабильность
  • Выбирайте prompts для лёгких и быстрых скриптов
  • Берите enquirer, если важен красивый интерфейс
  • Используйте readline-sync только для простейших задач

Помните про валидацию пользовательского ввода — это критически важно для продакшен скриптов. Всегда предусматривайте обработку ошибок и предоставляйте понятные сообщения пользователю.

И последний совет — документируйте свои скрипты! Через полгода вы сами забудете, что делает ваш "быстрый скрипт для настройки сервера" 😄


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked