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

تظل أدوات سطر الأوامر العمود الفقري لسير عمل المطورين. من 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 اليوم.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

AI SDK 4.0: الميزات الجديدة وحالات الاستخدام
اكتشف الميزات الجديدة وحالات الاستخدام لـ AI SDK 4.0، بما في ذلك دعم PDF واستخدام الكمبيوتر والمزيد.

بناء أداة استخراج بيانات ذكية من الويب باستخدام Playwright و Claude API في TypeScript
تعلّم كيف تبني أداة استخراج بيانات ذكية تستخدم Playwright للتحكم بالمتصفح وClaude AI لفهم صفحات الويب واستخراج بيانات منظّمة — بدون محددات CSS هشّة.

بناء واجهات REST API باستخدام Hono و Bun: البديل العصري لـ Express
تعلم كيفية بناء واجهات برمجية سريعة وآمنة باستخدام إطار Hono وبيئة تشغيل Bun. دليل شامل من الإعداد حتى النشر مع أمثلة عملية.