A command-line tool for provisioning and managing XenForo development environments with Docker.
- Go
- Docker with Docker Compose plugin
- Git
- System keychain (optional when using file storage or
XF_TOKEN)- macOS Keychain
- Windows Credential Manager
- Linux Secret Service
# Build locally
make build
# Authenticate
./xf auth login
# Initialize a project
./xf init ./my-project
# Show all built-in xf commands
./xf --help# Installs into $(go env GOBIN) or $(go env GOPATH)/bin
go install .
# Verify
xf versionxf supports two command types:
- Built-in commands such as
xf init,xf up,xf auth login. - XenForo commands: if the first token is not a built-in command,
xfforwards it to XenForo inside Docker. If Docker config is not initialized (nocompose.yaml),xffalls back to local execution asphp cmd.php <args...>.
Use this to discover available XenForo commands:
# Run from a XenForo directory (or set XF_DIR)
xf list
# Run a XenForo command directly
xf xf-dev:importIf you are not in a XenForo directory, set XF_DIR to a directory that contains src/XF.php.
php, php-debug, composer, compose, exec and debug wrap another tool.
For these commands:
xf's own flags go before the command name. Everything after it belongs to the wrapped tool.
# Flags after the command reach the tool
xf php -v # runs: php -v
xf composer outdated --direct # runs: composer outdated --direct
# xf's own flags go first
xf --verbose php my-script.php
# A leading -- is accepted and removed, for habit or clarity
xf php -- -vBecause everything after the command is forwarded, xf php --help shows PHP's
help, not xf's. Use xf help php (or xf help composer, and so on) to read
xf's help for a wrapped command.
All other commands parse flags normally and accept them in any position. xf logs --follow and xf logs xf --follow are equivalent, because --follow belongs to
xf rather than to a wrapped tool.
Global flags cannot currently be combined with the direct XenForo route, because that route is only taken when the first argument is not a flag:
xf xf-dev:import # works
xf -v xf-dev:import # prints xf's help and exits 0 without running anythingUse a wrapped command if you need this, for example xf --verbose debug xf-dev:import.
Choose the credential source that suits your environment:
- Keychain (default): browser login stores tokens in the system keychain.
- File: set
auth.storagetofileinconfig.json, or setXF_AUTH_STORAGE=filefor each invocation. Login, refresh, and logout then use this store. - Environment: a non-empty
XF_TOKENoverrides either store. Supply a raw OAuth access token, without aBearerprefix. The token is kept in memory and sent to the configured OAuth base URL (oauth.base_url/XF_OAUTH_BASE_URL).
To select file storage permanently, merge this setting into your configuration:
{
"auth": {
"storage": "file"
}
}Or select it through the environment:
export XF_AUTH_STORAGE=file
xf auth login
xf auth statusCredentials are stored in auth.json beside config.json (also when using --config). Without a custom configuration path, this is the OS user configuration directory under xf. The file includes access and refresh tokens, expiry, and the OAuth base URL. Unix files are created with mode 0600; overly broad permissions and symlinks are rejected for normal use. Login checks storage before opening the browser. Logout can read and delete a regular file with overly broad permissions so its tokens can be revoked, but it never follows a credential-file symlink. Refresh replaces the file atomically on Unix. Windows replacement uses the OS rename operation and does not offer the same atomicity guarantee. Keep this file private and out of version control. Login and status display the resolved credential-file path. Changing stores does not copy or remove credentials in the previous store; log in to populate the selected store.
For CI, inject XF_TOKEN using your CI secret settings, then run commands normally:
xf --no-interaction download --license … --download xenforo --version …XF_TOKEN must contain an existing OAuth access token; this option does not issue tokens or provide unattended login. Unset or empty values use the selected persistent store. Whitespace or control characters are rejected. Environment tokens cannot be refreshed by the CLI: replace the CI secret when it expires. While XF_TOKEN is active, auth login, auth refresh, and auth logout explain how to manage it externally; unset it to manage stored credentials. Unsetting a token does not revoke it on the server. An invalid environment token never falls back to stored credentials.
auth status reports the credential source without exposing the token. For environment tokens it validates with the server; JSON validity and expiry fields are null when unknown. auth status --json also returns JSON for credential errors, with a stable reason such as not_authenticated, store_unavailable, configuration_mismatch, invalid_credentials, or storage_error. A normal logged-out state has no error field. doctor checks the selected credential source for usability, preflighting writable persistent stores before reporting authentication healthy, reports storage failures separately from being logged out, and reports environment-token presence without claiming server validity.
# Log in
xf auth login
# Log in with a custom browser callback timeout (seconds)
xf auth login --timeout 600
# Check auth status
xf auth status
xf auth status --json
# Refresh token
xf auth refresh
# Log out and revoke tokens
xf auth logout# Interactive init
xf init ./my-project
# Interactive flow notes:
# - XenForo core is always installed
# - You choose only additional products
# - Core version picker shows the latest 10 versions + manual entry
# - A final review screen lets you edit all values before work starts
# Non-interactive init
xf init ./my-project \
--license 02306C2650 \
--version 2030871 \
--products xfmg,xfes \
--admin-user admin \
--admin-password secret \
--admin-email admin@example.com
# Existing directory mode
xf init ./existing-xf-project --existing
xf init ./existing-xf-project --existing --up
# Composer dependencies are installed automatically when the target
# tracks a composer.json (repository checkouts). Release packages ship
# vendor/ prebuilt and are skipped.
# .env overrides (file + inline; inline wins)
xf init ./my-project \
--env-file ./my.env \
--env XF_TITLE="My Site"
# Note: init defaults XF_DEBUG=1 and XF_DEVELOPMENT=1.
# You can override either key via --env-file/--env.# Interactive upgrade
xf upgrade ./my-project
# Upgrade to a specific version
xf upgrade ./my-project --version 2030971
# Skip running xf:upgrade
xf upgrade ./my-project --version 2030971 --skip-upgrade# List downloads for a license
xf download --license 02306C2650
# List versions for a product
xf download --license 02306C2650 --download xenforo
# Download a specific version
xf download --license 02306C2650 --download xenforo --version 12345
# Force re-download even if cached
xf download --license 02306C2650 --download xenforo --version 12345 --forcexf cache list
xf cache list --license 02306C2650
xf cache list --json
xf cache purge --license 02306C2650
xf cache purge --all
xf cache path# Lifecycle
xf up
xf down
xf reboot
# Status and logs
xf ps
xf logs
xf logs --follow
# Docker Compose passthrough
xf compose ps
xf compose exec xf mysql -u root
# Exec into a service
xf exec xf ls -laA worktree is a second checkout of the same repository on its own branch or
workspace, with its own Docker containers and database. Worktrees are created
alongside the source checkout: ~/Sites/main gains
~/Sites/main.worktrees/<branch>, named after the branch's last segment.
The source repository decides the backend. A repository that uses Jujutsu —
including one colocated with Git — gets a Jujutsu workspace named from
<branch>; no bookmark is created or moved. A plain Git repository gets a Git
worktree and branch. --base accepts the selected backend's revision syntax: a
Git ref or a Jujutsu revset.
By default create clones the source environment — database, data/ and
internal_data/ — and points the cloned board at its own URL, labelling its
title with the worktree name.
Each worktree's Docker instance name is derived from the branch's final segment
plus a short, stable suffix computed from the checkout path and the full branch
name. Because branch names are reduced to their final segment, two branches such
as dev/24x/feature and dev/xfs/feature would otherwise share a Compose
project — and therefore containers and volumes. The suffix keeps their
environments apart. Pass --instance to choose a name yourself.
create --json intentionally does not run setup: setup writes progress to
stdout, which would corrupt the JSON document. It therefore requires
--no-setup, and the accepted invocation writes exactly one JSON document to
stdout.
# Create a worktree and set up its environment
xf worktree create dev/24x/feature
# Base the new checkout on something other than the current HEAD
xf worktree create dev/24x/feature --base main
# Install a clean forum instead of cloning the source environment
xf worktree create dev/24x/feature --fresh
# Create the worktree without setting anything up
xf worktree create dev/24x/feature --no-setup
# Print the created worktree as JSON (requires --no-setup)
xf worktree create dev/24x/feature --no-setup --json
# Configure the environment but leave the containers stopped
xf worktree create dev/24x/feature --no-up
# List worktrees (this project / all known projects)
xf worktree list
xf worktree list-all
# Machine-readable output
xf worktree list --json
# Print the path of a worktree (bare output, shell-substitution safe)
cd "$(xf worktree path dev/24x/feature)"
# Remove a worktree and its containers and volumes
xf worktree remove dev/24x/feature
# Remove the worktree but leave its containers and volumes running
xf worktree remove dev/24x/feature --keep-containers
# Drop registry entries for worktrees that no longer exist
xf worktree pruneremove refuses a worktree that holds uncommitted changes or commits that
exist on no remote, and lists what would be lost. The files create generates
(compose*.yaml, .env, .dockerignore) do not count as changes. Passing
--force overrides both checks, so reach for it only once you have read what
the refusal reported.
Because cloning copies an installed database, --no-up and --fresh both skip
it: --fresh installs a clean forum instead, while --no-up leaves the
worktree with no database until you start it and install one yourself.
# PHP and Composer
xf php -v
xf composer install
xf composer outdated --direct
# XenForo command with XDebug enabled
xf debug xf-dev:import
# PHP with XDebug enabled
xf php-debug -v
# Read xf's help for a wrapped command
xf help phpxf licenses
xf doctor
xf self-update
xf self-update --check-only
xf version
xf version --json
xf version --short# Example: zsh
xf completion zshAvailable on all built-in commands:
-n, --no-interaction # Disable prompts (for CI/automation)
-v, --verbose # Enable verbose output
-c, --config <path> # Use an alternative config file- Config:
~/.config/xf/config.json - Cache:
~/.config/xf/cache - Project metadata file:
.xf.json - OAuth token storage: system keychain service
xfby default, orauth.jsonbeside the configuration file when file storage is selected.
make build # Build the binary
make run # Run without building (go run)
make test # Run all tests
make test-v # Run tests with verbose output
make test-cover # Run tests with coverage
make fmt # Format code
make vet # Check for common mistakes
make tidy # Update dependencies
make clean # Remove built binary
make all # Format, vet, test, and build# Build
go build -o xf .
# Run without building
go run . --help
go run . version
# Tests
go test ./...
go test ./... -v
go test ./... -coverxf/
├── cmd/ # CLI commands
├── internal/
│ ├── api/ # XenForo API client
│ ├── auth/ # OAuth and keychain integration
│ ├── cache/ # Download cache management
│ ├── config/ # Config and environment settings
│ ├── dockercompose/ # Docker Compose runner integration
│ ├── doctor/ # System diagnostics
│ ├── downloads/ # Download orchestration
│ ├── embed/ # Embedded Docker assets
│ ├── errors/ # Structured error types
│ ├── extract/ # Archive extraction
│ ├── selfupdate/ # Self-update logic
│ ├── stream/ # Streaming/progress helpers
│ ├── ui/ # CLI UI helpers
│ ├── version/ # Build/runtime version info
│ ├── xf/ # XenForo-specific helpers
│ └── xfcmd/ # XenForo command helpers
├── scripts/ # Install/test scripts
├── main.go # Entry point
├── go.mod # Go module definition
├── Makefile # Build automation
└── README.md
- OAuth tokens use the system keychain by default. File storage is an explicit opt-in and stores credentials as plaintext with owner-only file permissions on Unix. On Windows, access is controlled by the containing directory’s ACL; use a private user directory. There is no automatic plaintext fallback.
- PKCE is used for OAuth authorization flow security.
- Stored tokens are refreshed automatically when needed. Environment tokens must be replaced externally when expired or revoked.
# Build with embedded version information
make release VERSION=1.0.0