CLI
The datapages command creates projects, generates server code, checks application models and runs the development server.
Install the command with Go:
go install github.com/romshark/datapages/cmd/datapages@latest
Installation covers templ and the other tools a project needs.
| Command | What it does |
|---|---|
datapages init |
Creates a project, or rewrites the agent instructions of an existing one |
datapages gen |
Generates the server, href and action packages of every app package |
datapages lint |
Checks the application model and writes nothing |
datapages watch |
Runs the dev server, which regenerates, rebuilds and reloads on file changes |
datapages version |
Prints the version of the CLI |
datapages help <command> and datapages <command> --help print the help of a command. There is no --version flag and no shell completion command.
The commands write their messages to stderr. The progress lines of init and the output of version and help go to stdout. A command that fails prints a line that starts with error: and exits with status 1. A usage error, such as an unknown command or flag, exits with 1 too.
datapages init
datapages init [flags]
init sets up a Datapages project. When needed, it creates the Git repository and Go module. It writes datapages.yaml, the app package, development files and agent instructions. It also pins Datapages in go.mod and generates the code. It never deletes a file.
Without -n, init asks for every setting it needs. With -n it asks nothing and takes the settings from the flags.
In a project that is set up, init rewrites the agent instructions unless you decline them, adds .env to .gitignore when it is missing there, and prints Project already initialized. A project is set up when its Go module has a datapages.yaml.
Where init creates the project
- Outside a project that is set up,
initasksCreate a subdirectory for the project?. Yes asks for theDirectory nameand creates the directory in the current directory. The prompt suggests Yes outside a Go module in a directory that holds files other than dotfiles, such as a home directory.--namecreates the directory without the prompts.-nwithout--nameuses the current directory. - A new subdirectory gets a Go module of its own, even inside another module. Without one,
inituses the root of the Go module that contains the current directory, and printsUsing the Go module in <dir>when that is not the current directory. Outside a Go module it uses the current directory. In a project that is set up,initruns in the module root and asks nothing about the directory. - Outside a git repository,
initasksNot inside a git repository. Create one?and runsgit initin the project directory on Yes. No continues without a repository.--no-gitanswers No and--no-git=falseanswers Yes without the prompt, and-nanswers Yes. A project that is set up gets no repository and no prompt. - Without a
go.mod,initasks for theModule pathand runsgo mod init. The prompt suggests a path made from the URL of theoriginremote, or else the directory name, and an empty answer takes the suggestion.--modulegives the path without the prompt, and-nrequires it.
Files init writes
init writes these files when they are missing:
| File | Content |
|---|---|
datapages.yaml |
The scaffold configuration |
app/app.go, app/app.templ |
An app package with PageIndex, PageError404 and PageError500, written only in a module without a datapages.NewServer call |
cmd/server/main.go |
The entry point, written by the datapages gen run of init into the cmd directory |
.env |
NATS_URL and a random SESSION_ENCRYPTION_KEY |
compose.yaml |
A NATS server for make up |
Makefile |
The targets dev, up, down, build, gen and lint, and npm-install and bundle for TypeScript Web Components. gen runs datapages gen, then templ generate. With hidden generated code, gen runs them in reverse order and build runs gen first |
.vscode/extensions.json |
The templ and Go editor extensions |
.vscode/settings.json |
With hidden generated code, hides datapagesgen directories and *_templ.go files in VS Code |
.github/workflows/ci.yml |
With --ci github-actions, a CI job that runs templ generate and datapages gen, runs the tests and builds ./cmd/server/. With committed generated code, it fails when a generator changes a file |
init sets up the project only on a run that creates the git repository, go.mod, datapages.yaml or app/app.go. Only such a run writes the last 7 files of the table. A run that creates none of the 4 writes none of the 7, even when one of them is missing.
On such a run init also takes these steps, in order:
- Adds the
github.com/romshark/datapagesrequirement togo.modwhen it is missing. A release of the CLI, or a build with a pseudo-version, requires its own version aftergo listconfirms that Go can download it. Any other build, such as(devel), a+dirtybuild or a commit that Go cannot download, adds areplacedirective to the checkout it was built from and warns that the path exists only on this machine. Without a checkout, it warns and letsgo mod tidypick a version. - Runs
npm installandnode build.mjsinweb/for TypeScript Web Components. - Runs
templ generate ./app/with templ v0.3.1100 throughgo run, thengo mod tidy. - Fails when the selected Datapages version has no importable root package, as releases through v0.9.4.
- Runs
datapages gen, which writesapp/datapagesgen/andcmd/server/main.go. - With Web Components, writes
app/app.templagain to load them through the generatedassetspackage, and runstempl generateagain. - Runs
go mod tidyagain and printsProject initialized successfully. - Without
-n, asksRun the app now?and runsmake devon yes, which starts NATS with Docker Compose and thendatapages watch.
On every run, init asks Write AI agent instructions and skills?. Yes rewrites AGENTS.md, CLAUDE.md, GEMINI.md, .github/copilot-instructions.md, .cursor/rules/datapages.mdc and the Datapages skills in .agents/skills/ and .claude/skills/. --no-ai-skills answers No and --no-ai-skills=false answers Yes without the prompt, and -n answers Yes. init keeps a file you edited as <name>.bak before it replaces it. It leaves alone an AGENTS.md that does not end with the Datapages stamp, and skills with other names. See AI agents.
When init writes the app package, --web-components or a prompt selects the Web Components setup. Each setup adds an example component to the index page:
| Value | Setup |
|---|---|
rocket |
Datastar Rocket components in app/static/components/, loaded with the Rocket bundle of the Datastar release Datapages uses |
typescript |
TypeScript in web/src/, bundled by esbuild into app/static/components.js. Needs npm and node on the PATH. datapages.yaml gets a custom watcher that bundles again on every change |
javascript |
JavaScript modules in app/static/components/, served as they are |
none |
No Web Components |
When init sets up the project, --generated-code or the prompt Commit generated files? selects whether git keeps the generated code, datapagesgen/ and *_templ.go:
| Value | Setup |
|---|---|
committed |
git keeps the generated code. A clone builds without the generators, and so does go install of the module. Diffs and merges include the generated files |
hidden |
init adds datapagesgen/ and *_templ.go to .gitignore and writes .vscode/settings.json. A clone builds only after templ generate and datapages gen, which make build and the CI workflow run. go install of the module fails. On a clone, datapages gen fails while the app package uses the generated href, action or assets package, because the app package does not compile without them |
--ci or the prompt Set up CI? selects the CI system that init writes a workflow for:
| Value | Setup |
|---|---|
github-actions |
.github/workflows/ci.yml for GitHub Actions |
none |
No workflow |
Flags
| Flag | Default | Description |
|---|---|---|
-n, --non-interactive |
false |
Ask nothing. Uses the current directory unless --name is set, creates a git repository unless --no-git is set, and requires --module outside a Go module. Selects none for Web Components, committed for the generated code and github-actions for CI, writes the agent instructions, and skips Run the app now? |
--name |
"" |
Directory to create in the current directory and to set up a new project in, with a Go module of its own. Also applies in a project that is set up |
--no-git |
false |
Create no git repository. --no-git=false creates one without the prompt. Ignored inside a git repository and in a project that is set up |
--module |
"" |
Module path for go mod init. Ignored when go.mod exists |
--web-components |
"" |
rocket, typescript, javascript or none. Used only when init writes the app package. Otherwise init prints Ignored --web-components: init writes no app package here. |
--generated-code |
"" |
committed or hidden. Used only when init sets up the project. Otherwise init prints Ignored --generated-code: the project is already initialized. |
--ci |
"" |
github-actions or none. Used only when init sets up the project. Otherwise init prints Ignored --ci: the project is already initialized. |
--prometheus |
true |
Names datapages.EnablePrometheus in the cmd/server/main.go that init writes, which serves metrics on HOST_METRICS:PORT_METRICS, localhost:9090 by default. --prometheus=false names datapages.DisablePrometheus |
--no-ai-skills |
false |
Write no agent instructions and no skills. --no-ai-skills=false writes them without the prompt |
-h, --help |
Print the help |
Exit status
0 when init set up the project, and when the project was set up before. 1 when a step fails, such as a missing --module with -n outside a Go module, an unknown --web-components, --generated-code or --ci value, or a failed git, go, npm or node command. After Run the app now?, a failed make dev ends init with 1 too.
Examples
Create a project in the new directory shop without prompts:
datapages init -n --name shop --module example.com/shop --web-components none
Set up a project in the current directory without a git repository and without agent instructions:
datapages init -n --no-git --no-ai-skills --module example.com/shop
Update the agent instructions of a project to the installed CLI:
datapages init -n
datapages gen
datapages gen [flags]
gen generates code for every app package in the module. It reads go.mod, datapages.yaml, the module's Go files and the .templ files rendered by app packages. A successful run prints nothing except the agent-instructions notice described below. It takes these steps:
- Finds the module and reads
datapages.yaml. See Project discovery. - Compares the CLI with the Datapages requirement in
go.mod. See Version checks. - Finds the app packages from the
datapages.NewServercalls of the module. It fails when the directory of an app package does not exist. - Raises the Datapages requirement in
go.modto the CLI version when the CLI is newer. - Parses and checks every app package with the rules of
datapages lint, before it generates any of them. - Writes the generated package of each app package that passed. An app package with errors keeps the code an earlier run wrote. One that was never generated gets stubs, which let its imports of the generated packages resolve while you fix the errors.
- In a module without a
datapages.NewServercall, writesmain.gointo thecmddirectory when that directory holds no Go file. - Runs
go mod tidy.
gen writes these files:
| Path | Content |
|---|---|
<app>/datapagesgen/app_gen.go |
The server type, routing and handler wiring |
<app>/datapagesgen/href/href_gen.go |
Type-safe URL builders |
<app>/datapagesgen/action/action_gen.go |
Type-safe Datastar action expressions |
<app>/datapagesgen/assets/assets_gen.go |
Asset paths, when the app package declares static files |
<cmd>/main.go |
The entry point, written only in a module without a datapages.NewServer call |
go.mod, go.sum |
The raised requirement and the result of go mod tidy |
<app> is the directory of an app package, such as app. gen rewrites the files under datapagesgen/ on every run and removes an assets package the app package no longer needs. Do not edit them.
gen does not run templ generate. It type-checks the app package together with its *_templ.go files. After a change to the application model, run gen before templ generate, which lets templates use the new href and action helpers. When generated templ code refers to a helper that does not exist yet, the app package does not compile and gen fails. Remove the reference, run templ generate and gen, then restore the reference and run templ generate again. See Troubleshooting.
A release of the CLI prints this line when the skills of the project come from another version:
Agent instructions differ from this CLI; run `datapages init -n` to update them (edits are backed up).
Flags
| Flag | Default | Description |
|---|---|---|
-h, --help |
Print the help |
Exit status
0 when every app package was generated and go mod tidy succeeded. 1 on any error, such as an error in an application model, a datapages.NewServer call the scan rejects, an invalid datapages.yaml, a go.mod that requires a newer Datapages release, or a failed go mod tidy. The app packages without errors are generated even when gen exits with 1.
Examples
Regenerate after changing the application model and the templates:
datapages gen && templ generate && go build ./...
Fail a CI job when the committed generated code is out of date, as the .github/workflows/ci.yml of a scaffold with committed generated code does:
datapages gen
git diff --exit-code
datapages lint
datapages lint [flags]
lint parses and checks every app package of the module with the rules of datapages gen and writes nothing. It reports the errors of all app packages in 1 run, and the events of different app packages that claim the same subject. The specification lists the rules, the checks of .templ files included.
lint reads go.mod, datapages.yaml, the Go files of the module and the .templ files that the app packages render. It does not change go.mod and does not run go mod tidy. It fails when go.mod requires a newer Datapages release than the CLI, and when datapages.yaml is invalid.
Unless watch.toxiproxy.disabled is set, lint also reports each endpoint key of watch.toxiproxy.presets that names no route of any app package of the module. It skips this check while an app package has errors:
error: watch.toxiproxy.presets["Edge"].endpoints: no endpoint has the pattern "GET /not-found/"; did you mean "GET /not-found/{$}"?
Each error in an application model is printed as at <file>:<line>:<column>: <message>, often followed by a fix: line. The file is named without its directory:
at app.go:34:6: page has invalid path comment: PageIndex
fix: First doc comment line must be `// PageIndex is /`; put a blank `//` line before any description
error: parsing app package: 1 error(s)
An error in a datapages.NewServer call names the file relative to the module root:
error: cmd/admin/main.go:55:3: datapages.NewServer generates into datapagesgen.Server, but app/admin generates into app/admin/datapagesgen
fix: name example.com/shop/app/admin/datapagesgen.Server
Flags
| Flag | Default | Description |
|---|---|---|
-h, --help |
Print the help |
Exit status
0 when no app package has an error. 1 when one has, and when the module, the scan or the version check fails.
Examples
Check the model in CI before the tests:
datapages lint
go test ./...
Keep the colors in a CI log, which is no terminal:
FORCE_COLOR=1 datapages lint
datapages watch
datapages watch [flags]
watch runs the development server for 1 app package. It regenerates code, rebuilds and restarts the app after file changes, then reloads open browser tabs. Open the --host address, which defaults to http://localhost:7331. Pages loaded from the app's own address do not reload or show build errors. Ctrl+C stops the process.
watch also serves the app through a Toxiproxy proxy, which simulates slow and failing networks, and a settings page for it. It prints both addresses at startup. See Slow and failing networks.
watch reads its settings from the watch section of datapages.yaml. It needs templ on the PATH, and golangci-lint when lint is on.
Start watch in the module root. From a subdirectory, the module-relative main package path passed to go build, such as ./cmd/server/, names no package.
On startup
- Starts the update check.
- Finds the module and writes the watch lock.
- Reads
datapages.yaml. When neitherdatapages.yamlnordatapages.ymlexists, it writes the scaffold file, which applies from the next start. - Runs
datapages genfor every app package. It prints the errors and continues. - Selects the app package with
--app, and the main package to build. That is the main package with thedatapages.NewServercall for the app package, or thecmddirectory when no main package has one. - Unless
watch.toxiproxy.disabledis set, starts the Toxiproxy proxy and its settings page, and printstoxiproxy: <proxy URL> (settings: <settings URL>). It warns about endpoint keys of the presets that name no route of the app package. - Starts
templ generate --watchin the module root and the dev server on--host. - Runs the command of each custom watcher once. The first is the built-in watcher that runs
datapages gen. - Builds the main package with
go buildinto a temporary directory and starts it indir-work, with the arguments inflagsandTEMPL_DEV_MODE=true, which turns on dev mode. - Sends
OPTIONSrequests toapp-hostevery 100 ms until the app answers, then reloads the open tabs.
The dev server forwards each request to app-host. It adds a script to every HTML response, which connects to the dev server over a WebSocket at /__templier/events and reloads the tab when told to.
On a file change
| Changed file | What happens |
|---|---|
A .templ file |
templ generates its Go code, then tells watch to reload the tabs or to rebuild the app |
A .go file in or below the directory of an app package, outside datapagesgen/. A .go file in the directory of a package with a datapages.NewServer call |
The built-in watcher runs datapages gen, then watch rebuilds and restarts the app |
| A file that a custom watcher includes | The command of the watcher runs, then the action of its requires |
datapages.yaml and datapages.yml |
Nothing, unless a custom watcher includes them. The app keeps running and keeps its state in memory |
Any other file that exclude does not match |
watch rebuilds and restarts the app |
watch builds the new binary while the app runs. Then it stops the app with an interrupt signal, kills it when it has not exited after 5 seconds, and starts the new binary. watch reads datapages.yaml once. Restart it after changing the watch section. Only the presets of watch.toxiproxy are read again, by the Reload config button of the settings page.
Besides the files of datapages gen, watch writes the watch lock, the scaffold datapages.yaml when the module has none, the watch.toxiproxy section of datapages.yaml when Save to config of the settings page is clicked, and the *_templ.go files that templ generates. templ writes the text files of its development mode into the system temporary directory, or into TEMPL_DEV_MODE_ROOT when it is set. The app binaries go into a templier-* directory in the system temporary directory, which watch removes when it exits.
While a step fails, the dev server answers every request with an error page instead of forwarding it. The page shows the output of the failed step under its name, such as templ, go, golangci-lint, process for an app that exited while starting, or the name of a custom watcher with fail-on-error, datapages gen included. The open tabs reload when a step fails and when it succeeds again.
Flags
| Flag | Default | Description |
|---|---|---|
--app |
"" |
Directory of the app package to run, relative to the module root, as app/frontend. Required when the module has more than 1 app package. The last element alone, as frontend, is an error |
--host |
localhost:7331 |
Address the dev server listens on. The Toxiproxy proxy forwards to it, and listens on its host unless watch.toxiproxy.host is set. Port 0 turns the proxy off |
-h, --help |
Print the help |
Exit status
0 after Ctrl+C or SIGTERM, once watch stops the app, templ and the dev server.
1 when startup fails, including outside a Go module, with invalid configuration, without a required --app, without templ or with an address in use. A configured Toxiproxy section also makes a proxy startup failure fatal. Without the section, watch warns and runs without the proxy. A failed datapages gen does not stop startup.
Examples
Run the dev server of a module with 1 app package, as make dev of the scaffold does:
datapages watch
Run the app package app/admin of a module with 2 app packages:
datapages watch --app app/admin
Serve the dev server on another address, such as while another program uses port 7331:
datapages watch --host localhost:7332
1 module runs 1 watch at a time. Every watch of the module forwards to the same app-host, which 2 apps cannot both listen on.
datapages version
datapages version [flags]
version prints the version of the CLI to stdout and reads no file. A release prints its version without the v prefix, such as 0.10.1. A CLI built from source prints (devel) or a pseudo-version. --full adds the commit, the build date, the Go version, the operating system and architecture, and the modules the binary was built with.
Flags
| Flag | Default | Description |
|---|---|---|
--full |
false |
Print the build information and the dependencies |
-h, --help |
Print the help |
Exit status
- 1 for a usage error, such as an extra argument or an unknown flag.
Examples
Print the version:
datapages version
Print the build information to attach to a bug report:
datapages version --full
Project discovery
Module
gen and lint use the Go module whose go.mod is in the current directory or its nearest parent. watch uses the module root. Without a module, the commands fail with not inside a Go module (no go.mod found in any parent directory). --app, the cmd key and file names in datapages.NewServer errors are relative to the module root. init finds its directory as described in Where init creates the project.
App packages
The commands find the app packages through the datapages.NewServer calls of the module. They read the calls from the syntax tree, without type-checking, which lets them read a main.go that names a generated package that does not exist yet:
s, err := datapages.NewServer[
app.App, // the app package, app/
struct{}, // the session data type
datapages.EnablePrometheus, // the metrics mode
datapagesgen.Server, // the generated package, app/datapagesgen/
](a, messageBroker, opts...)
The scan reads every .go file of the module, test files included. It skips:
- directories named
vendor,node_modulesortestdata, and directories whose names start with.or_ - directories with a
go.modof their own, which belong to another module - files whose
//go:buildline excludes them from every build, such as//go:build ignore
A file with a syntax error fails the scan. So does a call that breaks these rules:
- The call has 4 or 5 type arguments, and the
datapagespackage is imported under a name. A dot import is an error. - The app type lives in a package of its own in the module, which is neither the module root nor a
datapagesgenpackage. - The fourth type argument is the
Servertype of thedatapagesgenpackage directly below the app package. Generated code always goes there. - The third type argument is
datapages.EnablePrometheusordatapages.DisablePrometheus. - Calls that name the same app package name the same session data type and the same metrics mode.
The scan also rejects every import of a package under github.com/romshark/datapages/runtime in a file that Datapages did not generate.
Each app package that a call names is 1 application. gen and lint process all of them, and watch runs the one that --app selects. See Multiple applications.
A module without a call has 1 app package, app/, generated into app/datapagesgen/. gen then writes the entry point <cmd>/main.go with a call, which ends this fallback. Without an app/ directory, the commands fail with app: no such directory, the default app package of a module without a datapages.NewServer call.
Version checks
A release of the CLI compares its version with the github.com/romshark/datapages requirement in go.mod:
-
When
go.modrequires a newer release,genandlintfail with this error.watchprints it withouterror:at startup and shows it on its error page after every regeneration:error: go.mod requires github.com/romshark/datapages v0.11.0 but you are running v0.10.1 run: go install github.com/romshark/datapages/cmd/datapages@v0.11.0 -
When the CLI is newer,
genand thegenruns ofwatchraise the requirement to the version of the CLI and rungo mod tidy.lintchanges nothing.
A module without the requirement skips both checks, and so does a CLI without a release version, such as (devel), a pseudo-version or a version with build metadata, as +dirty. See Upgrading.
Update check
datapages watch checks for a newer release when it starts. The other commands do not.
It requests https://api.github.com/repos/romshark/datapages/releases/latest in the background, which does not delay the dev server, and gives up after 5 seconds. When the latest release is newer than the CLI, it prints to stderr:
update available: v0.11.0
run: go install github.com/romshark/datapages/cmd/datapages@latest
changelog: https://github.com/romshark/datapages/releases/tag/v0.11.0
A CLI without a release version skips the check. A failed request, such as one without network or over the GitHub rate limit, prints nothing. No flag or environment variable turns the check off.
Watch lock
datapages watch keeps a lock file while it runs. It tells other tools that a dev server uses the module.
templ generate deletes the text files that templ development mode reads. A templ generate in a module while watch runs on it stops the app from rendering its templates. Restart watch to write the files again. datapages gen does not run templ generate. datapages init runs it only on a run that sets up a project.
- Directory:
$DATAPAGES_WATCH_LOCK_DIR, ordatapages-watchin the system temporary directory, which is$TMPDIRor/tmpon Unix. - File:
<hash>.lock, where<hash>is 16 hexadecimal digits of the SHA-256 of the absolute module path. Each module has 1 file. - Content: 1 line with the process ID, the module directory and the
--hostaddress, separated by tabs. - Refresh:
watchwrites the file again every 2 seconds. Readers treat a file older than 10 seconds as stale, which allows 5 missed refreshes. - Removal:
watchdeletes the file when it exits. Awatchkilled with SIGKILL leaves the file, which goes stale 10 seconds later. - Failure: when the file cannot be written,
watchprintswarning:with the cause and runs without the lock.
No datapages command reads the lock. The mage targets of the Datapages repository that run templ generate refuse to start while a fresh lock exists. A script of your own can make the same check. A .lock file in the directory that changed within the last 10 seconds means that a watch runs.
Environment variables
The CLI reads these variables:
| Variable | Commands | Effect |
|---|---|---|
NO_COLOR |
all | Any non-empty value turns ANSI colors off. It overrides the next 2 |
FORCE_COLOR |
all | Any non-empty value, 0 included, turns colors on when stderr is no terminal |
CLICOLOR_FORCE |
all | 1 turns colors on when stderr is no terminal |
DATAPAGES_WATCH_LOCK_DIR |
watch |
Directory of the watch lock files |
DATAPAGES_LOADCACHE |
gen, lint, watch, init |
off turns the load cache off. An absolute path moves it. A relative path turns it off. The default is datapages/load in the user cache directory, such as ~/Library/Caches on macOS and ~/.cache on Linux |
DATAPAGES_LOADCACHE_DEBUG |
gen, lint, watch, init |
Any non-empty value prints loadcache: lines with hits, misses and timings to stderr |
GOPACKAGESDRIVER |
gen, lint, watch, init |
A value other than off turns the load cache off. So does a gopackagesdriver on the PATH while the variable is unset |
Without these variables, colors are on when stderr is a terminal. Colors apply to the error: label and to the errors in application models. The update notice of watch is colored when stdout is a terminal, unless NO_COLOR is set or TERM is dumb.
The load cache keeps the package graph and the compiled type information of the standard library and the downloaded modules from the last load of each app package. A later load whose inputs did not change reuses them and runs no go list.
The go commands that the CLI runs, as go mod tidy and go build, read the Go environment variables, such as GOFLAGS and GOPROXY.
watch sets these variables for the processes it starts:
| Variable | Value | Set for |
|---|---|---|
TEMPL_DEV_MODE_WATCH_ROOT |
The module root | watch and every process it starts. templ development mode then applies only to the templates of the module, not to templ components of dependencies |
TEMPL_DEV_MODE |
true |
The app. It turns on templ development mode and Datapages dev mode |
FORCE_COLOR, CLICOLOR_FORCE |
1 |
go build, golangci-lint and the commands of custom watchers, datapages gen included. The error page shows their colors |
See also
- datapages.yaml: the
cmdkey and everywatchsetting. - Slow and failing networks: the Toxiproxy proxy of
watchand its settings page. - Installation: install the CLI, templ and the tools a project needs.
- Multiple applications: build several app packages in 1 module and select 1 with
--app. - Upgrading: move a project and its
go.modto a new release. - Troubleshooting: generation errors and dev server problems.
- Linting: the rules that
genandlintapply to templates.