بناء أدوات المطورين
بناء أدوات CLI مع AI
Project Goal: Build a production-ready CLI tool that developers will love using.
Why Build CLI Tools?
CLI tools are perfect for vibe coding because:
- Single-purpose - Focused functionality is easier for AI to implement
- No UI complexity - Text interface is simpler than GUI
- Composable - Works with pipes, scripts, and automation
- Developer-focused - Your target users understand technical tools
CLI Project Setup Prompt
Create a modern CLI tool with:
## Tech Stack
- Node.js with TypeScript
- Commander.js for argument parsing
- Inquirer.js for interactive prompts
- Chalk for colored output
- Ora for spinners
- tsx for running TypeScript directly
## Project Structure
/src /commands # Individual command implementations /lib # Shared utilities /utils # Helper functions index.ts # CLI entry point types.ts # TypeScript types /bin cli.js # Executable entry package.json # With "bin" field configured tsconfig.json # TypeScript config
## Initial Commands
1. Main command with --version and --help
2. Config management (init, get, set)
3. Core functionality commands
## Requirements
- Global installation support (npm i -g)
- Configuration file in ~/.config/[tool-name]/
- Colored output with proper exit codes
- Interactive mode and non-interactive flags
Project Initialization
// package.json
{
"name": "my-awesome-cli",
"version": "1.0.0",
"description": "A CLI tool built with AI assistance",
"type": "module",
"bin": {
"mycli": "./bin/cli.js"
},
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsup src/index.ts --format esm --dts",
"start": "node dist/index.js"
},
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"conf": "^13.0.1",
"inquirer": "^10.2.2",
"ora": "^8.1.1"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
"@types/node": "^22.9.0",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}
}
// bin/cli.js
#!/usr/bin/env node
import '../dist/index.js';
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
CLI Entry Point
// src/index.ts
import { Command } from 'commander';
import chalk from 'chalk';
import { initCommand } from './commands/init.js';
import { configCommand } from './commands/config.js';
import { runCommand } from './commands/run.js';
import { version } from '../package.json' assert { type: 'json' };
const program = new Command();
program
.name('mycli')
.description('A powerful CLI tool for developers')
.version(version, '-v, --version', 'Display version number');
// Register commands
program.addCommand(initCommand);
program.addCommand(configCommand);
program.addCommand(runCommand);
// Global error handling
program.exitOverride();
try {
await program.parseAsync(process.argv);
} catch (error) {
if (error instanceof Error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}
}
Configuration Management
// src/lib/config.ts
import Conf from 'conf';
import { z } from 'zod';
const configSchema = z.object({
apiKey: z.string().optional(),
defaultProject: z.string().optional(),
outputFormat: z.enum(['json', 'table', 'plain']).default('table'),
colorEnabled: z.boolean().default(true),
});
export type Config = z.infer<typeof configSchema>;
const defaults: Config = {
outputFormat: 'table',
colorEnabled: true,
};
export const config = new Conf<Config>({
projectName: 'my-awesome-cli',
defaults,
schema: {
apiKey: { type: 'string' },
defaultProject: { type: 'string' },
outputFormat: { type: 'string', enum: ['json', 'table', 'plain'] },
colorEnabled: { type: 'boolean' },
},
});
export function getConfig<K extends keyof Config>(key: K): Config[K] {
return config.get(key);
}
export function setConfig<K extends keyof Config>(
key: K,
value: Config[K]
): void {
config.set(key, value);
}
export function getAllConfig(): Config {
return config.store;
}
export function resetConfig(): void {
config.clear();
}
Interactive Commands
// src/commands/init.ts
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import { setConfig } from '../lib/config.js';
export const initCommand = new Command('init')
.description('Initialize configuration for the CLI')
.option('--api-key <key>', 'API key (can also set interactively)')
.option('--project <name>', 'Default project name')
.action(async (options) => {
console.log(chalk.blue('\n🚀 Welcome to MyCLI Setup!\n'));
const answers = await inquirer.prompt([
{
type: 'input',
name: 'apiKey',
message: 'Enter your API key:',
when: !options.apiKey,
validate: (input) =>
input.length > 0 || 'API key is required',
},
{
type: 'input',
name: 'defaultProject',
message: 'Default project name (optional):',
when: !options.project,
},
{
type: 'list',
name: 'outputFormat',
message: 'Preferred output format:',
choices: [
{ name: 'Table (readable)', value: 'table' },
{ name: 'JSON (machine-readable)', value: 'json' },
{ name: 'Plain text', value: 'plain' },
],
default: 'table',
},
{
type: 'confirm',
name: 'colorEnabled',
message: 'Enable colored output?',
default: true,
},
]);
const spinner = ora('Saving configuration...').start();
try {
// Merge CLI options with interactive answers
const apiKey = options.apiKey || answers.apiKey;
const defaultProject = options.project || answers.defaultProject;
if (apiKey) setConfig('apiKey', apiKey);
if (defaultProject) setConfig('defaultProject', defaultProject);
setConfig('outputFormat', answers.outputFormat);
setConfig('colorEnabled', answers.colorEnabled);
spinner.succeed('Configuration saved successfully!');
console.log(chalk.green('\n✓ MyCLI is ready to use!'));
console.log(chalk.dim('Run `mycli --help` to see available commands.\n'));
} catch (error) {
spinner.fail('Failed to save configuration');
throw error;
}
});
// src/commands/config.ts
import { Command } from 'commander';
import chalk from 'chalk';
import { getConfig, setConfig, getAllConfig, resetConfig } from '../lib/config.js';
export const configCommand = new Command('config')
.description('Manage CLI configuration');
configCommand
.command('get <key>')
.description('Get a configuration value')
.action((key) => {
const value = getConfig(key as any);
if (value === undefined) {
console.log(chalk.yellow(`Config key '${key}' is not set`));
} else {
console.log(value);
}
});
configCommand
.command('set <key> <value>')
.description('Set a configuration value')
.action((key, value) => {
// Parse boolean and number values
let parsedValue: any = value;
if (value === 'true') parsedValue = true;
else if (value === 'false') parsedValue = false;
else if (!isNaN(Number(value))) parsedValue = Number(value);
setConfig(key as any, parsedValue);
console.log(chalk.green(`✓ Set ${key} = ${parsedValue}`));
});
configCommand
.command('list')
.description('List all configuration values')
.action(() => {
const all = getAllConfig();
console.log(chalk.blue('\nCurrent Configuration:\n'));
for (const [key, value] of Object.entries(all)) {
const displayValue =
key === 'apiKey' && value
? `${String(value).slice(0, 8)}...`
: value ?? chalk.dim('(not set)');
console.log(` ${chalk.cyan(key)}: ${displayValue}`);
}
console.log();
});
configCommand
.command('reset')
.description('Reset all configuration to defaults')
.option('-y, --yes', 'Skip confirmation')
.action(async (options) => {
if (!options.yes) {
const { confirm } = await import('inquirer').then((m) =>
m.default.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to reset all configuration?',
default: false,
},
])
);
if (!confirm) {
console.log(chalk.yellow('Reset cancelled'));
return;
}
}
resetConfig();
console.log(chalk.green('✓ Configuration reset to defaults'));
});
Output Formatting
// src/lib/output.ts
import chalk from 'chalk';
import { getConfig } from './config.js';
interface TableColumn {
key: string;
header: string;
width?: number;
align?: 'left' | 'right' | 'center';
}
export function formatOutput<T extends Record<string, any>>(
data: T | T[],
columns?: TableColumn[]
): string {
const format = getConfig('outputFormat');
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'plain':
return formatPlain(data);
case 'table':
default:
return formatTable(Array.isArray(data) ? data : [data], columns);
}
}
function formatPlain<T extends Record<string, any>>(data: T | T[]): string {
const items = Array.isArray(data) ? data : [data];
return items
.map((item) =>
Object.entries(item)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
)
.join('\n---\n');
}
function formatTable<T extends Record<string, any>>(
data: T[],
columns?: TableColumn[]
): string {
if (data.length === 0) return chalk.dim('No data to display');
const cols =
columns ||
Object.keys(data[0]).map((key) => ({
key,
header: key.charAt(0).toUpperCase() + key.slice(1),
}));
// Calculate column widths
const widths = cols.map((col) => {
const headerWidth = col.header.length;
const dataWidth = Math.max(
...data.map((row) => String(row[col.key] ?? '').length)
);
return col.width || Math.max(headerWidth, dataWidth) + 2;
});
// Build header
const header = cols
.map((col, i) => chalk.bold(col.header.padEnd(widths[i])))
.join(' ');
const separator = widths.map((w) => '─'.repeat(w)).join('─');
// Build rows
const rows = data.map((row) =>
cols
.map((col, i) => {
const value = String(row[col.key] ?? '');
return value.padEnd(widths[i]);
})
.join(' ')
);
return [header, separator, ...rows].join('\n');
}
export function success(message: string): void {
const colored = getConfig('colorEnabled');
console.log(colored ? chalk.green(`✓ ${message}`) : `✓ ${message}`);
}
export function error(message: string): void {
const colored = getConfig('colorEnabled');
console.error(colored ? chalk.red(`✗ ${message}`) : `✗ ${message}`);
}
export function warn(message: string): void {
const colored = getConfig('colorEnabled');
console.warn(colored ? chalk.yellow(`⚠ ${message}`) : `⚠ ${message}`);
}
export function info(message: string): void {
const colored = getConfig('colorEnabled');
console.log(colored ? chalk.blue(`ℹ ${message}`) : `ℹ ${message}`);
}
Core Command Implementation
// src/commands/run.ts
import { Command } from 'commander';
import ora from 'ora';
import chalk from 'chalk';
import { getConfig } from '../lib/config.js';
import { formatOutput, error } from '../lib/output.js';
export const runCommand = new Command('run')
.description('Execute the main functionality')
.argument('<target>', 'Target to process')
.option('-o, --output <file>', 'Output file path')
.option('--dry-run', 'Show what would be done without executing')
.option('-q, --quiet', 'Suppress non-essential output')
.action(async (target, options) => {
const apiKey = getConfig('apiKey');
if (!apiKey) {
error('API key not configured. Run `mycli init` first.');
process.exit(1);
}
if (options.dryRun) {
console.log(chalk.yellow('Dry run mode - no changes will be made\n'));
}
const spinner = options.quiet ? null : ora(`Processing ${target}...`).start();
try {
// Simulate async operation
await new Promise((resolve) => setTimeout(resolve, 2000));
const results = [
{ id: 1, name: 'Result 1', status: 'success' },
{ id: 2, name: 'Result 2', status: 'success' },
{ id: 3, name: 'Result 3', status: 'pending' },
];
spinner?.succeed(`Processed ${target}`);
// Format and output results
const output = formatOutput(results, [
{ key: 'id', header: 'ID', width: 6 },
{ key: 'name', header: 'Name', width: 20 },
{ key: 'status', header: 'Status', width: 10 },
]);
console.log('\n' + output + '\n');
// Write to file if specified
if (options.output) {
const fs = await import('fs/promises');
await fs.writeFile(options.output, JSON.stringify(results, null, 2));
console.log(chalk.green(`Results written to ${options.output}`));
}
} catch (err) {
spinner?.fail('Processing failed');
error(err instanceof Error ? err.message : 'Unknown error');
process.exit(1);
}
});
Testing CLI Commands
// src/__tests__/commands.test.ts
import { execSync } from 'child_process';
import { describe, it, expect, beforeEach } from 'vitest';
const cli = (args: string) =>
execSync(`tsx src/index.ts ${args}`, { encoding: 'utf-8' });
describe('CLI Commands', () => {
it('shows help', () => {
const output = cli('--help');
expect(output).toContain('A powerful CLI tool');
});
it('shows version', () => {
const output = cli('--version');
expect(output).toMatch(/\d+\.\d+\.\d+/);
});
it('config list works', () => {
const output = cli('config list');
expect(output).toContain('Configuration');
});
});
Publishing to npm
# Build the project
npm run build
# Test locally
npm link
mycli --help
# Login to npm
npm login
# Publish
npm publish
# Or publish with access public (for scoped packages)
npm publish --access public
Key Takeaways
- Commander.js handles argument parsing and subcommands elegantly
- Inquirer.js creates professional interactive prompts
- Conf provides persistent configuration with validation
- Exit codes communicate success/failure to calling scripts
- Output formatting should support both human and machine consumption
بناء أدوات CLI مع AI
هدف المشروع: بناء أداة CLI جاهزة للإنتاج سيحب المطورون استخدامها.
لماذا نبني أدوات CLI؟
أدوات CLI مثالية للـ vibe coding لأن:
- غرض واحد - الوظائف المركزة أسهل للـ AI لتنفيذها
- لا تعقيد واجهة - واجهة النص أبسط من GUI
- قابلة للتركيب - تعمل مع الأنابيب والسكربتات والأتمتة
- موجهة للمطورين - المستخدمون المستهدفون يفهمون الأدوات التقنية
برومبت إعداد مشروع CLI
أنشئ أداة CLI حديثة مع:
## التقنيات
- Node.js مع TypeScript
- Commander.js لتحليل المعاملات
- Inquirer.js للمطالبات التفاعلية
- Chalk للمخرجات الملونة
- Ora للمؤشرات الدوارة
- tsx لتشغيل TypeScript مباشرة
## هيكل المشروع
/src /commands # تنفيذات الأوامر الفردية /lib # الأدوات المشتركة /utils # دوال المساعدة index.ts # نقطة دخول CLI types.ts # أنواع TypeScript /bin cli.js # الملف التنفيذي package.json # مع حقل "bin" مكوّن tsconfig.json # تكوين TypeScript
## الأوامر الأولية
1. الأمر الرئيسي مع --version و--help
2. إدارة التكوين (init، get، set)
3. أوامر الوظائف الأساسية
## المتطلبات
- دعم التثبيت العام (npm i -g)
- ملف تكوين في ~/.config/[tool-name]/
- مخرجات ملونة مع أكواد خروج صحيحة
- وضع تفاعلي وعلامات غير تفاعلية
تهيئة المشروع
// package.json
{
"name": "my-awesome-cli",
"version": "1.0.0",
"description": "أداة CLI مبنية بمساعدة AI",
"type": "module",
"bin": {
"mycli": "./bin/cli.js"
},
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsup src/index.ts --format esm --dts",
"start": "node dist/index.js"
},
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"conf": "^13.0.1",
"inquirer": "^10.2.2",
"ora": "^8.1.1"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
"@types/node": "^22.9.0",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}
}
// bin/cli.js
#!/usr/bin/env node
import '../dist/index.js';
نقطة دخول CLI
// src/index.ts
import { Command } from 'commander';
import chalk from 'chalk';
import { initCommand } from './commands/init.js';
import { configCommand } from './commands/config.js';
import { runCommand } from './commands/run.js';
import { version } from '../package.json' assert { type: 'json' };
const program = new Command();
program
.name('mycli')
.description('أداة CLI قوية للمطورين')
.version(version, '-v, --version', 'عرض رقم الإصدار');
// تسجيل الأوامر
program.addCommand(initCommand);
program.addCommand(configCommand);
program.addCommand(runCommand);
// معالجة الأخطاء العامة
program.exitOverride();
try {
await program.parseAsync(process.argv);
} catch (error) {
if (error instanceof Error) {
console.error(chalk.red('خطأ:'), error.message);
process.exit(1);
}
}
إدارة التكوين
// src/lib/config.ts
import Conf from 'conf';
import { z } from 'zod';
const configSchema = z.object({
apiKey: z.string().optional(),
defaultProject: z.string().optional(),
outputFormat: z.enum(['json', 'table', 'plain']).default('table'),
colorEnabled: z.boolean().default(true),
});
export type Config = z.infer<typeof configSchema>;
const defaults: Config = {
outputFormat: 'table',
colorEnabled: true,
};
export const config = new Conf<Config>({
projectName: 'my-awesome-cli',
defaults,
schema: {
apiKey: { type: 'string' },
defaultProject: { type: 'string' },
outputFormat: { type: 'string', enum: ['json', 'table', 'plain'] },
colorEnabled: { type: 'boolean' },
},
});
export function getConfig<K extends keyof Config>(key: K): Config[K] {
return config.get(key);
}
export function setConfig<K extends keyof Config>(
key: K,
value: Config[K]
): void {
config.set(key, value);
}
export function getAllConfig(): Config {
return config.store;
}
export function resetConfig(): void {
config.clear();
}
الأوامر التفاعلية
// src/commands/init.ts
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import { setConfig } from '../lib/config.js';
export const initCommand = new Command('init')
.description('تهيئة إعدادات CLI')
.option('--api-key <key>', 'مفتاح API (يمكن تعيينه تفاعلياً أيضاً)')
.option('--project <name>', 'اسم المشروع الافتراضي')
.action(async (options) => {
console.log(chalk.blue('\n🚀 مرحباً بك في إعداد MyCLI!\n'));
const answers = await inquirer.prompt([
{
type: 'input',
name: 'apiKey',
message: 'أدخل مفتاح API:',
when: !options.apiKey,
validate: (input) =>
input.length > 0 || 'مفتاح API مطلوب',
},
{
type: 'input',
name: 'defaultProject',
message: 'اسم المشروع الافتراضي (اختياري):',
when: !options.project,
},
{
type: 'list',
name: 'outputFormat',
message: 'تنسيق المخرجات المفضل:',
choices: [
{ name: 'جدول (قابل للقراءة)', value: 'table' },
{ name: 'JSON (للآلات)', value: 'json' },
{ name: 'نص عادي', value: 'plain' },
],
default: 'table',
},
{
type: 'confirm',
name: 'colorEnabled',
message: 'تفعيل المخرجات الملونة؟',
default: true,
},
]);
const spinner = ora('جاري حفظ التكوين...').start();
try {
const apiKey = options.apiKey || answers.apiKey;
const defaultProject = options.project || answers.defaultProject;
if (apiKey) setConfig('apiKey', apiKey);
if (defaultProject) setConfig('defaultProject', defaultProject);
setConfig('outputFormat', answers.outputFormat);
setConfig('colorEnabled', answers.colorEnabled);
spinner.succeed('تم حفظ التكوين بنجاح!');
console.log(chalk.green('\n✓ MyCLI جاهز للاستخدام!'));
console.log(chalk.dim('شغّل `mycli --help` لرؤية الأوامر المتاحة.\n'));
} catch (error) {
spinner.fail('فشل في حفظ التكوين');
throw error;
}
});
تنسيق المخرجات
// src/lib/output.ts
import chalk from 'chalk';
import { getConfig } from './config.js';
interface TableColumn {
key: string;
header: string;
width?: number;
align?: 'left' | 'right' | 'center';
}
export function formatOutput<T extends Record<string, any>>(
data: T | T[],
columns?: TableColumn[]
): string {
const format = getConfig('outputFormat');
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'plain':
return formatPlain(data);
case 'table':
default:
return formatTable(Array.isArray(data) ? data : [data], columns);
}
}
function formatPlain<T extends Record<string, any>>(data: T | T[]): string {
const items = Array.isArray(data) ? data : [data];
return items
.map((item) =>
Object.entries(item)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
)
.join('\n---\n');
}
function formatTable<T extends Record<string, any>>(
data: T[],
columns?: TableColumn[]
): string {
if (data.length === 0) return chalk.dim('لا توجد بيانات للعرض');
const cols =
columns ||
Object.keys(data[0]).map((key) => ({
key,
header: key.charAt(0).toUpperCase() + key.slice(1),
}));
// حساب عرض الأعمدة
const widths = cols.map((col) => {
const headerWidth = col.header.length;
const dataWidth = Math.max(
...data.map((row) => String(row[col.key] ?? '').length)
);
return col.width || Math.max(headerWidth, dataWidth) + 2;
});
// بناء الرأس
const header = cols
.map((col, i) => chalk.bold(col.header.padEnd(widths[i])))
.join(' ');
const separator = widths.map((w) => '─'.repeat(w)).join('─');
// بناء الصفوف
const rows = data.map((row) =>
cols
.map((col, i) => {
const value = String(row[col.key] ?? '');
return value.padEnd(widths[i]);
})
.join(' ')
);
return [header, separator, ...rows].join('\n');
}
export function success(message: string): void {
const colored = getConfig('colorEnabled');
console.log(colored ? chalk.green(`✓ ${message}`) : `✓ ${message}`);
}
export function error(message: string): void {
const colored = getConfig('colorEnabled');
console.error(colored ? chalk.red(`✗ ${message}`) : `✗ ${message}`);
}
النشر على npm
# بناء المشروع
npm run build
# اختبار محلياً
npm link
mycli --help
# تسجيل الدخول لـ npm
npm login
# النشر
npm publish
# أو النشر مع وصول عام (للحزم ذات النطاق)
npm publish --access public
النقاط الرئيسية
- Commander.js يتعامل مع تحليل المعاملات والأوامر الفرعية بأناقة
- Inquirer.js ينشئ مطالبات تفاعلية احترافية
- Conf يوفر تكويناً مستمراً مع التحقق
- أكواد الخروج تتواصل مع النجاح/الفشل للسكربتات المستدعية
- تنسيق المخرجات يجب أن يدعم الاستهلاك البشري والآلي