08 - Build a CLI Tool

📋 Jump to Takeaways

This lesson uses ES Modules. Make sure your package.json has "type": "module". See Modules if you need a refresher.

What We're Building

A command-line tool that organizes files in a directory by their extension. Run it on a messy Downloads folder and it creates subdirectories (images/, documents/, code/) and moves files into them.

node organize.js ~/Downloads

The problem: You have a folder full of mixed files. You want to sort them into subfolders by type automatically.

The steps:

  1. Read the target directory from command line arguments
  2. Define file categories (images, documents, code, etc.)
  3. Loop through files, determine each file's category by extension
  4. Create category folders and move files into them
  5. Add a --dry-run flag to preview without moving

Try building it yourself first using what you've learned (fs, path, process.argv). Then compare with the solution below.

Step 1: Parse Arguments

#!/usr/bin/env node
import path from 'node:path';
import fs from 'node:fs/promises';

const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const targetDir = args.find(a => !a.startsWith('--'));

if (!targetDir) {
  console.error('Usage: node organize.js <directory> [--dry-run]');
  process.exit(1);
}

const resolvedDir = path.resolve(targetDir);

Get the directory from arguments. Support a --dry-run flag from the start. Exit with an error if no directory provided.

Step 2: Define Categories

const CATEGORIES = {
  images: ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'],
  documents: ['.pdf', '.doc', '.docx', '.txt', '.md', '.csv'],
  code: ['.js', '.ts', '.py', '.go', '.rs', '.html', '.css', '.json'],
  audio: ['.mp3', '.wav', '.flac', '.aac'],
  video: ['.mp4', '.mov', '.avi', '.mkv'],
};

function getCategory(ext) {
  for (const [category, extensions] of Object.entries(CATEGORIES)) {
    if (extensions.includes(ext.toLowerCase())) return category;
  }
  return 'other';
}

Map extensions to folder names. Anything unrecognized goes to other/.

Step 3: Read and Organize

async function organize() {
  console.log(`${dryRun ? '[DRY RUN] ' : ''}Organizing: ${resolvedDir}`);

  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
  let moved = 0;

  for (const entry of entries) {
    if (!entry.isFile()) continue;

    const ext = path.extname(entry.name);
    if (!ext) continue;

    const category = getCategory(ext);
    const categoryDir = path.join(resolvedDir, category);
    const oldPath = path.join(resolvedDir, entry.name);
    const newPath = path.join(categoryDir, entry.name);

    if (dryRun) {
      console.log(`  ${entry.name} -> ${category}/`);
    } else {
      await fs.mkdir(categoryDir, { recursive: true });
      await fs.rename(oldPath, newPath);
      console.log(`  ${entry.name} -> ${category}/`);
    }
    moved++;
  }

  console.log(`\n${dryRun ? 'Would move' : 'Moved'} ${moved} files.`);
}

organize().catch((err) => {
  console.error('Error:', err.message);
  process.exit(1);
});

readdir with withFileTypes tells us if each entry is a file or directory without extra stat calls. We skip directories and files without extensions.

The Complete File

#!/usr/bin/env node
import path from 'node:path';
import fs from 'node:fs/promises';

const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const targetDir = args.find(a => !a.startsWith('--'));

if (!targetDir) {
  console.error('Usage: node organize.js <directory> [--dry-run]');
  process.exit(1);
}

const resolvedDir = path.resolve(targetDir);

const CATEGORIES = {
  images: ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'],
  documents: ['.pdf', '.doc', '.docx', '.txt', '.md', '.csv'],
  code: ['.js', '.ts', '.py', '.go', '.rs', '.html', '.css', '.json'],
  audio: ['.mp3', '.wav', '.flac', '.aac'],
  video: ['.mp4', '.mov', '.avi', '.mkv'],
};

function getCategory(ext) {
  for (const [category, extensions] of Object.entries(CATEGORIES)) {
    if (extensions.includes(ext.toLowerCase())) return category;
  }
  return 'other';
}

async function organize() {
  console.log(`${dryRun ? '[DRY RUN] ' : ''}Organizing: ${resolvedDir}`);

  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
  let moved = 0;

  for (const entry of entries) {
    if (!entry.isFile()) continue;

    const ext = path.extname(entry.name);
    if (!ext) continue;

    const category = getCategory(ext);
    const categoryDir = path.join(resolvedDir, category);
    const oldPath = path.join(resolvedDir, entry.name);
    const newPath = path.join(categoryDir, entry.name);

    if (dryRun) {
      console.log(`  ${entry.name} -> ${category}/`);
    } else {
      await fs.mkdir(categoryDir, { recursive: true });
      await fs.rename(oldPath, newPath);
      console.log(`  ${entry.name} -> ${category}/`);
    }
    moved++;
  }

  console.log(`\n${dryRun ? 'Would move' : 'Moved'} ${moved} files.`);
}

organize().catch((err) => {
  console.error('Error:', err.message);
  process.exit(1);
});

Test it:

# Preview first
node organize.js ~/Downloads --dry-run

# Then run for real
node organize.js ~/Downloads

Make It Executable

Add a shebang line at the top (already included above): #!/usr/bin/env node

Then make it executable:

chmod +x organize.js
./organize.js ~/Downloads

You can also register it as a CLI command in package.json:

{
  "name": "file-organizer",
  "type": "module",
  "bin": {
    "organize": "./organize.js"
  }
}

After npm link, you can run organize ~/Downloads from anywhere.

Key Takeaways

  • process.argv.slice(2) gets user arguments, validate them early
  • readdir with { withFileTypes: true } avoids extra stat calls
  • path.extname(), path.join(), and path.resolve() handle all path work
  • fs.mkdir with { recursive: true } creates directories safely
  • Add --dry-run flags so users can preview destructive operations
  • #!/usr/bin/env node + chmod +x makes scripts directly executable
  • The bin field in package.json registers CLI commands

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy