wats.sh
Reference

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 hidden before reading;
  • verify and service tokens may be left blank — WATS generates local random wats_wh_... and wats_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/whatsapp

Output includes:

  • webhook callback address: https://example.test/wats/webhooks/whatsapp
  • a locally generated WATS_VERIFY_TOKEN and WATS_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: 2

No 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 upgrade

After 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 be http:/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 /healthz
  • GET /readyz
  • GET /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.local

Rules:

  • --live, --yes-live, and --env-file .env.local must appear together; nothing is read implicitly.
  • --env-file must 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, and WATS_YES_LIVE plus 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 $PORT unless --port is given; the bind host defaults to 0.0.0.0 unless --host is given. Explicit flags always win.
  • Serve fails closed if --paas needs $PORT but it is missing or not an integer in 1..65535.
  • Without --paas, $PORT is ignored entirely — profile.service.port (or --port) governs.
  • --print-routes under --paas validates 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 --json

Both 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	to

The 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.

On this page