CLI reference
The @wats/cli wats command: offline config validation, diagnostics, OpenAPI export, setup, and a local serve wrapper.
experimental · reviewed 2026-05-25
@wats/cli provides the wats command for local onboarding and inspection: config validation, OpenAPI export, placeholder generation (init), a credential setup wizard (setup), offline diagnostics (doctor), a dry-run/live serve wrapper around @wats/service, and Bun-powered package upgrades.
No-live-credentials default
The CLI does not read Meta access tokens, app secrets, service bearer tokens, .env.local, or live credential values by default. It does not call Meta Graph APIs by default. Config commands read WATS config files only; secret fields stay env-secret references and are never resolved.
Do not paste production tokens into CLI arguments. Unknown arguments fail closed and do not echo path-like or token-like values.
Commands
wats --help / wats --version
Help exits 0. --version prints the installed @wats/cli package version and exits 0 — no config, env files, or credentials are read.
wats init [dir] [--dry-run] [--format yaml|json] [--profile <name>]
Generates wats.config.yaml / wats.config.json plus .env.example placeholder files. Env-secret references only; refuses to overwrite existing files; never resolves live credentials or reads .env.local.
wats init
wats init ./my-bot --format yaml --profile local
wats init ./my-bot --format=json --profile prod--dry-run previews a count-oriented summary without writing files:
init dry-run
files: 2
format: yaml
profile: [REDACTED_PROFILE]Success output is similarly redacted: no target paths, profile names, or env-secret reference names. Generated .env.example values are blank except the non-secret live-gate defaults (WATS_LIVE_ENABLE=0, WATS_YES_LIVE=0).
Failure behavior: unknown flags, duplicate flags, unsafe paths, unsupported formats, and unsafe profile names fail closed. Existing wats.config.* or .env.example targets fail with refusing to overwrite. Diagnostics never echo attacker-supplied paths or token-like values.
wats setup [dir] [--profile <name>]
Credential setup wizard for one local profile. Writes wats.config.yaml with env-secret references and an ignored .env.local containing the operator-entered values. The generated config is validated through @wats/config; the command refuses to overwrite existing wats.config.yaml or .env.local and rolls back the config file if the secret-file write fails.
Raw tokens never land in YAML. The env names are WATS_ACCESS_TOKEN, WATS_VERIFY_TOKEN, WATS_APP_SECRET, and WATS_SERVICE_TOKEN; .env.local also records WABA/phone-number ids plus WATS_LIVE_ENABLE=0 and WATS_YES_LIVE=0.
Prompted input policy:
- profile names: safe 1..32 character identifiers, no token/secret/password-like words;
- access token and app secret: required, non-empty, max 4096 characters, no control characters; prompts state
Input hiddenbefore reading; - verify and service tokens may be left blank — WATS generates local random
wats_wh_...andwats_srv_...values; - WABA and phone-number ids must be digit strings;
- webhook path and service API prefix must be absolute safe paths: no traversal, encoded traversal, query, fragment, or control characters;
- service host and port use the same safe local validation as dry-run serve.
Success output is count/status-only:
setup complete
files: 2
profile: [REDACTED_PROFILE]Error families: malformed args → CliUsageError; invalid prompt plumbing → PromptInputError; unsafe operator answers → SetupInputError; config-schema failures → ConfigValidationError; write conflicts → OutputError with refusing to overwrite.
The command does not print target paths, profile names, env-secret names, token values, or raw prompt answers. It does not read existing .env.local, validate tokens against Meta, manage multiple profiles, or start the service.
wats onboarding --public-url <https URL> [--webhook-path /webhooks/whatsapp]
Prints an operator checklist for Meta webhook setup: it combines your public HTTPS base URL with the configured webhook path and prints the callback address to paste into Meta App Dashboard > WhatsApp > Configuration.
wats onboarding --public-url https://example.test/wats
wats onboarding --public-url https://example.test --webhook-path /webhooks/whatsappOutput includes:
webhook callback address: https://example.test/wats/webhooks/whatsapp- a locally generated
WATS_VERIFY_TOKENandWATS_SERVICE_TOKEN(stdout only — no files written); - the values to copy from Meta:
WATS_ACCESS_TOKEN,WATS_APP_SECRET,WATS_WABA_ID,WATS_PHONE_NUMBER_ID.
--public-url must be HTTPS with no raw whitespace, credentials, query strings, or fragments. --webhook-path must be an absolute safe path with no traversal segments. The command does not read .env.local, resolve env secrets, or call Meta.
wats config validate <path>
Loads and validates a JSON/YAML config through @wats/config; exits 0 when valid. wats config validate --config <path> is an equivalent alias.
Success output is count-oriented and safe:
config valid
default profile: [REDACTED_PROFILE]
profiles: 2No profile names, env-secret reference names, or token values are printed. On failure: exits 1, prints ConfigValidationError with code, path, and a safe message; no stack traces; file-read errors are sanitized so attacker-supplied paths are not echoed; dynamic profile-name path segments are redacted.
wats doctor --config <path> [--profile <name>] [--check-env] [--format text|json]
Offline diagnostics: runtime compatibility, package imports, WATS package-version drift against the project package.json, config validation, profile availability, service route collision safety, and local OpenAPI generation.
doctor ok
runtime: ok
package-imports: ok
packages: ok
config: ok
profile: ok
routes: ok
openapi: ok
summary: ok=7 warning=0 error=0--format json returns a stable { ok, summary, checks } object. The packages check reads only package.json dependency ranges for the public WATS package set and warns when a listed dependency is older than the installed CLI — it does not call npm. --check-env adds one env-presence check reporting counts only (missing 1 required env value), never env names or values. Expected failures aggregate into safe findings instead of host stack traces.
No Meta calls, no credential resolution, no file writes. wats doctor --help prints usage and exits 0.
wats upgrade [--dry-run]
Updates the public WATS package set in the current Bun project. wats update is an alias.
wats upgrade --dry-run
wats upgradeAfter checking for a readable package.json, it runs:
bun update --latest @wats/cli @wats/core @wats/graph @wats/http @wats/config @wats/service--dry-run prints the Bun command without touching package.json or bun.lock. Output is status-only. wats upgrade --help prints usage and exits 0; unknown options fail closed.
wats openapi --config <path>
Prints OpenAPI 3.1 JSON for the WATS standalone service API (not Meta Graph) to stdout, using the config defaultProfile. No files created, no env-secret resolution.
wats openapi --config wats.config.json
wats openapi --config wats.config.json --profile prod
wats openapi --config wats.config.json --server-url https://service.example
wats openapi --config wats.config.json --out openapi.json--profile <name>selects a named profile; missing or blank profiles fail closed.--server-url <url>must behttp:/https:and acceptable to@wats/service; query strings and fragments are stripped.--out <path>writes JSON with exclusive-create semantics and refuses existing targets. Empty paths, directories, control characters, NUL, backslashes, and./..segments are rejected. Relative paths resolve under the current working directory.
Failures use the ConfigValidationError / WatsServiceError safe formats; unexpected host errors collapse to a generic usage hint; attacker-supplied values are not echoed.
wats serve --config <path> --dry-run [--profile <name>] [--host <host>] [--port <port>] [--paas] [--print-routes]
Starts the @wats/service Request-to-Response app as a local Bun process in dry-run mode: loads the config, selects the default or named profile, optionally overrides profile.service.host/profile.service.port, injects synthetic in-memory secrets plus a no-network Graph transport, and exposes:
GET /healthzGET /readyzGET /openapi.json- the configured webhook GET/POST route
- protected service message routes under
profile.service.apiPrefix
wats serve --config wats.config.yaml --dry-run --host 127.0.0.1 --port 3000
curl -fsS http://127.0.0.1:3000/healthz
curl -fsS http://127.0.0.1:3000/readyz
curl -fsS http://127.0.0.1:3000/openapi.json--print-routes validates the config/profile and prints the safe route inventory without binding a port. Dry-run serve never resolves env-secret values, reads .env.local, calls Meta, or prints config paths, profile names, env names, synthetic secret values, or token-like arguments.
wats serve --config <path> --live --yes-live --env-file .env.local [...]
Live mode starts the same service with real env-secret values and the fetch-backed Graph transport. Built for local live testing behind a secure HTTPS tunnel (Meta requires a public HTTPS webhook URL):
ngrok http 8787
wats onboarding --public-url https://<your-tunnel-host> --webhook-path /webhooks/whatsapp
WATS_LIVE_ENABLE=1 WATS_YES_LIVE=1 \
wats serve --config wats.config.yaml --live --yes-live --env-file .env.localRules:
--live,--yes-live, and--env-file .env.localmust appear together; nothing is read implicitly.--env-filemust be a relative local env filename; absolute paths, traversal, duplicate flags, and token-looking values fail closed.- The env file may contain
WATS_ACCESS_TOKEN,WATS_VERIFY_TOKEN,WATS_APP_SECRET,WATS_SERVICE_TOKEN,WATS_WABA_ID,WATS_PHONE_NUMBER_ID,WATS_LIVE_ENABLE, andWATS_YES_LIVEplus comments/blanks. Live serve resolves only the four secret keys. - Output is status-only: no env names, profile names, config paths, token values, or Graph responses.
- This is not a production hosting or Docker contract.
wats serve ... --paas
Opt-in PaaS mode for platforms that inject $PORT and require binding 0.0.0.0 (Railway, Fly, Render, Cloud Run). Works with both dry-run and live serve:
wats serve --config wats.config.yaml --dry-run --paas
wats serve --config wats.config.yaml --live --yes-live --env-file .env.local --paas- With
--paas, the bind port comes from$PORTunless--portis given; the bind host defaults to0.0.0.0unless--hostis given. Explicit flags always win. - Serve fails closed if
--paasneeds$PORTbut it is missing or not an integer in1..65535. - Without
--paas,$PORTis ignored entirely —profile.service.port(or--port) governs. --print-routesunder--paasvalidates and prints routes without binding, so it does not require$PORT.
This removes the need for a container entrypoint shim mapping $PORT/0.0.0.0 onto static flags. wats serve --help prints usage and exits 0.
wats messages list / wats messages show
Read local message projections from a running WATS service. These commands call the read-only local service message routes; they do not call Meta Graph.
wats messages list --config wats.config.yaml --env-file .env.local
wats messages list --config wats.config.yaml --env-file .env.local --limit 25 --cursor <row-id>
wats messages list --config wats.config.yaml --env-file .env.local --json
wats messages show wamid.HBgM... --config wats.config.yaml --env-file .env.local
wats messages show wamid.HBgM... --config wats.config.yaml --env-file .env.local --jsonBoth commands read the service bearer token from WATS_SERVICE_TOKEN (or the env var named by profile.service.bearerToken.env). The token is sent as an Authorization: Bearer header and is never printed. --config is required. --profile selects a profile. --env-file may point at a local .env.local next to the config.
list prints TSV by default:
createdAt direction waMessageId type status from toThe next-page cursor prints to stderr as nextCursor: <row-id> so stdout remains parseable. With --json, stdout is the raw { items, nextCursor } response from the service. show prints one key: value line per field by default, or the raw message JSON with --json.
Set WATS_CLI_STATUS_UI=1 to add an observed-status summary to stderr when stderr is a TTY. The summary is evidence-only: it prints the statuses recorded by the local service, and never infers delivered/read from send success.
Errors are folded into safe CLI output: missing token, unreachable local service, 401 unauthorized, 404 not_found, and malformed arguments never print token values, config paths, or stack traces.
wats webhook token
Prints one freshly generated verify token (prefix wats_wh_, Web Crypto random bytes with a crypto.randomUUID() fallback) and exits 0. Stdout only — no files touched. --help prints usage and exits 0.
Error behavior
Unknown commands and unsupported flags fail closed with exit code 1 and a generic usage hint. User-supplied path-like or secret-like values are never echoed.
Error families: CliUsageError, CliConfigError, ConfigValidationError, SecretResolutionError, LiveGuardError, OutputError, DoctorError, ServeError, WatsServiceError.
Not implemented yet: production hosting/deployment wrappers, wats config print --redacted, wats config paths, optional live checks behind explicit credential-gated flags, and richer conversation navigation. wats messages is local-service-only and reads only the service bearer token.