Console Commands

> Add your own commands to the per-project vel binary - scaffold them with vel gen command, register them with v.Commands, and run them with vel run.

Every project’s ./vel binary can carry commands you write: reports, imports, one-off maintenance, anything that needs the application’s services but not an HTTP request. They live under vel run so they never collide with a built-in.

Scaffold a command

vel gen command SyncInventory [--dir PATH]

Writes internal/commands/sync_inventory.go (or under --dir):

package commands

import (
    "fmt"

    "github.com/velocitykode/velocity"
)

// SyncInventoryCommand is a console command.
//
// Register this command in internal/commands/kernel.go:
//   r.Add(&SyncInventoryCommand{})
type SyncInventoryCommand struct{}

// Name returns the command name used to invoke it.
func (c SyncInventoryCommand) Name() string {
    return "sync-inventory"
}

// Description returns a short description of the command.
func (c SyncInventoryCommand) Description() string {
    return "TODO: describe sync-inventory"
}

// Handle executes the command logic.
func (c SyncInventoryCommand) Handle(s *velocity.Services, args []string) error {
    fmt.Println("Executing sync-inventory...")
    return nil
}

The generator appends the Command suffix and derives the invocation name in kebab-case, so SyncInventory becomes sync-inventory. Handle receives the full services container, so the database, cache, queue, mail, and every other service are available exactly as in a handler.

Register it

The starter kits ship internal/commands/kernel.go and wire it from main.go through the Commands chain method:

// internal/commands/kernel.go
func Register(r *velocity.Commands) {
    r.Add(&SyncInventoryCommand{})
}
// main.go
v.Modules(app.Configure).
    Routes(routes.Register).
    Commands(commands.Register).
    Seeders(seeders.Register).
    Run()

A module can contribute commands too by implementing CommandModule:

func (m *BillingModule) Commands(r *velocity.Commands) {
    r.Add(&ReconcileCommand{})
}

Module commands register first, in module order, then the Commands callback. Registering the same name twice panics at boot.

Run it

vel run <command> [arguments]

Everything after the command name is passed straight through to Handle, so vel run report --csv reaches your code with ["--csv"]. The application bootstraps first: modules, routes, events, and schedules are all wired before Handle runs.

vel run              # list every registered command
vel run report
vel run report --csv

vel run with no arguments prints the registered commands, or a hint to create one with vel gen command <Name>. An unknown name prints the same list and errors, and a flag-like first token (vel run --bogus) is rejected as an unknown flag before the app bootstraps.

Custom commands are reachable only through vel run. Typing one as a bare vel <name> is an unknown command, so a command sharing a name with a built-in never shadows it.