Build You Own CLI EP.1

มาสร้างเครื่องมือ Command-Line (CLI) ของคุณเองกันเถอะ บทความนี้จะพาคุณไปทีละขั้นตอน ตั้งแต่เริ่มต้นจนได้เป็น CLI ที่ใช้งานได้จริง เหมาะสำหรับนักพัฒนาที่อยากสร้างเครื่องมือไว้ใช้เองหรือต่อยอดเป็นโปรเจกต์ใหม่ๆ

Avatar Takai
24/06/2025

บทนำ

จะดีแค่ไหน ถ้าเราเริ่มโปรเจคใหม่ได้โดยไม่ต้องเสียเวลาไปกับการตั้งค่า (Config) เดิมๆ ที่น่าเบื่อ?

บทความนี้จะเปลี่ยนความเบื่อหน่ายนั้น ให้กลายเป็นการสร้าง ‘Generator’ Script สุดเท่ด้วย Bun เพื่อให้คุณสร้างโปรเจคใหม่ทั้งหมดได้ภายในคำสั่งเดียว!

เพื่อให้เข้าใจเนื้อหาได้ง่ายขึ้น ผู้อ่านควรมีพื้นฐานในเรื่องต่อไปนี้:

  • ความเข้าใจพื้นฐานเกี่ยวกับ Command-Line
  • ความรู้เบื้องต้นเกี่ยวกับ TypeScript
  • มี Bun ติดตั้งในเครื่อง
  • ใช้ git และมีบัญชีของ Github

ถ้าเตรียมตัวพร้อมแล้ว ก็มาเริ่มสร้าง CLI ของเรากันเลย!

ภาพรวมของโปรเจค

ในบทความนี้ เราจะมาสร้างเครื่องมือ Command-Line (CLI) ของเราเอง โดยหน้าที่หลักของมันคือการดึง (fetch) โครงสร้างโปรเจคเริ่มต้น (template) ที่เราได้เตรียมไว้จาก Public Repository บน GitHub ของเราเอง

ซึ่งเราจะทำให้ CLI สามารถเลือก “รูปแบบ” ของโปรเจคที่ต้องการได้ โดยใช้ประโยชน์จาก Branch บน Git เพื่อแยกโปรเจคแต่ละเวอร์ชันออกจากกัน เช่น เลือกระหว่างโปรเจคเวอร์ชันพื้นฐาน (จาก branch main) หรือเวอร์ชันที่มีการตั้งค่าเพิ่มเติม (จาก branch อื่นๆ)

เมื่อทำเสร็จแล้ว เราจะสามารถรันคำสั่งได้ในรูปแบบนี้:

mycli <command> <args>

ขั้นตอนการตั้งค่าโปรเจกต์

1. สร้างโปรเจกต์

เริ่มต้นด้วยการสร้างโปรเจกต์ใหม่ด้วยคำสั่งของ Bun ผ่าน Terminal

bun init -y

2. ติดตั้ง Dependencies

จากนั้น ติดตั้ง Package ที่จำเป็นสำหรับใช้สร้างแอปพลิเคชัน Command Line (CLI)

bun add commander consola degit inquirer kleur ora

หน้าที่ของแต่ละ Package:

  • commander: เครื่องมือหลักที่ช่วยให้เราสร้างแอปพลิเคชัน CLI ได้ง่ายขึ้น
  • consola: ใช้สำหรับตกแต่งข้อความที่แสดงผลใน Terminal ให้อ่านง่ายและสวยงาม
  • degit: เครื่องมือสำหรับดึงโค้ดจาก Git Repository (เช่น GitHub)
  • inquirer: ช่วยสร้างหน้าต่างโต้ตอบกับผู้ใช้ (Interactive Prompt) เช่น การตั้งคำถามให้ผู้ใช้เลือกตอบ
  • kleur: ใช้สำหรับเพิ่มสีสันให้กับตัวอักษรใน Terminal
  • ora: แสดงสัญลักษณ์ Loading (Spinner) ขณะที่โปรแกรมกำลังรอประมวลผล

3. เตรียมโครงสร้างไฟล์ (File Structure)

เพื่อให้ง่ายต่อการพัฒนา ในเบื้องต้นเราจะจัดโครงสร้างไฟล์ของโปรเจกต์ตามรูปแบบดังนี้ครับ

my-cli-app/
├── src/
│   └── index.ts
├── node_modules/
├── package.json
└── bun.lockb

การสร้างคำสั่ง CLI พื้นฐาน

1. เขียนโค้ดเริ่มต้น

เปิดไฟล์ src/index.ts และเพิ่มโค้ดต่อไปนี้เพื่อสร้างคำสั่งพื้นฐานที่สุดของ CLI

import { program } from "commander";
import consola from "consola";

program
  .version("1.0.0")
  .name("my-cli")
  .description("CLI สำหรับสร้างโปรเจกต์เริ่มต้น")
  .action(() => {
    consola.info("ยินดีต้อนรับสู่ My-CLI!");
  });

program.parse(process.argv);

คำอธิบายโค้ด:

  • program: คือ Object หลักจาก commander ที่ใช้กำหนดค่าต่างๆ ของ CLI เช่น ชื่อ (.name), คำอธิบาย (.description), และเวอร์ชัน (.version)
  • .action(): ฟังก์ชันที่กำหนดว่า CLI จะต้องทำอะไรเมื่อถูกเรียกใช้งาน
  • .parse(process.argv): คำสั่งที่ใช้ประมวลผล Input ที่ผู้ใช้ป้อนผ่าน Command Line โดย process.argv จะเก็บค่าทั้งหมดที่ส่งเข้ามา
  • consola.info(): เป็นการเรียกใช้ consola เพื่อแสดงข้อความพร้อมไอคอน สีฟ้า ทำให้ผลลัพธ์ดูสวยงามและเป็นระเบียบ

2. ทดลองรันคำสั่ง

บันทึกไฟล์และทดลองรันสคริปต์ผ่าน Terminal ด้วยคำสั่ง:

bun run src/index.ts

ผลลัพธ์ที่คาดหวัง:

Result One

การสร้างคำสั่งย่อยและ Input แบบมีตัวเลือก

ในส่วนนี้เราจะสร้างคำสั่งย่อย (Subcommand) สำหรับจัดการโปรเจกต์ Django และเพิ่มความสามารถในการรับ Input จากผู้ใช้

ส่วนที่ 1: สร้างคำสั่งย่อย django

1. สร้างไฟล์คำสั่งย่อย

เริ่มต้นด้วยการสร้างไฟล์สำหรับคำสั่งย่อยชื่อ django.command.ts ภายในโฟลเดอร์ src/commands/ และใส่โค้ดต่อไปนี้

// src/commands/django.command.ts
import { Command } from "commander";
import consola from "consola";

export const DjangoCommand = new Command("django")
  .alias("gdj")
  .description("Commands for generating Django projects")
  .action(async () => {
    consola.info("Django command is called!");
  });
  • new Command("django"): ใช้สำหรับสร้างคำสั่งย่อยชื่อ django
  • .alias("gdj"): กำหนดชื่อเรียกสั้นๆ (alias) ของคำสั่ง

2. เพิ่มคำสั่งย่อยเข้าคำสั่งหลัก

กลับไปที่ไฟล์ src/index.ts เพื่อนำเข้า DjangoCommand ให้เป็นส่วนหนึ่งของโปรแกรมหลัก

// src/index.ts
import { program } from "commander";
import consola from "consola";
import { DjangoCommand } from "./commands/django.command"; // เพิ่มบรรทัดนี้

program
  .version("1.0.0")
  .name("my-cli")
  .description("CLI สำหรับสร้างโปรเจกต์เริ่มต้น")
  .action(() => {
    consola.info("ยินดีต้อนรับสู่ My-CLI!");
  });

program.addCommand(DjangoCommand); // เพิ่มบรรทัดนี้

program.parse(process.argv);
  • program.addCommand(): เป็นเมธอดสำหรับเพิ่มคำสั่งย่อยเข้าไปในโปรแกรมหลัก

3. ตรวจสอบผลลัพธ์

ทดลองรันคำสั่งโดยเรียกดูหน้าช่วยเหลือ (-h) เพื่อดูว่ามีคำสั่งอะไรให้ใช้บ้าง

bun run src/index.ts -h

จะเห็นว่ามีคำสั่ง django พร้อมชื่อย่อ gdj ปรากฏขึ้นมาในรายการ Commands

Result One

ส่วนที่ 2: เพิ่มตัวเลือก (Prompt) และสถานะ Loading

1. แก้ไขไฟล์คำสั่งย่อย

กลับไปที่ไฟล์ src/commands/django.command.ts เพื่อเพิ่มโค้ดสำหรับรับ Input จากผู้ใช้ด้วย inquirer และแสดงสถานะ Loading ด้วย ora

// src/commands/django.command.ts
import { Command } from "commander";
import consola from "consola";
import inquirer from "inquirer"; // เพิ่มบรรทัดนี้
import ora from "ora"; // เพิ่มบรรทัดนี้

const PROJECT_TYPE_CHOICES = [
  { name: "DRF Project With PostgreSQL", value: "original" },
  { name: "DRF Mock Project", value: "mock" },
];

export const DjangoCommand = new Command("django")
  .alias("gdj")
  .description("Commands for generating Django projects")
  .action(async () => {
    // inquirer prompt
    const answers = await inquirer.prompt([
      {
        type: "list",
        name: "projectType",
        message: "Select a project type:",
        choices: PROJECT_TYPE_CHOICES,
      },
      {
        type: "input",
        name: "projectName",
        message: "Enter the project name:",
        default: "my-django-project",
      },
    ]);

    // ora spinner
    const spinner = ora("Cloning project template...").start();
    await new Promise((resolve) => setTimeout(resolve, 2000)); // จำลองการทำงาน 2 วินาที
    spinner.succeed("Project cloned successfully!");
  });
  • inquirer.prompt([...]): สร้างชุดคำถามเพื่อรับ Input จากผู้ใช้แบบโต้ตอบ
  • ora("...").start(): เริ่มแสดงผลสถานะ Loading (spinner)
  • spinner.succeed(): หยุดการ Loading และแสดงข้อความว่าทำสำเร็จ (หรือใช้ spinner.stop() เพื่อหยุดเฉยๆ)

2. ทดลองรันคำสั่ง

ทดลองรันคำสั่งย่อย django (หรือใช้ชื่อย่อ gdj)

bun run src/index.ts gdj

ผลลัพธ์ที่ได้คือ โปรแกรมจะแสดงรายการให้เลือกประเภทโปรเจกต์, ถามชื่อโปรเจกต์, และแสดงสถานะ Loading ก่อนจะจบการทำงานครับ

Result One

To Be Continue…

สำหรับ EP นี้ก็ขอตัดจบไว้เพียงเท่านี้ก่อนนะครับ เดี๋ยวบทความจะยาวเกินไป ใน EP หน้าเราจะมาทำในส่วนการ Generate Project กันต่อครับและจะปิดท้ายด้วยการ ใส่สีให้ตัวอักษรให้สวยงามกันนะครับ อย่าลืมมาติดตามกันต่อด้วยนะคร้าบ