Examples
Git Tool CLI
Git workflow helper with command organization and external tool integration
Overview
This example demonstrates advanced CLI patterns:
- Flat command structure with options-based organization
- Command aliases for common operations
- External tool integration with git commands
- Shell command execution with proper error handling
- Colored output for status and feedback
- Command organization patterns for scalability
Commands
branch - Branch Management
bun run cli.ts branch --name feature/new-feature
bun run cli.ts branch --name feature/new-feature --switch
bun run cli.ts branch --name feature/old-feature --deleteBranch management with git integration:
- Create new branches with
--nameflag - Switch between branches with
--switchflag - Delete branches with
--deleteflag - Force operations with
--forceflag
pr - Pull Request Management
bun run cli.ts pr --title "Add new feature" --draft
bun run cli.ts pr --title "Fix bug" --base develop --head feature-branch
bun run cli.ts pr --title "Update docs" --reviewers "alice,bob"Pull request workflow automation:
- Create PRs with
--titleand optional--description - Specify
--baseand--headbranches - Create draft PRs with
--draft - Add
--reviewersand--labels - Interactive prompts for uncommitted changes
sync - Repository Synchronization
bun run cli.ts sync --remote origin --branch main
bun run cli.ts sync --rebase --prune
# alias: bun run cli.ts pull --remote origin --branch mainSync with remote repositories:
- Pull latest changes
- Push local commits (via interactive prompt)
- Rebase on upstream
- Prune stale remote branches
- Handle merge conflicts
status - Enhanced Git Status
bun run cli.ts status --detailed
bun run cli.ts status --branches --remote
bun run cli.ts status --history 10Enhanced git status with additional information:
- Detailed status with
--detailed - Branch information with
--branches - Remote tracking with
--remote - Commit history with
--history <count>
Key Features Demonstrated
1. Flat Command Structure with Options
The git-tool uses flat commands with options instead of nested subcommands:
import { defineCommand, option } from "@bunli/core";
import { z } from "zod";
export const branchCommand = defineCommand({
name: "branch" as const,
description: "Create, switch, or manage branches",
alias: "br",
options: {
name: option(
z
.string()
.min(1, "Branch name cannot be empty")
.regex(/^[a-zA-Z0-9._/-]+$/, "Invalid branch name format"),
{ short: "n", description: "Branch name" },
),
base: option(z.string().default("main"), {
short: "b",
description: "Base branch to create from",
}),
switch: option(z.coerce.boolean().default(false), {
short: "s",
description: "Switch to the branch after creating",
}),
delete: option(z.coerce.boolean().default(false), {
short: "d",
description: "Delete the branch",
}),
force: option(z.coerce.boolean().default(false), {
short: "f",
description: "Force the operation",
}),
},
handler: async ({ flags, colors, spinner, shell }) => {
const spin = spinner("Working with branches...");
try {
if (flags.delete) {
spin.update(`Deleting branch '${flags.name}'...`);
await shell`git branch ${flags.force ? "-D" : "-d"} ${flags.name}`;
spin.succeed(`Deleted branch '${flags.name}'`);
} else {
spin.update(`Creating branch '${flags.name}' from '${flags.base}'...`);
await shell`git checkout -b ${flags.name} ${flags.base}`;
spin.succeed(`Created branch '${flags.name}'`);
}
} catch (error) {
spin.fail("Branch operation failed");
console.error(colors.red(String(error)));
}
},
});2. External Tool Integration
The shell utility is provided via handler arguments, not imported:
handler: async ({ shell, colors, spinner }) => {
const spin = spinner("Fetching latest changes...");
try {
// Pull latest changes
await shell`git pull origin main`;
// Push local commits
await shell`git push origin main`;
spin.succeed("Repository synchronized");
} catch (error) {
spin.fail("Sync failed");
console.error(colors.red(String(error)));
process.exit(1);
}
};3. Command Aliases
export const prCommand = defineCommand({
name: "pr" as const,
alias: "pull-request",
description: "Create and manage pull requests",
options: {
title: option(z.string().min(1).max(100), { short: "t", description: "Pull request title" }),
description: option(z.string().optional(), {
short: "d",
description: "Pull request description",
}),
base: option(z.string().default("main"), {
short: "b",
description: "Base branch to merge into",
}),
draft: option(z.coerce.boolean().default(false), {
description: "Create as draft pull request",
}),
},
handler: async ({ flags, colors, spinner, shell, prompt }) => {
// ... implementation
},
});4. Colored Output and Status
// colors is provided via handler args, not imported
handler: async ({ shell, colors }) => {
// Get git status
const statusOutput = await shell`git status --porcelain`;
const lines = statusOutput.split("\n").filter(Boolean);
if (lines.length === 0) {
console.log(colors.green("✓ Working directory clean"));
return;
}
console.log(colors.cyan("Changes:"));
lines.forEach((line) => {
const status = line.substring(0, 2);
const file = line.substring(3);
if (status.includes("M")) {
console.log(colors.yellow(` M ${file}`));
} else if (status.includes("A")) {
console.log(colors.green(` A ${file}`));
} else if (status.includes("D")) {
console.log(colors.red(` D ${file}`));
}
});
};Project Structure
git-tool/
├── cli.ts # Main CLI file
├── commands/
│ ├── branch.ts # Branch management commands
│ ├── pr.ts # Pull request commands
│ ├── sync.ts # Repository synchronization
│ └── status.ts # Enhanced git status
├── bunli.config.ts # Build configuration
├── package.json # Dependencies and scripts
└── README.md # Example documentationRunning the Example
# Navigate to the example
cd examples/git-tool
# Install dependencies
bun install
# Run in development mode
bun run dev
# Try the commands
bun run cli.ts branch --help
bun run cli.ts branch --name feature/test
bun run cli.ts status --detailed
bun run cli.ts syncCommand Organization Patterns
1. Flat Commands with Options
Instead of nested subcommands, use flat commands with boolean and string options:
// Single command with options for different operations
export const branchCommand = defineCommand({
name: "branch" as const,
description: "Create, switch, or manage branches",
options: {
name: option(z.string(), { short: "n", description: "Branch name" }),
switch: option(z.boolean().default(false), { short: "s", description: "Switch to branch" }),
delete: option(z.boolean().default(false), { short: "d", description: "Delete branch" }),
force: option(z.boolean().default(false), { short: "f", description: "Force operation" }),
},
handler: async ({ flags, shell, colors, spinner }) => {
const spin = spinner("Working with branches...");
if (flags.delete) {
await shell`git branch ${flags.force ? "-D" : "-d"} ${flags.name}`;
spin.succeed(`Deleted branch '${flags.name}'`);
} else {
await shell`git checkout -b ${flags.name}`;
if (flags.switch) {
await shell`git checkout ${flags.name}`;
}
spin.succeed(`Created branch '${flags.name}'`);
}
},
});2. Inlined Shell Calls
Shell calls are inlined directly in command handlers rather than extracted to a shared utility file. This keeps each command self-contained:
handler: async ({ shell, colors, spinner }) => {
const spin = spinner("Checking repository...");
// Inline git calls — no separate utils file needed
try {
await shell`git rev-parse --git-dir`;
} catch {
spin.fail("Not a git repository");
console.error(colors.red("Not in a git repository"));
process.exit(1);
}
};3. Error Handling
handler: async ({ shell, colors, spinner }) => {
const spin = spinner("Creating branch...");
try {
// Validate git repository first
await shell`git rev-parse --git-dir`;
// Proceed with operation
await shell`git checkout -b feature-branch`;
spin.succeed("Created branch: feature-branch");
} catch (error) {
spin.fail("Failed to create branch");
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("already exists")) {
console.error(colors.yellow("Branch already exists"));
} else {
console.error(colors.red(msg));
}
process.exit(1);
}
};External Tool Integration
1. Shell Command Execution
The shell utility is provided via handler arguments:
// shell comes from handler args, not imported
handler: async ({ shell }) => {
// Simple command execution — returns stdout
const { stdout } = await shell`git status`;
console.log(stdout.toString());
// Command with options
const { stdout: logOutput } = await shell`git log --oneline -10`;
const commits = logOutput.toString().trim().split("\n").filter(Boolean);
// Error handling
try {
await shell`git push origin main`;
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("rejected")) {
console.error("Push rejected - need to pull first");
}
}
};2. Command Validation
handler: async ({ shell, colors }) => {
// Check if git is available
try {
await shell`git --version`;
} catch {
console.error(colors.red("Git is not installed or not in PATH"));
process.exit(1);
}
// Check if we're in a git repository
try {
await shell`git rev-parse --git-dir`;
} catch {
console.error(colors.red("Not in a git repository"));
process.exit(1);
}
// Continue with git operations
};Key Takeaways
- Command Organization: Flat command structure with options for complex CLIs
- External Integration: Working with existing tools and commands
- Error Handling: Graceful handling of external command failures
- User Experience: Colored output and clear feedback
- Scalability: Patterns that work for large CLI applications
- Real-world Patterns: Practical patterns for tool integration
Next Steps
- Dev Server Example - Learn plugin system
- Command Organization Guide - Advanced patterns
- External Tool Integration - Working with other tools
- Shell Integration Guide - Command execution patterns