Blog Hoch
Aug 2026·2 tup leghDe'wI' chenmoHta'

Node.js Daq command-line tool simple ghItlh

#nodejs#cli#javascript

internal CLI tools law' framework poQ pagh. files batch rename, internal API Qu', repetitive git sequence wrap script - wa' Node file shebang line 'ej package.json entry. Commander pagh, Yargs pagh, build step pagh. real subcommands 'ej generated --help poQ vaj framework lo'; qaSpa' overhead (Qagh ghaj pagh).

minimum viable CLI

Node CLI Hoch rap qaS: shebang (shell Sov), process.argv raw arguments vaD.

#!/usr/bin/env node

const args = process.argv.slice(2);
console.log('You passed:', args);
chmod +x rename-files.js
./rename-files.js --dry-run photos/
# You passed: [ '--dry-run', 'photos/' ]

process.argv Node binary path 'ej script path vo' ghoS - .slice(2) cha' drop, user actual typed ghaj.

dependency pagh flags parse

wa' cha' flags Dung hand-rolled parser tedious 'ach full argument-parsing library poQ pagh. Node built-in util.parseArgs (Node 20 vo' stable) common cases cover:

#!/usr/bin/env node
import { parseArgs } from 'node:util';

const { values, positionals } = parseArgs({
  args: process.argv.slice(2),
  options: {
    'dry-run': { type: 'boolean', default: false },
    output: { type: 'string', short: 'o', default: './out' },
    verbose: { type: 'boolean', short: 'v', default: false },
  },
  allowPositionals: true,
});

console.log(values);      // { 'dry-run': true, output: './out', verbose: false }
console.log(positionals); // ['photos/']
./rename-files.js --dry-run -o ./renamed photos/

named flags, short aliases, defaults, type coercion (--dry-run real boolean, string "true" pagh) - external dependencies pagh. library habit qaSpa' Sov.

files laD 'ej actual work

rename tool directory laD, entry Hoch act. Node fs/promises API async/await readable (nested callbacks pagh):

import { readdir, rename } from 'node:fs/promises';
import { join, extname } from 'node:path';

async function renamePhotos(dir, { dryRun }) {
  const entries = await readdir(dir);
  const photos = entries.filter((name) => ['.jpg', '.png'].includes(extname(name).toLowerCase()));

  for (const [index, name] of photos.entries()) {
    const ext = extname(name);
    const newName = `photo-${String(index + 1).padStart(3, '0')}${ext}`;
    const from = join(dir, name);
    const to = join(dir, newName);

    if (dryRun) {
      console.log(`${name} -> ${newName} (dry run)`);
    } else {
      await rename(from, to);
      console.log(`${name} -> ${newName}`);
    }
  }
}

dry-run branch legh: filesystem mutate Hoch vaD preview mode (exactly nuq qaS, Qu'be') ten extra lines worth. real directory tool trust vs backup neH first dare.

useful exit codes 'ej errors

script reH 0 exit shell scripting 'ej CI Daq invisible - && chains if checks exit code Dung (log output parse pagh).

async function main() {
  const dir = positionals[0];

  if (!dir) {
    console.error('Error: missing target directory');
    console.error('Usage: rename-files.js [--dry-run] [-o <dir>] <directory>');
    process.exit(1);
  }

  try {
    await renamePhotos(dir, { dryRun: values['dry-run'] });
  } catch (err) {
    console.error('Failed:', err.message);
    process.exit(1);
  }
}

main();

process.exit(1) usage error 'ej caught exception cha' Daq: rename-files.js photos/ && echo done rename Qapla' vaj neH "done" print - pipeline pagh pre-commit hook vaD Dun.

real command installable

script wa' repo Daq node scripts/rename-files.js Qu' - one-off Dun. real command (rename-photos anywhere) bin field package.json Daq chel:

{
  "name": "rename-photos",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "rename-photos": "./rename-files.js"
  }
}
npm link
rename-photos --dry-run photos/

npm link symlink global node_modules/.bin Daq chenmoH, command system-wide available (development). npm Daq published bin field npx rename-photos pagh global npm install -g Qu'.

filesystem mock pagh test

renamePhotos directory path parameter nob (hardcode pagh) - tests Daq real temporary directory point (fs mock pagh):

import { mkdtemp, writeFile, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test, expect } from 'vitest';

test('renames jpg and png files sequentially', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'rename-test-'));
  await writeFile(join(dir, 'holiday.jpg'), '');
  await writeFile(join(dir, 'notes.txt'), '');

  await renamePhotos(dir, { dryRun: false });

  const files = await readdir(dir);
  expect(files).toContain('photo-001.jpg');
  expect(files).toContain('notes.txt'); // untouched — not a photo extension

  await rm(dir, { recursive: true });
});

real (temporary) directory path-joining 'ej extension-handling bugs catch (mocked fs module paper over).

framework lo'

tool real subcommands (mytool deploy, mytool rollback, mytool status) ghaj, flags 'ej help text Hoch, hand-rolled dispatch logic Dun pagh - Commander pagh similar library subcommand routing 'ej generated --help vaD. single-purpose script vaD parseArgs 'ej bin entry real shareable command nob, dependency pagh.

yaj

useful CLI framework poQ pagh - shebang line, argument parsing (modern Node parseArgs), meaningful exit codes, dry-run mode (filesystem touch vaj). bin field chel node scripts/thing.js type Qo' 'ej command name type.

chu'Debouncing 'ej Throttling: JavaScript high-frequency events QotlhqanWeb Accessibility Basics: ARIA guesswork pagh