بناء أداة سطر أوامر احترافية باستخدام Node.js و TypeScript في 2026

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

تظل أدوات سطر الأوامر العمود الفقري لسير عمل المطورين. من git و npm إلى eslint و prettier، تُشغّل أدوات CLI تجربة التطوير الحديثة بأكملها. في هذا الدليل، ستبني أداة سطر أوامر احترافية من الصفر باستخدام Node.js و TypeScript — مع تحليل الأوامر والمطالبات التفاعلية والمخرجات الملوّنة وكل ما تحتاجه لنشرها على npm.

ما الذي ستبنيه

سنقوم ببناء taskr — أداة إدارة مهام تتيح للمطورين إدارة مهام مشاريعهم مباشرة من الطرفية. بنهاية هذا الدليل، ستمتلك أداة تدعم:

  • إضافة المهام وعرضها وإكمالها وحذفها
  • وضع تفاعلي مع مطالبات ذكية
  • مخرجات ملوّنة ومنسّقة في الطرفية
  • تخزين دائم باستخدام ملف JSON
  • معالجة أخطاء سليمة ونصوص مساعدة
  • حزمة npm جاهزة للنشر

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 20+ مثبّت على جهازك
  • مدير حزم npm أو pnpm
  • معرفة أساسية بـ TypeScript
  • طرفية تتعامل معها بأريحية
  • محرر أكواد (يُنصح بـ VS Code)

الخطوة 1: إعداد المشروع

أنشئ مجلدًا جديدًا وهيّئ المشروع:

mkdir taskr && cd taskr
npm init -y

ثبّت TypeScript والمكتبات المطلوبة:

npm install commander chalk inquirer conf ora
npm install -D typescript @types/node @types/inquirer tsx tsup

إليك ما تفعله كل حزمة:

الحزمةالغرض
commanderتحليل الأوامر والمعاملات
chalkتلوين مخرجات الطرفية
inquirerمطالبات تفاعلية
confتخزين دائم للإعدادات والبيانات
oraمؤشرات تحميل أنيقة
tsxتشغيل TypeScript مباشرة أثناء التطوير
tsupتجميع TypeScript للتوزيع

الخطوة 2: إعداد TypeScript

أنشئ ملف tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

الخطوة 3: تعريف نموذج البيانات

أنشئ مجلد الكود المصدري وأنواع البيانات:

mkdir -p src

أنشئ ملف src/types.ts:

export interface Task {
  id: string;
  title: string;
  description?: string;
  status: "todo" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
  createdAt: string;
  completedAt?: string;
}
 
export interface TaskStore {
  tasks: Task[];
}

الخطوة 4: بناء طبقة التخزين

أنشئ ملف src/store.ts للتعامل مع التخزين الدائم:

import Conf from "conf";
import { randomUUID } from "node:crypto";
import type { Task, TaskStore } from "./types.js";
 
const config = new Conf<TaskStore>({
  projectName: "taskr",
  defaults: {
    tasks: [],
  },
});
 
export function getAllTasks(): Task[] {
  return config.get("tasks");
}
 
export function getTaskById(id: string): Task | undefined {
  return getAllTasks().find((t) => t.id === id || t.id.startsWith(id));
}
 
export function addTask(
  title: string,
  options: { description?: string; priority?: Task["priority"] } = {}
): Task {
  const task: Task = {
    id: randomUUID().slice(0, 8),
    title,
    description: options.description,
    status: "todo",
    priority: options.priority ?? "medium",
    createdAt: new Date().toISOString(),
  };
 
  const tasks = getAllTasks();
  tasks.push(task);
  config.set("tasks", tasks);
  return task;
}
 
export function completeTask(id: string): Task | null {
  const tasks = getAllTasks();
  const task = tasks.find((t) => t.id === id || t.id.startsWith(id));
 
  if (!task) return null;
 
  task.status = "done";
  task.completedAt = new Date().toISOString();
  config.set("tasks", tasks);
  return task;
}
 
export function removeTask(id: string): boolean {
  const tasks = getAllTasks();
  const index = tasks.findIndex((t) => t.id === id || t.id.startsWith(id));
 
  if (index === -1) return false;
 
  tasks.splice(index, 1);
  config.set("tasks", tasks);
  return true;
}
 
export function clearAllTasks(): void {
  config.set("tasks", []);
}

مكتبة conf تحدد تلقائيًا مسار التخزين المناسب لكل نظام تشغيل — ~/.config/taskr على لينكس، و ~/Library/Preferences/taskr على macOS، و %APPDATA%/taskr على ويندوز.

الخطوة 5: إنشاء منسّق المخرجات

أنشئ ملف src/formatter.ts لإخراج جميل في الطرفية:

import chalk from "chalk";
import type { Task } from "./types.js";
 
const priorityColors = {
  low: chalk.gray,
  medium: chalk.yellow,
  high: chalk.red,
};
 
const statusIcons = {
  todo: "○",
  "in-progress": "◑",
  done: "●",
};
 
export function formatTask(task: Task): string {
  const icon = statusIcons[task.status];
  const priority = priorityColors[task.priority](`[${task.priority}]`);
  const id = chalk.dim(`#${task.id}`);
  const title =
    task.status === "done" ? chalk.strikethrough(task.title) : task.title;
 
  let line = `  ${icon} ${id} ${title} ${priority}`;
 
  if (task.description) {
    line += `\n    ${chalk.dim(task.description)}`;
  }
 
  return line;
}
 
export function formatTaskList(tasks: Task[]): string {
  if (tasks.length === 0) {
    return chalk.dim("  لا توجد مهام. أضف واحدة بـ: taskr add <title>");
  }
 
  const groups = {
    "قيد التنفيذ": tasks.filter((t) => t.status === "in-progress"),
    "للتنفيذ": tasks.filter((t) => t.status === "todo"),
    "مكتملة": tasks.filter((t) => t.status === "done"),
  };
 
  const lines: string[] = [];
 
  for (const [label, group] of Object.entries(groups)) {
    if (group.length === 0) continue;
    lines.push(`\n${chalk.bold.underline(label)} (${group.length})`);
    group.forEach((task) => lines.push(formatTask(task)));
  }
 
  return lines.join("\n");
}
 
export function success(message: string): void {
  console.log(chalk.green("✓"), message);
}
 
export function error(message: string): void {
  console.error(chalk.red("✗"), message);
}
 
export function info(message: string): void {
  console.log(chalk.blue("ℹ"), message);
}

الخطوة 6: إضافة الوضع التفاعلي

أنشئ ملف src/interactive.ts للمطالبات التفاعلية:

import inquirer from "inquirer";
import type { Task } from "./types.js";
import { addTask, getAllTasks, completeTask, removeTask } from "./store.js";
import { formatTaskList, success, error } from "./formatter.js";
 
export async function interactiveMode(): Promise<void> {
  const { action } = await inquirer.prompt([
    {
      type: "list",
      name: "action",
      message: "ماذا تريد أن تفعل؟",
      choices: [
        { name: "📋 عرض جميع المهام", value: "list" },
        { name: "➕ إضافة مهمة جديدة", value: "add" },
        { name: "✅ إكمال مهمة", value: "complete" },
        { name: "🗑️  حذف مهمة", value: "remove" },
        { name: "🚪 خروج", value: "exit" },
      ],
    },
  ]);
 
  switch (action) {
    case "list":
      console.log(formatTaskList(getAllTasks()));
      break;
 
    case "add":
      await interactiveAdd();
      break;
 
    case "complete":
      await interactiveComplete();
      break;
 
    case "remove":
      await interactiveRemove();
      break;
 
    case "exit":
      return;
  }
 
  // العودة للقائمة الرئيسية
  await interactiveMode();
}
 
async function interactiveAdd(): Promise<void> {
  const answers = await inquirer.prompt([
    {
      type: "input",
      name: "title",
      message: "عنوان المهمة:",
      validate: (input: string) =>
        input.trim().length > 0 || "العنوان لا يمكن أن يكون فارغًا",
    },
    {
      type: "input",
      name: "description",
      message: "الوصف (اختياري):",
    },
    {
      type: "list",
      name: "priority",
      message: "الأولوية:",
      choices: ["low", "medium", "high"],
      default: "medium",
    },
  ]);
 
  const task = addTask(answers.title, {
    description: answers.description || undefined,
    priority: answers.priority,
  });
 
  success(`تمت الإضافة: ${task.title} (#${task.id})`);
}
 
async function interactiveComplete(): Promise<void> {
  const tasks = getAllTasks().filter((t) => t.status !== "done");
 
  if (tasks.length === 0) {
    error("لا توجد مهام معلقة لإكمالها.");
    return;
  }
 
  const { taskId } = await inquirer.prompt([
    {
      type: "list",
      name: "taskId",
      message: "اختر المهمة لإكمالها:",
      choices: tasks.map((t) => ({
        name: `${t.title} [${t.priority}]`,
        value: t.id,
      })),
    },
  ]);
 
  const task = completeTask(taskId);
  if (task) {
    success(`تم الإكمال: ${task.title}`);
  }
}
 
async function interactiveRemove(): Promise<void> {
  const tasks = getAllTasks();
 
  if (tasks.length === 0) {
    error("لا توجد مهام لحذفها.");
    return;
  }
 
  const { taskId } = await inquirer.prompt([
    {
      type: "list",
      name: "taskId",
      message: "اختر المهمة لحذفها:",
      choices: tasks.map((t) => ({
        name: `${t.title} (${t.status})`,
        value: t.id,
      })),
    },
  ]);
 
  const { confirm } = await inquirer.prompt([
    {
      type: "confirm",
      name: "confirm",
      message: "هل أنت متأكد؟",
      default: false,
    },
  ]);
 
  if (confirm) {
    removeTask(taskId);
    success("تم حذف المهمة.");
  }
}

الخطوة 7: ربط نقطة دخول الأداة

أنشئ ملف src/cli.ts — نقطة الدخول الرئيسية:

#!/usr/bin/env node
 
import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import {
  addTask,
  getAllTasks,
  completeTask,
  removeTask,
  clearAllTasks,
} from "./store.js";
import { formatTaskList, formatTask, success, error, info } from "./formatter.js";
import { interactiveMode } from "./interactive.js";
 
const program = new Command();
 
program
  .name("taskr")
  .description("مدير مهام بسيط للطرفية")
  .version("1.0.0");
 
// الوضع التفاعلي (افتراضي عند عدم تحديد أمر)
program
  .action(async () => {
    console.log(chalk.bold("\n🚀 Taskr — مدير المهام في الطرفية\n"));
    await interactiveMode();
  });
 
// أمر الإضافة
program
  .command("add <title>")
  .description("إضافة مهمة جديدة")
  .option("-d, --description <desc>", "وصف المهمة")
  .option("-p, --priority <level>", "الأولوية: low, medium, high", "medium")
  .action((title: string, options) => {
    const task = addTask(title, {
      description: options.description,
      priority: options.priority,
    });
    success(`تمت الإضافة: ${task.title} ${chalk.dim(`(#${task.id})`)}`);
  });
 
// أمر العرض
program
  .command("list")
  .alias("ls")
  .description("عرض جميع المهام")
  .option("-s, --status <status>", "تصفية حسب الحالة: todo, in-progress, done")
  .option("-p, --priority <level>", "تصفية حسب الأولوية: low, medium, high")
  .action((options) => {
    let tasks = getAllTasks();
 
    if (options.status) {
      tasks = tasks.filter((t) => t.status === options.status);
    }
    if (options.priority) {
      tasks = tasks.filter((t) => t.priority === options.priority);
    }
 
    console.log(formatTaskList(tasks));
    console.log();
  });
 
// أمر الإكمال
program
  .command("done <id>")
  .description("تحديد مهمة كمكتملة")
  .action((id: string) => {
    const task = completeTask(id);
    if (task) {
      success(`تم الإكمال: ${task.title}`);
    } else {
      error(`المهمة غير موجودة: ${id}`);
    }
  });
 
// أمر الحذف
program
  .command("rm <id>")
  .description("حذف مهمة")
  .action((id: string) => {
    const removed = removeTask(id);
    if (removed) {
      success("تم حذف المهمة.");
    } else {
      error(`المهمة غير موجودة: ${id}`);
    }
  });
 
// أمر المسح
program
  .command("clear")
  .description("حذف جميع المهام")
  .action(() => {
    const spinner = ora("جارٍ مسح جميع المهام...").start();
    clearAllTasks();
    spinner.succeed("تم مسح جميع المهام.");
  });
 
program.parse();

الخطوة 8: إعداد الحزمة

حدّث ملف package.json لتهيئة أداة CLI:

{
  "name": "taskr-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "taskr": "./dist/cli.js"
  },
  "files": ["dist"],
  "scripts": {
    "dev": "tsx src/cli.ts",
    "build": "tsup src/cli.ts --format esm --dts --clean",
    "prepublishOnly": "npm run build"
  }
}

أنشئ ملف tsup.config.ts لإعدادات البناء:

import { defineConfig } from "tsup";
 
export default defineConfig({
  entry: ["src/cli.ts"],
  format: ["esm"],
  target: "node20",
  clean: true,
  dts: true,
  banner: {
    js: "#!/usr/bin/env node",
  },
});

الخطوة 9: الاختبار أثناء التطوير

شغّل الأداة في وضع التطوير باستخدام tsx:

# الوضع التفاعلي
npx tsx src/cli.ts
 
# إضافة مهمة
npx tsx src/cli.ts add "كتابة التوثيق" -p high
 
# عرض المهام
npx tsx src/cli.ts list
 
# إكمال مهمة (استخدم المعرّف القصير من القائمة)
npx tsx src/cli.ts done a1b2c3d4
 
# تصفية المهام
npx tsx src/cli.ts list --status todo --priority high

يمكنك أيضًا ربطها عالميًا للاختبار:

npm run build
npm link
taskr add "مهمتي الأولى"
taskr ls

الخطوة 10: إضافة اختبارات الوحدة

ثبّت إطار الاختبار:

npm install -D vitest

أنشئ ملف src/__tests__/store.test.ts:

import { describe, it, expect, beforeEach } from "vitest";
import { addTask, getAllTasks, completeTask, removeTask, clearAllTasks } from "../store.js";
 
describe("Task Store", () => {
  beforeEach(() => {
    clearAllTasks();
  });
 
  it("should add a task", () => {
    const task = addTask("مهمة اختبارية");
    expect(task.title).toBe("مهمة اختبارية");
    expect(task.status).toBe("todo");
    expect(task.priority).toBe("medium");
  });
 
  it("should list all tasks", () => {
    addTask("مهمة 1");
    addTask("مهمة 2");
    const tasks = getAllTasks();
    expect(tasks).toHaveLength(2);
  });
 
  it("should complete a task", () => {
    const task = addTask("مهمة اختبارية");
    const completed = completeTask(task.id);
    expect(completed?.status).toBe("done");
    expect(completed?.completedAt).toBeDefined();
  });
 
  it("should remove a task", () => {
    const task = addTask("مهمة اختبارية");
    const removed = removeTask(task.id);
    expect(removed).toBe(true);
    expect(getAllTasks()).toHaveLength(0);
  });
 
  it("should find tasks by partial ID", () => {
    const task = addTask("مهمة اختبارية");
    const shortId = task.id.slice(0, 4);
    const completed = completeTask(shortId);
    expect(completed?.title).toBe("مهمة اختبارية");
  });
});

أضف سكربت الاختبار إلى package.json:

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  }
}

الخطوة 11: التحضير للنشر على npm

تأكد من وجود البيانات الوصفية المطلوبة في package.json:

{
  "name": "taskr-cli",
  "version": "1.0.0",
  "description": "مدير مهام بسيط للطرفية",
  "type": "module",
  "bin": {
    "taskr": "./dist/cli.js"
  },
  "files": ["dist"],
  "keywords": ["cli", "task", "todo", "terminal", "productivity"],
  "author": "اسمك",
  "license": "MIT",
  "engines": {
    "node": ">=20.0.0"
  }
}

ابنِ وانشر:

npm run build
npm login
npm publish

بعد النشر، يمكن لأي شخص تثبيت واستخدام أداتك:

npx taskr-cli
# أو
npm install -g taskr-cli
taskr add "مرحبا بالعالم"
taskr ls

الخطوة 12: إضافة خط أنابيب CI عبر GitHub Actions

أنشئ ملف .github/workflows/ci.yml:

name: CI
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20, 22]
 
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test
      - run: npm run build

استكشاف الأخطاء وإصلاحها

أخطاء "Cannot find module" عند تشغيل الملفات المُجمّعة: تأكد من أن tsconfig.json يحتوي على "moduleResolution": "bundler" وأن جميع عمليات الاستيراد تستخدم امتداد .js (حتى لملفات .ts). يتطلب TypeScript هذا لمخرجات ESM.

"Permission denied" عند تشغيل الأداة عالميًا: بعد البناء، تأكد من أن ملف الإخراج قابل للتنفيذ:

chmod +x dist/cli.js

ألوان Chalk لا تظهر: بعض بيئات CI أو الطرفيات البسيطة لا تدعم الألوان. يكتشف Chalk هذا تلقائيًا، لكن يمكنك فرض الألوان بـ FORCE_COLOR=1.

الخطوات التالية

الآن بعد أن لديك أداة CLI تعمل، فكّر في هذه التحسينات:

  • إضافة مجموعات أوامر فرعية للأدوات المعقدة باستخدام أوامر Commander المتداخلة
  • تنفيذ إضافات عبر تحميل الوحدات ديناميكيًا من مجلد إعدادات
  • إضافة إكمال تلقائي للصدفة باستخدام حزمة omelette أو tabtab
  • إنشاء واجهة نصية (TUI) باستخدام blessed أو ink لتجربة أغنى
  • دعم صيغ إخراج متعددة (JSON، جدول، YAML) للتكامل مع السكربتات

الخلاصة

لقد بنيت أداة سطر أوامر احترافية كاملة بـ TypeScript — من إعداد المشروع وتحليل الأوامر إلى المطالبات التفاعلية والمخرجات الملوّنة والاختبار والنشر على npm. أدوات CLI هي طريقة قوية لأتمتة سير العمل ومشاركة الأدوات مع فريقك والمساهمة في منظومة المصادر المفتوحة. الأنماط التي تعلمتها هنا — Commander للتحليل، Chalk للمخرجات، Inquirer للتفاعل، و Conf للتخزين — تشكّل مجموعة الأدوات القياسية التي تستخدمها معظم أدوات CLI الشهيرة في Node.js اليوم.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على مقدمة إلى GPT-4o وGPT-4o mini.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة