App Runner

Wrap a Forge application in a production-ready CLI with built-in serve, migrate, and extension commands.

App Runner

cli.RunApp() wraps a Forge application in a complete CLI with built-in commands for serving, database migrations, health checks, and extension introspection — all in a single function call.

Quick Start

package main

import (
    "github.com/xraph/forge"
    "github.com/xraph/forge/cli"
)

func main() {
    app := forge.New(
        forge.WithAppName("my-app"),
        forge.WithAppVersion("1.0.0"),
    )

    cli.RunApp(app)
}

This gives you a fully functional CLI:

$ my-app serve        # Start the application server
$ my-app migrate up   # Run pending migrations
$ my-app migrate down # Rollback the last migration batch
$ my-app migrate status # Show migration status
$ my-app info         # Show app information
$ my-app health       # Check app health
$ my-app extensions   # List registered extensions

Default Behavior

When invoked with no arguments, RunApp defaults to the serve command:

$ my-app              # Same as: my-app serve

Built-in Commands

CommandAliasesDescription
servestart, runStart the application server
migrate upRun all pending migrations
migrate downRollback the last migration batch
migrate statusShow migration status table
infoDisplay app name, version, and metadata
healthRun health checks on all extensions
extensionsList all registered extensions

Migration commands only appear when at least one registered extension implements MigratableExtension. Extensions that implement CLICommandProvider contribute additional commands automatically.

Options Reference

Configure RunApp behavior with functional options:

WithAutoMigrate

Runs pending migrations before the HTTP server starts listening (during PhaseBeforeRun). Migrations run after all extensions are initialized but before the app accepts requests.

cli.RunApp(app, cli.WithAutoMigrate())

WithExtraCommands

Add custom commands alongside the built-in ones:

cli.RunApp(app,
    cli.WithExtraCommands(
        cli.NewCommand("seed", "Seed the database", handleSeed),
        cli.NewCommand("reindex", "Rebuild search index", handleReindex),
    ),
)

WithDisableMigrationCommands

Disable auto-registration of migrate commands even when MigratableExtension extensions are present:

cli.RunApp(app, cli.WithDisableMigrationCommands())

WithDisableServeCommand

Disable the built-in serve command (useful for CLI-only tools):

cli.RunApp(app, cli.WithDisableServeCommand())

WithCLIName / WithCLIVersion / WithCLIDescription

Override the CLI metadata (defaults to the Forge app's name and version):

cli.RunApp(app,
    cli.WithCLIName("myctl"),
    cli.WithCLIVersion("2.0.0"),
    cli.WithCLIDescription("MyApp management CLI"),
)

Auto-Migration on Serve

When WithAutoMigrate() is enabled, the serve command registers a PhaseBeforeRun lifecycle hook (priority 1000) that:

  1. Discovers all extensions implementing MigratableExtension
  2. Calls Migrate(ctx) on each one
  3. Logs the count of applied migrations per extension
  4. Fails the startup if any migration errors
func main() {
    app := forge.New(
        forge.WithAppName("my-app"),
        forge.WithExtensions(groveExt),
    )

    // Migrations run automatically before HTTP server starts
    cli.RunApp(app, cli.WithAutoMigrate())
}

Auto-migration uses priority 1000, so it runs before your other PhaseBeforeRun hooks (which default to priority 0). If a migration fails, the app will not start.

Extension Command Discovery

Extensions implementing CLICommandProvider automatically contribute their commands to the CLI:

type MyExtension struct { /* ... */ }

func (e *MyExtension) CLICommands() []any {
    return []any{
        cli.NewCommand("seed", "Seed the database", e.handleSeed),
        cli.NewCommand("dump", "Dump database schema", e.handleDump),
    }
}

When this extension is registered with the app:

$ my-app seed   # Provided by MyExtension
$ my-app dump   # Provided by MyExtension

Migration Commands

The migrate parent command provides three subcommands:

migrate up

Starts the app (initializes extensions without the HTTP server), discovers all MigratableExtension extensions, and runs pending migrations:

$ my-app migrate up
 Applied: core/create_users
 Applied: core/add_email_index
 Applied: billing/create_invoices
Applied 3 migration(s)

migrate down

Rolls back the last batch of migrations. Prompts for confirmation unless --force is used:

$ my-app migrate down
? Are you sure you want to rollback? (y/N) y
Rolled back 1 migration(s) for grove
 core/add_email_index

$ my-app migrate down --force  # Skip confirmation

migrate status

Displays a table showing applied and pending migrations for each extension:

$ my-app migrate status

grove Migrations (grove v0.12.0):
  Group: core
  ┌────────────────┬──────────────────┬─────────┬──────────────────────┐
 Version Name Status Applied At
  ├────────────────┼──────────────────┼─────────┼──────────────────────┤
 20240101000000 create_users applied 2024-01-01T12:00:00Z
 20240201000000 add_email_index pending
  └────────────────┴──────────────────┴─────────┴──────────────────────┘

Complete Example

package main

import (
    "github.com/xraph/forge"
    "github.com/xraph/forge/cli"

    groveext "github.com/xraph/grove/extension"
    _ "github.com/xraph/grove/drivers/pgdriver/pgmigrate" // register pg executor

    "myapp/migrations/core"
    "myapp/migrations/billing"
)

func main() {
    grove := groveext.New(
        groveext.WithDSN("postgres://localhost:5432/myapp"),
        groveext.WithMigrations(core.Migrations, billing.Migrations),
    )

    app := forge.New(
        forge.WithAppName("myapp"),
        forge.WithAppVersion("1.0.0"),
        forge.WithExtensions(grove),
    )

    cli.RunApp(app,
        cli.WithAutoMigrate(),
        cli.WithExtraCommands(
            cli.NewCommand("seed", "Seed test data", handleSeed),
        ),
    )
}

Next Steps

How is this guide?

On this page