Tutoriels

Créer un outil CLI avec Node.js et Commander

Mark Toledo

Mark Toledo

1 avril 2026

Créer un outil CLI avec Node.js et Commander

Les outils en ligne de commande font partie de mon quotidien depuis que je code. Au début, j'écrivais des scripts shell basiques. Puis j'ai découvert qu'on pouvait créer des CLIs vraiment solides avec Node.js — avec de l'autocomplétion, une gestion d'erreurs propre, des messages d'aide générés automatiquement, et tout ce qu'on attend d'un bon outil.

Le déclic a eu lieu quand j'ai eu besoin d'automatiser des tâches de migration de base de données pour un client. J'aurais pu écrire un script bash, mais j'avais besoin de logique complexe, d'accès à des APIs, et d'une interface utilisable par l'équipe entière. Node.js avec Commander, c'était la réponse évidente.

Initialiser le projet

mkdir mon-cli
cd mon-cli
npm init -y
npm install commander

Le fichier package.json a besoin de quelques ajustements pour qu'on puisse appeler notre CLI directement depuis le terminal :

{
  "name": "mon-cli",
  "version": "1.0.0",
  "bin": {
    "mon-cli": "./bin/index.js"
  },
  "type": "module"
}

Le champ bin indique à npm quels fichiers rendre exécutables. Après npm link (développement) ou npm install -g (production), la commande mon-cli sera disponible globalement.

La structure du projet

mon-cli/
  bin/
    index.js         # Point d'entrée
  src/
    commands/
      init.js        # Commande init
      generer.js     # Commande generer
    utils/
      afficher.js    # Helpers affichage
  package.json

Le point d'entrée

// bin/index.js
#!/usr/bin/env node

import { Command } from 'commander';
import { commandeInit } from '../src/commands/init.js';
import { commandeGenerer } from '../src/commands/generer.js';

const programme = new Command();

programme
  .name('mon-cli')
  .description('Outil CLI pour automatiser mes tâches courantes')
  .version('1.0.0');

programme
  .command('init')
  .description('Initialiser un nouveau projet')
  .argument('<nom>', 'Nom du projet')
  .option('-t, --template <type>', 'Template à utiliser', 'default')
  .option('--git', 'Initialiser un dépôt git')
  .action(commandeInit);

programme
  .command('generer')
  .description('Générer des fichiers depuis un template')
  .argument('<type>', 'Type de fichier à générer')
  .option('-n, --nom <nom>', 'Nom du fichier généré')
  .option('-f, --force', 'Écraser si le fichier existe')
  .action(commandeGenerer);

programme.parse();

La première ligne #!/usr/bin/env node est essentielle — c'est le shebang qui indique au système d'exploitation quel interpréteur utiliser pour exécuter le fichier.

Implémenter une commande

// src/commands/init.js
import fs from 'fs/promises';
import path from 'path';
import { execSync } from 'child_process';

export async function commandeInit(nom, options) {
  console.log(`Création du projet "${nom}"...`);

  const dossier = path.join(process.cwd(), nom);

  try {
    await fs.mkdir(dossier, { recursive: true });
    
    // Créer la structure de base
    const sousDossiers = ['src', 'tests', 'docs'];
    await Promise.all(
      sousDossiers.map(d => fs.mkdir(path.join(dossier, d)))
    );

    // Générer le package.json
    const packageJson = {
      name: nom,
      version: '0.1.0',
      type: 'module',
      scripts: {
        start: 'node src/index.js',
        test: 'node --test'
      }
    };

    await fs.writeFile(
      path.join(dossier, 'package.json'),
      JSON.stringify(packageJson, null, 2)
    );

    if (options.git) {
      execSync('git init', { cwd: dossier, stdio: 'ignore' });
      console.log('Dépôt git initialisé.');
    }

    console.log(`✓ Projet "${nom}" créé dans ./${nom}/`);
  } catch (erreur) {
    if (erreur.code === 'EEXIST') {
      console.error(`Erreur : le dossier "${nom}" existe déjà.`);
      process.exit(1);
    }
    throw erreur;
  }
}

Améliorer l'expérience utilisateur avec ora et chalk

Un CLI sans retour visuel, c'est anxiogène. L'utilisateur ne sait pas si l'outil tourne ou s'il est planté. Deux packages rendent l'expérience bien meilleure :

npm install ora chalk
import ora from 'ora';
import chalk from 'chalk';

export async function commandeAvecProgress(argument, options) {
  const spinner = ora('Téléchargement des données...').start();

  try {
    const donnees = await recupererDonnees(argument);
    spinner.succeed(chalk.green('Données récupérées avec succès'));
    
    spinner.start('Traitement en cours...');
    const resultat = await traiter(donnees);
    spinner.succeed(chalk.green(`${resultat.length} éléments traités`));
    
    console.log(chalk.cyan('\nRésumé :'));
    console.log(`  Traités : ${chalk.bold(resultat.length)}`);
    console.log(`  Durée : ${chalk.bold(resultat.duree)}ms`);
  } catch (erreur) {
    spinner.fail(chalk.red(`Erreur : ${erreur.message}`));
    process.exit(1);
  }
}

Lire la configuration depuis un fichier

La plupart des bons CLIs permettent une configuration persistante. Voici un pattern simple pour lire depuis un fichier .mon-cli.json dans le répertoire home de l'utilisateur.

// src/utils/config.js
import fs from 'fs/promises';
import os from 'os';
import path from 'path';

const CONFIG_PATH = path.join(os.homedir(), '.mon-cli.json');

export async function lireConfig() {
  try {
    const contenu = await fs.readFile(CONFIG_PATH, 'utf-8');
    return JSON.parse(contenu);
  } catch {
    return {}; // Pas de config = valeurs par défaut
  }
}

export async function ecrireConfig(config) {
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
}

Prompts interactifs avec @inquirer/prompts

Pour les commandes qui nécessitent des saisies utilisateur, Inquirer.js (maintenant modulaire) est la référence.

npm install @inquirer/prompts
import { input, select, confirm } from '@inquirer/prompts';

export async function commandeInteractive() {
  const nom = await input({
    message: 'Nom du projet :',
    validate: (val) => val.length > 0 || 'Le nom ne peut pas être vide'
  });

  const template = await select({
    message: 'Choisir un template :',
    choices: [
      { name: 'API REST (Express)', value: 'api' },
      { name: 'CLI (Commander)', value: 'cli' },
      { name: 'Application web (Astro)', value: 'web' }
    ]
  });

  const confirme = await confirm({
    message: `Créer "${nom}" avec le template "${template}" ?`,
    default: true
  });

  if (!confirme) {
    console.log('Annulé.');
    return;
  }

  await creerProjet(nom, template);
}

Rendre le fichier exécutable

Avant de tester, il faut rendre le fichier bin exécutable sur Linux/macOS :

chmod +x bin/index.js
npm link  # Installe le CLI globalement en mode développement

Tu peux maintenant appeler mon-cli --help depuis n'importe quel répertoire.

Tests des commandes CLI

Tester un CLI, c'est souvent délicat. J'utilise execa pour appeler les commandes dans les tests et vérifier stdout/stderr.

import { execa } from 'execa';
import { describe, it } from 'node:test';
import assert from 'node:assert';

describe('commande init', () => {
  it('crée un dossier de projet', async () => {
    const { stdout, exitCode } = await execa('./bin/index.js', [
      'init', 'projet-test'
    ]);
    
    assert.equal(exitCode, 0);
    assert.match(stdout, /Projet "projet-test" créé/);
    
    // Nettoyer
    await fs.rm('./projet-test', { recursive: true });
  });
});

Publication sur npm

Une fois le CLI prêt, le publier sur npm prend deux minutes :

npm login
npm publish

Les utilisateurs peuvent ensuite l'installer avec npm install -g mon-cli ou l'utiliser sans installation via npx mon-cli.

Quelques points importants pour un CLI publié : bien remplir la section engines dans package.json pour indiquer la version Node.js minimale requise, et inclure un README.md avec des exemples d'utilisation clairs.

Ce qu'on retient

Créer un CLI avec Node.js et Commander n'est pas plus compliqué que n'importe quel autre projet Node.js. L'essentiel : penser à l'expérience utilisateur dès le début (messages d'aide, spinners, couleurs), gérer les erreurs avec des codes de sortie explicites, et tester les commandes comme n'importe quelle autre interface.

Un bon CLI, c'est un outil que les autres membres de l'équipe adoptent naturellement parce qu'il fait une chose, bien, avec une interface claire.