Bunli
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 --delete

Branch management with git integration:

  • Create new branches with --name flag
  • Switch between branches with --switch flag
  • Delete branches with --delete flag
  • Force operations with --force flag

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 --title and optional --description
  • Specify --base and --head branches
  • Create draft PRs with --draft
  • Add --reviewers and --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 main

Sync 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 10

Enhanced 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 documentation

Running 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 sync

Command 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

  1. Command Organization: Flat command structure with options for complex CLIs
  2. External Integration: Working with existing tools and commands
  3. Error Handling: Graceful handling of external command failures
  4. User Experience: Colored output and clear feedback
  5. Scalability: Patterns that work for large CLI applications
  6. Real-world Patterns: Practical patterns for tool integration

Next Steps

On this page