Skip to content

v1.3.0

Choose a tag to compare

@xanojustin xanojustin released this 15 Sep 18:31
· 17 commits to main since this release
4a696f5

What's New

This is the release that lands the ephemeral tenant family, and it is large: 19 new commands and a long tail of push-safety fixes. Note that the paging and test-runner exit-code work often associated with this line already shipped in v1.2.0 via backports (#46, #50) and is not repeated here.

Ephemeral tenants

Short-lived, auto-expiring tenants provisioned from the CLI — the whole lifecycle, plus static hosting scoped to one.

xano ephemeral create "pr-1234 preview" --expires-hours 4
xano ephemeral list
xano ephemeral impersonate pr-1234 --guest --url-only
xano ephemeral push pr-1234 -d ./workspace --wait
xano ephemeral static_host build push pr-1234 -H site -d ./dist
  • create, get, edit, delete, list, impersonate, pull, push (#38, #40)
  • ephemeral static_host — list, create, get, edit, deploy, and build list|get|push|pull|delete (#39)
  • --guest on impersonate mints a read-only session: browse only, no schema, data, or config changes. Operator impersonation is unchanged by default. (#42)
  • --wait on push polls auto-deployed microservices and renders per-service status until they settle, so a push can gate a script instead of racing it.
  • --expires-hours accepts 1–24. The server's TTL window narrowed from 72 to 24, and it clamps rather than refuses so older CLIs keep working through a rolling deploy. The flag's own bound had to move too — otherwise --expires-hours 48 would be accepted and then silently become 24, which is a quietly wrong TTL rather than an honest refusal. (#61)

Push safety: a tree carrying two workspace documents is now refused

The sharpest bug fixed in this release. A push directory can silently hold two workspace/*.xs documents. The server applies the first one to the workspace being pushed into, discards the rest, and never checks that the document describes that workspace — so the destination gets renamed to a foreign workspace's name while all of its content stays intact. No error, no audit entry, and which name wins flips with whatever is in the pushed set, so it presents as a workspace name that "keeps changing and reverts". It was reported from the field, where it caused AI-development hallucination.

Trees pick up a second document easily, and nothing warned: pull names the file after the workspace (workspace/{name}.xs), so pulling a different workspace into the same directory adds a file rather than overwriting, and renaming a workspace leaves the old-name file behind. The push preview couldn't show it either — workspace docs key as the singleton workspace:workspace, so two files collapsed into one Workspace Settings ~1 updated line.

workspace push and sandbox push now refuse such a tree and name the offending files. There is deliberately no --force override: only you know which workspace the tree is meant for. (#62)

Push convergence: pushes stop reporting the same objects as changed forever

Three separate defects, all of which made push output untrustworthy. (#60)

  • Canonicals are now written back. A canonical is minted by the instance and unique on (canonical, branch.id) across every workspace, so a tree cloned from another workspace names one that is already taken and the import keeps its own. With no return path, the local file kept asking and the import kept refusing — every push reported the same objects as changed, forever. It now rides back in guid_map beside the guid and is written into the local .xs. Unlike the guid it never inserts a missing line: the server omits the key for kinds that have no canonical, so an absent line means "this kind has none", not "not yet assigned".
  • Filter patterns that silently matched nothing. minimatch has no directory syntax, so both spellings anyone reaches for first matched zero files: -e "table/" (a trailing slash isn't directory syntax) and -e "table" (no slash, so matchBase compares against the basename). A pattern naming a real directory now expands to <dir>/**, and each pattern reports its own match count — the aggregate Kept: N of M line hid a dead pattern among live ones.
  • -e is now a guard, not a suggestion.

xano flatten — split multi-document .xs bundles

A hand-authored .xs can hold several documents joined by ---. That is convenient to edit but unsupported by push: the partial-diff filter and GUID writeback each parse only the first document in a file, so a bundle silently pushed nothing in partial mode, or corrupted GUIDs in full mode.

push now detects these and prompts to flatten in place and continue rather than hard-erroring, and the split is available on demand — purely local, no profile or network:

xano flatten ./bundle.xs --dry-run      # preview the layout
xano flatten ./bundle.xs -o ./workspace # write elsewhere
xano flatten ./dir-of-bundles --keep-source

It produces the exact tree pull would have written, via the same shared placement logic, so the two can't drift. (#48)

function run --datasource

xano function run calcScore --datasource test

function run always hit the live data source, so a function that reads or writes tables could not be exercised against test — the community reporter called the command "mostly useless" for testing without it. The label is forwarded as the X-Data-Source header; omitting the flag preserves the previous behavior byte for byte. Validation stays server-side, since it owns the workspace's data-source list. (#59, DEV-7512)

Realtime v2 objects now nest by ownership on disk

A realtime_server owns channels, which own messages — and the pulled tree now reflects that:

realtime/server/<server>/<server>.xs
realtime/server/<server>/channel/<channel>/<channel>.xs
realtime/server/<server>/channel/<channel>/message/<message>.xs
realtime/server/<server>/channel/<channel>/trigger/...
realtime/server/<server>/trigger/...

Two real bugs are fixed along the way:

  • Channel and server triggers landed at the output root. channel_trigger and realtime_server_trigger had no branch in the resolver, so they fell through to the default handler and were written outside realtime/ entirely. They now nest beside the object they fire on. (#57)
  • Messages detached from their channels in three of five pull commands. The type→disk logic had been copy-pasted across workspace, tenant, release, sandbox, and ephemeral pull, and the copies drifted — the v2 branches existed only in workspace and tenant. The other three dumped v2 objects into flat top-level folders, and since a message name is unique only within a channel, two channels' say messages clobbered each other. All five now route through a single shared resolveDocumentPath() so the layout cannot drift again (net −275 lines). (#41, #43, #44)

Knowledge tags survive a round trip

tag is threaded through frontmatter read/write as tags:, so tags now survive pull and push on both workspace and sandbox. Tag-only edits are also detected as updates in the dry-run preview, instead of appearing as no change. The deprecated scope/enabled frontmatter fields are dropped. (#37)

Every command failed with "fetch failed" on Node 26

The CLI passes an Agent built from its bundled userland undici to Node's built-in fetch as the dispatcher option. Node 26 rejects a foreign dispatcher with UND_ERR_INVALID_ARG, which fetch surfaces only as the generic "fetch failed" — so every command read like an instance outage or an expired token, while curl against the same URL returned 200.

The dispatcher can't simply be dropped: it lifts undici's hidden 300s headersTimeout, which otherwise kills large multidoc pushes. So verboseFetch now catches that specific error, disables the dispatcher for the rest of the process, and retries once without it. The per-request AbortSignal.timeout already bounds the whole request to the same 15-minute ceiling, so large-push protection does not regress. Node 24 takes the original path unchanged. (#58, DEV-7773)

⚠️ Upgrade notes

Realtime pull paths changed. If you have a working tree from a 1.2.0 pull, realtime objects will be written to new nested paths and the old flat files will remain behind as stale copies. Delete the previous realtime/ output (and any root-level channel_trigger/, realtime_server_trigger/) before re-pulling.

Nothing else is breaking for 1.2.0 users. The ephemeral family is entirely new in this release, so the mid-beta changes to it — pull/push taking the tenant as a positional instead of -t, static host names moving to -H/--static-host (#40), and the --expires-hours ceiling dropping to 24 (#61) — only affect anyone who was tracking the 1.0.6-beta.* line.

Housekeeping

npm test passes for the first time in many releases. Its posttest hook runs lint, which reported 464 errors (401 on the 1.2.0 tree). Now 0 errors, 372 tests passing.

Most of those errors were the stock oclif preset disagreeing with deliberate, documented choices rather than flagging defects — camelcase alone was 205 of them, and snake_case here is load-bearing: CLAUDE.md mandates underscores, and flag names like --per_page and --tenant_name are the public CLI surface. The config now records a reason per rule. no-explicit-any was downgraded to a warning rather than switched off, since retyping untyped Metadata API payloads is a typing project rather than a lint pass.

Clearing the genuine remainder surfaced two bugs:

  • profile wizard never handled Ctrl+C. It matched with error instanceof ExitPromptError imported from @inquirer/core — a package that was never declared in dependencies and resolved only by hoisting luck. In-repo it resolved to @inquirer/core@9.2.1 hoisted from the oclif devDependency, while inquirer@13 throws from its own nested 11.1.5: different class identities, so the check was always false and Ctrl+C fell through to a stack trace. It now matches on error.name, exactly as commands/auth/index.ts already did — that file's comment describes this precise failure mode. (A clean npm install happened to hoist one shared copy, so end users were not hitting the crash; the real exposure was running from source, and pnpm/yarn-PnP, which don't hoist undeclared deps at all.)
  • microservice-wait carried a vestigial counter. It incremented consecutiveErrors under a comment claiming a "tolerate-with-cap" policy, but no cap was ever implemented and nothing read the counter. Removed, with the comment corrected to the actual contract: transient poll failures are tolerated and the timeout is the only failure signal. No behavior change.

Docs: xano flatten shipped in #48 with no README entry; it is now documented in the push section it relates to.

Verification

  • npm test — exit 0: 372 passing, 0 lint errors
  • all 163 built modules import cleanly (checked after rewriting imports across 50+ files)
  • oclif manifest — 148 commands
  • packed tarball installed with --omit=dev into a clean project and run

Published to npm as @xano/cli@1.3.0.

Full Changelog: v1.2.0...v1.3.0