cli-tool-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCLI Tool Development
CLI工具开发
Project Structure
项目结构
src/
index.ts # Entry point with shebang
cli.ts # Commander setup
commands/ # Command handlers
ui/ # Ink components (if interactive)
utils/ # Helpers
types.ts # Type definitionssrc/
index.ts # 带有shebang的入口文件
cli.ts # Commander配置
commands/ # 命令处理器
ui/ # Ink组件(若为交互式)
utils/ # 辅助工具
types.ts # 类型定义Commander.js Setup
Commander.js 配置
typescript
#!/usr/bin/env node
import { Command } from 'commander';
import packageJson from '../package.json' with { type: 'json' };
const program = new Command();
program
.name('mytool')
.description('My awesome CLI tool')
.version(packageJson.version);
program
.command('init')
.description('Initialize a new project')
.option('-t, --template <name>', 'Template to use', 'default')
.option('-f, --force', 'Overwrite existing files', false)
.action(async (options) => {
await initCommand(options);
});
program.parseAsync();typescript
#!/usr/bin/env node
import { Command } from 'commander';
import packageJson from '../package.json' with { type: 'json' };
const program = new Command();
program
.name('mytool')
.description('我的出色CLI工具')
.version(packageJson.version);
program
.command('init')
.description('初始化新项目')
.option('-t, --template <name>', '使用的模板', 'default')
.option('-f, --force', '覆盖现有文件', false)
.action(async (options) => {
await initCommand(options);
});
program.parseAsync();User Feedback with Chalk & Ora
使用Chalk & Ora实现用户反馈
typescript
import chalk from 'chalk';
import ora from 'ora';
// Status messages
console.log(chalk.green('✓') + ' Operation complete');
console.log(chalk.red('✗') + ' Operation failed');
console.log(chalk.yellow('⚠') + ' Warning message');
// Progress spinner
const spinner = ora('Loading...').start();
try {
await longOperation();
spinner.succeed('Done!');
} catch (error) {
spinner.fail('Failed');
}typescript
import chalk from 'chalk';
import ora from 'ora';
// 状态消息
console.log(chalk.green('✓') + ' 操作完成');
console.log(chalk.red('✗') + ' 操作失败');
console.log(chalk.yellow('⚠') + ' 警告消息');
// 进度加载动画
const spinner = ora('加载中...').start();
try {
await longOperation();
spinner.succeed('完成!');
} catch (error) {
spinner.fail('失败');
}Interactive Prompts with Enquirer
使用Enquirer实现交互式提示
typescript
import enquirer from 'enquirer';
const { name } = await enquirer.prompt<{ name: string }>({
type: 'input',
name: 'name',
message: 'Project name:',
validate: (v) => v.length > 0 || 'Name required',
});
const { confirm } = await enquirer.prompt<{ confirm: boolean }>({
type: 'confirm',
name: 'confirm',
message: 'Continue?',
initial: true,
});typescript
import enquirer from 'enquirer';
const { name } = await enquirer.prompt<{ name: string }>({
type: 'input',
name: 'name',
message: '项目名称:',
validate: (v) => v.length > 0 || '名称为必填项',
});
const { confirm } = await enquirer.prompt<{ confirm: boolean }>({
type: 'confirm',
name: 'confirm',
message: '继续吗?',
initial: true,
});Ink for Rich TUI
使用Ink构建丰富的TUI
tsx
import React, { useState } from 'react';
import { render, Box, Text, useInput } from 'ink';
function App() {
const [selected, setSelected] = useState(0);
const items = ['Option 1', 'Option 2', 'Option 3'];
useInput((input, key) => {
if (key.downArrow) setSelected(s => Math.min(s + 1, items.length - 1));
if (key.upArrow) setSelected(s => Math.max(s - 1, 0));
if (key.return) process.exit(0);
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={i} color={i === selected ? 'cyan' : undefined}>
{i === selected ? '>' : ' '} {item}
</Text>
))}
</Box>
);
}
render(<App />);tsx
import React, { useState } from 'react';
import { render, Box, Text, useInput } from 'ink';
function App() {
const [selected, setSelected] = useState(0);
const items = ['选项1', '选项2', '选项3'];
useInput((input, key) => {
if (key.downArrow) setSelected(s => Math.min(s + 1, items.length - 1));
if (key.upArrow) setSelected(s => Math.max(s - 1, 0));
if (key.return) process.exit(0);
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={i} color={i === selected ? 'cyan' : undefined}>
{i === selected ? '>' : ' '} {item}
</Text>
))}
</Box>
);
}
render(<App />);Configuration Management
配置管理
typescript
import fs from 'fs-extra';
import path from 'node:path';
import os from 'node:os';
const CONFIG_DIR = path.join(os.homedir(), '.mytool');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
async function loadConfig(): Promise<Config> {
await fs.ensureDir(CONFIG_DIR);
if (await fs.pathExists(CONFIG_FILE)) {
return fs.readJson(CONFIG_FILE);
}
return getDefaultConfig();
}
async function saveConfig(config: Config): Promise<void> {
await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
}typescript
import fs from 'fs-extra';
import path from 'node:path';
import os from 'node:os';
const CONFIG_DIR = path.join(os.homedir(), '.mytool');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
async function loadConfig(): Promise<Config> {
await fs.ensureDir(CONFIG_DIR);
if (await fs.pathExists(CONFIG_FILE)) {
return fs.readJson(CONFIG_FILE);
}
return getDefaultConfig();
}
async function saveConfig(config: Config): Promise<void> {
await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
}Error Handling
错误处理
typescript
// Graceful exit handling
process.on('SIGINT', () => {
console.log('\nCancelled');
process.exit(0);
});
// Top-level error handler
try {
await program.parseAsync();
} catch (error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}typescript
// 优雅退出处理
process.on('SIGINT', () => {
console.log('\n已取消');
process.exit(0);
});
// 顶层错误处理器
try {
await program.parseAsync();
} catch (error) {
console.error(chalk.red('错误:'), error.message);
process.exit(1);
}Package.json Setup
Package.json 配置
json
{
"bin": {
"mytool": "./dist/index.js"
},
"files": ["dist"],
"type": "module",
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsx src/index.ts"
}
}json
{
"bin": {
"mytool": "./dist/index.js"
},
"files": ["dist"],
"type": "module",
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsx src/index.ts"
}
}Best Practices
最佳实践
- Add shebang to entry file
#!/usr/bin/env node - Support both flags () and options (
-f)--force - Provide helpful error messages with suggestions
- Support and
--helpflags--version - Use exit codes: 0 for success, 1 for error
- Support piping and stdin when appropriate
- Respect environment variable
NO_COLOR
- 在入口文件中添加shebang
#!/usr/bin/env node - 同时支持短标志()和长选项(
-f)--force - 提供带有建议的友好错误提示
- 支持和
--help标志--version - 使用退出码:0表示成功,1表示错误
- 适当时支持管道和标准输入
- 遵循环境变量
NO_COLOR