Setiap alat yang tumbuh akan sulit dirawat bila seluruh logika menumpuk dalam satu berkas. Karena itu kita memulai proyek ini dari kerangkanya: memisahkan pintu masuk, parsing argumen, dan kontrak galat ke dalam modul-modul kecil. Pada bagian ini kerangka tersebut harus sudah bisa berjalan — perintah help tampil rapi, pemakaian yang salah ditolak dengan pesan yang jelas, dan setiap keadaan punya kode keluar yang tetap. Operasi berkas sungguhan baru menyusul di Bagian 2 dan 3, tetapi kontrak yang kita tetapkan di sini akan dipakai terus sampai pengujian di Bagian 4.

Anatomi Modul #

Empat berkas pertama yang kita bangun adalah fondasi seluruh proyek. Tabel berikut merangkum peran masing-masing modul; tiga modul sisanya (format.js, perintah-baca.js, perintah-tulis.js) dibahas pada bagian berikutnya.

BerkasPeran
src/cli.jsPintu masuk: menerjemahkan argumen menjadi panggilan fungsi perintah dan menentukan kode keluar
src/argumen.jsMemvalidasi bentuk pemanggilan dengan util.parseArgs
src/bantuan.jsMenyimpan teks bantuan yang dicetak perintah help
src/galat.jsDua jenis galat bawaan proyek serta penerjemah pesan galat sistem

Pemisahan ini memberi satu bonus penting untuk pengujian: fungsi perintah tidak mencetak apa pun ke layar, melainkan mengembalikan teks. Test di Bagian 4 cukup membandingkan nilai kembalian, tanpa perlu menangkap keluaran terminal.

Parsing Argumen dengan util.parseArgs #

Modul src/argumen.js memakai parseArgs dari node:util dengan tiga pengaturan: allowPositionals: true agar argumen tanpa awalan -- diterima sebagai argumen posisi, options yang mendeklarasikan dua opsi boolean (--ya dan --lanjut), dan mode ketat yang aktif secara default sehingga opsi asing langsung melempar galat. Fungsi parseArgumen membungkus semuanya, lalu memvalidasi perintah dan jumlah argumennya.

js src/argumen.js
export function parseArgumen(argv) {
  let hasil;
  try {
    hasil = parseArgs({
      args: argv,
      allowPositionals: true,
      options: {
        ya: { type: 'boolean' },
        lanjut: { type: 'boolean' },
      },
    });
  } catch (galat) {
    throw new GalatPemakaian('opsi tidak dikenal (jalankan help untuk daftar opsi)');
  }

  const [perintah, ...posisi] = hasil.positionals;
  if (perintah === undefined) {
    throw new GalatPemakaian('perintah tidak diberikan (jalankan help untuk bantuan)');
  }
  if (!(perintah in JUMLAH_ARGUMEN)) {
    throw new GalatPemakaian(`perintah tidak dikenal: ${perintah} (jalankan help)`);
  }

  const jumlah = JUMLAH_ARGUMEN[perintah];
  if (posisi.length !== jumlah) {
    throw new GalatPemakaian(
      `perintah ${perintah} butuh ${jumlah} argumen, diterima ${posisi.length}`,
    );
  }
  return { perintah, posisi, opsi: hasil.values };
}

Mari kita bedah fungsi di atas baris per baris:

  1. Blok try: parseArgs menerima salinan argv (bukan process.argv langsung) supaya test dapat mengirim daftar argumen buatan. Tanpa allowPositionals: true, semua argumen posisi justru ditolak.
  2. Blok catch: galat dari parseArgs (misalnya opsi tidak dikenal) ditangkap lalu dibungkus ulang sebagai GalatPemakaian. Pesan aslinya berbahasa Inggris dan panjang, seperti contoh di bawah — kita ganti dengan satu kalimat Indonesia yang menunjuk ke help.
  3. const [perintah, ...posisi]: destructuring memisahkan elemen pertama sebagai nama perintah dan sisanya sebagai argumen posisi.
  4. Tiga pemeriksaan berurutan: perintah kosong, perintah di luar tabel JUMLAH_ARGUMEN (help: 0, daftar: 1, baca: 1, info: 1, tulis: 2, salin: 2, hapus: 1), dan jumlah argumen yang tidak sesuai. Semuanya melempar GalatPemakaian — kelas galat pada src/galat.js yang membawa kodeKeluar bernilai 1.

Untuk merasakan mengapa pembungkusan perlu, jalankan parseArgs mentah dengan opsi yang tidak dikenal:

js
import { parseArgs } from 'node:util';

try {
  parseArgs({ args: ['--hmm', 'x'], allowPositionals: true });
} catch (galat) {
  console.log(galat.message.split('.')[0]);
}

Output yang Dihasilkan:

text
Unknown option '--hmm'

Kalimat pertama pesan aslinya sudah cukup untuk diagnosis: opsi --hmm memang tidak ada dalam deklarasi. Kalimat lanjutannya masih panjang karena mengingatkan pemakaian pemisah -- untuk argumen posisi yang kebetulan berawalan tanda minus — misalnya nama berkas -awal.txt — supaya tidak dianggap opsi. Daripada menampilkan teks Inggris sepanjang itu, alat kita menerjemahkan seluruh kegagalan parsing menjadi satu baris pesan Indonesia yang menunjuk ke help.

Dispatch Perintah dan Kode Keluar #

Pintu masuk src/cli.js menghubungkan hasil parsing dengan fungsi perintah lewat tabel AKSI. Nama perintah menjadi kunci; nilainya fungsi panah yang menerima argumen posisi dan opsi lalu mengembalikan teks keluaran. Fungsi main tinggal melihat ke tabel itu, dengan help sebagai satu-satunya pengecualian karena isinya murni teks bantuan.

js src/cli.js
const AKSI = {
  daftar: (posisi) => daftar(posisi[0]),
  baca: (posisi) => baca(posisi[0]),
  info: (posisi) => info(posisi[0]),
  tulis: (posisi, opsi) => tulis(posisi[0], posisi[1], opsi),
  salin: (posisi) => salin(posisi[0], posisi[1]),
  hapus: (posisi, opsi) => hapus(posisi[0], opsi),
};

// Menjalankan satu perintah lalu mengembalikan teks keluarannya.
export async function main(argv) {
  const { perintah, posisi, opsi } = parseArgumen(argv);
  if (perintah === 'help') return BANTUAN;
  return AKSI[perintah](posisi, opsi);
}

Sisa pekerjaan cli.js adalah menangani keberhasilan dan kegagalan di satu tempat. Fungsi jalankan mencetak hasil main ke stdout, menangkap semua galat, menyeragamkannya lewat ubahGalat dari src/galat.js, mencetak pesannya ke stderr, lalu mengembalikan kode keluar yang dibawa galat tersebut.

js src/cli.js
// Membungkus main: mencetak hasil, menangkap galat, menentukan kode keluar.
export async function jalankan(argv) {
  try {
    console.log(await main(argv));
    return 0;
  } catch (galat) {
    const akhir = ubahGalat(galat);
    console.error(`manajer: ${akhir.message}`);
    return akhir.kodeKeluar;
  }
}

// Jalankan hanya bila berkas ini dieksekusi langsung, bukan saat diimpor.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  process.exitCode = await jalankan(process.argv.slice(2));
}

Dua keputusan pada potongan terakhir patut dicermati. Pertama, kode keluar dipasang lewat process.exitCode, bukan process.exit(), agar Node.js sempat menuntaskan penulisan keluaran sebelum proses berakhir. Kedua, blok if terakhir membandingkan import.meta.url dengan path berkas yang dieksekusi — jadi seluruh isi cli.js tetap bisa diimpor oleh test tanpa efek samping, sementara node src/cli.js tetap berjalan normal.

Help sebagai Dokumen Kontrak #

Perintah help mencetak seluruh kontrak alat: daftar perintah, opsi, kode keluar, dan contoh pemakaian. Teksnya disimpan sebagai satu konstanta BANTUAN pada src/bantuan.js, sehingga mudah dirawat di satu tempat.

bash
node src/cli.js help

Output yang Dihasilkan:

text
Manajer Berkas — alat CLI untuk mengelola berkas lewat terminal.

Pemakaian:
  node src/cli.js <perintah> [argumen] [opsi]

Perintah:
  daftar <folder>          Tampilkan isi folder (nama, ukuran, jenis)
  baca <berkas>            Tampilkan isi berkas dengan nomor baris
  info <berkas>            Tampilkan metadata berkas (ukuran, waktu diubah)
  tulis <berkas> <isi>     Tulis isi ke berkas (menimpa isi lama)
  salin <asal> <tujuan>    Salin berkas ke lokasi baru
  hapus <berkas>           Hapus berkas (wajib dengan opsi --ya)
  help                     Tampilkan pesan bantuan ini

Opsi:
  --lanjut                 Sambung isi di akhir berkas (khusus tulis)
  --ya                     Konfirmasi penghapusan (khusus hapus)

Kode keluar:
  0                        Perintah berhasil dijalankan
  1                        Pemakaian salah (perintah atau argumen tidak sesuai)
  2                        Galat berkas (berkas tidak ditemukan, dsb.)

Contoh:
  node src/cli.js daftar data
  node src/cli.js tulis data/halo.txt 'Halo dari manajer berkas.'
  node src/cli.js salin data/halo.txt data/halo-salinan.txt
  node src/cli.js hapus data/halo-salinan.txt --ya

Kontrak kode keluar kini bisa kita uji langsung. Pemanggilan tanpa perintah, dengan perintah asing, maupun dengan argumen kurang semuanya ditolak dengan kode 1 — perhatikan bahwa pesan galat muncul di stderr, bukan stdout.

bash
node src/cli.js

Output yang Dihasilkan:

text
manajer: perintah tidak diberikan (jalankan help untuk bantuan)
kode keluar: 1
bash
node src/cli.js buang data

Output yang Dihasilkan:

text
manajer: perintah tidak dikenal: buang (jalankan help)
kode keluar: 1
bash
node src/cli.js salin data/asal.txt

Output yang Dihasilkan:

text
manajer: perintah salin butuh 2 argumen, diterima 1
kode keluar: 1

Kerangka kita kini hidup: argumen tervalidasi, bantuan tersedia, dan tiga jenis pemakaian salah ditolak dengan pesan yang seragam. Yang masih kosong adalah isi perintahnya — daftar, baca, dan info baru mengembalikan teks, belum benar-benar membaca disk. Bagian 2 mengisi ketiganya dengan node:fs/promises lengkap dengan format tampilan dan penerjemahan galat sistem menjadi pesan Indonesia.