Bunli
Examples

Task Runner CLI

Task automation CLI with validation and interactivity

Overview

This example consolidates validation and interactive patterns into a cohesive task automation tool:

  • Schema validation with Zod for type-safe options
  • Interactive prompts and confirmations for user input
  • Progress indicators and spinners for long-running tasks
  • Conditional flows based on command options
  • Error handling and user feedback

Commands

build - Build Project

bun run cli.ts build --env production

Builds the project with validation and transformation:

  • Environment selection (development/production)
  • Output directory configuration
  • Progress tracking with spinners
  • Validation of build configuration

test - Run Tests

bun run cli.ts test --pattern "**/*.test.ts" --coverage 80 --watch

Runs tests with filtering and coverage:

  • Test file pattern specification
  • Coverage reporting
  • Watch mode for development
  • Complex validation patterns

deploy - Deploy Application

bun run cli.ts deploy --environment production

Deploys with interactive confirmation:

  • Environment selection
  • Interactive confirmation prompts
  • Deployment validation
  • Rollback options

setup - Interactive Setup

bun run cli.ts setup

Interactive setup wizard:

  • Multi-step configuration
  • Dynamic prompts based on previous answers
  • Validation of user input
  • Configuration file generation

Key Features Demonstrated

1. Schema Validation

import { defineCommand, option } from "@bunli/core";
import { z } from "zod";

export const buildCommand = defineCommand({
  name: "build" as const,
  options: {
    env: option(z.enum(["development", "staging", "production"]).default("development"), {
      short: "e",
      description: "Build environment",
    }),
    outdir: option(z.string().min(1).default("dist"), {
      short: "o",
      description: "Output directory",
    }),
    config: option(
      z
        .string()
        .transform((val) => JSON.parse(val))
        .optional(),
      { short: "c", description: "JSON configuration object" },
    ),
    memory: option(
      z
        .string()
        .regex(/^\d+[kmg]?$/i)
        .optional()
        .transform((val) => {
          const raw = val ?? "512m";
          const num = parseInt(raw);
          const unit = raw.slice(-1).toLowerCase();
          const multipliers = { k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024 };
          return num * (multipliers[unit as keyof typeof multipliers] || 1);
        }),
      { short: "m", description: "Memory limit (e.g., 512m, 2g)" },
    ),
    variables: option(
      z
        .string()
        .transform((val) => {
          const vars: Record<string, string> = {};
          val.split(",").forEach((pair) => {
            const [key, value] = pair.split("=");
            if (key && value) vars[key.trim()] = value.trim();
          });
          return vars;
        })
        .optional(),
      { short: "v", description: "Environment variables (key1=value1,key2=value2)" },
    ),
    watch: option(z.coerce.boolean().default(false), {
      short: "w",
      description: "Watch for changes",
    }),
  },
  handler: async ({ flags, spinner, colors }) => {
    // Type-safe access to validated options
    const { env, outdir, memory, variables, watch } = flags;
    // ... implementation
  },
});

2. Interactive Prompts

// prompt is provided via handler args by Bunli

// Text input
const name = await prompt.text("Project name?");

// Selection
const framework = await prompt.select("Runtime framework?", {
  options: [
    { label: "Bun", value: "bun" },
    { label: "Node.js", value: "node" },
    { label: "Deno", value: "deno" },
  ],
});

// Confirmation
const confirmed = await prompt.confirm("Deploy to production?");

// Multi-select
const features = await prompt.multiselect("Features?", {
  options: [
    { label: "TypeScript", value: "typescript" },
    { label: "ESLint", value: "eslint" },
    { label: "Prettier", value: "prettier" },
    { label: "Testing", value: "testing" },
  ],
});

3. Progress Indicators

const spin = spinner("Building project...");

// Simulate build steps
const steps = ["Compiling", "Bundling", "Optimizing", "Writing"];
for (const step of steps) {
  spin.update(step);
  await new Promise((resolve) => setTimeout(resolve, 500));
}

spin.succeed("Build completed!");

4. Conditional Flows

handler: async ({ flags, prompt, colors }) => {
  if (flags.environment === "production") {
    const confirmed = await prompt.confirm(
      colors.red("⚠️  Deploy to production? This cannot be undone!"),
    );

    if (!confirmed) {
      console.log(colors.yellow("Deployment cancelled"));
      return;
    }
  }

  // Continue with deployment
};

Project Structure

task-runner/
├── cli.ts              # Main CLI file
├── commands/
│   ├── build.ts        # Build command with validation
│   ├── test.ts         # Test command with filtering
│   ├── deploy.ts       # Deploy command with prompts
│   └── setup.ts        # Interactive setup wizard
├── bunli.config.ts     # Build configuration
├── package.json        # Dependencies and scripts
└── README.md          # Example documentation

Running the Example

# Navigate to the example
cd examples/task-runner

# Install dependencies
bun install

# Run in development mode
bun run dev

# Try the commands
bun run cli.ts build --help
bun run cli.ts test --pattern "**/*.test.ts" --coverage 80
bun run cli.ts deploy --environment staging
bun run cli.ts setup

Validation Patterns

Complex Option Validation

options: {
  pattern: option(
    z.string().min(1).default('**/*.test.ts'),
    { short: 'p', description: 'Test file pattern' }
  ),
  coverage: option(
    z.coerce.number().min(0).max(100).default(80),
    { short: 'c', description: 'Minimum coverage percentage' }
  ),
  watch: option(
    z.coerce.boolean().default(false),
    { short: 'w', description: 'Watch for changes' }
  )
}

Runtime Validation

handler: async ({ flags, colors }) => {
  // Validate test files exist
  const testFiles = await glob("**/*.test.ts");
  if (testFiles.length === 0) {
    console.error(colors.red("No test files found"));
    process.exit(1);
  }

  // Validate coverage threshold
  if (flags.coverage < 50) {
    console.warn(colors.yellow("Low coverage threshold"));
  }
};

Interactive Patterns

Multi-step Setup

handler: async ({ prompt, colors, spinner }) => {
  console.log(colors.cyan("Welcome to the setup wizard!"));

  // Step 1: Project details
  const name = await prompt.text("Project name?");
  const description = await prompt.text("Description?");

  // Step 2: Runtime selection
  const framework = await prompt.select("Runtime framework?", {
    options: [
      { label: "Bun", value: "bun" },
      { label: "Node.js", value: "node" },
      { label: "Deno", value: "deno" },
    ],
  });

  // Step 3: Features
  const features = await prompt.multiselect("Features?", {
    options: [
      { label: "TypeScript", value: "typescript" },
      { label: "ESLint", value: "eslint" },
      { label: "Prettier", value: "prettier" },
      { label: "Testing", value: "testing" },
    ],
  });

  // Step 4: Confirmation
  console.log(colors.cyan("\nConfiguration:"));
  console.log(`Name: ${name}`);
  console.log(`Framework: ${framework}`);
  console.log(`Features: ${features.join(", ")}`);

  const confirmed = await prompt.confirm("Create project?");
  if (confirmed) {
    // Create project files
    const spin = spinner("Creating project...");
    spin.succeed("Project created successfully!");
  }
};

Key Takeaways

  1. Schema Validation: Type-safe options with runtime validation
  2. Interactive UX: User-friendly prompts and confirmations
  3. Progress Feedback: Spinners and progress indicators
  4. Conditional Logic: Smart flows based on user input
  5. Error Handling: Graceful error handling and user feedback
  6. Real-world Patterns: Practical patterns for task automation

Next Steps

On this page