Forge Integration
Integrate CLI applications with the Forge web framework for powerful command-line tools
Forge Integration
The Forge CLI framework provides seamless integration with the Forge web framework, allowing you to build powerful command-line tools that can interact with your web application's services, models, and configuration.
Overview
Forge integration enables your CLI applications to:
- Access Forge services through dependency injection
- Use the same configuration as your web application
- Share models and business logic
- Leverage Forge's observability and logging
- Access database connections and migrations
- Utilize Forge's security and authentication systems
Basic Integration
Setting Up Forge CLI
package main
import (
"context"
"fmt"
"os"
"github.com/forge/cli"
"github.com/forge/forge"
"github.com/forge/forge/config"
"github.com/forge/forge/database"
"github.com/forge/forge/logger"
)
func main() {
// Initialize Forge application
app := forge.New(forge.Config{
Name: "myapp",
Version: "1.0.0",
})
// Initialize CLI with Forge integration
cliApp := cli.New(cli.Config{
Name: "myapp-cli",
Description: "Command-line interface for MyApp",
Version: "1.0.0",
ForgeApp: app, // Enable Forge integration
})
// Add commands
cliApp.AddCommand(
cli.NewCommand("users").
WithDescription("User management commands").
WithSubcommands(
cli.NewCommand("list").
WithDescription("List all users").
WithHandler(listUsersHandler),
cli.NewCommand("create").
WithDescription("Create a new user").
WithHandler(createUserHandler),
cli.NewCommand("delete").
WithDescription("Delete a user").
WithArgs(cli.Arg{
Name: "id",
Description: "User ID",
Required: true,
}).
WithHandler(deleteUserHandler),
),
)
cliApp.AddCommand(
cli.NewCommand("migrate").
WithDescription("Database migration commands").
WithSubcommands(
cli.NewCommand("up").
WithDescription("Run pending migrations").
WithHandler(migrateUpHandler),
cli.NewCommand("down").
WithDescription("Rollback migrations").
WithFlags(
cli.IntFlag("steps", "Number of migrations to rollback").
WithDefault(1),
).
WithHandler(migrateDownHandler),
cli.NewCommand("status").
WithDescription("Show migration status").
WithHandler(migrateStatusHandler),
),
)
if err := cliApp.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}Accessing Forge Services
Service Injection
When Forge integration is enabled, you can access Forge services through the command context:
func listUsersHandler(ctx cli.CommandContext) error {
// Access Forge services
db := ctx.ForgeService("database").(*database.DB)
logger := ctx.ForgeService("logger").(logger.Logger)
config := ctx.ForgeService("config").(config.Config)
logger.Info("Listing users", "command", "users:list")
// Query users from database
var users []User
if err := db.Find(&users).Error; err != nil {
return fmt.Errorf("failed to fetch users: %v", err)
}
if len(users) == 0 {
ctx.Info("No users found")
return nil
}
// Display users in table format
table := ctx.Table()
table.SetHeaders([]string{"ID", "Name", "Email", "Created At"})
for _, user := range users {
table.AddRow([]string{
fmt.Sprintf("%d", user.ID),
user.Name,
user.Email,
user.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
table.Render()
ctx.Success(fmt.Sprintf("Found %d users", len(users)))
return nil
}
func createUserHandler(ctx cli.CommandContext) error {
// Access user service
userService := ctx.ForgeService("user_service").(*services.UserService)
// Interactive user creation
name, err := ctx.Prompt("User name:")
if err != nil {
return err
}
email, err := ctx.Prompt("Email address:")
if err != nil {
return err
}
password, err := ctx.PromptPassword("Password:")
if err != nil {
return err
}
// Validate input
if name == "" || email == "" || password == "" {
return fmt.Errorf("all fields are required")
}
// Create user using service
user, err := userService.Create(context.Background(), services.CreateUserRequest{
Name: name,
Email: email,
Password: password,
})
if err != nil {
return fmt.Errorf("failed to create user: %v", err)
}
ctx.Success(fmt.Sprintf("✅ Created user: %s (ID: %d)", user.Name, user.ID))
return nil
}Configuration Access
Access your Forge application's configuration:
func configHandler(ctx cli.CommandContext) error {
config := ctx.ForgeService("config").(config.Config)
// Access configuration values
dbURL := config.GetString("database.url")
redisURL := config.GetString("redis.url")
appEnv := config.GetString("app.environment")
ctx.Info("Application Configuration:")
ctx.Info(fmt.Sprintf("Environment: %s", appEnv))
ctx.Info(fmt.Sprintf("Database: %s", maskURL(dbURL)))
ctx.Info(fmt.Sprintf("Redis: %s", maskURL(redisURL)))
return nil
}
func maskURL(url string) string {
// Mask sensitive parts of URLs
if strings.Contains(url, "@") {
parts := strings.Split(url, "@")
if len(parts) == 2 {
return "***@" + parts[1]
}
}
return url
}Database Operations
Migration Commands
func migrateUpHandler(ctx cli.CommandContext) error {
migrator := ctx.ForgeService("migrator").(*database.Migrator)
ctx.Info("Running database migrations...")
progress := ctx.Spinner("Applying migrations...")
migrations, err := migrator.GetPendingMigrations()
if err != nil {
progress.Stop("Failed to get migrations")
return fmt.Errorf("failed to get pending migrations: %v", err)
}
if len(migrations) == 0 {
progress.Stop("No pending migrations")
ctx.Success("✅ Database is up to date")
return nil
}
progress.Stop(fmt.Sprintf("Found %d pending migrations", len(migrations)))
// Show pending migrations
table := ctx.Table()
table.SetHeaders([]string{"Migration", "Description"})
for _, migration := range migrations {
table.AddRow([]string{
migration.Name,
migration.Description,
})
}
table.Render()
// Confirm before applying
confirmed, err := ctx.Confirm("Apply these migrations?")
if err != nil {
return err
}
if !confirmed {
ctx.Info("Migration cancelled")
return nil
}
// Apply migrations with progress
progressBar := ctx.ProgressBar(len(migrations))
for i, migration := range migrations {
progressBar.SetMessage(fmt.Sprintf("Applying %s", migration.Name))
if err := migrator.ApplyMigration(migration); err != nil {
return fmt.Errorf("failed to apply migration %s: %v", migration.Name, err)
}
progressBar.Increment()
}
progressBar.Finish("Migrations completed successfully!")
return nil
}
func migrateStatusHandler(ctx cli.CommandContext) error {
migrator := ctx.ForgeService("migrator").(*database.Migrator)
applied, err := migrator.GetAppliedMigrations()
if err != nil {
return fmt.Errorf("failed to get applied migrations: %v", err)
}
pending, err := migrator.GetPendingMigrations()
if err != nil {
return fmt.Errorf("failed to get pending migrations: %v", err)
}
ctx.Info(fmt.Sprintf("Applied migrations: %d", len(applied)))
ctx.Info(fmt.Sprintf("Pending migrations: %d", len(pending)))
if len(pending) > 0 {
ctx.Warning("⚠️ Database migrations are pending")
table := ctx.Table()
table.SetHeaders([]string{"Pending Migration", "Description"})
for _, migration := range pending {
table.AddRow([]string{
migration.Name,
migration.Description,
})
}
table.Render()
} else {
ctx.Success("✅ Database is up to date")
}
return nil
}Data Seeding
func seedHandler(ctx cli.CommandContext) error {
db := ctx.ForgeService("database").(*database.DB)
logger := ctx.ForgeService("logger").(logger.Logger)
// Check if data already exists
var userCount int64
if err := db.Model(&User{}).Count(&userCount).Error; err != nil {
return fmt.Errorf("failed to check existing data: %v", err)
}
if userCount > 0 {
confirmed, err := ctx.Confirm(fmt.Sprintf("Database contains %d users. Continue seeding?", userCount))
if err != nil {
return err
}
if !confirmed {
ctx.Info("Seeding cancelled")
return nil
}
}
// Seed data
seedData := []struct {
Name string
Email string
Role string
}{
{"Admin User", "admin@example.com", "admin"},
{"John Doe", "john@example.com", "user"},
{"Jane Smith", "jane@example.com", "user"},
}
progress := ctx.ProgressBar(len(seedData))
for _, data := range seedData {
progress.SetMessage(fmt.Sprintf("Creating user: %s", data.Name))
user := User{
Name: data.Name,
Email: data.Email,
Role: data.Role,
}
if err := db.Create(&user).Error; err != nil {
logger.Error("Failed to create user", "error", err, "email", data.Email)
return fmt.Errorf("failed to create user %s: %v", data.Email, err)
}
logger.Info("Created user", "id", user.ID, "email", user.Email)
progress.Increment()
}
progress.Finish("Seeding completed successfully!")
return nil
}Service Integration
Custom Service Access
// Define your service interface
type EmailService interface {
SendEmail(to, subject, body string) error
SendTemplate(to, template string, data interface{}) error
}
func sendEmailHandler(ctx cli.CommandContext) error {
emailService := ctx.ForgeService("email_service").(EmailService)
// Interactive email composition
to, err := ctx.Prompt("To:")
if err != nil {
return err
}
subject, err := ctx.Prompt("Subject:")
if err != nil {
return err
}
body, err := ctx.PromptMultiline("Body:")
if err != nil {
return err
}
// Send email
spinner := ctx.Spinner("Sending email...")
if err := emailService.SendEmail(to, subject, body); err != nil {
spinner.Stop("Failed to send email")
return fmt.Errorf("failed to send email: %v", err)
}
spinner.Stop("Email sent successfully")
ctx.Success("✅ Email sent to " + to)
return nil
}
func bulkEmailHandler(ctx cli.CommandContext) error {
emailService := ctx.ForgeService("email_service").(EmailService)
userService := ctx.ForgeService("user_service").(*services.UserService)
// Get email template
template, err := ctx.Select("Select email template:", []string{
"welcome",
"newsletter",
"notification",
"reminder",
})
if err != nil {
return err
}
// Get user filter
filter, err := ctx.Select("Send to:", []string{
"all_users",
"active_users",
"inactive_users",
"admin_users",
})
if err != nil {
return err
}
// Get users based on filter
users, err := userService.GetUsersByFilter(context.Background(), filter)
if err != nil {
return fmt.Errorf("failed to get users: %v", err)
}
if len(users) == 0 {
ctx.Info("No users found for the selected filter")
return nil
}
// Confirm before sending
confirmed, err := ctx.Confirm(fmt.Sprintf("Send %s email to %d users?", template, len(users)))
if err != nil {
return err
}
if !confirmed {
ctx.Info("Bulk email cancelled")
return nil
}
// Send emails with progress
progress := ctx.ProgressBar(len(users))
var successCount, errorCount int
for _, user := range users {
progress.SetMessage(fmt.Sprintf("Sending to %s", user.Email))
if err := emailService.SendTemplate(user.Email, template, user); err != nil {
errorCount++
ctx.Error(fmt.Sprintf("Failed to send to %s: %v", user.Email, err))
} else {
successCount++
}
progress.Increment()
}
progress.Finish(fmt.Sprintf("Bulk email completed: %d sent, %d failed", successCount, errorCount))
if errorCount > 0 {
ctx.Warning(fmt.Sprintf("⚠️ %d emails failed to send", errorCount))
} else {
ctx.Success("✅ All emails sent successfully")
}
return nil
}Advanced Integration
Background Jobs
func jobsHandler(ctx cli.CommandContext) error {
jobQueue := ctx.ForgeService("job_queue").(jobs.Queue)
subcommand := ctx.Args()[0]
switch subcommand {
case "list":
return listJobs(ctx, jobQueue)
case "retry":
return retryJob(ctx, jobQueue)
case "clear":
return clearJobs(ctx, jobQueue)
default:
return fmt.Errorf("unknown subcommand: %s", subcommand)
}
}
func listJobs(ctx cli.CommandContext, queue jobs.Queue) error {
jobs, err := queue.GetJobs(jobs.FilterAll)
if err != nil {
return fmt.Errorf("failed to get jobs: %v", err)
}
if len(jobs) == 0 {
ctx.Info("No jobs found")
return nil
}
table := ctx.Table()
table.SetHeaders([]string{"ID", "Type", "Status", "Created", "Attempts"})
for _, job := range jobs {
table.AddRow([]string{
job.ID,
job.Type,
job.Status,
job.CreatedAt.Format("2006-01-02 15:04:05"),
fmt.Sprintf("%d/%d", job.Attempts, job.MaxAttempts),
})
}
table.Render()
return nil
}
func retryJob(ctx cli.CommandContext, queue jobs.Queue) error {
if len(ctx.Args()) < 2 {
return fmt.Errorf("job ID required")
}
jobID := ctx.Args()[1]
if err := queue.RetryJob(jobID); err != nil {
return fmt.Errorf("failed to retry job: %v", err)
}
ctx.Success(fmt.Sprintf("✅ Job %s queued for retry", jobID))
return nil
}Cache Management
func cacheHandler(ctx cli.CommandContext) error {
cache := ctx.ForgeService("cache").(cache.Cache)
subcommand := ctx.Args()[0]
switch subcommand {
case "clear":
return clearCache(ctx, cache)
case "stats":
return cacheStats(ctx, cache)
case "get":
return getCache(ctx, cache)
case "set":
return setCache(ctx, cache)
default:
return fmt.Errorf("unknown subcommand: %s", subcommand)
}
}
func clearCache(ctx cli.CommandContext, cache cache.Cache) error {
pattern := "*"
if len(ctx.Args()) > 1 {
pattern = ctx.Args()[1]
}
confirmed, err := ctx.Confirm(fmt.Sprintf("Clear cache keys matching '%s'?", pattern))
if err != nil {
return err
}
if !confirmed {
ctx.Info("Cache clear cancelled")
return nil
}
spinner := ctx.Spinner("Clearing cache...")
count, err := cache.Clear(pattern)
if err != nil {
spinner.Stop("Failed to clear cache")
return fmt.Errorf("failed to clear cache: %v", err)
}
spinner.Stop("Cache cleared")
ctx.Success(fmt.Sprintf("✅ Cleared %d cache keys", count))
return nil
}
func cacheStats(ctx cli.CommandContext, cache cache.Cache) error {
stats, err := cache.Stats()
if err != nil {
return fmt.Errorf("failed to get cache stats: %v", err)
}
ctx.Info("Cache Statistics:")
ctx.Info(fmt.Sprintf("Total Keys: %d", stats.KeyCount))
ctx.Info(fmt.Sprintf("Memory Usage: %s", formatBytes(stats.MemoryUsage)))
ctx.Info(fmt.Sprintf("Hit Rate: %.2f%%", stats.HitRate*100))
ctx.Info(fmt.Sprintf("Uptime: %s", stats.Uptime))
return nil
}Configuration
Environment-Specific Commands
func environmentHandler(ctx cli.CommandContext) error {
config := ctx.ForgeService("config").(config.Config)
env := config.GetString("app.environment")
switch env {
case "production":
return productionCommands(ctx)
case "staging":
return stagingCommands(ctx)
case "development":
return developmentCommands(ctx)
default:
return fmt.Errorf("unknown environment: %s", env)
}
}
func productionCommands(ctx cli.CommandContext) error {
// Production-safe commands only
ctx.Warning("⚠️ Running in PRODUCTION environment")
// Require additional confirmation for destructive operations
if isDestructiveCommand(ctx.Command()) {
confirmed, err := ctx.Confirm("This is a destructive operation in PRODUCTION. Are you sure?")
if err != nil {
return err
}
if !confirmed {
ctx.Info("Operation cancelled")
return nil
}
// Require second confirmation
confirmed, err = ctx.Confirm("Type 'yes' to confirm:")
if err != nil {
return err
}
if !confirmed {
ctx.Info("Operation cancelled")
return nil
}
}
return nil
}
func isDestructiveCommand(command string) bool {
destructive := []string{"delete", "drop", "clear", "reset", "purge"}
for _, cmd := range destructive {
if strings.Contains(command, cmd) {
return true
}
}
return false
}Best Practices
Error Handling
func robustHandler(ctx cli.CommandContext) error {
logger := ctx.ForgeService("logger").(logger.Logger)
// Log command execution
logger.Info("Command started",
"command", ctx.Command(),
"args", ctx.Args(),
"user", getCurrentUser(),
)
defer func() {
if r := recover(); r != nil {
logger.Error("Command panicked", "panic", r, "command", ctx.Command())
ctx.Error("An unexpected error occurred")
}
}()
// Your command logic here
if err := performOperation(); err != nil {
logger.Error("Command failed", "error", err, "command", ctx.Command())
return fmt.Errorf("operation failed: %v", err)
}
logger.Info("Command completed successfully", "command", ctx.Command())
return nil
}Resource Cleanup
func resourceHandler(ctx cli.CommandContext) error {
db := ctx.ForgeService("database").(*database.DB)
// Begin transaction
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
// Perform operations
if err := performDatabaseOperations(tx); err != nil {
tx.Rollback()
return fmt.Errorf("database operation failed: %v", err)
}
// Commit transaction
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit transaction: %v", err)
}
ctx.Success("✅ Operation completed successfully")
return nil
}Next Steps
- Commands for command structure and organization
- Examples for complete application examples
- Plugins for extending CLI functionality
- Forge Documentation for web framework features
How is this guide?
Last updated on