The CLI implementation lives downstream. This document records the platform-side
contract that downstream wdl CLIs must satisfy when deploying Wrangler projects into
WDL, managing namespace-scoped resources, and driving admin APIs without talking
directly to Redis, S3, or runtime services.
The downstream CLI is published as the @wdl-dev/cli package and may also be developed
from a standalone checkout. This repository keeps only the control-plane and runtime
contracts that the CLI talks to. Integration tests use the wdl executable on PATH
by default, with CI installing the @wdl-dev/cli version pinned in
the top-level WDL_CLI_PACKAGE value in .github/workflows/ci.yml before the
integration job. Local unpublished CLI validation should link or wrap the checkout so
wdl is on PATH; WDL_CLI_BIN is only a focused integration override when a run
must bypass PATH resolution. Command syntax, command grouping, and user-facing
wording are downstream concerns. This document only records the platform behavior
that CLI calls must preserve.
Ordinary CLI commands resolve a control URL, admin token, and namespace before making an HTTP request to control.
The CLI reads shell/CI environment variables and an optional project .env file. The
.env file supports base KEY=value entries plus per-namespace INI sections:
CONTROL_URL=http://admin.test:8080
WDL_NS=demo
[demo]
ADMIN_TOKEN=local-dev-token
[prod]
CONTROL_URL=https://ctl.prod.example
ADMIN_TOKEN=<prod-token>Precedence is:
- CLI flag
- shell/CI environment
- selected
[namespace]section - base
.env - code default
Canonical spelling is --control-url / CONTROL_URL. Platform integration tests
only provide CONTROL_URL.
Namespace selection is --ns, then WDL_NS from shell/base .env, then a command
fallback if that command has one. If no namespace resolves, only base .env values are
loaded. If --ns foo has no [foo] section, base values are used silently. Section
names use the CLI-local isAdminAcceptableNs() rule: ordinary tenant namespaces and
delimiter-safe __...__ reserved-looking section names are accepted. This is not the
server's exact reserved namespace literal set. WDL_NS inside a selected section is
ignored with a warning so a section cannot redirect itself.
Bare production control hosts default to https://. Bare local-development hosts such
as admin.test:8080 and localhost:8080 default to http://; any bare :8080
control URL is treated as local HTTP. Include an explicit scheme when this heuristic is
not desired.
CONTROL_CONNECT_HOST is a debug/transport override for direct connection while keeping
the logical control URL host intact. It is not part of the ordinary tenant contract.
Downstream CLI diagnostics may call GET /whoami through the configured control URL to
confirm which token and endpoint are active. The response is intentionally a self-view:
it includes principal, tokenId, requestId, platformVersion, minCliVersion,
and urls, but never token plaintext, token hashes, other token records, or the raw
workerd version.
CLI output may display:
platformVersion: the WDL platform version reported by control. The canonical derivation is documented incontrol-auth.mdunder/whoami; the CLI should display the value without trying to reconstruct it from package metadata.minCliVersion: the minimum downstream CLI version supported by this platform build.urls.control: the control origin that the request actually reached.urls.namespace: the tenant namespace origin, returned only for namespace tokens.urls.assets: the configured public assets base URL, returned only when the control plane has a safe absolutehttp/httpsASSETS_CDN_BASE; query and fragment are stripped before returning the hint.
The CLI must treat these fields as diagnostics and defaults for user-facing guidance,
not as a replacement for explicit user configuration. If minCliVersion is greater than
the running CLI version, the CLI should warn or fail before attempting mutating
commands. Missing optional URL hints should be displayed as unavailable rather than
guessed.
wdl deploy <project> is the supported worker bundling path. It shells out to the
project-local wrangler binary, or to WDL_WRANGLER_BIN when that env var is set, and
uses Wrangler's dry-run output as the bundle source. The CLI sets a dummy
CLOUDFLARE_API_TOKEN for dry-run bundling so ordinary projects do not need real
Cloudflare credentials to build locally.
WDL worker names follow the platform grammar, not Wrangler's narrower deployment-name
grammar: [A-Za-z0-9][A-Za-z0-9_-]{0,254}. Uppercase letters, digits, underscores, and
hyphens are valid. If Wrangler dry-run validation would reject the real platform worker
name, the downstream CLI may pass a dummy Wrangler name for bundling, but the control
payload and deployed WDL worker name must remain the user-requested platform worker name.
Successful Wrangler dry-run output is hidden by default and WDL progress is shown
instead. --verbose streams Wrangler's raw output for debugging.
After bundling, the CLI walks the entire Wrangler output directory and sends every emitted artifact to control:
- JavaScript chunks
- Wasm modules
- imported text, JSON, CSS, and other data assets
- all files except source maps and Wrangler's generated output
README.md
Binary files use base64 in the control JSON payload and are decoded exactly once before control stores raw bytes. Runtime never sees base64 bundle bytes.
The CLI package owns Wrangler dry-run bundling and bundle-artifact collection. The platform repository should not duplicate that packaging path.
The CLI reads wrangler.toml, wrangler.jsonc, or wrangler.json; all three use the
same snake_case field shape. Named environments are selected with --env <name> or
CLOUDFLARE_ENV.
If named environments exist, selecting one is required. WDL does not silently deploy a
top-level default when [env.<name>] tables are present. env.<name>.name is rejected:
the deployed worker name is always the top-level name. Staging/production side by
side should use separate namespaces.
WDL follows Wrangler inheritance for selected environments:
- Non-inheritable keys must be redeclared per env:
vars,kv_namespaces,r2_buckets,d1_databases,services,queues,workflows, durable object bindings, and similar binding tables. - Inheritable keys such as
assetsfollow Wrangler's selected-env behavior and may be overridden explicitly. - Top-level-only keys such as
nameandmigrationsare rejected inside env tables.
Supported config surfaces:
| Field | WDL behavior |
|---|---|
name, main, compatibility_date, compatibility_flags |
Stored in immutable bundle metadata. Control rejects malformed, future, or bundled-workerd-unsupported compatibility_date values before commit. |
[vars] |
String, number, and boolean values are accepted and stringified into env. |
[[kv_namespaces]] |
id is a platform-local KV namespace id, not a Cloudflare UUID. |
[[r2_buckets]] |
binding plus bucket_name become a namespace-scoped virtual R2 bucket under the platform S3 bucket. |
[assets] |
directory contents upload to S3-compatible assets storage and auto-inject ASSETS. |
[[d1_databases]] |
Binding resolves by database_id first, then namespace-local database_name; migrations use matching config. |
[[durable_objects.bindings]] |
Same-worker classes from [[migrations]].new_classes or new_sqlite_classes; script_name and rename/delete migrations are unsupported. |
[[services]] |
Freezes target namespace, worker, version, and entrypoint at caller deploy time. Cross-namespace ns is a WDL extension and requires target opt-in. |
[[platform_bindings]] |
Resolves a SCREAMING_SNAKE_CASE symbolic platform export from platform-tier namespaces and freezes it into the caller. |
route / routes |
Sent raw to control; control owns pattern grammar and platform-domain rejection. |
[triggers] crons and [[triggers.schedules]] |
UTC Cloudflare-compatible crons plus WDL timezone extension. |
[[queues.producers]] and [[queues.consumers]] |
Producer and consumer metadata. max_concurrency is rejected. |
[[workflows]] |
Same-worker Workflows V2 bindings. |
[[analytics_engine_datasets]] is rejected at deploy at both top level and selected-env
level. Unsupported fields should fail loudly rather than be silently dropped when they
would imply platform behavior WDL does not implement.
The downstream CLI may expose these surfaces with its own command shape, but the platform-side behavior is fixed:
- Worker listing reads active versions, retained versions, and secret-only entries from control.
- Worker deletion hard-deletes routes, retained versions, worker secrets, queue consumers, and crons, then stages asset cleanup after the Redis commit.
- Version deletion hard-deletes one retained non-active version.
- Secret mutation requires an explicit worker scope or namespace scope. Namespace-wide writes must not be accidental. A submitted empty string is a set secret, not unset.
- D1 commands manage namespace D1 databases and forward-only migration files. The migration filename is the migration id; already-applied files should not be renamed or edited.
- R2 commands operate under the namespace prefix
r2/<ns>/. Empty declared virtual buckets are not visible from prefix-derived listings until their first object is written. - Workflows commands talk to the workflows service; the CLI must not write DB2 directly.
- Tail commands open live SSE sessions through control.
Destructive commands ask for confirmation by default. Automation should pass --yes
only after it has already checked the target. Commands that support --json return the
raw control response for automation instead of the human summary.
Tail streams live fetch invocation events, console.* output, uncaught fetch-handler
exceptions, and scheduled/queue invocation events. Multiple worker names create one
explicit fan-in terminal.
The downstream CLI may expose raw output, bounded stream resume, and reconnect knobs, but control remains the only tail session owner.
Tail is a live debug path, not audit storage. Details of the tail protocol live in Log Tail And Observability.
- The CLI never writes Redis directly for ordinary operations.
- Control remains the authority for auth, validation, Redis commit, routing, lifecycle, and cleanup intent.
- CLI parsing can warn about missing caller secrets, but deploy may still succeed when pre-deploy secret flow is valid.
- If a bundle artifact fails to round-trip from Wrangler output to control/runtime, it is a WDL bug, not an intentional silent drop.
The platform repository exercises the published @wdl-dev/cli command through
integration files marked // @wdl-cli-integration:
tests/integration/auth-platform.test.jstests/integration/cli-multi-env.test.jstests/integration/cli-smoke.test.jstests/integration/log-tail.test.jstests/integration/pages-assets-demo.test.jstests/integration/r2-cli-binding.test.jstests/integration/route-demo.test.jstests/integration/s3-cleanup.test.js