Skip to content

Latest commit

 

History

History
1050 lines (864 loc) · 50.3 KB

cli.md

File metadata and controls

1050 lines (864 loc) · 50.3 KB

Gombit CLI

gombit is a Cobra command tree (D13 / ADR-014). Root help lists the command families. Nested families and app-registered management commands attach with Cobra AddCommand (or cli.AddCommand, a thin wrapper). There is no second command router and no reflection discovery of commands.

The framework binary is go run ./cmd/gombit in this repository. Generated apps get their own cmd/gombit that reuses cli.NewRoot and then calls product.RegisterCommands(root) (and any internal/commands.RegisterCommands). App-owned commands are discoverable via go run ./cmd/gombit from the application module.

go run ./cmd/gombit --help

Command families

Family Role Milestone
gombit new Scaffold an application M4-1
gombit dev Run API + Vite with HMR and live client regen M4-2
gombit build --embed Optional single-binary (collectstatic + go:embed) M5-5
gombit make resource Generate a feature-package resource (AST-safe) M4-3
gombit make command Scaffold a Cobra management command (AST-safe) M4-7
gombit generate Regenerate model-first resource files (*.gen.go); --check gates drift RESGEN-1
gombit db … Atlas-backed migrations M2, migrated onto Cobra in M4-1
gombit db verify Classify + verify migration safety manifests HOST-3
gombit db plan / lint / repair / hash / check Plan a schema change, lint and repair the migration directory, recompute atlas.sum, check the whole schema chain SCHEMA-2, SCHEMA-4, SCHEMA-6; hash #219
gombit openapi generate Write the live OpenAPI 3.1 document M3-3
gombit contract app Emit the machine-readable application contract HOST-1
gombit client generate / check TypeScript client + drift M3-4, M3-5
gombit routes Print HTTP routes M4-4
gombit doctor Environment and config checks M4-4
gombit config show Print typed config with secrets redacted M4-4
gombit createsuperuser Create a superuser (admin) account M4-6
gombit worker Run the app's background-job worker JOBS-3
gombit jobs … Inspect, retry, and delete failed jobs JOBS-6
gombit version Print version and build metadata REL-4
gombit upgrade --dry-run Plan an upgrade to a framework release, changing nothing UPGRADE-3
gombit upgrade --to Apply an upgrade's dependency changes (go.mod, go.sum) with the go command UPGRADE-4
gombit upgrade check Report uses of deprecated framework APIs (from type information); exits 1 when there are any UPGRADE-5
gombit upgrade config Report an upgrade's configuration changes for the app (never a value from its env files); --write upgrades .env templates UPGRADE-7
gombit upgrade fix Run the framework's codemods: print the patch (default), --patch FILE, or --write UPGRADE-6
gombit upgrade contract Report stale generated contract artifacts (resource *.gen.go, OpenAPI document, TypeScript client); --write regenerates them UPGRADE-8
gombit upgrade baseline Show (or record) the app's upgrade baseline: framework and scaffold versions UPGRADE-1
gombit upgrade notes Print upgrade notes from the compatibility manifest UPGRADE-2

Generator golden tests

goldentest runs each generator against a fixed, non-interactive fixture, diffs the output tree against goldentest/testdata/golden, compiles the generated backend (go build with a local replace in a temp copy — never committed), typechecks the frontend with npx tsc --noEmit when Node is on PATH (t.Skip otherwise), and checks that a second run is idempotent (gombit new --force, make resource without --force, make command without --force, client generate without --force). make resource runs with --skip-migrations, so Atlas is not invoked and timestamped migration filenames stay out of the trees; the make-resource tree does include the deterministic database/migrations/models.json registry.

go test ./goldentest
go test ./goldentest -update   # regenerate committed goldens

CI picks this up via go test ./.... Do not commit replace directives or machine-specific paths in the goldens.

gombit new

Non-interactive (the acceptance criterion):

gombit new demo --database sqlite

That writes a compiling app under ./demo. The default Go module path is github.com/example/demo. Override it with --module.

cd demo
go run ./cmd/server

No go get step: the generated go.mod requires the same gombit version as the binary that scaffolded it, and gombit new then runs go mod tidy to populate go.sum. An installed release therefore produces a tree that builds immediately.

Once go mod tidy succeeds, gombit new also seeds an initial bootstrap migration under database/migrations/ for the framework's own auth tables, and applies it immediately for --database sqlite (the default) — db status shows it applied before you've run db migrate yourself. Requires Atlas on PATH; prints the equivalent gombit db makemigrations / gombit db migrate command instead of failing when it isn't (or when the driver isn't sqlite yet). See migrations.md for why this exists.

Version resolution follows gombit version (see below) and falls back to v0.0.0 — which the proxy cannot resolve — when the CLI reports something unpublishable:

CLI build Requirement written Tidy
Release binary (ldflags) that tag, e.g. v0.1.0 runs
go install ...@latest the module version, e.g. a pseudo-version runs
go build / go run from a checkout (dev, +dirty) v0.0.0 skipped
--framework-version vX.Y.Z that version runs

In the fallback case the command prints why, and the app needs a replace pointing at your checkout (never commit a machine-specific path):

cd demo
go mod edit -replace github.com/gombit-dev/gombit=/path/to/gombit
go mod tidy

Flags

Flag Values Default
--database sqlite, postgres, mysql sqlite
--cache memory, redis, noop memory
--auth jwt, cookie jwt
--ui minimal, mui minimal
--module Go module path github.com/example/<name>
--framework-version gombit version the generated go.mod requires the gombit version this binary reports
--skip-tidy do not run go mod tidy (offline); also skips seeding the bootstrap migration, which needs a tidied module
--dry-run print the file list without writing
--force required to write into a non-empty destination

--auth cookie wires HttpOnly session cookies + CSRF end to end (backend routes, middleware, and the generated frontend); see auth-cookie.md for the threat model. --ui mui scaffolds the opt-in MUI CRUD preset (ThemeProvider, AppBar, Table, TextField); the default UI stays minimal/headless. See frontend-mui.md. The generated frontend/ directory is a Vite + React + TypeScript skeleton (router, generated client, React Hook Form). Bearer login is documented in auth.md. See frontend.md.

If the project name is omitted and stdin is a TTY, gombit new prompts for name and the choices above. Tests and CI pass flags so the command never hangs.

Generators are additive: --dry-run writes nothing; a non-empty destination is refused unless --force is set. --force overwrites scaffold files and leaves other files in the destination alone.

Layout

The scaffold matches build plan §3.2:

demo/
├── cmd/server/main.go
├── cmd/gombit/main.go    # framework tree + product.RegisterCommands
├── internal/
│   ├── platform/
│   ├── product/          # model, handler.go, routes.go, commands.go
│   └── web/              # go:embed hook (static/.keep placeholder; no index.html)
├── database/migrations/  # a bootstrap_*.sql migration + models.json are seeded (and, for sqlite, applied) here
├── database/seeds/
├── config/
├── frontend/             # Vite + React skeleton (main.tsx, router, generated client)
├── gombit.yaml
├── .air.toml
├── .env                  # gitignored; per-project random GOMBIT_JWT_SECRET, loaded automatically
├── .env.example
├── go.mod
└── README.md

cmd/server/main.go calls config.Load(), framework.New (including framework.WithEmbeddedFrontend), registers internal/product routes explicitly (no reflection), and framework.Run. The placeholder embed has no index.html, so go run ./cmd/server after gombit new does not install SPA fallback. See build.md for gombit build --embed. cmd/gombit/main.go calls cli.NewRoot, product.RegisterCommands(root), and cli.ExecuteRoot. Public product routes are Huma-typed under /api/v1. There is no generated service.go or repo.go until gombit make resource --service / --repo.

.env.example lists every GOMBIT_* server variable the config package reads (optional ones commented out with their defaults; the cookie-auth settings only in --auth cookie apps) and public VITE_API_URL (empty means same-origin for the Vite /api proxy). VITE_* is baked into the browser bundle — never put secrets there. Access tokens stay in memory; generated source does not use localStorage.

Files the app stores go to ./storage by default (the local driver, GOMBIT_STORAGE_LOCAL_ROOT). gombit new does not create it: the driver creates it on the first write, and the scaffold's .gitignore already ignores it. internal/platform migrates the framework's tables (auth, and the storage_claims table that file fields need) with the app's models. See storage.md.

gombit dev

From an application directory (the output of gombit new):

gombit dev

One command starts:

  1. The Go API with reload when air or watchexec is on PATH. If neither is installed, Gombit runs go run ./cmd/server and prints a hint.
  2. The Vite frontend with HMR (pnpm when available, otherwise npm). Vite proxies /api, /openapi.json, /docs, and /admin to the Go origin. Prefixes that do not start with /api get an extra proxy entry from the live GOMBIT_API_PREFIX.
  3. An OpenAPI watcher that regenerates frontend/src/api/generated when the live /openapi.json document changes (gombit client generate).

A service table is printed at startup:

Backend      http://127.0.0.1:8080
Frontend     http://127.0.0.1:5173
OpenAPI      http://127.0.0.1:8080/openapi.json
API docs     http://127.0.0.1:8080/docs

Cookie-mode apps also print the admin SPA (the same origin the Vite /admin proxy forwards to):

Admin        http://127.0.0.1:8080/admin/

Flags

Flag Default Role
--http GOMBIT_HTTP_ADDR or :8080 Go API listen address
--frontend-host 127.0.0.1 Vite bind host
--frontend-port 5173 Vite port
--client-out frontend/src/api/generated TypeScript client output
--poll 1s OpenAPI poll interval

frontend/package.json is required. A missing file is an error — backend-only mode is not supported. Node.js is required for Vite.

--http and the Vite proxy origin are written into the child environment (GOMBIT_HTTP_ADDR, GOMBIT_DEV_BACKEND, GOMBIT_API_PREFIX, VITE_API_URL=), replacing any parent values so a shell-exported .env.example cannot keep the API on :8080 while the service table prints --http :9090. Empty VITE_API_URL is same-origin so OpenAPI path keys such as /api/v1/products hit the Vite proxy; createAppClient rewrites them to the live GOMBIT_API_PREFIX. Vite replaces __GOMBIT_API_PREFIX__ in index.html during vite dev. Production Vite builds leave the placeholder so gombit build --embed can inject it; a split/CDN host must substitute that token itself.

SIGINT/SIGTERM stops the child processes. On Unix, Gombit signals the process group so air/npm grandchildren exit. On Windows, teardown uses taskkill /T /F /PID for the same process tree.

The scaffold is a Vite + React + TypeScript skeleton. See frontend.md. --ui mui is documented in frontend-mui.md. Bearer login is documented in auth.md. Split deploy is the default; production embed is gombit build --embed.

gombit build --embed

From an application directory (the output of gombit new):

gombit build --embed
gombit build --embed --out bin/server
gombit build --embed --dry-run

Split deploy is the default (C5). A bare gombit build without --embed is an error — it does not silently switch production to a single binary.

--embed runs the Django collectstatic equivalent folded into the production build:

  1. Require frontend/package.json (same class of error as gombit dev).
  2. Vite production build in frontend/ (pnpm when available or when frontend/pnpm-lock.yaml exists, otherwise npm run build).
  3. Copy frontend/dist → internal/web/static (replace previous assets; do not delete internal/web/embed.go). Fail if frontend/dist/index.html is missing after the Vite build.
  4. go build ./cmd/server to --out (default bin/server).

The compiled binary serves Huma /api/*, /openapi.json, /docs, /livez, /readyz, /metrics, real files from the embed FS, and index.html for unmatched GET frontend routes (/, /login, /products/new). Non-GET unmatched routes stay 404. See build.md.

Flags

Flag Default Role
--embed off (required to run this command; split deploy stays the default) Opt in to collectstatic + go:embed
--out bin/server Output binary path
--dry-run Print the plan without writing or compiling

frontend/package.json is required. --dry-run writes nothing.

gombit make resource

From an application directory (the output of gombit new):

gombit make resource Widget name:string:required price:int
gombit make resource Invoice --service --repo --dry-run
gombit make resource Widget --force
gombit make resource Session --id uuid

--id chooses the primary key. uint (the default) is an auto-increment ID. uuid scaffolds an application-assigned uuid.UUID primary key (char(36), portable across SQLite, PostgreSQL, and MySQL). Both come with CreatedAt and UpdatedAt and no soft-delete DeletedAt: Gombit deletes rows physically, so the database's ON DELETE policy is what deletion does (ADR-019). The model sets the id in BeforeCreate when it is still uuid.Nil. Composite primary keys are rejected. A belongs_to or one_to_one foreign key uses the target model's primary key when that model is already on disk, and uint when it is not. A self relation uses this resource's --id. A model on disk whose key is not uint or uuid.UUID is rejected. The foreign key stays filterable, so GET /children?<fk>=<id> lists a parent's rows for a UUID key the same way it does for uint.

make is a Cobra parent (AddCommand); resource is the subcommand. Root help lists make.

make resource is model-first (ADR-016): it scaffolds the human-owned model, then runs gombit generate to derive the generator-owned plumbing. It writes a feature-package under internal/<snake>/:

File Owner When
<snake>.go you GORM model (an auto-increment ID, or a uuid.UUID key with --id uuid, plus CreatedAt / UpdatedAt and the fields; no soft delete), plus the gombit:"..." field policy translated from the CLI modifiers. Scaffolded once; edit it freely — re-running make resource never overwrites it (use --force to re-scaffold). No DO-NOT-EDIT banner.
.gombit-resource generator Marker that makes the package a model-first resource gombit generate regenerates.
dto.gen.go generator Request/response DTOs + model↔DTO mappers. DO NOT EDIT — regenerated from the model.
handler.gen.go generator Huma list/get/create over GORM + Register(app *framework.App) (D10 envelope; list honors page/per_page and the declared filter/sort/search/aggregate surface; not_found/conflict mapping). DO NOT EDIT.
hooks.go you A default no-op BeforeCreate hook. Set server-managed columns (tenant, owner, …) here. Seeded once, never overwritten.
service.go you Only with --service (pass-through)
repo.go you Only with --repo (pass-through)

Customization happens in the model, its field policy, and hooks.go — never by editing the generated *.gen.go (regeneration overwrites them). See the migration guide if you have resources from the old human-owned-handler.go layout.

Default API prefix is /api/v1. Enum fields (status:enum(draft=Draft,published=Published)) keep the stored values on the model's validate tag and the display labels in label= when they differ. gombit generate emits the stored values as a Huma enum on the create body. The form and the admin show the label. --service and --repo are C6 opt-in and are not used by the generated handler.

Route registration is appended in cmd/server/main.go via go/ast + go/parser + go/format (never regex), next to product.Register(app). internal/platform AutoMigrate is updated the same way. Re-running does not duplicate the Register call. A second resource whose plural HTTP path collides with an existing feature-package (Bus and Buse both become /buses) is refused. Product is reserved: every gombit new app already owns internal/product and the /products SPA routes (same as make command --package product). The module path is read from go.mod with trailing // comments stripped, so module example.com/demo // app does not leak into generated imports.

Frontend pages are React + TypeScript (list/table + React Hook Form create) under frontend/src/<feature>/. They import types from frontend/src/api/generated — no hand-written API DTOs — and map D10 error.fields through frontend/src/api/formErrors.ts. A non-pointer integer coerces a cleared number input to 0 (setValueAs). A pointer number (*uint and any other *T) submits JSON null, which is what that Go type accepts. A generated frontend/src/resources.tsx registry is the React Router registration point (not regex-patched main.tsx). When gombit.yaml has ui: mui, list/form pages use MUI Table and TextField instead of raw HTML. Generated list/form OpenAPI path keys stay /api/v1/<resource>; createAppClient rewrites them to the live GOMBIT_API_PREFIX (same as the product pages).

After generating routes, run gombit client generate or gombit dev so frontend/src/api/generated includes the new paths.

Field grammar

Design §27 subset:

name:type[:modifier[,modifier…]]

The type is any generated kind in fields.md: string, text, int / integer, int64 / integer64, uint / unsigned, float / float64, decimal, bool / boolean, date, time / datetime, time_of_day, duration, uuid, json, email, url, slug, ip, enum(value) or enum(value=Label), file, image, and the relations belongs_to, one_to_one, has_many, and many_to_many. An unknown type errors with the list of supported scalar types (one spelling each; relation kinds are not listed).

Modifiers are required, nullable (the opposite of required), unique, index, the list-query modifiers filterable, sortable, searchable, and aggregatable, and the constraints default=, min=, max=, max_length=, and regex= (see fields.md § Constraints). Relations also take on_delete=. time is a datetime. time_of_day is a clock (HH:MM:SS). duration is a Go duration stored as nanoseconds.

Type Go type Column / contract
decimal types.Decimal (wraps shopspring/decimal) decimal(19,4); JSON string, exact — no float rounding. A value that does not fit is a 422, never rounded; on SQLite at most 15 significant digits (see below)
decimal(p,s) types.Decimal decimal(p,s), e.g. decimal(10,2)
time time.Time RFC3339 date-time in JSON, 1000-01-02T00:00:00Z..9999-12-30T23:59:59Z on every write (see database.md)
time_of_day types.TimeOfDay char(8) clock. HH:MM, HH:MM:SS, and 15:04:05+07:00 are one pattern, stored as HH:MM:SS. Optional is a pointer; a blank submits null
duration types.Duration bigint nanoseconds; JSON is a Go duration (1h30m0s). Optional is a pointer
enum(draft=Draft) string stored value draft, display label Draft. The API enum is the stored value
file types.File the object key, size:512 unique; the file lives in App.Storage(). An upload-grant operation per field; reads return a file object. See fields.md § Storage-backed fields. Optional is a pointer
image types.Image as file, accepting images only (detected from the bytes)
belongs_to:Target FK TargetID + Target target.Target DTO exposes target_id; admin renders a picker. The FK type is the target primary key (uint or uuid.UUID). nullable makes the FK a pointer. on_delete is restrict (the default), cascade, or set_null
one_to_one:Target unique FK TargetID + Target target.Target same wire as belongs_to; the foreign key is unique
has_many:Target []target.Target model-only, read via the admin; the child must carry the parent FK
many_to_many:Target []target.Target (many2many: join) model-only, edited via the admin

A decimal column never stores a value changed. Any value written to one is checked first: a types.Decimal, or the string, number, pointer, named type, or sql.Null* value a map, a struct field, or an upsert carries. One that does not fit the column's decimal(p,s) (more than p-s digits before the point, or more than s after it, ignoring trailing zeros) is refused with a 422 rather than rounded by PostgreSQL or MySQL, and one that is not a decimal number at all ("1,5", "NaN", "") is refused too. The column type is the one GORM emits for the model, which must match the migrated column: type:decimal(p,s). A precision:/scale: tag does not reach the column for types.Decimal, and a type without (p,s), such as an untagged types.Decimal, is MySQL's DECIMAL(10,0) there (whole numbers only, so pin type:decimal(p,s) for money) and an unbounded numeric on PostgreSQL. SQLite has no fixed-point type: its decimal column converts through float64, which keeps 15 significant digits, so on SQLite a decimal may carry at most 15 digits (database.SQLiteDecimalDigits) and a longer one is a 422 instead of being silently changed. 99999999999.9999 round-trips on every driver; 99999999999999.9999 fits decimal(19,4) on PostgreSQL and MySQL but is refused on SQLite. A create responds with the row as stored, the same body a later get returns.

types.Decimal is the framework money/decimal type. Because a single Go type flows through the model, the handler DTO, the OpenAPI/TS contract, and GORM, adding one of these types does not reproduce the model/DTO drift of #218. A time, date, decimal, uuid, time_of_day, or duration field without :required becomes a pointer (*time.Time / *types.Date / *types.Decimal / *uuid.UUID / *types.TimeOfDay / *types.Duration) on the model and DTO, because those value types cannot be submitted empty — the generated forms send null for a blank optional value. The same holds for an optional email, url, slug, or ip, and for a string whose regex= rejects "", so a blank value is null rather than an invalid "".

Relations use name:kind:Target, where Target is a model in internal/<target>/ (imported as target.Target). belongs_to generates the foreign key (EngineID, uint or uuid.UUID to match the target's primary key) plus the association and exposes engine_id in the REST DTO; has_many and many_to_many generate the association on the model (the join table for m2m), not in the thin REST handler. In the admin, many_to_many is editable through a relation widget and has_many is shown read-only. A has_many child model must carry the parent foreign key itself (e.g. RentalID); the generator does not edit the child (that would be an import cycle).

A self-referential belongs_to or one_to_one is allowed when it is nullable, so a tree root stores NULL rather than 0:

parent:belongs_to:Category,nullable,on_delete=set_null

The association is a pointer (Parent *Category). A value field of the enclosing struct would not compile. on_delete=set_null requires nullable. has_many and many_to_many onto the same model are still rejected: they need explicit join keys.

Example:

gombit make resource Rental \
  price:decimal:required \
  starts_at:time \
  status:string:filterable \
  engine:belongs_to:Engine \
  warehouses:many_to_many:Warehouse

Idempotency

Generators print created/modified files. --dry-run writes nothing. Identical re-runs are no-ops. A user-owned file (no generated banner) or a generated file that differs from this run is refused unless --force. frontend/src/resources.tsx is the exception: it is always rewritten as a scanned registry of generated feature pages (banner present), so edits to that file are not preserved.

Migrations

Gombit does not invent a migration DSL. The generated GORM model is Atlas-loader ready. make resource runs migrations.MakeMigrations with every &pkg.Type{} already registered in internal/platform AutoMigrate plus the new model, merged with database/migrations/models.json (see migrations.md) — so a model that isn't in the AutoMigrate list for some reason but is still tracked in the registry isn't dropped either.

Because that step needs the atlas binary, and whether Atlas happens to be on PATH must not change the committed tree, make resource fails before writing anything when Atlas is missing:

Atlas is required to generate database migrations ("atlas" not found on PATH).

Install Atlas and retry, or run:

    gombit make resource Book title:string:required --skip-migrations

to create the resource without generating migration SQL

Pass --skip-migrations to scaffold the resource and persist the loader/registry state (models.json) now, deferring only the SQL diff — a registry-ahead-of-SQL state you opt into explicitly. Generate the SQL later, once Atlas is installed, with gombit db makemigrations:

gombit db makemigrations create_books \
  --model github.com/example/demo/internal/product.Product \
  --model github.com/example/demo/internal/book.Book

See migrations.md.

gombit generate

Regenerate the generator-owned *.gen.go (DTOs, mappers, CRUD handler) for the application's model-first resources from their current models plus gombit field policy. Run it after editing a model — regeneration is how the DTOs, mappers, and handler stay in sync with the model (ADR-016). make resource runs it for you after scaffolding a new resource.

gombit generate            # rewrite the *.gen.go from the models
gombit generate --check    # verify they are current; exit non-zero on drift (CI gate)
gombit generate --dry-run  # print what would be written without writing

Run it from an application directory. It reads the app's real compiled models via a throwaway program run inside the app module (like gombit db makemigrations), so a model change is reflected exactly.

  • It discovers resources by the .gombit-resource marker make resource writes — an AutoMigrated model without that marker (a join table, an audit-log model) is not a resource and is left alone.
  • The human-owned hooks.go is seeded when absent and then never overwritten or drift-checked; only the *.gen.go are compared by --check.
  • It fails closed on a package still using the legacy human-owned handler.go layout — migrate it first.

--check is the drift gate: commit the *.gen.go, and a stale copy (model changed but not regenerated) fails the check. Regenerate with gombit generate.

gombit make command

From an application directory (the output of gombit new):

gombit make command greet
gombit make command hello --package hello --dry-run
gombit make command greet --force

make is a Cobra parent; command is the subcommand. This writes a feature-package management command and registers it on the app-owned cmd/gombit tree — not the framework binary (go run github.com/gombit-dev/gombit/cmd/gombit). The generator requires a gombit new app (cmd/server/main.go and/or gombit.yaml) and refuses a framework-shaped tree so it cannot rewrite the framework cmd/gombit/main.go.

Default package is internal/commands. --package hello writes internal/hello/ instead. Files:

File Role
internal/<pkg>/<name>.go New<Name>Command() *cli.Command
internal/<pkg>/commands.go RegisterCommands(root *cli.Command) calling cli.AddCommand
cmd/gombit/main.go AST-appends <pkg>.RegisterCommands(root) next to product.RegisterCommands(root)

The module path is read from go.mod with trailing // comments stripped (same as make resource).

cli.AddCommand is a thin wrapper around Cobra AddCommand (D13 / ADR-014). Generated apps do not invent a second router and do not discover commands by reflection.

After generating:

go run ./cmd/gombit greet
go run ./cmd/gombit --help

Registration edits use go/ast + go/parser + go/format (never regex). --dry-run writes nothing. Re-running does not duplicate RegisterCommands or AddCommand calls. A generated command file that differs from this run (or a user-owned file) is refused unless --force. commands.go and cmd/gombit/main.go are additive AST edits of known registration points.

Command names that collide with framework families (new, dev, worker, jobs, build, make, generate, db, openapi, contract, client, routes, doctor, config, createsuperuser, version, help, completion) are rejected, as are gombit and register.

gombit worker

gombit worker [--queue default] [--concurrency N] [--lease 5m] [--shutdown-timeout 30s] [--metrics-addr :9091]

Runs the app's background-job worker from an application directory: it builds ./cmd/server and runs server worker with the same flags, stopping it gracefully on Ctrl+C and exiting with its status. In production run the built binary: ./server worker. It needs GOMBIT_JOBS_DRIVER=redis. See jobs.md § Running the worker.

gombit jobs

gombit jobs failed [--queue q] [--limit 50] [--json]
gombit jobs inspect <id> [--queue q] [--json]
gombit jobs retry <id>... | --all [--queue q]
gombit jobs forget <id>... [--queue q]
gombit jobs purge --force [--older-than 720h] [--queue q]

Lists, inspects, retries, and deletes the jobs a worker gave up on, straight from the Redis queue (GOMBIT_JOBS_DRIVER=redis); it does not need the app's code. See jobs.md § Failed jobs.

gombit db

Subcommands (each takes --help for its full flag list):

gombit db makemigrations create_products --model github.com/example/demo/internal/product.Product
gombit db plan [--allow <id>] [--json]
gombit db migrate
gombit db rollback
gombit db status
gombit db seed
gombit db reset [--force]
gombit db verify [--write] [--json] [--strict]
gombit db hash
gombit db lint [--latest N] [--json]
gombit db repair [--write-manifests]
gombit db check [--no-db] [--db-timeout 30s] [--openapi-url URL] [--json]

gombit db hash wraps atlas migrate hash to recompute the directory checksum (atlas.sum) after a migration is hand-edited (e.g. to backfill a renamed column), so recovery from a checksum mismatch never needs the raw Atlas CLI. atlas migrate hash is part of Atlas Community Edition (ADR-012). gombit db lint checks the directory's integrity, layout, and the safety of every migration, from the schema before and after it and from its statements (a destructive or unsafe change needs a -- gombit:allow <id> line in the migration), and gombit db repair restores consistency after a hand edit. See migrations.md § Linting and repairing.

gombit db check is the single schema-integrity gate for local development and CI: it runs the generated-contract, model-registry, migration-directory, migration-safety, models ↔ migrations, pending-migration, and database-schema checks (plus the TypeScript client with --openapi-url), names the layer that is inconsistent with its fix, and exits non-zero on any drift. --no-db skips the database layers. See migrations.md § Checking the whole chain.

gombit db plan classifies the change the models imply against the migration directory before a migration is written: destructive (a dropped table or column, a narrowed type), unsafe (fails on a populated table, such as a new NOT NULL column with no default), review, or safe. It uses atlas schema inspect and Atlas's own diff, not atlas migrate lint, which ADR-012 keeps outside the Community Edition dependency surface. It exits non-zero on an unacknowledged destructive or unsafe step. makemigrations --rename-table old:new renames a table with a native ALTER TABLE ... RENAME TO and, with --forget-model / --model, swaps the renamed model in the registry (see migrations.md § Renaming a table). gombit db makemigrations runs the same plan and refuses to write such a migration until each step is acknowledged with --allow <id|code>. See migrations.md § Planning a change.

See migrations.md for Atlas behavior and migration-safety.md for gombit db verify — the migration safety manifest and verifier a deployment host uses to gate destructive migrations (HOST-3).

gombit routes

Print a table of HTTP method and path:

gombit routes
gombit routes --url http://127.0.0.1:8080

Default listing constructs an in-process framework.App (memory cache, docs on) and prints framework-owned routes: /livez, /readyz, /metrics, /openapi.json (and Huma siblings), and /docs. Application feature routes are registered by the app; they are not discovered by reflection. Against a running server, --url fetches /openapi.json and lists the live contract paths (probes and other raw Gin routes stay out of that spec).

gombit doctor

gombit doctor
gombit doctor --dir database/migrations

Prints a status table. A FAIL row exits non-zero.

Check Role
go go version on PATH
node node --version; warn if missing (gombit dev needs it)
config config.Load() / FieldErrors
database database.Open + ping when a driver/DSN is set (timeout-bounded)
redis ping when GOMBIT_CACHE_DRIVER=redis (timeout-bounded)
migrations pending Atlas SQL in --dir (warn); skip if the directory is absent
http GOMBIT_HTTP_ADDR parses; warn if the port is in use
insecure production /docs, unwritable SQLite path, production JWT secret shorter than 32 characters or equal to the generated-app development placeholder

Network checks use a short timeout so CI cannot hang. Doctor does not start Postgres/Redis containers; an invalid driver, broken config.Load(), or an unwritable SQLite path is enough to flag a deliberately-broken config.

Appendix C rejects a production JWT secret shorter than 32 characters, and the generated-app development placeholder, on the config row (config.Load) and the insecure row when config is stubbed past Load. Production cookie-mode auth without GOMBIT_COOKIE_SECURE=true, trusted-proxy, and Redis TLSInsecure rejections live in config.Validate() and show up on the config row. Gombit has no CORS configuration, so there is no CORS+credentials check.

gombit config show

gombit config show

Prints the typed config.Load() result as aligned key<TAB>value lines. Database DSN userinfo/passwords, Cache.Redis.Password, and Auth.JWTSecret are replaced with ***** and must never appear in the output.

See config.md.

gombit createsuperuser

gombit createsuperuser --email admin@example.com --password correct-horse-battery-staple
gombit createsuperuser --no-input --email admin@example.com --password correct-horse-battery-staple
gombit createsuperuser

Django createsuperuser → gombit createsuperuser (M4-6). It seeds an admin account against the same auth users table as POST /auth/register, using the same auth.Service bcrypt hasher — there is no second hash path.

Requires GOMBIT_JWT_SECRET to be set (cfg.Auth.Enabled()): without it Bearer auth is unmounted by framework.New, so a superuser could never log in. It loads config with config.Load, opens the database with database.Open, then calls auth.Service.CreateSuperuser.

In development and test, the command runs auth.Migrate (GORM AutoMigrate) so a fresh local DB works before Atlas migrations exist. In production it never AutoMigrates: schema changes belong to gombit db migrate. If the users table is missing, the command fails and tells you to migrate first. CreateSuperuser shares Register's unique-email path — duplicate emails are refused with the same error as /auth/register — and its rules (auth.CheckNewAccount), checked before the database is opened: the email must be a bare address of at most 255 characters (admin@example.com, not admin or Root Admin <root@example.com>), what POST /auth/login accepts, so the account can sign in; and the password must meet the policy a user's own password meets on register, change and reset (at least 8 characters, at most 72 bytes). Anything else is refused and nothing is created. Accounts created before are not touched: one whose email is not an address still cannot sign in (delete it and create it again with this command); one with a short password keeps working until it is changed.

Flags

Flag Role
--email Superuser account email
--password Superuser account password (prefer the interactive prompt; visible in shell history and process listings when passed as a flag)
--no-input Never prompt; require both --email and --password (use in scripts and tests)

If --email or --password is omitted and stdin is a TTY, the command prompts interactively (password entry is hidden). If stdin is not a TTY (CI, tests, pipes), the missing flags are a hard error instead of a hang. --no-input skips TTY detection entirely.

createsuperuser sets auth.User.IsSuperuser; ADMIN-3 treats that flag as a bypass for all direct and group permission checks. See admin.md.

gombit version

gombit version
gombit version --short
gombit --version
gombit:   v0.6.0
commit:   617b487f7abe353bf9bbbb8fc78bc69709bee18c
built:    2026-09-28T08:03:45Z
go:       go1.26.0
platform: linux/amd64

Include this output when filing a bug report — the issue form asks for it.

Version resolution has three tiers, in order:

  1. ldflags. Release binaries are stamped by .github/workflows/release.yml with -X github.com/gombit-dev/gombit/cli.Version=<tag> (plus Commit and BuildDate).

  2. Module build info. A binary from go install github.com/gombit-dev/gombit/cmd/gombit@v0.6.0 carries no ldflags, so the version comes from runtime/debug.ReadBuildInfo(). commit and built come from the embedded vcs.revision / vcs.time settings when the build had them.

    A scaffolded app ships its own cmd/gombit, and in that binary the main module is the application while Gombit is a dependency, so the version is that dependency's entry in the build info: an app tagged v1.2.0 still reports the Gombit it was built with, not v1.2.0. When the app replaces the framework with a local checkout (or with a fork), no Gombit release is named, so dev is reported — the same rule gombit contract app and gombit upgrade apply to a go.mod, read from a different source: this command describes the binary you are holding, those commands the go.mod in the current directory (internal/frameworkmod reads go.mod alone and deliberately knows nothing of a go.work). The vcs.revision and vcs.time settings are recorded for the main module only, so an app's binary reports unknown for commit and built instead of the app's own commit: pairing a Gombit release with a commit nobody can find in the Gombit repository is the same misleading artifact the version line used to be.

  3. dev. A local go run ./cmd/gombit or a build from a checkout reports dev rather than Go's (devel), with unknown for the fields that have no source.

--short prints the bare version string and nothing else, which is what you want in scripts:

test "$(gombit version --short)" = "v0.6.0"

See installation.md and releasing.md.

gombit openapi and gombit client

See openapi.md and client.md.

gombit contract app

Emits the machine-readable application contract (HOST-1) — how a deployment host builds, health-checks, and migrates the app, projected from declared config and go.mod, never inferred from the source tree. Writes JSON to stdout or --out; --dir selects the project directory. Fails loudly when the framework version is missing or replaced by a local checkout. See app-contract.md.

gombit upgrade --dry-run

Plans upgrading the application to a framework release from the compatibility manifest built into this gombit, and changes nothing: the current and target versions, the dependency change, the automatic changes the app needs, every manual action with what to do, the breaking changes, the informational ones, and the framework schema changes with the migration steps they need for this app (from its model registry). A version the manifest does not list, a downgrade, or an app whose baseline names no framework release (a go.work, a local checkout, a fork) fails with the reason. It never prompts, so it runs in CI. See upgrade.md.

Without --dry-run, --to vX.Y.Z applies the plan's dependency changes: it edits the framework's replaces the plan names (go mod edit), runs go get github.com/gombit-dev/gombit@vX.Y.Z, and prints every go command and the go.mod and go.sum lines they changed. A go command that fails is shown with its full output, a result that does not build the target is refused, and either way go.mod and go.sum are restored. The plan's automatic and manual changes are printed as still to do. It never runs a migration: when the framework's tables change, it says the upgrade is not complete and lists the migration steps (migration_required in --json). --to is required there. See upgrade.md.

Flag Meaning Default
--dry-run Plan the upgrade and print it off
--to Target framework release: applied without --dry-run, planned with it with --dry-run, the newest release this CLI knows; required to apply
--dir Application directory (go.mod, gombit.yaml) .
--json Print the plan (or what was applied) as JSON off
--migrations The app's migration directory (its model registry), relative to --dir, as gombit db --dir database/migrations

gombit upgrade check

Reports every use, in the application (every package, tests included), of a framework API whose doc comment has a Deprecated: paragraph: file, line and column, the symbol, and what to use instead. Uses are resolved from type information (go/packages, go/types), never by matching text; the app must type-check. When the deprecation names its release (Deprecated: since vX.Y.Z, ...), a line shows the first release the compatibility policy lets remove the API, and --json adds since, removable_from and removable_from_major. It exits 1 when there is any use. See upgrade.md.

Flag Meaning Default
--dir Application directory .
--json Print the uses as JSON off

gombit upgrade config

Reports what an upgrade's configuration changes mean for the application, from the compatibility manifest:

  • What it covers: each variable the releases between the app's framework version (or --from) and the target rename, remove, re-default, newly require, or newly refuse values of.
  • Categories: renamed, default changed, action required, or security review.
  • For each change: the app's environment files that set it, and what to do.
  • Never a secret: it reads .env and .env.* and never prints a value from them. Only names are read from files that are not templates.
  • --write: upgrades the templates (.env.example and the like), renaming renamed variables and commenting out removed ones, each with a note. It never writes .env.
  • Exit status: 1 while any change needs action or review.

See upgrade.md.

Flag Meaning Default
--dir Application directory .
--from Release to analyze from the framework version the app builds against
--to Target release the newest release this CLI knows
--write Upgrade the environment templates (never .env) off
--json Print the report as JSON off

gombit upgrade contract

Reports whether the application's generated contract artifacts are what generation produces now; run it after gombit upgrade --to.

  • Resource files: the generator-owned *.gen.go, rendered as gombit generate --check renders them, with the app's framework version.
  • OpenAPI document: the committed openapi.json, compared with the running app's (--url).
  • TypeScript client: compared as gombit client check compares it.
  • --write: regenerates stale artifacts with that same generation, generator-owned files only. A handwritten file is reported, never overwritten.
  • Dependencies: while the resource files are not current, the checks against --url are unchecked (rebuild and restart the app, then rerun), and --write does not write the document or the client.
  • Exit status: 1 while anything is left stale, or on a failed write. An artifact it could not check does not fail it unless --strict, which a CI gate uses (with --url).

See upgrade.md.

Flag Meaning Default
--dir Application directory .
--url The running app's OpenAPI document, to check the committed one and generate the client from —
--spec Committed OpenAPI document, relative to --dir openapi.json
--out Generated client directory, relative to --dir frontend/src/api/generated
--write Regenerate stale artifacts (generator-owned files only) off
--npx npx binary that runs openapi-typescript npx
--json Print the report as JSON off
--strict Also exit 1 when an artifact could not be checked off

gombit upgrade fix

Runs the framework's codemods: mechanical rewrites of the app's uses of framework APIs that a release renamed, moved or reshaped. Uses are resolved from type information and rewritten in the syntax tree, never as text. Changed files are gofmt-formatted, and the rewrite is type-checked before it is proposed. By default it writes nothing: it prints the patch (a unified diff, for git apply) on stdout and a summary on stderr. --write rewrites the files atomically, and a second run changes nothing. A use a codemod cannot rewrite safely is left unchanged and listed, with the reason, and the command exits 1. This gombit ships no codemods yet. See upgrade.md.

Flag Meaning Default
--dir Application directory .
--only Run only these codemods (IDs, comma-separated) all
--patch Write the patch to this file instead of stdout —
--write Rewrite the files in place (not with --patch) off
--json Print the result, patch included, as JSON off

gombit upgrade baseline

Shows the application's upgrade baseline: the framework version it builds against (from go.mod) and the scaffold conventions it was generated with (recorded in gombit.yaml by gombit new). An app generated before that record existed is detected as scaffold 0; --write records it (appending the metadata block to gombit.yaml, nothing else). --json prints JSON, --dir selects the app. Nothing is written without --write. See upgrade.md.

Flag Meaning Default
--dir Application directory (go.mod, gombit.yaml) .
--json Print the baseline as JSON off
--write Record a detected baseline in gombit.yaml off

gombit upgrade notes

Prints upgrade notes from the compatibility manifest built into this gombit: each release's upgrade-relevant changes, classified as manual, automatic, or informational, with breaking ones marked. The release notes and upgrade-notes.md are rendered from the same manifest. With no flags, it prints every release, newest first. A version the manifest does not list fails with the reason (older than its first release, newer than this CLI, a pseudo-version, a downgrade). It never guesses. See upgrade.md.

Flag Meaning Default
--release One release's notes (a version, or unreleased) —
--from The notes of an upgrade from this version —
--to With --from, the target version the newest release this CLI knows
--json Print the releases and their classified changes as JSON off