@xanots/sdk 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +34 -1
  3. package/dist/agent-file-refresh-IJ7GN2FJ.js +17 -0
  4. package/dist/bin.js +12 -153
  5. package/dist/{capture-HUV5BNTC.js → capture-7PO6SGB4.js} +2 -2
  6. package/dist/{chunk-CMZLPTGW.js → chunk-7SISR3CS.js} +2 -2
  7. package/dist/{chunk-7DKX2SPN.js → chunk-ACQCK4DT.js} +4 -4
  8. package/dist/{chunk-XQ22GLYS.js → chunk-AJ7JQ4OU.js} +5 -5
  9. package/dist/{chunk-ZO3HJOCJ.js → chunk-ALNQCWAW.js} +2 -2
  10. package/dist/{chunk-55VHDNW5.js → chunk-BPEEGJJA.js} +108 -3
  11. package/dist/{chunk-U3G2UW65.js → chunk-CYJ7R3AD.js} +3 -3
  12. package/dist/{agent-file-refresh-6M5M7T24.js → chunk-D6OGU2AI.js} +5 -6
  13. package/dist/{chunk-3NFUWXOC.js → chunk-DUC3PXDG.js} +142 -45
  14. package/dist/{chunk-HQ2CBRVI.js → chunk-EAIR6VUL.js} +10 -8
  15. package/dist/chunk-ERQZFWIW.js +22 -0
  16. package/dist/{chunk-TQUO2OXY.js → chunk-FN4OQT2J.js} +4 -4
  17. package/dist/{chunk-6USV65XA.js → chunk-KGNJM4LN.js} +2 -2
  18. package/dist/{chunk-HJPTWBLH.js → chunk-MFIHIS6B.js} +2 -2
  19. package/dist/{chunk-F6JCFRKO.js → chunk-PXXLBXOP.js} +2 -2
  20. package/dist/{chunk-YZ4GU6F5.js → chunk-QUUB7HYK.js} +5 -838
  21. package/dist/{chunk-W2G2WPTB.js → chunk-SWIXWJIY.js} +3 -3
  22. package/dist/{chunk-76QBEIGO.js → chunk-T4XPCJRF.js} +3 -2
  23. package/dist/chunk-T6N3VMMO.js +173 -0
  24. package/dist/{chunk-2V4YE6QC.js → chunk-VKD3EBKG.js} +112 -21
  25. package/dist/{chunk-BT2CSEC5.js → chunk-VWGJQTNA.js} +3 -3
  26. package/dist/chunk-WGDAOOXG.js +845 -0
  27. package/dist/{chunk-4YMD2OOZ.js → chunk-XHEXOES3.js} +1 -1
  28. package/dist/{chunk-4HT3BNZ7.js → chunk-YUBJLB6G.js} +10 -2
  29. package/dist/{chunk-5L4X5LS6.js → chunk-YUPQOLFX.js} +77 -2
  30. package/dist/{chunk-DYCLVQXW.js → chunk-YYSJAAJM.js} +25 -1
  31. package/dist/{chunk-P3TTMWUP.js → chunk-ZOYMZZ3S.js} +8 -1
  32. package/dist/chunk-ZSYZTGJH.js +81 -0
  33. package/dist/cli.d.ts +6 -0
  34. package/dist/cli.js +8 -8
  35. package/dist/{codegen-command-FIADGXRN.js → codegen-command-Z7WQMOJ7.js} +21 -20
  36. package/dist/{completion-BKAFCZBE.js → completion-4GYN752B.js} +2 -2
  37. package/dist/{deploy-command-4R6BYC6G.js → deploy-command-M2S4HHLC.js} +26 -26
  38. package/dist/{env-target-XWS2ZZ2Y.js → env-target-GSFCYHKZ.js} +6 -6
  39. package/dist/{ephemeral-command-JL4TPIRQ.js → ephemeral-command-6WJW7LP6.js} +24 -24
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +13 -7
  42. package/dist/init-command-GJUPWTKA.js +30 -0
  43. package/dist/internal.d.ts +2 -2
  44. package/dist/internal.js +50 -5
  45. package/dist/{io-AMIKRLPC.js → io-7VIA5SON.js} +3 -3
  46. package/dist/{live-diff-RXSJCVJ7.js → live-diff-FP4SFNT4.js} +2 -2
  47. package/dist/{lock-3CVKALKT.js → lock-KXOJIGCG.js} +2 -2
  48. package/dist/{lock-commands-XOYS75YQ.js → lock-commands-7IH7RSSP.js} +9 -9
  49. package/dist/{login-command-ACJF6KWQ.js → login-command-QM5SBBI2.js} +2 -2
  50. package/dist/{logout-command-MX3MJS5U.js → logout-command-THPASOHM.js} +2 -2
  51. package/dist/{loop-SAWAOUFO.js → loop-7SAIGRCZ.js} +3 -3
  52. package/dist/{marketplace-command-UVV3XAOL.js → marketplace-command-463DT7O7.js} +11 -25
  53. package/dist/meta-client-OW5WKWW7.js +1 -1
  54. package/dist/node.d.ts +2 -2
  55. package/dist/node.js +18 -12
  56. package/dist/onboard-command-EHOHKQQU.js +36 -0
  57. package/dist/{profile-command-SWJ3SPKR.js → profile-command-EUWPVWED.js} +6 -6
  58. package/dist/{release-command-HZUY2XZX.js → release-command-M4CVNTWL.js} +25 -25
  59. package/dist/{sandbox-details-command-HJE5SPVG.js → sandbox-details-command-BRII3QHG.js} +5 -5
  60. package/dist/{sandbox-export-command-QCJY4GMV.js → sandbox-export-command-PCZ5FL4U.js} +8 -8
  61. package/dist/scaffold.js +4 -2
  62. package/dist/{store-g45zwB33.d.ts → store-BJONDJoZ.d.ts} +171 -2
  63. package/dist/{test-command-72Y5S22H.js → test-command-U4G5Y3UM.js} +10 -10
  64. package/dist/upgrade-command-CKJ4BS2P.js +177 -0
  65. package/dist/{validate-command-KDGH537H.js → validate-command-S5N37JRP.js} +12 -12
  66. package/dist/{workspace-command-ICSI6PKO.js → workspace-command-RALSEJSW.js} +28 -27
  67. package/dist/workspace-export-AJMGN3CQ.js +1 -1
  68. package/guides/README.md +30 -0
  69. package/guides/authoring.md +642 -0
  70. package/guides/cli.md +191 -0
  71. package/guides/codegen.md +83 -0
  72. package/guides/coverage.md +67 -0
  73. package/guides/deploying.md +340 -0
  74. package/guides/environment.md +132 -0
  75. package/guides/object-kinds.md +376 -0
  76. package/guides/project-structure.md +43 -0
  77. package/guides/scaffold.md +198 -0
  78. package/guides/typed-frontend.md +201 -0
  79. package/llms/kinds-knowledge.md +20 -0
  80. package/llms/object-kinds.md +1 -0
  81. package/llms-full.txt +25 -2
  82. package/llms.txt +3 -2
  83. package/manifest.json +26 -3
  84. package/package.json +5 -2
  85. package/dist/.build-fingerprint +0 -1
  86. package/dist/chunk-WUSKBXXD.js +0 -25
  87. package/dist/init-command-LOK75W64.js +0 -29
@@ -0,0 +1,340 @@
1
+ # Signing in & deploying
2
+
3
+ Authentication, ephemeral environments, static hosting, releasing to production, and validating against a live instance.
4
+
5
+ ## Signing in & deploying (in depth)
6
+
7
+ **Sign in once** with `xanots login`. It runs the standard authorization-code + PKCE
8
+ browser flow (powered by the OpenID-certified [`openid-client`](https://github.com/panva/openid-client)):
9
+ opens your browser, you approve, and the CLI captures the redirect on a `127.0.0.1`
10
+ callback. On first use it dynamically registers its own OAuth client (RFC 7591) so the
11
+ authorize step never depends on the server tolerating an arbitrary loopback port; the
12
+ registration is cached in `~/.xano/xanots-clients.json`. The instance you're bound to is
13
+ read from the token's own `aud` claim.
14
+
15
+ `login` also **pins the numeric workspace** you consented to into the credential, so every
16
+ later command acts on exactly that workspace without looking it up again. There is **no
17
+ `--workspace` flag** — a credential addresses exactly one instance and one workspace. Run
18
+ `xanots workspace details` to see which.
19
+
20
+ The credential caches by default in a **shared** `~/.xanots/auth.json`, reusable
21
+ from **any** project directory — so a single `xanots login` covers all your projects.
22
+ Override the OAuth host with `--origin`/`$XANO_ORIGIN` and the loopback port with `--port`.
23
+
24
+ Running `login` again when a usable credential is already cached prints what you're
25
+ signed in to and stops, rather than spending another browser round trip — pass `--force`
26
+ to sign in anyway. That guard is also what keeps `login` from silently replacing a
27
+ hand-authored `type: "token"` credential (below).
28
+
29
+ **Credential formats.** `auth.json` holds one credential, discriminated by `type`:
30
+
31
+ ```jsonc
32
+ // type: "oauth" — written by `xanots login`. Do not hand-edit.
33
+ { "type": "oauth", "instance": "https://your-instance.xano.io", "workspace_id": 3, /* …tokens… */ }
34
+ ```
35
+
36
+ ```jsonc
37
+ // type: "token" — WRITE THIS YOURSELF. A meta API bearer token for the same
38
+ // meta APIs, for automation. No login flow, no refresh, no rotation.
39
+ {
40
+ "type": "token",
41
+ "instance_base_url": "https://your-instance.xano.io",
42
+ "workspace_id": 3,
43
+ "meta_api_token": "your-meta-api-token"
44
+ }
45
+ ```
46
+
47
+ Both formats work at **either** location (project-local `./.xano/auth.json` or global
48
+ `~/.xanots/auth.json`) on the same precedence ladder below, and both determine the same
49
+ thing: one instance, one workspace. A `token` credential is never created, refreshed, or
50
+ revoked by the CLI — `xanots logout` just deletes the file, and the token itself stays
51
+ valid until you revoke it wherever you minted it. Save it with owner-only permissions
52
+ (`chmod 600`) and keep it out of git.
53
+
54
+ > **Upgrading:** this format is a break. An `auth.json` written before it is rejected with a
55
+ > message naming the fix — run `xanots login` again.
56
+
57
+ **Project-local credentials** — pass `--local` to `login` to cache tokens in a
58
+ **project-local** `./.xano/auth.json` instead (which `login` **auto-adds to `.gitignore`**),
59
+ scoping the sign-in to that directory. Every command that **reads** credentials
60
+ (`deploy`/`details`, `profile me`, token refresh) resolves them **project-local first, global
61
+ as a fallback**: it uses `./.xano/auth.json` when present, otherwise `~/.xanots/auth.json` —
62
+ so a `--local` project keeps working without repeating the flag. `login` and `logout` do
63
+ **not** fall back: they target the shared global cache unless you pass `--local`. An explicit
64
+ `--config`/`$XANO_CONFIG` always wins over everything.
65
+
66
+ `deploy <file>` runs the exact same pipeline as `export` (including `xano.lock`
67
+ seeding), then create-or-refreshes the target environment and imports the compiled
68
+ workspace into it as a full replace. `deploy --bundle <path>` skips the compile and uploads a
69
+ bundle a previous `export` wrote (handy in CI). A **projected, secret-free summary** prints to stdout
70
+ as JSON — `baseUrl` plus the workspace `id`/`name`, and the static URL (plus a `verified`
71
+ boolean reporting whether the frontend was confirmed live) when `--static` is used — while the
72
+ human-readable progress (and the live URLs) echoes to stderr. The raw
73
+ workspace blob is deliberately never dumped: it carries per-tenant secrets that must not land
74
+ in shell history or CI logs.
75
+
76
+ **Where it goes** — the instance your **token is bound to** (the token's `aud`), never a
77
+ flag. `xanots deploy` create-or-refreshes an **ephemeral** (default) or resolves your
78
+ throwaway **sandbox** (`--dest sandbox`); production promotion is the separate
79
+ `xanots release` path to your main instance workspace, which merges rather than replacing.
80
+
81
+ **Static host** — `deploy --static <dir>` archives a directory and deploys it to a
82
+ static host after the backend import. Its target follows the destination: with `--dest
83
+ ephemeral` the frontend lands **on the ephemeral itself** (backend + frontend in one
84
+ environment); with `--dest sandbox` it lands on your **own (parent) workspace**, since the
85
+ sandbox tenant does not serve static hosting. For the parent-workspace case the target is the
86
+ workspace your credential is already pinned to, and the CLI uploads the archive to
87
+ `/api:meta/workspace/{id}/static_host/default/build` with your ordinary bearer. That route
88
+ auto-creates the `default` host and **auto-deploys to `dev`**, returning the live URL — so
89
+ the static step is independent of the backend deploy (the backend still runs first because
90
+ it's the primary action).
91
+
92
+ **Liveness verification** — the build endpoint returning `200` only means the archive was
93
+ *accepted*; the edge may still be starting a cold host (which `503`s for tens of seconds) or
94
+ briefly routing the previous build. So after the upload the CLI polls the deployed URL until
95
+ the static server reports it is serving **this** build — its `X-Xano-Canonical` response header
96
+ matches the canonical returned for the build just pushed — then prints `Frontend is live`. It
97
+ polls every second for the first 30s, then every two seconds out to 120s. An unconfirmed poll
98
+ is a **warning, not a failure** (the build uploaded fine and usually comes online moments
99
+ later; the exit code stays `0` and the summary records `"verified": false`). Verification is
100
+ skipped when the response carries no canonical to compare against. Pass **`--no-verify`** to
101
+ skip the wait entirely — useful for fast iterative deploys or when the deployed URL isn't
102
+ reachable from the machine running the CLI.
103
+
104
+ **Config injection** — before archiving, the deploy rewrites EVERY `.html` document in the
105
+ build, inserting an inline `<script>` at the top of `<head>` that assigns each config value to a
106
+ `window.<KEY>` global (so it runs before the app bundle). The backend URL is seeded
107
+ automatically as `window.XANO_HOST` (from the backend deploy's own response), and
108
+ `--static-env KEY=VALUE` (repeatable) merges in extra keys, overriding the seed on a name
109
+ clash. A static host has no server runtime — it serves these files verbatim — so injected
110
+ values are **public**: base URLs and *publishable* keys only, never secrets (those go in
111
+ backend env, read via `env(name)`). Rewriting every document, not just the root, is what a
112
+ prerendered build needs: it serves a different document per route, so a root-only injection
113
+ leaves every deep link and refresh running with the global unset — and the page still renders.
114
+ Injection is skipped (reported as a warning, not a failure) when the archive has no document
115
+ with a `<head>` to anchor to, and any individual document that lacks one is named; values are
116
+ `<`-escaped so one containing `</script>` can't break out of the element. This is why a
117
+ prebuilt `frontend/dist` can retarget any sandbox with no rebuild.
118
+
119
+ > **Caching — verify with a cache buster.** The static host serves `index.html` with
120
+ > `Cache-Control: public, max-age=3600`, so a browser (or CDN) that loaded the page before
121
+ > your latest deploy can hold the old HTML — including a *pre-injection* `<script>`-less
122
+ > version — for up to an hour. If `window.XANO_HOST` looks missing, it's almost always this:
123
+ > hard-reload (Cmd/Ctrl+Shift+R) or open DevTools with "Disable cache" checked. When
124
+ > verifying from a script or agent, append a throwaway query param so you never read a cached
125
+ > copy — `curl -s "$URL/?nocache=$(date +%s)"` — and check the fetched HTML for the injected
126
+ > `window.XANO_HOST` line rather than retrying the same cached URL.
127
+
128
+ `xanots sandbox details` prints the same **sandbox base URL** (`GET /api:meta/sandbox/me`,
129
+ projected to JSON) out of band, for cases where you'd rather bake it in at build time.
130
+ (`xanots profile me` prints the *instance* base URL, i.e. the account's origin rather than
131
+ the sandbox tenant.) A static failure after a committed backend deploy **does not roll
132
+ back**: it exits with code `3` and a resumable message telling you to re-run with `--static`
133
+ to retry just that step.
134
+
135
+ `deploy` reuses cached tokens and **refreshes them automatically** when the access token
136
+ expires (Xano rotates the refresh token on every use; the new one is persisted). A rejected
137
+ refresh (`invalid_grant`) clears the stale cache and tells you to `xanots login` again.
138
+
139
+ **CI & agents** run non-interactively. The credential to reach for is the **meta credential
140
+ as three environment variables** — the `type: "token"` record above, with no file:
141
+
142
+ ```bash
143
+ XANO_INSTANCE_URL=https://your-instance.xano.io \
144
+ XANO_WORKSPACE_ID=3 \
145
+ XANO_META_TOKEN=your-meta-api-token \
146
+ npx xanots deploy ./xano/index.ts
147
+ ```
148
+
149
+ Nothing is read from or written to disk, and nothing rotates, so the same three secrets keep
150
+ working run after run — which is what makes this the right shape for a CI job. It **outranks
151
+ every other credential**, including an explicit `--config` path and `$XANO_REFRESH_TOKEN`;
152
+ whichever it displaces is named on stderr, so it never wins silently.
153
+
154
+ All three are required **together**. Setting some but not others is a hard error naming the
155
+ rest, rather than a quiet fallback to another credential — a workflow with one misspelled
156
+ secret must not deploy against whatever happens to be on the runner.
157
+
158
+ > **Automated agents:** do **not** invoke `xanots login` — it blocks on interactive browser
159
+ > consent. Use the three variables above.
160
+ >
161
+ > The older `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works (both copied once
162
+ > from `auth.json` after a local `xanots login`; the target instance is read from the refresh
163
+ > token's `aud` and the workspace resolved per run), but Xano **rotates refresh tokens on
164
+ > use** — a stored one is spent by its first exchange, so a job that runs twice fails the
165
+ > second time. Prefer the meta credential.
166
+
167
+ ## Validating against a live instance (xanots validate)
168
+
169
+ `xanots validate` proves your compiled output against a **real, running Xano
170
+ instance** — not a static snapshot. It compiles your workspace, imports it into a
171
+ **fresh ephemeral environment created for that run**, exports it back, and diffs it
172
+ against what you compiled, so you catch three classes of problem a local build can't:
173
+
174
+ 1. **Import accepts** — the engine actually accepts the bundle (malformed-but-shaped output is rejected here).
175
+ 2. **Round-trip parity** — the workspace the engine stores, re-exported in the same bundle format, matches your compiled JSON after normalization (full object logic included). Every authored kind is diffed — tables, functions, queries, triggers, tasks, and more — each object matched by identity and reported per kind.
176
+ 3. **Runtime** (`--runtime`) — each deployed function actually runs on the engine, with logs surfaced on failure.
177
+
178
+ It talks only to public meta API routes — the **same** archive import `xanots
179
+ deploy` uses, plus the workspace export — and never touches XanoScript. There is one
180
+ way into an instance, so a transport bug is one `validate` reproduces rather than
181
+ routes around.
182
+
183
+ It is non-destructive: nothing you own is written to. Each run creates its own
184
+ ephemeral environment, imports into that, and deletes it afterwards — including when
185
+ the import is rejected or a transport error is thrown. The environment carries a
186
+ short expiry, so even a killed process leaves nothing permanent behind. A fresh
187
+ environment per run is also what makes the diff trustworthy: the objects read back
188
+ can only have come from this bundle, never from what a previous run left.
189
+
190
+ **Setup** — copy `.env.example` to `.env` (gitignored) and fill in a base URL +
191
+ token. Switching between a cloud dev instance and a local Docker one is just a
192
+ different `XANO_VALIDATE_INSTANCE`:
193
+
194
+ ```bash
195
+ # .env
196
+ XANO_VALIDATE_INSTANCE=https://your-instance.xano.io # or http://localhost:8080 for local Docker
197
+ XANO_VALIDATE_TOKEN=your-meta-bearer-token
198
+ # XANO_VALIDATE_WORKSPACE_ID=… # optional; PARENT workspace the run's env is created under (default 1)
199
+ ```
200
+
201
+ ```bash
202
+ xanots validate ./xano/index.ts # import + round-trip diff, reports per object (every authored kind)
203
+ xanots validate ./xano/index.ts --runtime # + run each deployed function
204
+ xanots validate ./xano/index.ts --capture # + write fetched JSON to ./validate-out (fixture candidates)
205
+ xanots validate ./xano/index.ts --instance http://localhost:8080 # override the target for one run
206
+ xanots validate --bundle ws.json # validate an already-exported bundle
207
+ ```
208
+
209
+ Config comes from the environment (a `.env` is autoloaded; a real env var wins),
210
+ `--instance` overrides per run, and the token is env-only — never a flag. This
211
+ harness is deliberately separate from the `auth.json` credential the rest of the
212
+ CLI uses. A non-zero exit means a check failed; `--verbose` prints full diffs and raw
213
+ engine detail instead of a projected summary.
214
+
215
+
216
+ ## Wiring the frontend to the backend
217
+
218
+ **Wiring the frontend to the backend.** The deploy bakes the environment's backend URL into
219
+ every HTML document in your build automatically, as a `window.XANO_HOST` global evaluated
220
+ *before* your app bundle — every document, so a prerendered build's deep links and refreshes
221
+ boot with the same backend the root does. So read it at runtime with a build-time fallback and you never have to
222
+ know the URL ahead of time:
223
+
224
+ ```ts
225
+ const HOST = (typeof window !== "undefined" && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
226
+ ```
227
+
228
+ `window.XANO_HOST` is the **sandbox tenant** URL your deployed APIs answer at (the same
229
+ value `xanots sandbox details` prints as `baseUrl`); it is *not* `xanots profile me`,
230
+ which prints your account's instance origin. Because injection happens at deploy time, a
231
+ prebuilt `frontend/dist` retargets any sandbox with **no rebuild** — ideal for headless agents.
232
+ Add your own public config (base URLs, *publishable* keys) with `--static-env KEY=VALUE`
233
+ (repeatable), exposed the same way as `window.<KEY>`. A static host serves these files
234
+ verbatim to the browser, so everything injected is **public** — never put secrets here;
235
+ those belong in backend env, read server-side via `env(name)`.
236
+
237
+ **Showing a stored file.** A file column comes back as `{ path, name, type, size, meta,
238
+ access, url }`. Don't use its `url`: on a tenant-scoped environment that field addresses the
239
+ instance host *without* the `/tenant/<name>` segment and 404s — as a broken `<img>`, while
240
+ every assertion about the response still passes. Build the URL from `path` and the host you
241
+ already have:
242
+
243
+ ```ts
244
+ import { fileUrl } from "@xanots/sdk";
245
+
246
+ <img src={fileUrl(row.avatar, HOST) ?? ""} /> // null for an absent file
247
+ ```
248
+
249
+ **Verifying the injection:** the served `index.html` writes the global in **bracket
250
+ notation** — `window["XANO_HOST"]="…";` — so grep for the bare token `XANO_HOST`, not the
251
+ exact string `window.XANO_HOST` (the dot form is valid to *read* the global in your app,
252
+
253
+
254
+ ## Deploy targets, and what a release changes
255
+
256
+ **Two targets**, so the dev loop and the production step stay distinct:
257
+
258
+ | Command | Where it goes |
259
+ |---|---|
260
+ | `xanots deploy` | A disposable **ephemeral** environment (default) — create-or-refreshed each run, auto-expiring, with its own URL. `--dest sandbox` targets your throwaway singleton instead. |
261
+ | `xanots release` | Your **main Xano instance** workspace — the production target. **Merges** by default: objects are updated in place or added, and your table data is never touched unless you ask. |
262
+ | `xanots test` | Nothing — it only reads. Runs the tests an already-deployed environment carries; `--dest` picks which one, `workspace` included. |
263
+
264
+ Not every instance has ephemeral environments enabled. Where they are off, the default
265
+ `deploy` says so and points at `--dest sandbox` — note that `--static` then publishes the
266
+ frontend to your own (parent) workspace, replacing whatever it is hosting.
267
+
268
+ Every `deploy` is a **full replace** of the disposable environment — always fresh, no
269
+ merge mode, no flags to get wrong. A `release` is the opposite by design: it changes what
270
+ your code defines and leaves the rest of the workspace — including every row in every
271
+ table — alone.
272
+
273
+ | `release` flag | What it does to your workspace |
274
+ |---|---|
275
+ | *(none)* | Adds new objects, updates existing ones. Nothing is deleted, no rows are written. |
276
+ | `--dry-run` | Prints the plan and exits without sending anything. |
277
+ | `--prune` | Also deletes objects **this project released** and no longer defines. Requires `xano.lock` — see below. |
278
+ | `--reset-data` | Empties every table the bundle carries. |
279
+ | `--seed` | Writes the bundle's table rows. Combine with `--reset-data` to reset **and** re-seed. |
280
+ | `--replace` | The disposable-environment behavior: wipe the workspace and import in its place. |
281
+
282
+ **An unchanged project is a no-op.** Before importing, the release compares the bundle it is
283
+ about to send against the workspace it is about to send it to, object by object. When every
284
+ object is already there, nothing is sent: the plan prints `no changes`, the JSON summary
285
+ reports `"upToDate": true` with `"operations": 0`, and no `updated_at` moves. That makes
286
+ `release` usable as a reconcile step — safe to run on a schedule, in CI on every merge, or
287
+ behind a "make production match main" button.
288
+
289
+ The comparison is deliberately one-sided: a false "changed" costs one import that was
290
+ happening anyway, while a false "unchanged" would silently skip a real release. So anything
291
+ it cannot prove equal counts as changed, and it is skipped entirely for `--replace` (which
292
+ mints fresh identities) and for `--seed`/`--reset-data` (which write table rows, about which
293
+ the comparison knows nothing). Under `--prune`, an object the workspace holds and the project
294
+ no longer defines is work to do, so that is not a no-op either.
295
+
296
+ Anything destructive is **previewed first** — the CLI fetches the plan, prints what would
297
+ change, and asks. `--yes` skips the prompt for CI but never skips the preview. Start with
298
+ `xanots release ./xano/index.ts --dry-run` to see the plan without committing to it.
299
+
300
+ > ⚠️ `--prune` removes tables this project released and no longer defines, and a removed
301
+ > table takes its rows with it — no flag prevents that. The preview reports it explicitly;
302
+ > read it before confirming.
303
+
304
+ **`--prune` is scoped to what this project released**, and `xano.lock` is what defines that
305
+ scope: it holds an entry for every object the project has ever exported. A planned deletion
306
+ with no lock entry is an object the project never created — a table built in the UI, another
307
+ team's API group — and the release is refused rather than deleting it. A prune with no lock
308
+ at all is refused too: without one there is no record of what belongs to the project, so
309
+ every deletion would be a guess. To delete something that is genuinely yours to remove,
310
+ adopt it first (`xanots lock adopt`). Releasing a pre-exported `--bundle` carries no entry
311
+ file to find a lock beside, so name it with `--lock=<path>`.
312
+
313
+ **A dropped column is destructive, and does not need a flag to happen.** Removing a column
314
+ from a table schema and releasing destroys the column and every value in it. The server's
315
+ plan calls that a routine in-place update, so the release compares your schema against the
316
+ live workspace, names each column that would be dropped, and asks before doing it — on an
317
+ ordinary release, with no destructive flag passed.
318
+
319
+ **Environment variables are add-only on a release.** A merge creates keys that do not exist
320
+ yet and leaves existing ones as they are, so changing a value in code and releasing will not
321
+ change it on the workspace. The preview names any key it will decline to update. To change
322
+ one, set it on the workspace directly, or use `--replace` (which rebuilds the workspace).
323
+
324
+ A merge matches objects by the stable identity your project assigns them, so it only
325
+ recognizes a workspace it has released to before. Releasing into one built by hand — or
326
+ populated by `--replace`, which assigns its own — matches nothing: every object is a
327
+ create, and with `--prune` the workspace is emptied and rebuilt rather than updated. The
328
+ preview says so in as many words when it happens. To adopt objects that are already there,
329
+ pin their `guid` on the matching defs first. Deploys are **authenticated over OAuth** — sign in once,
330
+ and the CLI refreshes tokens automatically. The target instance comes from your token
331
+ (never a stray flag), and the CLI prints what it's about to do before it touches anything.
332
+
333
+ **CI & agents** run fully headless from two env vars — no browser needed:
334
+
335
+ ```bash
336
+ XANO_REFRESH_TOKEN=… XANO_CLIENT_ID=… npx @xanots/sdk deploy --bundle ws.json
337
+ ```
338
+
339
+ > ⚠️ A deploy is a full replace of the target environment, including its table records,
340
+ > before importing. The blast radius is your own disposable ephemeral/sandbox — but anything
@@ -0,0 +1,132 @@
1
+ # Environment & identity
2
+
3
+ The environment variables XanoTS reads, and how `xano.lock` pins object identity across deploys.
4
+
5
+ ## Environment variables
6
+
7
+ Every variable the CLI and SDK read. All are optional — the defaults in the right column
8
+ are what you get when the variable is unset.
9
+
10
+ **Authentication** (see [Signing in & deploying](deploying.md) for the full precedence ladder)
11
+
12
+ | Variable | What it does |
13
+ |---|---|
14
+ | `XANO_INSTANCE_URL` | Instance origin for the meta credential (CI, agents), e.g. `https://your-instance.xano.io`. Set with `XANO_WORKSPACE_ID` + `XANO_META_TOKEN` — all three together, or none. |
15
+ | `XANO_WORKSPACE_ID` | Numeric workspace the meta credential acts on. |
16
+ | `XANO_META_TOKEN` | Meta API bearer token. With the two above it forms a complete credential that outranks every other source, reads no file, and never rotates. |
17
+ | `XANO_REFRESH_TOKEN` | OAuth refresh token for non-interactive runs (CI, agents). Paired with `XANO_CLIENT_ID`; the target instance comes from the token's own `aud` claim. Rotates on use — prefer the meta credential above. |
18
+ | `XANO_CLIENT_ID` | OAuth client id that goes with `XANO_REFRESH_TOKEN`. Both are copied once out of `auth.json` after a local `xanots login`. |
19
+ | `XANO_CONFIG` | Explicit path to the credential file. Wins over `--local` and over both default locations — the same thing `--config <path>` does. |
20
+ | `XANO_GLOBAL_CONFIG` | Moves the **shared** credential cache off `~/.xanots/auth.json`. Only changes where the global cache lives; the project-local `./.xano/auth.json` and the `XANO_CONFIG`/`--config` override are unaffected. |
21
+ | `XANO_CLIENT_FILE` | Moves the OAuth **client-registration** cache off `~/.xano/xanots-clients.json`. That file holds the `client_id` minted per auth host + redirect URI, not a credential. |
22
+ | `XANO_ORIGIN` | OAuth host to sign in against, instead of the default — the same thing `--origin` does. |
23
+ | `XANO_NO_BROWSER` | Set to anything non-empty and `xanots login` will **not** launch a browser; it prints the authorize URL to stderr for you to open yourself. The loopback server still runs and still waits for the redirect, so the flow completes once you visit the URL. For a machine that has no browser to launch, or a remote shell. |
24
+
25
+ **`xanots validate`** — its own target, deliberately separate from the deploy login
26
+
27
+ | Variable | What it does |
28
+ |---|---|
29
+ | `XANO_VALIDATE_INSTANCE` | Base URL of the instance to validate against (`https://your-instance.xano.io`, or `http://localhost:8080` for local Docker). Required; `--instance <url>` overrides it. |
30
+ | `XANO_VALIDATE_TOKEN` | Meta API bearer token for that instance. Required. |
31
+ | `XANO_VALIDATE_WORKSPACE_ID` | Parent workspace the run's throwaway environment is created under. Defaults to `1`. |
32
+
33
+ **Output & diagnostics**
34
+
35
+ | Variable | What it does |
36
+ |---|---|
37
+ | `XANOTS_DEBUG` | Appends the untouched underlying error to a failure message, instead of only the mapped explanation. |
38
+ | `NO_COLOR` | Suppresses ANSI color on the stderr progress output ([no-color.org](https://no-color.org)). Color is off by default whenever stderr is not a TTY. |
39
+ | `FORCE_COLOR` | Forces color on even when stderr is not a TTY; `FORCE_COLOR=0` forces it off and beats `NO_COLOR`'s absence either way. |
40
+
41
+ **Update notifier**
42
+
43
+ | Variable | What it does |
44
+ |---|---|
45
+ | `XANOTS_NO_UPDATE_CHECK` | Turns the once-a-day "a newer version is out" notice off. |
46
+ | `NO_UPDATE_NOTIFIER` | The de-facto convention, honored identically. `CI` being set also silences the notice. |
47
+ | `XANOTS_INSTALL_MODE` | `global` or `local` — pins whether the notice suggests `npm i -g` or a project-local upgrade, instead of detecting it. |
48
+ | `XANOTS_UPDATE_REGISTRY` | Registry URL the check reads, instead of the npm endpoint for `@xanots/sdk`. |
49
+ | `XANOTS_UPDATE_CACHE` | Moves the check's cache file off `~/.xanots/update-check.json`. |
50
+
51
+ **Escape hatches**
52
+
53
+ | Variable | What it does |
54
+ |---|---|
55
+ | `XANOTS_MARKETPLACE_URL` | Base URL the `xanots marketplace` reads hit, instead of the published catalogue. Repoints the three read verbs without waiting for a release. |
56
+ | `XANOTS_PROVE_DIFF` | A file path. Codegen appends one JSON line per statement that fell back to `raw()` — the arm that declined and the key paths where the re-encode disagreed. The decline *reason* is on the report either way; this adds the machine-readable detail. |
57
+
58
+ > `process.env` read inside a **workspace definition** is a different thing entirely: it
59
+ > resolves at export time and bakes the literal into the bundle. For a value the deployed
60
+ > stack reads at runtime, use `workspaceConfig({ env })` + `env("NAME")`.
61
+
62
+ ## Identity & the xano.lock file
63
+
64
+ Every top-level object carries a stable `guid` — Xano's identity anchor. On a sync import
65
+ the engine matches an incoming object to an existing one **by guid** and updates it in
66
+ place; no match means a new object. So re-running `export`/`deploy` on the same code maps
67
+ cleanly onto the same workspace — **no duplicates**. By default the guid derives from the
68
+ object's `name`; set an explicit `guid` to pin identity across a rename, or to adopt an
69
+ existing workspace object into code.
70
+
71
+ The opt-in **`xano.lock`** freezes the whole workspace's identities at once — every
72
+ auto-derived guid, plus the `canonical` URL tokens of API groups and toolsets (which the
73
+ engine otherwise randomizes, giving the same code different public URLs per environment).
74
+ Create it once with `xanots export ./xano/index.ts --lock`; from then on it's read automatically and
75
+ updated on every export (written atomically before the bundle). **Commit it next to your
76
+ code.** A project from `xanots init` is locked from its first export — both its `xano:export`
77
+ and `xano:deploy` scripts pass `--lock`, and `npm run xano:check` is the `--frozen-lock` CI
78
+ guard. Adopt it early either way: once identities have drifted, the only way back is
79
+ `lock adopt` against the deployed workspace.
80
+
81
+ Precedence at emit is always **explicit in-code value → lock entry → name derivation**.
82
+
83
+ **When the lock is actually load-bearing.** Because the default derivation is deterministic
84
+ — `md5("<type>:<name>")` — a project that created all of its own objects can regenerate a
85
+ byte-identical lock from its own source. Delete that lock, release again, and the same guids
86
+ come back: the objects match and update in place. For that project the lock is a *cache*, and
87
+ losing it costs nothing.
88
+
89
+ The lock is load-bearing exactly where a live guid **diverges** from that derivation, which
90
+ happens two ways:
91
+
92
+ - **Adopted** objects — anything built in the Xano UI first and taken over with `lock adopt`.
93
+ The engine assigned those guids randomly; nothing in your code can re-derive them.
94
+ - **Renamed** objects — `lock rename` pins the original guid under the new name, so the
95
+ derivation no longer reproduces it.
96
+
97
+ For those entries the lock is irreplaceable, and losing it means the next release matches
98
+ nothing and creates a duplicate of every diverged object. A workspace adopted wholesale from
99
+ the UI can be almost entirely divergent, so treat *that* lock as the critical artifact.
100
+ Either way, commit it — the cache is worth having, and you generally will not know which
101
+ entries have diverged without looking.
102
+
103
+ **Renames** — with a lock, a rename in code no longer means delete+create on sync. The
104
+ export warns about the orphaned entry and names the fix-up:
105
+
106
+ ```bash
107
+ # code: defineFunction({ name: "signup" }) → { name: "register" }
108
+ xanots export ./xano/index.ts # stderr: lock entry "function:signup" matches no exported object…
109
+ xanots lock rename --entry=xano/index.ts function signup register
110
+ xanots export ./xano/index.ts # emits signup's original guid under "register" → engine renames in place
111
+ ```
112
+
113
+ `rename`/`adopt` take no entry file, so on their own they look for `xano.lock` in the
114
+ **current directory**. Pass `--entry=<path>` to derive it beside the entry the way
115
+ `export`/`deploy`/`prune` do, or `--lock=<path>` to name the file outright. They never
116
+ reach for a lock you did not point them at — when they spot one next door they say so and
117
+ stop, rather than writing a file you did not name.
118
+
119
+ **After `--replace`** — a replace rebuilds the workspace with fresh engine identities, so the
120
+ lock is stale the moment it finishes and the next ordinary release would match nothing and
121
+ try to create everything. `xanots release ./xano/index.ts --replace` now re-pins the lock from the rebuilt
122
+ workspace itself and tells you to commit it. If there is no lock to re-pin, it says so —
123
+ without one, the next release duplicates every object.
124
+
125
+ **Adopting a live workspace** — `xanots lock adopt <bundle.json>` seeds the lock from a
126
+ real engine `packageExport`, capturing the live workspace's random guids by `(type, name)`
127
+ so code takes over an existing workspace and the first sync updates in place instead of
128
+ duplicating.
129
+
130
+ **CI** — `xanots export ./xano/index.ts --frozen-lock` fails instead of changing the lock, so a canonical
131
+ minted in a throwaway container can never silently diverge public URLs. Mint locally, commit
132
+ the lock.