@stamhoofd/cli
v2.152.0
Published
`stam` is the development CLI for this repository. It manages local setup, shared services, app processes, development configuration, database helpers, SSO helpers, tests, and cleanup.
Readme
Stamhoofd CLI
stam is the development CLI for this repository. It manages local setup, shared services, app processes, development configuration, database helpers, SSO helpers, tests, and cleanup.
Quick Start
yarn install
yarn stam setup
yarn stam dev allRun yarn stam setup shell to install the CLI alias stam in your .zshrc or .bashrc (that removes the need to type yarn and the need to always run commands in the project root).
Open the dashboard URL printed by the CLI, or run yarn stam status to see active services, instances, URLs, and credentials.
Run yarn stam --help or yarn stam <topic> --help for command help.
For CLI Users
Commands
| Area | Command | Purpose |
| ----------- | -------------------------------------- | -------------------------------------------------------------------------- |
| Build | yarn stam build | Build shared packages and all app packages for the selected environment. |
| Setup | yarn stam setup | Check the machine and offer recommended setup fixes. |
| Setup | yarn stam setup node | Install the Node.js version from .nvmrc using fnm or nvm. |
| Setup | yarn stam setup dns | Configure local .stamhoofd DNS. |
| Setup | yarn stam setup cert | Trust the local Caddy HTTPS authority. |
| Development | yarn stam dev all | Start shared services and the full app stack. |
| Development | yarn stam dev backend | Start backend apps for the current instance. |
| Development | yarn stam dev frontend | Start frontend apps only. |
| Development | yarn stam dev instance | Start this workspace instance using shared services. |
| Services | yarn stam services up | Start shared Docker services. |
| Services | yarn stam services status | Show shared service status. |
| Services | yarn stam services logs | Tail shared service logs. |
| Services | yarn stam services restart | Restart shared services with interactive progress output. |
| Services | yarn stam services down | Stop shared services. |
| Services | yarn stam services stop | Alias for yarn stam services down. |
| Status | yarn stam status | Show shared services, active instances, URLs, and credentials. |
| Config | yarn stam config explain | Explain resolved instance config. |
| Config | yarn stam config print | Print resolved domains and backend environment values as JSON. |
| Database | yarn stam db shell | Open a MySQL shell for the current local database. |
| Database | yarn stam db migrate | Build shared packages and run migrations. |
| SSO | yarn stam sso config | Print local SSO client, user, and issuer settings. |
| SSO | yarn stam sso start <redirect-uri> | Start Keycloak and import the local realm. |
| SSO | yarn stam sso logs | Tail Keycloak logs. |
| SSO | yarn stam sso stop | Stop the local Keycloak container. |
| Report | yarn stam platform-report start | Migrate the statistics database, start Metabase with the report, and sync. |
| Report | yarn stam platform-report dashboards | Rewrite the report's questions and dashboards in Metabase. |
| Report | yarn stam platform-report config | Print the Metabase URL, login, and data source settings. |
| Report | yarn stam platform-report logs | Tail Metabase logs. |
| Report | yarn stam platform-report stop | Stop the local Metabase container. |
| Tests | yarn stam test unit | Run unit tests with isolated MySQL. |
| Tests | yarn stam test e2e | Run Playwright tests. |
| Tests | yarn stam test all --ci | Run unit and E2E tests in CI mode. |
| Checks | yarn stam check lint | Run ESLint across the monorepo. |
| Checks | yarn stam check typecheck | Run TypeScript checks across the monorepo. |
| Checks | yarn stam check all | Run build, lint, typecheck, unit tests, and E2E tests. |
| Cleanup | yarn stam clean build | Remove build artifacts. |
| Cleanup | yarn stam clean db | Drop the selected local MySQL database after confirmation. |
| Cleanup | yarn stam clean sso | Stop the local SSO server. |
| Cleanup | yarn stam clean metabase | Stop Metabase and drop its application database after confirmation. |
| Cleanup | yarn stam clean services | Stop shared services. |
| Cleanup | yarn stam clean all | Clean build artifacts and stop shared services. |
Development Configuration
shared/cli owns local development configuration. Backend and frontend development builds load domains, ports, database settings, storage settings, and app environment values from @stamhoofd/cli.
The main config contract lives in src/config/development-config.ts. Keep local-development settings there first, then consume the resolved config from commands, workflows, Caddy, SSO, Stripe, status output, or app bootstrapping.
Inspect the current config with:
yarn stam config explain
yarn stam config print
yarn stam config print --env keeoInstances And Ports
The CLI infers an instance from the workspace, selected environment, and optional overrides.
Useful flags:
--env <name>selects the development environment. The default isstamhoofd.--name <name>overrides the inferred instance name.--verboseprints extra diagnostics and commands.
Useful environment variables:
STAMHOOFD_WORKSPACE_NAMEoverrides the workspace name.STAMHOOFD_PRIMARY_INSTANCE=1forces primary-instance behavior.STAMHOOFD_INSTANCE_PREFIXoverrides the domain prefix.STAMHOOFD_PORT_OFFSEToverrides the deterministic port offset.STAMHOOFD_DOMAINoverrides the shared local domain, defaulting tostamhoofd.MYSQL_PORToverrides the local MySQL host port, defaulting to3307.STAMHOOFD_E2E_MYSQL_PORTruns the e2e tests against a MySQL that is already running on127.0.0.1instead of starting a container for them (see Tests).STAMHOOFD_E2E_MYSQL_USERandSTAMHOOFD_E2E_MYSQL_PASSWORDoverride the credentials of that MySQL, defaulting toroot/root.STAMHOOFD_MYSQL_INNODB_BUFFER_POOL_SIZEtunes the MySQL container InnoDB buffer pool size (e.g.512M,1G), defaulting to4G.STAMHOOFD_MYSQL_INNODB_BUFFER_POOL_INSTANCEStunes the MySQL container InnoDB buffer pool instances defaulting to4.STAMHOOFD_MYSQL_SORT_BUFFER_SIZEtunes the MySQL container sort buffer size (e.g.8M), defaulting to2M.METABASE_PORToverrides the local Metabase host port, defaulting to3030.METABASE_ADMIN_EMAILandMETABASE_ADMIN_PASSWORDoverride the Metabase admin account the CLI signs in with, for an instance that was set up by hand (see Local Metabase).PUBLIC_IP: Publish DNS records to your computers public IP address, and make Caddy listen on 0.0.0.0 instead of localhost. Useful for testing on local devices. E.g.PUBLIC_IP=192.168.1.7 stam services restartPUBLIC_IP=192.168.1.7 stam dev all
The primary stamhoofd instance uses base ports. With Git, the primary instance is the first worktree in git worktree list --porcelain. With jj, it is the first workspace in jj workspace list. Other worktrees and workspaces get deterministic offsets based on the workspace name so multiple workspaces can run on the same machine without changing databases when branches change.
When needing a heavy duty MySQL instance to test migrations, you can restart MySQL using:
STAMHOOFD_MYSQL_INNODB_BUFFER_POOL_INSTANCES=10 \
STAMHOOFD_MYSQL_SORT_BUFFER_SIZE=64M \
STAMHOOFD_MYSQL_INNODB_BUFFER_POOL_SIZE=16G \
stam services restartShared Services
Shared services run as Docker containers:
- MySQL:
stamhoofd-mysql - MailDev:
stamhoofd-maildev - RustFS:
stamhoofd-rustfs - CoreDNS:
stamhoofd-coredns - Caddy:
stamhoofd-caddy
Metabase (stamhoofd-metabase) is not part of that baseline: it is a JVM service that costs a lot of
memory and start-up time, so it runs on demand through stam platform-report start (see Local
platform report).
The setup is intentionally different where Docker behaves differently:
- Linux runs Caddy on unprivileged ports
8080/8443and usessudo iptablesredirects from80/443. - Linux configures split DNS through
systemd-resolved, pointing.stamhoofdto CoreDNS on127.0.0.1:1053. - macOS uses Docker Desktop bridge networking for Caddy, publishes
80/443directly, and proxies back to host app ports throughhost.docker.internal. - macOS configures
/etc/resolver/stamhoofdwithnameserver 127.0.0.1, so only thestamhoofdresolver domain and its subdomains use local CoreDNS on port53. - MySQL listens on host port
3307by default and still uses container port3306.
Start and inspect them with:
yarn stam services up
yarn stam services status
yarn stam services logsStop them with:
yarn stam services downLocal SSO
Use yarn stam sso config to print the issuer, client credentials, test user, and example command.
Start Keycloak with the redirect URI copied from the app:
yarn stam sso start "https://<organization-id>.api.stamhoofd/openid/callback"The command imports a local realm with the printed client and test user.
yarn stam test e2e starts a second Keycloak from the same SsoService, on its own container, port (6400) and host (playwright-sso.stamhoofd), with a realm that allows the /openid/callback of every Playwright worker. It is started and stopped by the Playwright global setup, so it never restarts the server you started for manual testing. Like the other e2e services it binds a fixed port, so only one e2e run can be up at a time.
Local platform report
The ledenstatistieken read off a platform statistics database that a syncer keeps in step with the administration, and are drawn by a local Metabase that only costs memory while you use it. One command brings up all three:
yarn stam platform-report startIt starts the shared services if needed, reloads Caddy, migrates the statistics database of the
environment, starts Metabase and waits until it answers /api/health (the first start migrates its
application database and takes a few minutes), registers the statistics database as a data source,
writes the report, and then runs the syncer until you stop it with Ctrl+C. No wizard, no manual data
source, no separate migrate step.
Metabase keeps running after Ctrl+C, so the dashboards stay readable while nothing syncs. Stop it
with stam platform-report stop.
The syncer is the same service stam dev backend runs, so there is no need for this command while a
development session is up: in development it syncs on every cron tick.
While working on the report itself, rewrite its questions and dashboards without restarting anything:
yarn stam platform-report dashboards --env keeoPlatform statistics per environment
Each environment reports on its own platform statistics database, so a dashboard built against
Keeo never reads Ravot numbers. stam platform-report start creates and migrates that database, and
registers it as a data source named after the environment:
| Command | MySQL database |
| --------------------------------------- | --------------------------------- |
| stam platform-report start | statistics-development |
| stam platform-report start --env keeo | statistics-keeo |
| stam platform-report start --env ravot| statistics-ravot |
| stam platform-report start --env jambo| statistics-jamboree |
The names come from databases.platformStatistics in src/config/development-config.ts, which
follows the same rules as the main development database: environments keep their historical label
(stamhoofd → development, jambo → jamboree), and a secondary instance suffixes its own name so
worktrees never share data.
The tables in that database belong to backend/app/statistics-syncer, which keeps a migration
history of its own. stam platform-report start runs those migrations before it registers the data
source, because Metabase caches the schema it found and only refreshes on its own schedule: a
database registered while it was still empty keeps showing no tables. Every run asks Metabase to
re-read the schema, and warns when the database has no tables. stam db migrate runs the same
migrations along with the ones of the main database.
Every run also tidies up the instance: it drops Metabase's demo database (new containers never create
one) and hides the migrations table of the data source it configured, which is schema history
rather than something to report on. The tables listed in metabaseHiddenTables are the ones hidden.
One Metabase serves every environment, and shows one report: Ledenstatistieken, holding the
platform that was written last. Starting --env keeo points the questions in that collection at
keeo's data source, so there is never a second report to pick the wrong platform from, and nothing
carries an environment in its name. Switching back is the same command with another --env.
The data sources stay registered, all of them, one per environment. An already registered data
source is left untouched, so edits made in the UI survive. Print the settings of an environment with
stam platform-report config --env keeo.
Login
The CLI completes the setup wizard itself and owns the admin account, so it can keep configuring the
instance later. stam platform-report config prints the credentials.
If you set Metabase up by hand with your own account, the CLI cannot sign in. Either point it at your
account with METABASE_ADMIN_EMAIL and METABASE_ADMIN_PASSWORD, or start over with
stam clean metabase.
Storage
Metabase keeps its own questions, dashboards and users in an application database, which gets its
own stamhoofd-metabase database on the shared MySQL container rather than the embedded H2 database
Metabase defaults to. H2 is unsupported for anything but a throwaway trial and cannot be migrated in
place later, so local and server setups both use a real database.
Reset Metabase (drops all local questions and dashboards, keeps the statistics data) with:
yarn stam clean metabaseMETABASE_PORT overrides the host port, which defaults to 3030.
Tests
yarn stam test runs build:shared first and only starts a MySQL container when a selected package needs one. That container runs off a data volume that persists between runs (so the data dir + migrations are reused, mapped DB_PORT), and it is shut down after the run. Both the container and volume are namespaced per worktree so runs don't collide:
yarn stam test unit # every unit package (excludes Playwright)
yarn stam test api # one package (api, models, sql, structures, renderer, redirecter, queues, utility, sgv, object-differ, eslint, cli, components, networking)
yarn stam test unit SomeFile # filter by filename across all packages
yarn stam test structures bundle-discounts # package + filename filter
yarn stam test structures -t 'partial test name' # package + test-name filter (passed to vitest -t)
yarn stam test api --skip-build # skip the automatic build:shared step
yarn stam test api --clear # reset the test database (drop its volume) before running
yarn stam test components ImageComponent # frontend browser tests (vitest browser mode)components and networking run in vitest browser mode and need a Playwright Chromium (yarn playwright install chromium); they are part of stam test unit too.
Run Playwright tests with:
yarn stam test e2e # full build + suite
yarn stam test e2e --ui # run with interactive UI to view and pause tests
yarn stam test e2e --grep @tag # only tests matching a name/tag (playwright --grep)
yarn stam test e2e --grep @tag --skip-build # skip build:shared + API/frontend rebuild (only test files changed)Like the unit runs, the e2e command keeps its data volume between runs (so migrated worker databases are reused) and shuts the container down afterward. Reset that persistent e2e database with:
yarn stam test e2e --clearUse --workers <number> to override Playwright's default worker count for a run.
Use --extra to include tests tagged @extra.
Running e2e tests without a MySQL container
A MySQL that is already running on this machine can serve the e2e tests instead, which skips both the e2e MySQL container and the shared stamhoofd-mysql container (the e2e tests never use the development database):
yarn stam test e2e --local-db # MySQL on 127.0.0.1:3306
STAMHOOFD_E2E_MYSQL_PORT=3307 yarn stam test e2e # the shared stamhoofd-mysql container
STAMHOOFD_E2E_MYSQL_PORT=3306 STAMHOOFD_E2E_MYSQL_USER=tests STAMHOOFD_E2E_MYSQL_PASSWORD=secret yarn stam test e2eSet STAMHOOFD_E2E_MYSQL_PORT in your shell profile to make this the default, and pass --no-local-db for a single run that should use a container after all. The server has to be reachable already: the CLI never starts or stops it, and --clear drops the worker databases of the run instead of a data volume.
The worker databases are named stamhoofd-playwright[-<instance>]-<slot>, after the slot a worker runs on. Since a run reserves a block of slots no other run on this machine uses, several runs can share one MySQL server without sharing a database. Unit tests still always use a container: they all use the same stamhoofd-tests database.
Run the full validation flow with:
yarn stam check allFor CLI Maintainers
The sections below are for contributors working on shared/cli itself rather than only using the CLI.
Architecture Overview
shared/cli is organized around a small set of responsibilities:
src/commands/: oclif command entrypoints that parse flags and dispatch work.src/workflows/: multi-step flows such as machine setup or starting a full development session.src/services/: shared Docker service abstractions, service definitions, and service orchestration.src/config/: development configuration and generated infrastructure config such as Caddy routing.src/context/: workspace, instance, and port resolution.src/runtime/: process execution, output rendering, manifests, help rendering, and external CLI helpers.
The intended flow is:
- Commands parse user input.
- Commands create a
CliContext. - Commands call workflows, runtime helpers, or services.
- Workflows orchestrate long-running behavior, service startup, manifests, and output.
- Services encapsulate Docker-specific behavior.
Try to keep that direction intact. Commands should stay thin, workflows should own coordination, and runtime utilities should stay generic enough to be reused from multiple commands.
Runtime Concepts
Some runtime concepts show up across many commands and are worth understanding before changing behavior.
Instance Inference
The CLI computes a CliContext from:
- the repository root
- the selected environment
- the workspace name
- optional overrides such as
--nameor environment variables
That context decides the instance name, domain prefix, whether the instance is considered primary, and the port offset used by local apps.
Port Allocation
Base ports come from src/context/ports.ts.
For secondary instances, src/context/instance.ts and src/context/port-allocation.ts assign deterministic offsets so multiple clones of the repository can run at once. If a computed range is occupied, the CLI steps to the next bucket until it finds a free range or fails with a clear error.
Manifests
The CLI writes JSON manifests under .development/cli/generated to describe what is currently running.
- instance manifests let
stam statusand Caddy discover active local instances - the shared services manifest records that shared infrastructure was started
When changing startup or shutdown behavior, keep manifest creation and cleanup in sync or the CLI will show stale instances or stale routes.
Output Flow
Long-running commands do not write directly to console.log unless they intentionally bypass the CLI output system. Instead they use runtime helpers so status lines, live output, tables, and command logging can coexist without corrupting terminal output.
When changing interactive output, check src/runtime/live-output.ts, src/runtime/output-target.ts, and src/runtime/ux.ts together.
Service Model
Shared infrastructure is modeled through ServiceDefinition in src/services/service.ts.
The common patterns are:
DockerService: base class for services that start throughdocker runSharedDockerService: convenience base for shared services without per-command optionsmanager.ts: orchestration for status, start, stop, restart, log tailing, and interactive tablesregistry.ts: the ordered list of shared services used by the CLI
The normal lifecycle is:
- Read current status.
- Optionally prepare files or derived config.
- Decide whether an existing container can be reused.
- Stop the old container if needed.
- Run setup hooks.
- Start the container.
- Run post-start hooks.
- Return any environment variables needed by callers.
When adding a service, keep Docker-specific behavior inside the service class instead of spreading it across commands or workflows.
As a rule of thumb:
- add a new service definition in
src/services/definitions/ - register it in
src/services/registry.tsif it is part of the shared baseline - use
startServicesInteractiveorrestartServicesInteractivewhen the command should show progress for multiple services - use
startServicesorstopServiceswhen the caller already controls user-facing output
Development Config Contract
src/config/development-config.ts is the main contract exported by @stamhoofd/cli for the rest of the monorepo.
It is responsible for resolving:
- local domains
- port numbers
- backend environment variables
- frontend and backend app environment objects
- environment-specific presets such as platform name or user mode
Prefer putting local-development defaults there first and reading the resolved config elsewhere instead of re-deriving values in commands, services, app bootstrapping, or generated infrastructure config.
Good candidates for this file:
- anything that should stay consistent across app startup, status output, Caddy routing, and service helpers
- environment-specific local development behavior
- values consumed by backend or frontend dev builds through
@stamhoofd/cli
Bad candidates for this file:
- one-off command-only formatting
- transient workflow state
- Docker lifecycle logic
- generic runtime helpers that are unrelated to development configuration
Working On The CLI
yarn install builds shared/cli so normal CLI startup stays fast. When changing CLI source code, use stam-dev to rebuild before running:
yarn stam-dev --helpFor CLI-only changes, run:
yarn --cwd shared/cli -s build
yarn --cwd shared/cli -s lint
yarn --cwd shared/cli -s testCLI tests live next to source files as *.test.ts.
After changing CLI behavior, validate at least the package-local checks:
yarn --cwd shared/cli -s test
yarn --cwd shared/cli -s buildFor command-surface changes, it is also useful to compare the generated help output with the README:
yarn stam --help
yarn stam services --help
yarn stam clean --helpTroubleshooting
Use yarn stam setup first. It checks Node, Docker, Caddy, DNS, and certificate trust.
The active Node.js version differs from
.nvmrcRun
source .development/install-node.sh. The script uses fnm or nvm, whichever is available.stam statusalso reports this mismatch, andstam devwill stop before starting processes with the wrong version.
If that does not tell you enough, use the first matching case below.
- DNS names like
dashboard.stamhoofddo not resolve: Runyarn stam setup dns, then retryyarn stam setup check. yarn stam setupreports missing privileged port redirects on Linux: Let the setup command apply the recommendedsudo iptablesrules, then retry.- HTTPS works badly or the browser does not trust local certificates:
Run
yarn stam setup cert, then retryyarn stam setup check. - Docker commands fail or services do not start:
Start Docker, retry the command, and use
yarn stam services statusto confirm which service is still down. - Caddy fails to reload or URLs do not open locally:
Check
yarn stam services status, then tryyarn stam services restart. - URLs, instance names, or ports look wrong:
Run
yarn stam status,yarn stam config explain, and check whether--env,--name, or environment variables such asSTAMHOOFD_WORKSPACE_NAMEare overriding the inferred instance. - Two workspaces conflict on ports:
Check whether
STAMHOOFD_PORT_OFFSETis forcing the same offset in multiple clones. Otherwise rerun the command and let automatic port allocation pick another bucket. - Local database state is broken:
Use
yarn stam clean dbfor the selected instance oryarn stam clean allif generated state is broadly stale. - Shared services state is broken:
Use
yarn stam services restart, oryarn stam clean servicesif volumes or generated service files need to be cleared manually. - SSO redirect or Keycloak issues appear locally:
Re-run
yarn stam sso config, make sure the redirect URI still ends in/openid/callback, then restart SSO withyarn stam sso start "<redirect-uri>". - Stale build or type errors keep appearing after code changes:
Run
yarn build:shared, then retry the CLI command or app startup flow.
