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| 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 |
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 goldensCI picks this up via go test ./.... Do not commit replace directives or
machine-specific paths in the goldens.
Non-interactive (the acceptance criterion):
gombit new demo --database sqliteThat 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/serverNo 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| 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.
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.
From an application directory (the output of gombit new):
gombit devOne command starts:
- The Go API with reload when
airorwatchexecis onPATH. If neither is installed, Gombit runsgo run ./cmd/serverand prints a hint. - The Vite frontend with HMR (
pnpmwhen available, otherwisenpm). Vite proxies/api,/openapi.json,/docs, and/adminto the Go origin. Prefixes that do not start with/apiget an extra proxy entry from the liveGOMBIT_API_PREFIX. - An OpenAPI watcher that regenerates
frontend/src/api/generatedwhen the live/openapi.jsondocument 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/
| 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.
From an application directory (the output of gombit new):
gombit build --embed
gombit build --embed --out bin/server
gombit build --embed --dry-runSplit 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:
- Require
frontend/package.json(same class of error asgombit dev). - Vite production build in
frontend/(pnpmwhen available or whenfrontend/pnpm-lock.yamlexists, otherwisenpm run build). - Copy
frontend/dist→internal/web/static(replace previous assets; do not deleteinternal/web/embed.go). Fail iffrontend/dist/index.htmlis missing after the Vite build. go build ./cmd/serverto--out(defaultbin/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.
| 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.
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.
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_nullThe 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:WarehouseGenerators 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.
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.BookSee migrations.md.
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 writingRun 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-resourcemarkermake resourcewrites — anAutoMigrated model without that marker (a join table, an audit-log model) is not a resource and is left alone. - The human-owned
hooks.gois seeded when absent and then never overwritten or drift-checked; only the*.gen.goare compared by--check. - It fails closed on a package still using the legacy human-owned
handler.golayout — 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.
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 --forcemake 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 --helpRegistration 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 [--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 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.
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).
Print a table of HTTP method and path:
gombit routes
gombit routes --url http://127.0.0.1:8080Default 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 --dir database/migrationsPrints 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 showPrints 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 --email admin@example.com --password correct-horse-battery-staple
gombit createsuperuser --no-input --email admin@example.com --password correct-horse-battery-staple
gombit createsuperuserDjango 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.
| 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 --short
gombit --versiongombit: 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:
-
ldflags. Release binaries are stamped by
.github/workflows/release.ymlwith-X github.com/gombit-dev/gombit/cli.Version=<tag>(plusCommitandBuildDate). -
Module build info. A binary from
go install github.com/gombit-dev/gombit/cmd/gombit@v0.6.0carries no ldflags, so the version comes fromruntime/debug.ReadBuildInfo().commitandbuiltcome from the embeddedvcs.revision/vcs.timesettings 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 taggedv1.2.0still reports the Gombit it was built with, notv1.2.0. When the app replaces the framework with a local checkout (or with a fork), no Gombit release is named, sodevis reported — the same rulegombit contract appandgombit upgradeapply to ago.mod, read from a different source: this command describes the binary you are holding, those commands thego.modin the current directory (internal/frameworkmodreadsgo.modalone and deliberately knows nothing of ago.work). Thevcs.revisionandvcs.timesettings are recorded for the main module only, so an app's binary reportsunknownforcommitandbuiltinstead 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. -
dev. A localgo run ./cmd/gombitor a build from a checkout reportsdevrather than Go's(devel), withunknownfor 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.
See openapi.md and client.md.
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.
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 |
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 |
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
.envand.env.*and never prints a value from them. Only names are read from files that are not templates. --write: upgrades the templates (.env.exampleand 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 |
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 asgombit generate --checkrenders 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 checkcompares 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
--urlare unchecked (rebuild and restart the app, then rerun), and--writedoes 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 |
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 |
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 |
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 |