Examples

Complete examples and real-world use cases for building CLI applications

Examples

This section provides complete, real-world examples of CLI applications built with the Forge CLI framework. These examples demonstrate best practices, common patterns, and advanced features.

Simple CLI Application

Basic Todo CLI

A simple todo list manager demonstrating core CLI features:

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    "strconv"
    "time"
    
    "github.com/forge/cli"
)

type Todo struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    Completed   bool      `json:"completed"`
    CreatedAt   time.Time `json:"created_at"`
    CompletedAt *time.Time `json:"completed_at,omitempty"`
}

type TodoStore struct {
    todos    []Todo
    filePath string
    nextID   int
}

func NewTodoStore() *TodoStore {
    homeDir, _ := os.UserHomeDir()
    filePath := filepath.Join(homeDir, ".todos.json")
    
    store := &TodoStore{
        todos:    []Todo{},
        filePath: filePath,
        nextID:   1,
    }
    
    store.load()
    return store
}

func (s *TodoStore) load() error {
    data, err := os.ReadFile(s.filePath)
    if err != nil {
        if os.IsNotExist(err) {
            return nil // File doesn't exist yet
        }
        return err
    }
    
    var todos []Todo
    if err := json.Unmarshal(data, &todos); err != nil {
        return err
    }
    
    s.todos = todos
    
    // Find next ID
    for _, todo := range todos {
        if todo.ID >= s.nextID {
            s.nextID = todo.ID + 1
        }
    }
    
    return nil
}

func (s *TodoStore) save() error {
    data, err := json.MarshalIndent(s.todos, "", "  ")
    if err != nil {
        return err
    }
    
    return os.WriteFile(s.filePath, data, 0644)
}

func (s *TodoStore) add(title, description string) *Todo {
    todo := Todo{
        ID:          s.nextID,
        Title:       title,
        Description: description,
        Completed:   false,
        CreatedAt:   time.Now(),
    }
    
    s.todos = append(s.todos, todo)
    s.nextID++
    
    return &todo
}

func (s *TodoStore) complete(id int) (*Todo, error) {
    for i := range s.todos {
        if s.todos[i].ID == id {
            if s.todos[i].Completed {
                return nil, fmt.Errorf("todo %d is already completed", id)
            }
            
            now := time.Now()
            s.todos[i].Completed = true
            s.todos[i].CompletedAt = &now
            return &s.todos[i], nil
        }
    }
    
    return nil, fmt.Errorf("todo %d not found", id)
}

func (s *TodoStore) delete(id int) error {
    for i, todo := range s.todos {
        if todo.ID == id {
            s.todos = append(s.todos[:i], s.todos[i+1:]...)
            return nil
        }
    }
    
    return fmt.Errorf("todo %d not found", id)
}

func (s *TodoStore) list(showCompleted bool) []Todo {
    var result []Todo
    for _, todo := range s.todos {
        if showCompleted || !todo.Completed {
            result = append(result, todo)
        }
    }
    return result
}

func main() {
    store := NewTodoStore()
    
    app := cli.New(cli.Config{
        Name:        "todo",
        Description: "A simple todo list manager",
        Version:     "1.0.0",
    })
    
    // Add command
    app.AddCommand(
        cli.NewCommand("add").
            WithDescription("Add a new todo item").
            WithHandler(func(ctx cli.CommandContext) error {
                title, err := ctx.Prompt("Todo title:")
                if err != nil {
                    return err
                }
                
                description, err := ctx.PromptWithDefault("Description (optional):", "")
                if err != nil {
                    return err
                }
                
                todo := store.add(title, description)
                if err := store.save(); err != nil {
                    return fmt.Errorf("failed to save: %v", err)
                }
                
                ctx.Success(fmt.Sprintf("✅ Added todo #%d: %s", todo.ID, todo.Title))
                return nil
            }),
    )
    
    // List command
    app.AddCommand(
        cli.NewCommand("list").
            WithDescription("List todo items").
            WithAlias("ls").
            WithFlags(
                cli.BoolFlag("all", "Show completed todos").
                    WithAlias("a"),
            ).
            WithHandler(func(ctx cli.CommandContext) error {
                showCompleted := ctx.Bool("all")
                todos := store.list(showCompleted)
                
                if len(todos) == 0 {
                    ctx.Info("No todos found")
                    return nil
                }
                
                table := ctx.Table()
                table.SetHeaders([]string{"ID", "Title", "Status", "Created"})
                
                for _, todo := range todos {
                    status := "⏳ Pending"
                    if todo.Completed {
                        status = "✅ Completed"
                    }
                    
                    table.AddRow([]string{
                        strconv.Itoa(todo.ID),
                        todo.Title,
                        status,
                        todo.CreatedAt.Format("2006-01-02 15:04"),
                    })
                }
                
                table.Render()
                return nil
            }),
    )
    
    // Complete command
    app.AddCommand(
        cli.NewCommand("complete").
            WithDescription("Mark a todo as completed").
            WithAlias("done").
            WithArgs(cli.Arg{
                Name:        "id",
                Description: "Todo ID to complete",
                Required:    true,
            }).
            WithHandler(func(ctx cli.CommandContext) error {
                idStr := ctx.Args()[0]
                id, err := strconv.Atoi(idStr)
                if err != nil {
                    return fmt.Errorf("invalid todo ID: %s", idStr)
                }
                
                todo, err := store.complete(id)
                if err != nil {
                    return err
                }
                
                if err := store.save(); err != nil {
                    return fmt.Errorf("failed to save: %v", err)
                }
                
                ctx.Success(fmt.Sprintf("✅ Completed todo #%d: %s", todo.ID, todo.Title))
                return nil
            }),
    )
    
    // Delete command
    app.AddCommand(
        cli.NewCommand("delete").
            WithDescription("Delete a todo item").
            WithAlias("rm").
            WithArgs(cli.Arg{
                Name:        "id",
                Description: "Todo ID to delete",
                Required:    true,
            }).
            WithHandler(func(ctx cli.CommandContext) error {
                idStr := ctx.Args()[0]
                id, err := strconv.Atoi(idStr)
                if err != nil {
                    return fmt.Errorf("invalid todo ID: %s", idStr)
                }
                
                confirmed, err := ctx.Confirm(fmt.Sprintf("Delete todo #%d?", id))
                if err != nil {
                    return err
                }
                
                if !confirmed {
                    ctx.Info("Deletion cancelled")
                    return nil
                }
                
                if err := store.delete(id); err != nil {
                    return err
                }
                
                if err := store.save(); err != nil {
                    return fmt.Errorf("failed to save: %v", err)
                }
                
                ctx.Success(fmt.Sprintf("🗑️  Deleted todo #%d", id))
                return nil
            }),
    )
    
    if err := app.Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

File Management CLI

Advanced File Operations

A file management CLI with advanced features:

package main

import (
    "crypto/md5"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "strings"
    "time"
    
    "github.com/forge/cli"
)

type FileInfo struct {
    Path     string
    Size     int64
    ModTime  time.Time
    IsDir    bool
    Checksum string
}

func main() {
    app := cli.New(cli.Config{
        Name:        "fmgr",
        Description: "Advanced file management CLI",
        Version:     "1.0.0",
    })
    
    // Copy command with progress
    app.AddCommand(
        cli.NewCommand("copy").
            WithDescription("Copy files with progress indication").
            WithAlias("cp").
            WithArgs(
                cli.Arg{Name: "source", Description: "Source file or directory", Required: true},
                cli.Arg{Name: "destination", Description: "Destination path", Required: true},
            ).
            WithFlags(
                cli.BoolFlag("recursive", "Copy directories recursively").WithAlias("r"),
                cli.BoolFlag("preserve", "Preserve file attributes").WithAlias("p"),
                cli.BoolFlag("verify", "Verify copy with checksums").WithAlias("v"),
            ).
            WithHandler(copyHandler),
    )
    
    // Find command with filters
    app.AddCommand(
        cli.NewCommand("find").
            WithDescription("Find files with advanced filtering").
            WithArgs(cli.Arg{Name: "path", Description: "Search path", Required: true}).
            WithFlags(
                cli.StringFlag("name", "File name pattern").WithAlias("n"),
                cli.StringFlag("type", "File type (f=file, d=directory)").WithAlias("t"),
                cli.StringFlag("size", "Size filter (e.g., +1M, -100K)").WithAlias("s"),
                cli.IntFlag("days", "Modified within N days").WithAlias("d"),
                cli.BoolFlag("empty", "Find empty files/directories").WithAlias("e"),
            ).
            WithHandler(findHandler),
    )
    
    // Duplicate finder
    app.AddCommand(
        cli.NewCommand("duplicates").
            WithDescription("Find duplicate files").
            WithAlias("dupes").
            WithArgs(cli.Arg{Name: "path", Description: "Search path", Required: true}).
            WithFlags(
                cli.BoolFlag("delete", "Delete duplicates interactively").WithAlias("d"),
                cli.StringFlag("output", "Output format (table, json)").WithDefault("table"),
            ).
            WithHandler(duplicatesHandler),
    )
    
    // Disk usage analyzer
    app.AddCommand(
        cli.NewCommand("usage").
            WithDescription("Analyze disk usage").
            WithAlias("du").
            WithArgs(cli.Arg{Name: "path", Description: "Path to analyze", Required: true}).
            WithFlags(
                cli.IntFlag("depth", "Maximum depth to analyze").WithDefault(3),
                cli.BoolFlag("sort", "Sort by size").WithAlias("s"),
                cli.StringFlag("format", "Output format (tree, table)").WithDefault("tree"),
            ).
            WithHandler(usageHandler),
    )
    
    if err := app.Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

func copyHandler(ctx cli.CommandContext) error {
    source := ctx.Args()[0]
    dest := ctx.Args()[1]
    recursive := ctx.Bool("recursive")
    verify := ctx.Bool("verify")
    
    // Check if source exists
    sourceInfo, err := os.Stat(source)
    if err != nil {
        return fmt.Errorf("source not found: %v", err)
    }
    
    if sourceInfo.IsDir() && !recursive {
        return fmt.Errorf("source is a directory, use --recursive flag")
    }
    
    // Calculate total size for progress
    totalSize, fileCount, err := calculateSize(source, recursive)
    if err != nil {
        return fmt.Errorf("failed to calculate size: %v", err)
    }
    
    ctx.Info(fmt.Sprintf("Copying %d files (%s)...", fileCount, formatBytes(totalSize)))
    
    progress := ctx.ProgressBar(fileCount)
    var copiedSize int64
    
    err = copyWithProgress(source, dest, recursive, verify, func(file string, size int64) {
        copiedSize += size
        progress.SetMessage(fmt.Sprintf("Copying %s (%s/%s)", 
            filepath.Base(file), 
            formatBytes(copiedSize), 
            formatBytes(totalSize)))
        progress.Increment()
    })
    
    if err != nil {
        return fmt.Errorf("copy failed: %v", err)
    }
    
    progress.Finish("Copy completed successfully!")
    
    if verify {
        ctx.Info("Verifying checksums...")
        if err := verifyChecksums(source, dest, recursive); err != nil {
            return fmt.Errorf("verification failed: %v", err)
        }
        ctx.Success("✅ Verification passed!")
    }
    
    return nil
}

func findHandler(ctx cli.CommandContext) error {
    searchPath := ctx.Args()[0]
    namePattern := ctx.String("name")
    fileType := ctx.String("type")
    sizeFilter := ctx.String("size")
    days := ctx.Int("days")
    empty := ctx.Bool("empty")
    
    spinner := ctx.Spinner("Searching files...")
    
    var results []FileInfo
    err := filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return nil // Skip errors
        }
        
        // Apply filters
        if namePattern != "" && !matchPattern(info.Name(), namePattern) {
            return nil
        }
        
        if fileType != "" {
            if fileType == "f" && info.IsDir() {
                return nil
            }
            if fileType == "d" && !info.IsDir() {
                return nil
            }
        }
        
        if sizeFilter != "" && !matchSize(info.Size(), sizeFilter) {
            return nil
        }
        
        if days > 0 && time.Since(info.ModTime()) > time.Duration(days)*24*time.Hour {
            return nil
        }
        
        if empty && ((info.IsDir() && !isDirEmpty(path)) || (!info.IsDir() && info.Size() > 0)) {
            return nil
        }
        
        results = append(results, FileInfo{
            Path:    path,
            Size:    info.Size(),
            ModTime: info.ModTime(),
            IsDir:   info.IsDir(),
        })
        
        return nil
    })
    
    spinner.Stop(fmt.Sprintf("Found %d files", len(results)))
    
    if err != nil {
        return fmt.Errorf("search failed: %v", err)
    }
    
    if len(results) == 0 {
        ctx.Info("No files found matching criteria")
        return nil
    }
    
    // Display results
    table := ctx.Table()
    table.SetHeaders([]string{"Path", "Type", "Size", "Modified"})
    
    for _, file := range results {
        fileType := "File"
        if file.IsDir {
            fileType = "Directory"
        }
        
        table.AddRow([]string{
            file.Path,
            fileType,
            formatBytes(file.Size),
            file.ModTime.Format("2006-01-02 15:04"),
        })
    }
    
    table.Render()
    return nil
}

func duplicatesHandler(ctx cli.CommandContext) error {
    searchPath := ctx.Args()[0]
    deleteMode := ctx.Bool("delete")
    outputFormat := ctx.String("output")
    
    ctx.Info("Scanning for duplicate files...")
    spinner := ctx.Spinner("Calculating checksums...")
    
    // Map of checksum to file paths
    checksums := make(map[string][]string)
    
    err := filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
        if err != nil || info.IsDir() {
            return nil
        }
        
        checksum, err := calculateChecksum(path)
        if err != nil {
            return nil // Skip files we can't read
        }
        
        checksums[checksum] = append(checksums[checksum], path)
        return nil
    })
    
    spinner.Stop("Scan complete")
    
    if err != nil {
        return fmt.Errorf("scan failed: %v", err)
    }
    
    // Find duplicates
    var duplicateGroups [][]string
    for _, paths := range checksums {
        if len(paths) > 1 {
            duplicateGroups = append(duplicateGroups, paths)
        }
    }
    
    if len(duplicateGroups) == 0 {
        ctx.Success("No duplicate files found!")
        return nil
    }
    
    ctx.Warning(fmt.Sprintf("Found %d groups of duplicate files", len(duplicateGroups)))
    
    if outputFormat == "json" {
        return ctx.OutputJSON(map[string]interface{}{
            "duplicate_groups": duplicateGroups,
            "total_groups":     len(duplicateGroups),
        })
    }
    
    // Display duplicates
    for i, group := range duplicateGroups {
        ctx.Info(fmt.Sprintf("\nDuplicate group %d:", i+1))
        
        table := ctx.Table()
        table.SetHeaders([]string{"Path", "Size", "Modified"})
        
        for _, path := range group {
            info, _ := os.Stat(path)
            table.AddRow([]string{
                path,
                formatBytes(info.Size()),
                info.ModTime().Format("2006-01-02 15:04"),
            })
        }
        
        table.Render()
        
        if deleteMode {
            // Interactive deletion
            keep, err := ctx.Select("Which file to keep?", group)
            if err != nil {
                continue
            }
            
            for _, path := range group {
                if path != keep {
                    confirmed, err := ctx.Confirm(fmt.Sprintf("Delete %s?", path))
                    if err != nil || !confirmed {
                        continue
                    }
                    
                    if err := os.Remove(path); err != nil {
                        ctx.Error(fmt.Sprintf("Failed to delete %s: %v", path, err))
                    } else {
                        ctx.Success(fmt.Sprintf("Deleted %s", path))
                    }
                }
            }
        }
    }
    
    return nil
}

func usageHandler(ctx cli.CommandContext) error {
    path := ctx.Args()[0]
    maxDepth := ctx.Int("depth")
    sortBySize := ctx.Bool("sort")
    format := ctx.String("format")
    
    spinner := ctx.Spinner("Analyzing disk usage...")
    
    usage, err := analyzeDiskUsage(path, maxDepth)
    if err != nil {
        return fmt.Errorf("analysis failed: %v", err)
    }
    
    spinner.Stop("Analysis complete")
    
    if sortBySize {
        // Sort by size (implementation omitted for brevity)
    }
    
    if format == "table" {
        table := ctx.Table()
        table.SetHeaders([]string{"Path", "Size", "Files", "Directories"})
        
        for _, item := range usage {
            table.AddRow([]string{
                item.Path,
                formatBytes(item.Size),
                fmt.Sprintf("%d", item.FileCount),
                fmt.Sprintf("%d", item.DirCount),
            })
        }
        
        table.Render()
    } else {
        // Tree format (implementation omitted for brevity)
        displayUsageTree(ctx, usage, 0)
    }
    
    return nil
}

// Helper functions (implementations omitted for brevity)
func calculateSize(path string, recursive bool) (int64, int, error) {
    // Implementation
    return 0, 0, nil
}

func copyWithProgress(source, dest string, recursive, verify bool, callback func(string, int64)) error {
    // Implementation
    return nil
}

func verifyChecksums(source, dest string, recursive bool) error {
    // Implementation
    return nil
}

func matchPattern(name, pattern string) bool {
    // Implementation
    return strings.Contains(strings.ToLower(name), strings.ToLower(pattern))
}

func matchSize(size int64, filter string) bool {
    // Implementation
    return true
}

func isDirEmpty(path string) bool {
    // Implementation
    return false
}

func calculateChecksum(path string) (string, error) {
    file, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer file.Close()
    
    hash := md5.New()
    if _, err := io.Copy(hash, file); err != nil {
        return "", err
    }
    
    return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

func analyzeDiskUsage(path string, maxDepth int) ([]UsageInfo, error) {
    // Implementation
    return nil, nil
}

func displayUsageTree(ctx cli.CommandContext, usage []UsageInfo, depth int) {
    // Implementation
}

func formatBytes(bytes int64) string {
    const unit = 1024
    if bytes < unit {
        return fmt.Sprintf("%d B", bytes)
    }
    div, exp := int64(unit), 0
    for n := bytes / unit; n >= unit; n /= unit {
        div *= unit
        exp++
    }
    return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}

type UsageInfo struct {
    Path      string
    Size      int64
    FileCount int
    DirCount  int
}

API Client CLI

REST API Client with Authentication

A comprehensive API client CLI:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strings"
    "time"
    
    "github.com/forge/cli"
)

type APIClient struct {
    baseURL    string
    token      string
    httpClient *http.Client
}

type Config struct {
    BaseURL string `json:"base_url"`
    Token   string `json:"token"`
}

func main() {
    app := cli.New(cli.Config{
        Name:        "api-cli",
        Description: "REST API client with authentication",
        Version:     "1.0.0",
    })
    
    // Global flags
    app.AddGlobalFlags(
        cli.StringFlag("config", "Configuration file path").
            WithDefault(getDefaultConfigPath()),
        cli.StringFlag("base-url", "API base URL").
            WithAlias("u"),
        cli.StringFlag("token", "Authentication token").
            WithAlias("t"),
        cli.StringFlag("format", "Output format (json, table, yaml)").
            WithDefault("json").
            WithAlias("f"),
        cli.BoolFlag("verbose", "Verbose output").
            WithAlias("v"),
    )
    
    // Auth commands
    authCmd := cli.NewCommand("auth").
        WithDescription("Authentication commands").
        WithSubcommands(
            cli.NewCommand("login").
                WithDescription("Login and save credentials").
                WithHandler(loginHandler),
            
            cli.NewCommand("logout").
                WithDescription("Logout and clear credentials").
                WithHandler(logoutHandler),
            
            cli.NewCommand("status").
                WithDescription("Check authentication status").
                WithHandler(authStatusHandler),
        )
    
    // API commands
    getCmd := cli.NewCommand("get").
        WithDescription("GET request to API endpoint").
        WithArgs(cli.Arg{Name: "endpoint", Description: "API endpoint", Required: true}).
        WithFlags(
            cli.StringSliceFlag("header", "Additional headers (key:value)").
                WithAlias("H"),
            cli.StringFlag("query", "Query parameters (JSON)").
                WithAlias("q"),
        ).
        WithHandler(getHandler)
    
    postCmd := cli.NewCommand("post").
        WithDescription("POST request to API endpoint").
        WithArgs(cli.Arg{Name: "endpoint", Description: "API endpoint", Required: true}).
        WithFlags(
            cli.StringSliceFlag("header", "Additional headers (key:value)").
                WithAlias("H"),
            cli.StringFlag("data", "Request body (JSON)").
                WithAlias("d"),
            cli.StringFlag("file", "Read request body from file").
                WithAlias("f"),
        ).
        WithHandler(postHandler)
    
    putCmd := cli.NewCommand("put").
        WithDescription("PUT request to API endpoint").
        WithArgs(cli.Arg{Name: "endpoint", Description: "API endpoint", Required: true}).
        WithFlags(
            cli.StringSliceFlag("header", "Additional headers (key:value)").
                WithAlias("H"),
            cli.StringFlag("data", "Request body (JSON)").
                WithAlias("d"),
            cli.StringFlag("file", "Read request body from file").
                WithAlias("f"),
        ).
        WithHandler(putHandler)
    
    deleteCmd := cli.NewCommand("delete").
        WithDescription("DELETE request to API endpoint").
        WithArgs(cli.Arg{Name: "endpoint", Description: "API endpoint", Required: true}).
        WithFlags(
            cli.StringSliceFlag("header", "Additional headers (key:value)").
                WithAlias("H"),
        ).
        WithHandler(deleteHandler)
    
    // Utility commands
    configCmd := cli.NewCommand("config").
        WithDescription("Configuration management").
        WithSubcommands(
            cli.NewCommand("show").
                WithDescription("Show current configuration").
                WithHandler(configShowHandler),
            
            cli.NewCommand("set").
                WithDescription("Set configuration value").
                WithArgs(
                    cli.Arg{Name: "key", Description: "Configuration key", Required: true},
                    cli.Arg{Name: "value", Description: "Configuration value", Required: true},
                ).
                WithHandler(configSetHandler),
        )
    
    app.AddCommands(authCmd, getCmd, postCmd, putCmd, deleteCmd, configCmd)
    
    if err := app.Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

func loginHandler(ctx cli.CommandContext) error {
    baseURL, err := ctx.Prompt("API Base URL:")
    if err != nil {
        return err
    }
    
    username, err := ctx.Prompt("Username:")
    if err != nil {
        return err
    }
    
    password, err := ctx.PromptPassword("Password:")
    if err != nil {
        return err
    }
    
    // Authenticate
    spinner := ctx.Spinner("Authenticating...")
    
    client := &APIClient{
        baseURL:    baseURL,
        httpClient: &http.Client{Timeout: 30 * time.Second},
    }
    
    token, err := client.authenticate(username, password)
    if err != nil {
        spinner.Stop("Authentication failed")
        return fmt.Errorf("authentication failed: %v", err)
    }
    
    spinner.Stop("Authentication successful")
    
    // Save configuration
    config := Config{
        BaseURL: baseURL,
        Token:   token,
    }
    
    if err := saveConfig(ctx.String("config"), config); err != nil {
        return fmt.Errorf("failed to save config: %v", err)
    }
    
    ctx.Success("✅ Logged in successfully!")
    return nil
}

func logoutHandler(ctx cli.CommandContext) error {
    configPath := ctx.String("config")
    
    if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
        return fmt.Errorf("failed to remove config: %v", err)
    }
    
    ctx.Success("✅ Logged out successfully!")
    return nil
}

func authStatusHandler(ctx cli.CommandContext) error {
    config, err := loadConfig(ctx.String("config"))
    if err != nil {
        ctx.Warning("Not logged in")
        return nil
    }
    
    client := newAPIClient(config)
    
    spinner := ctx.Spinner("Checking authentication status...")
    
    valid, user, err := client.validateToken()
    if err != nil {
        spinner.Stop("Status check failed")
        return fmt.Errorf("failed to check status: %v", err)
    }
    
    spinner.Stop("Status check complete")
    
    if valid {
        ctx.Success(fmt.Sprintf("✅ Authenticated as %s", user))
        ctx.Info(fmt.Sprintf("Base URL: %s", config.BaseURL))
    } else {
        ctx.Warning("❌ Authentication expired")
    }
    
    return nil
}

func getHandler(ctx cli.CommandContext) error {
    return makeRequest(ctx, "GET", ctx.Args()[0], nil)
}

func postHandler(ctx cli.CommandContext) error {
    body, err := getRequestBody(ctx)
    if err != nil {
        return err
    }
    
    return makeRequest(ctx, "POST", ctx.Args()[0], body)
}

func putHandler(ctx cli.CommandContext) error {
    body, err := getRequestBody(ctx)
    if err != nil {
        return err
    }
    
    return makeRequest(ctx, "PUT", ctx.Args()[0], body)
}

func deleteHandler(ctx cli.CommandContext) error {
    endpoint := ctx.Args()[0]
    
    confirmed, err := ctx.Confirm(fmt.Sprintf("Delete %s?", endpoint))
    if err != nil {
        return err
    }
    
    if !confirmed {
        ctx.Info("Operation cancelled")
        return nil
    }
    
    return makeRequest(ctx, "DELETE", endpoint, nil)
}

func makeRequest(ctx cli.CommandContext, method, endpoint string, body []byte) error {
    config, err := loadConfig(ctx.String("config"))
    if err != nil {
        return fmt.Errorf("not logged in, use 'auth login' first")
    }
    
    // Override config with command-line flags
    if baseURL := ctx.String("base-url"); baseURL != "" {
        config.BaseURL = baseURL
    }
    if token := ctx.String("token"); token != "" {
        config.Token = token
    }
    
    client := newAPIClient(config)
    
    // Parse additional headers
    headers := make(map[string]string)
    for _, header := range ctx.StringSlice("header") {
        parts := strings.SplitN(header, ":", 2)
        if len(parts) == 2 {
            headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
        }
    }
    
    // Parse query parameters
    var queryParams map[string]interface{}
    if queryStr := ctx.String("query"); queryStr != "" {
        if err := json.Unmarshal([]byte(queryStr), &queryParams); err != nil {
            return fmt.Errorf("invalid query JSON: %v", err)
        }
    }
    
    verbose := ctx.Bool("verbose")
    if verbose {
        ctx.Info(fmt.Sprintf("Making %s request to %s", method, endpoint))
    }
    
    spinner := ctx.Spinner(fmt.Sprintf("Making %s request...", method))
    
    response, err := client.makeRequest(method, endpoint, body, headers, queryParams)
    if err != nil {
        spinner.Stop("Request failed")
        return fmt.Errorf("request failed: %v", err)
    }
    
    spinner.Stop("Request complete")
    
    // Display response
    format := ctx.String("format")
    return displayResponse(ctx, response, format, verbose)
}

func getRequestBody(ctx cli.CommandContext) ([]byte, error) {
    if filePath := ctx.String("file"); filePath != "" {
        return os.ReadFile(filePath)
    }
    
    if data := ctx.String("data"); data != "" {
        return []byte(data), nil
    }
    
    // Interactive input
    data, err := ctx.Prompt("Request body (JSON):")
    if err != nil {
        return nil, err
    }
    
    return []byte(data), nil
}

func displayResponse(ctx cli.CommandContext, response *APIResponse, format string, verbose bool) error {
    if verbose {
        ctx.Info(fmt.Sprintf("Status: %d %s", response.StatusCode, response.Status))
        ctx.Info(fmt.Sprintf("Content-Type: %s", response.ContentType))
        ctx.Info(fmt.Sprintf("Content-Length: %d", len(response.Body)))
        
        if len(response.Headers) > 0 {
            ctx.Info("Headers:")
            for key, value := range response.Headers {
                ctx.Info(fmt.Sprintf("  %s: %s", key, value))
            }
        }
        ctx.Info("")
    }
    
    switch format {
    case "json":
        if response.IsJSON() {
            var formatted interface{}
            if err := json.Unmarshal(response.Body, &formatted); err == nil {
                return ctx.OutputJSON(formatted)
            }
        }
        fmt.Print(string(response.Body))
        
    case "table":
        if response.IsJSON() {
            var data interface{}
            if err := json.Unmarshal(response.Body, &data); err == nil {
                return displayAsTable(ctx, data)
            }
        }
        fmt.Print(string(response.Body))
        
    case "yaml":
        if response.IsJSON() {
            var data interface{}
            if err := json.Unmarshal(response.Body, &data); err == nil {
                return ctx.OutputYAML(data)
            }
        }
        fmt.Print(string(response.Body))
        
    default:
        fmt.Print(string(response.Body))
    }
    
    return nil
}

// Helper types and functions
type APIResponse struct {
    StatusCode  int
    Status      string
    Headers     map[string]string
    Body        []byte
    ContentType string
}

func (r *APIResponse) IsJSON() bool {
    return strings.Contains(r.ContentType, "application/json")
}

func (c *APIClient) authenticate(username, password string) (string, error) {
    // Implementation
    return "mock-token", nil
}

func (c *APIClient) validateToken() (bool, string, error) {
    // Implementation
    return true, "user@example.com", nil
}

func (c *APIClient) makeRequest(method, endpoint string, body []byte, headers map[string]string, queryParams map[string]interface{}) (*APIResponse, error) {
    // Implementation
    return &APIResponse{
        StatusCode:  200,
        Status:      "OK",
        Headers:     make(map[string]string),
        Body:        []byte(`{"message": "success"}`),
        ContentType: "application/json",
    }, nil
}

func newAPIClient(config Config) *APIClient {
    return &APIClient{
        baseURL:    config.BaseURL,
        token:      config.Token,
        httpClient: &http.Client{Timeout: 30 * time.Second},
    }
}

func loadConfig(path string) (Config, error) {
    var config Config
    data, err := os.ReadFile(path)
    if err != nil {
        return config, err
    }
    
    err = json.Unmarshal(data, &config)
    return config, err
}

func saveConfig(path string, config Config) error {
    // Ensure directory exists
    if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
        return err
    }
    
    data, err := json.MarshalIndent(config, "", "  ")
    if err != nil {
        return err
    }
    
    return os.WriteFile(path, data, 0600) // Secure permissions for credentials
}

func getDefaultConfigPath() string {
    homeDir, _ := os.UserHomeDir()
    return filepath.Join(homeDir, ".api-cli", "config.json")
}

func displayAsTable(ctx cli.CommandContext, data interface{}) error {
    // Implementation to convert JSON to table format
    return nil
}

func configShowHandler(ctx cli.CommandContext) error {
    config, err := loadConfig(ctx.String("config"))
    if err != nil {
        return fmt.Errorf("no configuration found")
    }
    
    ctx.Info("Current configuration:")
    ctx.Info(fmt.Sprintf("Base URL: %s", config.BaseURL))
    ctx.Info(fmt.Sprintf("Token: %s", maskToken(config.Token)))
    
    return nil
}

func configSetHandler(ctx cli.CommandContext) error {
    key := ctx.Args()[0]
    value := ctx.Args()[1]
    
    config, err := loadConfig(ctx.String("config"))
    if err != nil {
        config = Config{} // Create new config
    }
    
    switch key {
    case "base-url":
        config.BaseURL = value
    case "token":
        config.Token = value
    default:
        return fmt.Errorf("unknown configuration key: %s", key)
    }
    
    if err := saveConfig(ctx.String("config"), config); err != nil {
        return fmt.Errorf("failed to save config: %v", err)
    }
    
    ctx.Success(fmt.Sprintf("✅ Set %s = %s", key, value))
    return nil
}

func maskToken(token string) string {
    if len(token) <= 8 {
        return strings.Repeat("*", len(token))
    }
    return token[:4] + strings.Repeat("*", len(token)-8) + token[len(token)-4:]
}

Next Steps

These examples demonstrate:

  • Simple CLI: Basic CRUD operations with file persistence
  • File Management: Advanced file operations with progress indication
  • API Client: REST API interaction with authentication and multiple output formats

For more examples, check out:

How is this guide?

Last updated on