CLI reference
The px CLI drives a project from scaffold to production. Install it once with bun add -g @palinx/cli and the px command is on your PATH. A scaffolded app also wires the common commands into its package.json scripts, so you'll often run them through bun run, but each is available directly.
Run px --help at any time for the full list.
scaffold
Scaffold a new project.
px scaffold webapp my-appscaffold writes the project from a template, installs dependencies, and leaves you ready to run. See Scaffolding for the available templates and how to choose between them.
Run it bare and it goes interactive: a menu picks the template, asks for the styling when the template has a choice, and prompts for the project name.
px scaffoldFor webapp and frontend, --styles picks the styling: css (plain CSS, the default), sass or tailwind. --styles=sass works too.
px scaffold webapp my-app --styles tailwindAny other template rejects --styles, as does an unknown value. Run px scaffold webapp with no project name to see the usage line:
Usage: px scaffold webapp <project-name> [--styles css|sass|tailwind] [--into-existing]px --help describes it the same way:
scaffold Scaffold a new project (minimal, webapp, frontend, api, taskrunner).
For webapp and frontend, --styles css|sass|tailwind picks the
styling (plain CSS by default; the menu asks when omitted).See Styling for what each option sets up.
Pass --into-existing to scaffold alongside files that are already there; it writes per-file and skips anything that already exists, so it won't clobber your README.md, .git/, or other files you manage:
px scaffold webapp . --into-existingAny other option is an error, and the nearest known one is suggested:
Unknown option "--style". Did you mean --styles? Options: --styles css|sass|tailwind, --into-existing.dev
Start the development server.
px devdev serves your app, watches src/, and reloads on change. The port resolves in this order: --port= flag, then the PORT environment variable, then server.port from palinx.config.ts, then the default 2500. An invalid value in one tier falls through to the next. If your project has a src/scheduled/ directory, dev also boots the scheduler.
px dev --port=3000 # override the configured port
px dev --no-scheduler # skip booting src/scheduled/ tasks
px dev --inspect # boot under Bun's inspector--port=NNNNoverrides the port for this run. Note the equals form:--port=3000. A space-separated value (--port 3000) is ignored and the next tier wins.- Because
PORTsits above the config in that order, aPORTexported globally in your shell is picked up bydevand beatsserver.port. If the dev server binds a port you didn't expect, check your environment. --no-schedulerstarts the server without booting the tasks insrc/scheduled/, which is useful when you're iterating on the app and don't want jobs firing alongside it.--inspectboots the dev server under Bun's inspector for server-side debugging; it prints adebug.bun.shlink to attach to. Pass--inspect=host:portto pin where the inspector listens. See Debugging for the full workflow.
Reaching dev from another device
dev listens on 127.0.0.1, so only your own machine can open it. http://localhost:2500 works as usual. To open it from a phone, another computer or a container's host, give it an address to listen on:
px dev --host # every interface (0.0.0.0)
px dev --host 192.168.1.20 # one address; --host=192.168.1.20 works too
PALINX_HOST=0.0.0.0 px dev # or via the environmentA bare --host, or one followed by another flag (--host --port=3000), means every interface. The address resolves like the port: --host flag, then the PALINX_HOST environment variable, then server.host from palinx.config.ts, then the default 127.0.0.1. An invalid value prints a warning and the next tier wins.
When dev listens on every interface, the banner lists a Network address for each of your machine's network connections and warns that other devices can open it:
Local: http://localhost:2500
Network: http://192.168.1.20:2500
WS: ws://localhost:2500
! Listening on every interface (0.0.0.0): other devices on your network can
open this server and read the app's client source. Use it on networks you trust.Allowed host names
dev only answers requests addressed to a name it expects: localhost, 127.0.0.1, [::1], the address it listens on, and, after --host, your machine's network addresses and name. A request for any other name gets a 403:
Blocked request: px dev doesn't answer to the host "myapp.test". If you meant to open it under that name, add it to server.allowedHosts in palinx.config.ts or pass --allowed-hosts=myapp.test.This stops a web page on another site from reaching your dev server by pointing its own name at your machine. If you open dev under a custom name, such as a hosts-file entry or a tunnel, allow it:
px dev --allowed-hosts=myapp.test,.preview.testor set server.allowedHosts in palinx.config.ts. A leading dot (.preview.test) allows that name and every subdomain of it. all (or *) turns the check off, and the banner then warns that the host checks are off.
The live-reload connection has one more rule: it only accepts pages from the dev server's own address, so a page on another site can't listen in on your build errors.
Development server security
dev serves only what the browser side of your app needs: your pages and layouts, the modules they import, the framework's own browser code, and the files in public/. It never serves .env, palinx.config.ts, API handlers, guards, server-only pages or the services only they use, whichever address it listens on. Together with the 127.0.0.1 default and the host and origin checks above, that keeps your project's source and secrets on your machine while you develop. Versions before 0.3.2 were less strict; see the 0.3.2 release notes.
make
Generate a component, service, route, or API endpoint with the right boilerplate.
px make component user-badge
px make service billing
px make route dashboard
px make api billing-reportsEach writes a correctly-shaped file into the matching src/ directory so you start from a working stub instead of an empty file:
| Command | Writes |
|---|---|
px make component user-badge |
src/components/user-badge.ts |
px make service billing |
src/services/billing.service.ts |
px make route dashboard |
src/routes/dashboard.route.ts |
px make api billing-reports |
src/api/billing-reports.ts |
Those four kinds (component, service, route, api) are the full set. The generated api class mounts at /api/<name> and answers with an empty JSON list until you fill it in.
test
Run your test suite through Bun's test runner.
px testExits non-zero on failure, so it fits straight into CI. Every argument is forwarded verbatim to bun test, so anything Bun's runner accepts works here:
px test --watch # re-run on change
px test tests/auth.test.ts # a single file
px test -t "logs in" # only tests matching a name filterSee Testing for writing tests.
build
Produce the production build.
px buildbuild compiles your app into a self-contained build under dist/ (configurable via build.outDir): the prerendered pages and client bundle, plus a ready-to-run server at dist/server/index.js. Your palinx.config.ts is read at this point and its values are built into that server, so a config change takes effect on the next build. See Deployment for what it produces and how to run it.
serve
Run the production build.
px serveserve runs the server that build wrote to dist/server/index.js, the same one you would start with bun dist/server/index.js. It serves your prerendered pages, static assets, and API routes, runs your guards, and boots scheduled tasks. Run build first; with no build, serve stops and tells you to.
Every scaffolded project has a start script for it, so bun run start does the same thing.
px serve --port=8080 # override the port
PORT=8080 px serve # or via the environment
px serve --no-scheduler # skip booting src/scheduled/ tasksThe port resolves exactly as in dev: --port= flag, then the PORT environment variable, then server.port from palinx.config.ts, as built, then the default 2500, with invalid values falling through to the next tier. The flag uses the equals form; --port 8080 with a space is ignored. --no-scheduler runs the server without booting scheduled tasks, which fits setups where a separate instance owns the jobs.
Unlike dev, the production server listens on every interface (0.0.0.0) by default, which is what a container or a reverse proxy on another machine needs. The address resolves in the same order as in dev: --host flag, then PALINX_HOST, then server.host as built, then 0.0.0.0. Narrow it when a proxy on the same machine is the only client:
px serve --host 127.0.0.1
PALINX_HOST=127.0.0.1 bun dist/server/index.jsIf server.host is a loopback address such as 127.0.0.1, build warns that the server will be unreachable from outside its machine or container; start it there with PALINX_HOST=0.0.0.0.
Config comes from the build
serve does not read palinx.config.ts. The default port and address from server.port and server.host, and the TLS, CORS, headers, and rate limits it uses, are the values your config had when you ran build. --port= or PORT, and --host or PALINX_HOST, still override those defaults when the server starts. If your config reads an environment variable for one of them, that variable has to be set when you build, not when you serve. This matters most for TLS: a config that only turns TLS on when, say, TLS_CERT is set builds a plain HTTP server if that variable was missing at build time. The certificate and key files themselves are read when the server starts.
The one exception is the CSRF secret. It is never written into dist/, so the server reads PALINX_CSRF_SECRET from its environment each time it starts. If your config set a secret at build time and the variable is missing when the server starts, it refuses to start rather than run without one.
--version
Report what you are actually running.
px --versionRun this before filing a bug report. Your package.json records the version ranges you asked for, which is a different thing from the versions that ended up installed; a range of latest tells nobody anything. --version reads the packages resolved under node_modules, so it reports what your app is really executing.
Outside a project it prints just the CLI's own version:
px 0.5.0Inside a project it also lists the resolved @palinx/* packages and the Bun version:
px 0.5.0
Project packages (resolved from node_modules):
@palinx/cli 0.5.0
@palinx/compiler 0.5.0
@palinx/core 0.5.0
@palinx/router 0.5.0
@palinx/scheduler 0.5.0
Bun 1.3.14-v is an alias for the same thing.
A package your project declares but has not installed is listed too, marked so you can see the gap rather than having it silently omitted. If you see that, run bun install.
update
Move a project onto the newest @palinx/* packages its version ranges allow.
px updateThis updates every @palinx/* dependency in the current project, lockfile included. It is the command to reach for when a release has shipped and you want it; a scaffolded project usually permits the new version already, and what pins you to the old one is the lockfile rather than the range.
To update the globally installed CLI instead:
px update --selfThe default is deliberately the project, not the CLI. A global install is shared by every project on your machine, so a command that quietly changed it would be a surprise. Run px update outside a project and it stops with an error pointing you at --self.
Both forms report what moved, and say so plainly when nothing did:
Updated:
@palinx/core 0.1.1 -> 0.2.0 (range latest -> ^0.2.0)
@palinx/router 0.1.1 -> 0.2.0 (range latest -> ^0.2.0)Open ranges gain a ceiling
Notice the (range latest -> ^0.2.0) above. Updating rewrites the range in your package.json as well as the lockfile, and when the old range was open, latest or >=0.1.0 or *, the replacement carries an upper bound that you never wrote. The command calls this out when it happens:
Note: bun update narrowed an open version range for @palinx/core, @palinx/router.
Those now carry an upper bound they did not have before, so future
updates will stop at it. Edit package.json if you meant to stay open.Nothing is broken, but your manifest is now stricter than it was, and the next release outside that bound will not arrive on its own. Edit package.json if you would rather stay open. The command does not rewrite the range back for you, because fighting your package manager over your own manifest is worse than telling you what it did.
Ranges that already had a ceiling, anything starting with ^ or ~, are not affected and are not reported this way.
Updates stop at the next minor
A caret range on a 0.x version accepts patch releases only, so ^0.3.0 takes 0.3.1 and 0.3.2 but stops before 0.4.0. Below 1.0 a caret treats every minor bump as a breaking change, which means each new minor sits outside the range your project already carries. Once your project carries ^0.3.0, px update will keep reporting this even after 0.4.0 is published:
Already up to date. Nothing changed.
@palinx/core 0.3.2That is semver behaving correctly rather than a failure. The new minor sits on the other side of the ceiling your range declares, and the update command will not cross it for you. To take it, either widen the range in package.json and run px update again:
"@palinx/core": "^0.4.0"or install the version directly:
bun add @palinx/core@latestPre-1.0, minor releases are where the meaningful changes land, so it is worth checking for a new one rather than trusting "already up to date" to mean "on the newest release".
Next
Deployment puts build and serve together into a production workflow.