@xanots/sdk 0.0.9 → 0.0.10

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 (98) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +28 -21
  3. package/dist/.build-fingerprint +1 -1
  4. package/dist/{agent-file-refresh-4W37DGXS.js → agent-file-refresh-QNKN5RYD.js} +3 -3
  5. package/dist/bin.js +8 -7
  6. package/dist/{branch-commands-PBQ36L4A.js → branch-commands-2BLOC2GR.js} +13 -11
  7. package/dist/{capture-7PO6SGB4.js → capture-4WVJY4DQ.js} +2 -2
  8. package/dist/{chunk-ZOYMZZ3S.js → chunk-3INK4Y4E.js} +1 -1
  9. package/dist/{chunk-NDZFBZHC.js → chunk-3UABJA4X.js} +1 -1
  10. package/dist/{chunk-7OXFMCTX.js → chunk-5R73LFWK.js} +3 -3
  11. package/dist/{chunk-OW2QZEWL.js → chunk-5YCQ2QHH.js} +2 -2
  12. package/dist/{chunk-5YQOIFT4.js → chunk-75Z74TA7.js} +2 -2
  13. package/dist/{chunk-QTNO2WD6.js → chunk-7ZYW652H.js} +1 -1
  14. package/dist/{chunk-K3YSQDXJ.js → chunk-ACCBOMCB.js} +91 -74
  15. package/dist/{chunk-4PTSTDIG.js → chunk-AIZKXUNP.js} +2 -2
  16. package/dist/{chunk-P5C2YKAM.js → chunk-ANUDXFEX.js} +18 -27
  17. package/dist/chunk-CTD5ZCV6.js +28 -0
  18. package/dist/{chunk-5HCDZ2XN.js → chunk-DBFU47BJ.js} +2 -2
  19. package/dist/{chunk-OKNSR7MT.js → chunk-DIA7CT7J.js} +129 -150
  20. package/dist/{chunk-JQLR64UC.js → chunk-F6CYJ7TN.js} +21 -6
  21. package/dist/{chunk-BKDJN76G.js → chunk-FE5I6S6N.js} +1 -1
  22. package/dist/{chunk-L6TPHWAU.js → chunk-I7DQDJAM.js} +30 -48
  23. package/dist/{chunk-QIWBCS7N.js → chunk-JGCWTCA7.js} +2 -2
  24. package/dist/{chunk-MDXR5E5Q.js → chunk-K5IOND4K.js} +2 -2
  25. package/dist/{chunk-4PE4ZAB7.js → chunk-KA6G2L7U.js} +5 -5
  26. package/dist/{chunk-QONOJO45.js → chunk-LBYWGMOA.js} +1 -1
  27. package/dist/{chunk-ZZLPTHT5.js → chunk-P6TAVLOX.js} +2 -2
  28. package/dist/{chunk-3QW5NFZJ.js → chunk-QKM4U5UK.js} +3 -3
  29. package/dist/{chunk-T4XPCJRF.js → chunk-TCFIPDB3.js} +1 -1
  30. package/dist/{chunk-UDL46O35.js → chunk-VKSOTZK3.js} +2 -2
  31. package/dist/{chunk-6DHBYBTO.js → chunk-VNQM3V2C.js} +1 -2
  32. package/dist/{chunk-KSV7VOEX.js → chunk-W24FJHPD.js} +3 -3
  33. package/dist/chunk-WGPXT2G2.js +22 -0
  34. package/dist/{chunk-6AAT2AYQ.js → chunk-WP4OZZV4.js} +2 -2
  35. package/dist/{chunk-MU3O43L2.js → chunk-YBC3IKMF.js} +2 -2
  36. package/dist/{chunk-6Z5CNWFC.js → chunk-YYFXVYPX.js} +37 -6
  37. package/dist/chunk-ZQ2PKR6R.js +40 -0
  38. package/dist/cli.d.ts +57 -31
  39. package/dist/cli.js +6 -5
  40. package/dist/codegen-command-GB3H2KQ7.js +46 -0
  41. package/dist/{completion-A6BZ3XGU.js → completion-WF46272M.js} +2 -2
  42. package/dist/{config-NL33PN4D.js → config-476F3PT5.js} +1 -1
  43. package/dist/{deploy-command-5TTI4TUP.js → deploy-command-QTIVA22A.js} +67 -99
  44. package/dist/{ephemeral-command-7TK52YQM.js → ephemeral-command-U4AQ3TXX.js} +25 -28
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +5 -5
  47. package/dist/{init-command-OVFFW4QS.js → init-command-MAXULNAD.js} +13 -10
  48. package/dist/{onboard-command-EHOHKQQU.js → init-web-3JNFG6GI.js} +10 -10
  49. package/dist/internal.d.ts +2 -2
  50. package/dist/internal.js +27 -36
  51. package/dist/{live-diff-QK4KC2FJ.js → live-diff-IXKBVG4K.js} +3 -3
  52. package/dist/{lock-commands-ET7NIOLH.js → lock-commands-DQ7CUMIV.js} +20 -19
  53. package/dist/{login-command-DFCURZLC.js → login-command-Z6CHTA57.js} +7 -7
  54. package/dist/{logout-command-WXVZQCAV.js → logout-command-ER6IKAYJ.js} +2 -2
  55. package/dist/{loop-7SAIGRCZ.js → loop-D5NPL4VH.js} +3 -3
  56. package/dist/{marketplace-command-BCOAAN2I.js → marketplace-command-P4IPLJ6J.js} +6 -6
  57. package/dist/{meta-client-57ZWVHST.js → meta-client-K2J4XH64.js} +6 -6
  58. package/dist/node.d.ts +2 -2
  59. package/dist/node.js +9 -8
  60. package/dist/{validate-command-SK5FPCV7.js → preflight-command-GZ2GE5RN.js} +17 -16
  61. package/dist/{profile-command-QJAAUZIV.js → profile-command-LJSBDV2L.js} +5 -5
  62. package/dist/{release-command-4EXVLYV2.js → release-command-YUBTNHVX.js} +36 -33
  63. package/dist/{routes-manifest-SP3ZXLMR.js → routes-manifest-PWZHDOI5.js} +2 -2
  64. package/dist/scaffold.js +2 -2
  65. package/dist/{static-host-D6KS7X45.js → static-host-3WMV7IZO.js} +1 -1
  66. package/dist/status-command-AL47VG7H.js +162 -0
  67. package/dist/{store-CUCBSYLj.d.ts → store-BG1UPZ3Z.d.ts} +5 -5
  68. package/dist/{test-command-2F5LKMI6.js → test-command-YAZLKLGQ.js} +45 -28
  69. package/dist/{upgrade-command-QL523I62.js → upgrade-command-FB5QJ363.js} +13 -12
  70. package/dist/{workspace-K72NP7SX.js → workspace-2COHDBM3.js} +1 -1
  71. package/dist/{workspace-command-HYA6JEKK.js → workspace-command-43P42FBP.js} +31 -32
  72. package/dist/{workspace-export-AJMGN3CQ.js → workspace-export-DURY5WYL.js} +2 -2
  73. package/guides/README.md +2 -2
  74. package/guides/cli.md +17 -19
  75. package/guides/codegen.md +21 -5
  76. package/guides/coverage.md +1 -1
  77. package/guides/deploying.md +31 -34
  78. package/guides/environment.md +4 -4
  79. package/guides/object-kinds.md +3 -3
  80. package/guides/project-structure.md +1 -1
  81. package/guides/scaffold.md +25 -2
  82. package/guides/typed-frontend.md +1 -1
  83. package/llms/kinds-agent-mcp.md +1 -1
  84. package/llms/kinds-core.md +1 -1
  85. package/llms/kinds-realtime.md +1 -1
  86. package/llms/lock.md +5 -5
  87. package/llms/tests.md +1 -1
  88. package/llms-full.txt +25 -34
  89. package/llms.txt +16 -25
  90. package/manifest.json +95 -426
  91. package/package.json +1 -1
  92. package/dist/chunk-3EYUR3TX.js +0 -100
  93. package/dist/chunk-JQJPFUZI.js +0 -118
  94. package/dist/chunk-YNZFIY4J.js +0 -78
  95. package/dist/codegen-command-YSCSCVT5.js +0 -43
  96. package/dist/env-target-4T7AT347.js +0 -16
  97. package/dist/sandbox-details-command-EDO56IY3.js +0 -18
  98. package/dist/sandbox-export-command-MO6DEZ7Z.js +0 -24
package/guides/codegen.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Pulling an existing workspace
2
2
 
3
- What `xanots codegen` writes, how faithful the pull is, and how to read its report.
3
+ What `xanots init --from` writes, how faithful the pull is, and how to read its report.
4
+
5
+ ## Running it
6
+
7
+ A pull is `init` with `xano/` filled from an existing backend instead of the empty starter,
8
+ so it is the scaffold command with one extra flag:
9
+
10
+ ```bash
11
+ xanots init my-app --from workspace # your real workspace (the one your login is scoped to)
12
+ xanots init my-app --from ephemeral:my-env # a named ephemeral environment
13
+ xanots init my-app --from ./ws.json # a bundle already on disk — offline, no login
14
+ ```
15
+
16
+ Everything else about the project is unchanged: `--framework`, the theme flags, `--ai` and
17
+ `--web` mean the same thing here as they do without `--from`. The two flags that describe
18
+ the pull itself — `--report` and `--skip-roundtrip` — are refused without it rather than
19
+ silently ignored.
4
20
 
5
21
  ## The generated tree
6
22
 
@@ -49,7 +65,7 @@ it round-trips exactly.
49
65
 
50
66
  Then it checks its own work: the project it just wrote is loaded, exported, and diffed
51
67
  against the workspace it came from. A mismatch names the object and fails the command
52
- (`--no-verify` opts out). So "it compiled" and "it means the same thing" are separate
68
+ (`--skip-roundtrip` opts out). So "it compiled" and "it means the same thing" are separate
53
69
  claims, and you get both.
54
70
 
55
71
  The findings above are printed either way. Verification runs after decoding is finished,
@@ -68,7 +84,7 @@ count and a collapsed object list, and each names the generated file it landed i
68
84
  the same data is written into `xano/.xanots-codegen.json` on every pull, so parity is
69
85
  trackable release over release and gateable in CI without scraping output.
70
86
 
71
- Re-pulling is a real workflow: a second `codegen` into the same directory refreshes
87
+ Re-pulling is a real workflow: a second `init --from` into the same directory refreshes
72
88
  `xano/` and leaves the rest of the project — your `package.json`, your `frontend/` —
73
89
  exactly as you left it. No `--force` needed, because the tree carries a marker saying it
74
90
  was machine-written.
@@ -76,8 +92,8 @@ was machine-written.
76
92
  > ⚠️ **`xano/` is a scratch surface, and there is no `xanots workspace deploy`.**
77
93
  > Regenerating rewrites it (a directory that isn't a previous pull still needs `--force`),
78
94
  > it carries schema only — no table rows — and deploying it is a *full replace* of the
79
- > target. Pull from your real workspace, edit, and `deploy` to a disposable ephemeral or
80
- > sandbox. Workspace env var **values** ride inline in `xano/workspace.ts` (that is what a
95
+ > target. Pull from your real workspace, edit, and `deploy` to a disposable ephemeral
96
+ > environment. Workspace env var **values** ride inline in `xano/workspace.ts` (that is what a
81
97
  > deploy sends), so treat a pulled tree as secret-bearing before you commit it.
82
98
 
83
99
  ---
@@ -34,7 +34,7 @@ runtime (XanoTS only compiles), and generating engine-side numeric ids/timestamp
34
34
 
35
35
  **Deferred (by design)** — folder auto-discovery, and the `service` / `vault` / `branch`
36
36
  payload sections. (Round-trip decompile is no longer deferred: that is
37
- `xanots codegen`, in the [CLI guide](cli.md); nor is `workflow_test` — it is a
37
+ `xanots init --from`, in the [CLI guide](cli.md); nor is `workflow_test` — it is a
38
38
  first-class kind, see `workflowTest` in [Object kinds](object-kinds.md).) `InferResponse`
39
39
  auto-derivation covers the object-literal and single-`db`-variable cases (matching the engine's
40
40
  static walk), and follows a CALL into the object it invokes (`s.function.call`/`.run`,
@@ -75,7 +75,7 @@ valid until you revoke it wherever you minted it. Save it with owner-only permis
75
75
  **Project-local credentials** — pass `--local` to `login` to cache tokens in a
76
76
  **project-local** `./.xano/auth.json` instead (which `login` **auto-adds to `.gitignore`**),
77
77
  scoping the sign-in to that directory. Every command that **reads** credentials
78
- (`deploy`/`details`, `profile me`, token refresh) resolves them **project-local first, global
78
+ (`deploy`/`details`, `whoami`, token refresh) resolves them **project-local first, global
79
79
  as a fallback**: it uses `./.xano/auth.json` when present, otherwise `~/.xanots/auth.json` —
80
80
  so a `--local` project keeps working without repeating the flag. `login` and `logout` do
81
81
  **not** fall back: they target the shared global cache unless you pass `--local`. An explicit
@@ -92,16 +92,13 @@ workspace blob is deliberately never dumped: it carries per-tenant secrets that
92
92
  in shell history or CI logs.
93
93
 
94
94
  **Where it goes** — the instance your **token is bound to** (the token's `aud`), never a
95
- flag. `xanots deploy` create-or-refreshes an **ephemeral** (default) or resolves your
96
- throwaway **sandbox** (`--dest sandbox`); production promotion is the separate
97
- `xanots release` path to your main instance workspace, which merges rather than replacing.
95
+ flag. `xanots deploy` create-or-refreshes an **ephemeral**, which is the only environment it
96
+ writes to; production promotion is the separate `xanots release` path to your main instance
97
+ workspace, which merges rather than replacing.
98
98
 
99
99
  **Static host** — `deploy --static <dir>` archives a directory and deploys it to a
100
- static host after the backend import. Its target follows the destination: with `--dest
101
- ephemeral` the frontend lands **on the ephemeral itself** (backend + frontend in one
102
- environment); with `--dest sandbox` it lands on your **own (parent) workspace**, since the
103
- sandbox tenant does not serve static hosting. For the parent-workspace case the target is the
104
- workspace your credential is already pinned to, and the CLI uploads the archive to
100
+ static host after the backend import. The frontend lands **on the ephemeral itself**, so the
101
+ backend and the frontend live in one environment. The CLI uploads the archive to
105
102
  `/api:meta/workspace/{id}/static_host/default/build` with your ordinary bearer. That route
106
103
  auto-creates the `default` host and **auto-deploys to `dev`**, returning the live URL — so
107
104
  the static step is independent of the backend deploy (the backend still runs first because
@@ -115,7 +112,7 @@ matches the canonical returned for the build just pushed — then prints `Fronte
115
112
  polls every second for the first 30s, then every two seconds out to 120s. An unconfirmed poll
116
113
  is a **warning, not a failure** (the build uploaded fine and usually comes online moments
117
114
  later; the exit code stays `0` and the summary records `"verified": false`). Verification is
118
- skipped when the response carries no canonical to compare against. Pass **`--no-verify`** to
115
+ skipped when the response carries no canonical to compare against. Pass **`--skip-liveness`** to
119
116
  skip the wait entirely — useful for fast iterative deploys or when the deployed URL isn't
120
117
  reachable from the machine running the CLI.
121
118
 
@@ -132,7 +129,7 @@ leaves every deep link and refresh running with the global unset — and the pag
132
129
  Injection is skipped (reported as a warning, not a failure) when the archive has no document
133
130
  with a `<head>` to anchor to, and any individual document that lacks one is named; values are
134
131
  `<`-escaped so one containing `</script>` can't break out of the element. This is why a
135
- prebuilt `frontend/dist` can retarget any sandbox with no rebuild.
132
+ prebuilt `frontend/dist` can retarget any environment with no rebuild.
136
133
 
137
134
  > **Caching — verify with a cache buster.** The static host serves `index.html` with
138
135
  > `Cache-Control: public, max-age=3600`, so a browser (or CDN) that loaded the page before
@@ -143,10 +140,10 @@ prebuilt `frontend/dist` can retarget any sandbox with no rebuild.
143
140
  > copy — `curl -s "$URL/?nocache=$(date +%s)"` — and check the fetched HTML for the injected
144
141
  > `window.XANO_HOST` line rather than retrying the same cached URL.
145
142
 
146
- `xanots sandbox details` prints the same **sandbox base URL** (`GET /api:meta/sandbox/me`,
147
- projected to JSON) out of band, for cases where you'd rather bake it in at build time.
148
- (`xanots profile me` prints the *instance* base URL, i.e. the account's origin rather than
149
- the sandbox tenant.) A static failure after a committed backend deploy **does not roll
143
+ `xanots status` prints the same **environment base URL** out of band, for cases where you'd
144
+ rather bake it in at build time along with the environment's name and expiry, without your
145
+ having to know either. (`xanots whoami` prints the *instance* base URL, i.e. the account's
146
+ origin rather than the environment.) A static failure after a committed backend deploy **does not roll
150
147
  back**: it exits with code `3` and a resumable message telling you to re-run with `--static`
151
148
  to retry just that step.
152
149
 
@@ -182,9 +179,9 @@ secret must not deploy against whatever happens to be on the runner.
182
179
  > use** — a stored one is spent by its first exchange, so a job that runs twice fails the
183
180
  > second time. Prefer the meta credential.
184
181
 
185
- ## Validating against a live instance (xanots validate)
182
+ ## Checking against a live instance (xanots preflight)
186
183
 
187
- `xanots validate` proves your compiled output against a **real, running Xano
184
+ `xanots preflight` proves your compiled output against a **real, running Xano
188
185
  instance** — not a static snapshot. It compiles your workspace, imports it into a
189
186
  **fresh ephemeral environment created for that run**, exports it back, and diffs it
190
187
  against what you compiled, so you catch three classes of problem a local build can't:
@@ -195,7 +192,7 @@ against what you compiled, so you catch three classes of problem a local build c
195
192
 
196
193
  It talks only to public meta API routes — the **same** archive import `xanots
197
194
  deploy` uses, plus the workspace export — and never touches XanoScript. There is one
198
- way into an instance, so a transport bug is one `validate` reproduces rather than
195
+ way into an instance, so a transport bug is one `preflight` reproduces rather than
199
196
  routes around.
200
197
 
201
198
  It is non-destructive: nothing you own is written to. Each run creates its own
@@ -217,11 +214,11 @@ XANO_VALIDATE_TOKEN=your-meta-bearer-token
217
214
  ```
218
215
 
219
216
  ```bash
220
- xanots validate ./xano/index.ts # import + round-trip diff, reports per object (every authored kind)
221
- xanots validate ./xano/index.ts --runtime # + run each deployed function
222
- xanots validate ./xano/index.ts --capture # + write fetched JSON to ./validate-out (fixture candidates)
223
- xanots validate ./xano/index.ts --instance http://localhost:8080 # override the target for one run
224
- xanots validate --bundle ws.json # validate an already-exported bundle
217
+ xanots preflight ./xano/index.ts # import + round-trip diff, reports per object (every authored kind)
218
+ xanots preflight ./xano/index.ts --runtime # + run each deployed function
219
+ xanots preflight ./xano/index.ts --capture # + write fetched JSON to ./validate-out (fixture candidates)
220
+ xanots preflight ./xano/index.ts --instance http://localhost:8080 # override the target for one run
221
+ xanots preflight --bundle ws.json # check an already-exported bundle
225
222
  ```
226
223
 
227
224
  Config comes from the environment (a `.env` is autoloaded; a real env var wins),
@@ -243,10 +240,10 @@ know the URL ahead of time:
243
240
  const HOST = (typeof window !== "undefined" && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
244
241
  ```
245
242
 
246
- `window.XANO_HOST` is the **sandbox tenant** URL your deployed APIs answer at (the same
247
- value `xanots sandbox details` prints as `baseUrl`); it is *not* `xanots profile me`,
248
- which prints your account's instance origin. Because injection happens at deploy time, a
249
- prebuilt `frontend/dist` retargets any sandbox with **no rebuild** — ideal for headless agents.
243
+ `window.XANO_HOST` is the **environment** URL your deployed APIs answer at (the same value
244
+ `xanots status` prints as the environment's URL); it is *not* `xanots whoami`, which prints
245
+ your account's instance origin. Because injection happens at deploy time, a prebuilt
246
+ `frontend/dist` retargets any environment with **no rebuild** — ideal for headless agents.
250
247
  Add your own public config (base URLs, *publishable* keys) with `--static-env KEY=VALUE`
251
248
  (repeatable), exposed the same way as `window.<KEY>`. A static host serves these files
252
249
  verbatim to the browser, so everything injected is **public** — never put secrets here;
@@ -275,13 +272,13 @@ exact string `window.XANO_HOST` (the dot form is valid to *read* the global in y
275
272
 
276
273
  | Command | Where it goes |
277
274
  |---|---|
278
- | `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. |
275
+ | `xanots deploy` | A disposable **ephemeral** environment — create-or-refreshed each run, auto-expiring, with its own URL. There is no other destination and no flag to choose one. |
279
276
  | `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. |
280
- | `xanots test` | Nothing — it only reads. Runs the tests an already-deployed environment carries; `--dest` picks which one, `workspace` included. |
277
+ | `xanots test` | Nothing — it only reads. Runs the tests an already-deployed environment carries; `--env` picks which one, `workspace` included. |
281
278
 
282
- Not every instance has ephemeral environments enabled. Where they are off, the default
283
- `deploy` says so and points at `--dest sandbox` note that `--static` then publishes the
284
- frontend to your own (parent) workspace, replacing whatever it is hosting.
279
+ Not every instance has ephemeral environments enabled. Where they are off, `deploy` says so
280
+ and names who can turn them onthere is no second destination to fall back to. `xanots
281
+ export` and `xanots preflight` still work meanwhile.
285
282
 
286
283
  Every `deploy` is a **full replace** of the disposable environment — always fresh, no
287
284
  merge mode, no flags to get wrong. A `release` is the opposite by design: it changes what
@@ -325,7 +322,7 @@ with no lock entry is an object the project never created — a table built in t
325
322
  team's API group — and the release is refused rather than deleting it. A prune with no lock
326
323
  at all is refused too: without one there is no record of what belongs to the project, so
327
324
  every deletion would be a guess. To delete something that is genuinely yours to remove,
328
- adopt it first (`xanots lock adopt`). Releasing a pre-exported `--bundle` carries no entry
325
+ import its identities first (`xanots lock import`). Releasing a pre-exported `--bundle` carries no entry
329
326
  file to find a lock beside, so name it with `--lock=<path>`.
330
327
 
331
328
  **A dropped column is destructive, and does not need a flag to happen.** Removing a column
@@ -355,4 +352,4 @@ XANO_REFRESH_TOKEN=… XANO_CLIENT_ID=… npx @xanots/sdk deploy --bundle ws.jso
355
352
  ```
356
353
 
357
354
  > ⚠️ A deploy is a full replace of the target environment, including its table records,
358
- > before importing. The blast radius is your own disposable ephemeral/sandbox — but anything
355
+ > before importing. The blast radius is your own disposable ephemeral — but anything
@@ -22,7 +22,7 @@ are what you get when the variable is unset.
22
22
  | `XANO_ORIGIN` | OAuth host to sign in against, instead of the default — the same thing `--origin` does. |
23
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 only if the browser you open it in can reach this machine's `127.0.0.1`. For a machine that has no browser to launch. When the browser is on a **different** machine, that redirect cannot arrive — use `xanots login --paste` instead. |
24
24
 
25
- **`xanots validate`** — its own target, deliberately separate from the deploy login
25
+ **`xanots preflight`** — its own target, deliberately separate from the deploy login
26
26
 
27
27
  | Variable | What it does |
28
28
  |---|---|
@@ -78,7 +78,7 @@ updated on every export (written atomically before the bundle). **Commit it next
78
78
  code.** A project from `xanots init` is locked from its first export — both its `xano:export`
79
79
  and `xano:deploy` scripts pass `--lock`, and `npm run xano:check` is the `--frozen-lock` CI
80
80
  guard. Adopt it early either way: once identities have drifted, the only way back is
81
- `lock adopt` against the deployed workspace.
81
+ `lock import` against the deployed workspace.
82
82
 
83
83
  Precedence at emit is always **explicit in-code value → lock entry → name derivation**.
84
84
 
@@ -92,7 +92,7 @@ losing it costs nothing.
92
92
  The lock is load-bearing exactly where a live guid **diverges** from that derivation, which
93
93
  happens two ways:
94
94
 
95
- - **Adopted** objects — anything built in the Xano UI first and taken over with `lock adopt`.
95
+ - **Adopted** objects — anything built in the Xano UI first and taken over with `lock import`.
96
96
  The engine assigned those guids randomly; nothing in your code can re-derive them.
97
97
  - **Renamed** objects — `lock rename` pins the original guid under the new name, so the
98
98
  derivation no longer reproduces it.
@@ -125,7 +125,7 @@ try to create everything. `xanots release ./xano/index.ts --replace` now re-pins
125
125
  workspace itself and tells you to commit it. If there is no lock to re-pin, it says so —
126
126
  without one, the next release duplicates every object.
127
127
 
128
- **Adopting a live workspace** — `xanots lock adopt <bundle.json>` seeds the lock from a
128
+ **Importing a live workspace's identities** — `xanots lock import <bundle.json>` seeds the lock from a
129
129
  real engine `packageExport`, capturing the live workspace's random guids by `(type, name)`
130
130
  so code takes over an existing workspace and the first sync updates in place instead of
131
131
  duplicating.
@@ -122,7 +122,7 @@ query({
122
122
 
123
123
  A pull brings tests back, along with a query's saved request/response `example`. The one
124
124
  thing it withholds is a test's auth `token` — that is an expiring credential rather than
125
- authored configuration, so `xanots codegen` reports it as a deliberate omission instead of
125
+ authored configuration, so `xanots init --from` reports it as a deliberate omission instead of
126
126
  writing it into a committed tree.
127
127
 
128
128
  **Four of the `Run …` statements only run inside a workflow test.** `s.api.call`,
@@ -311,8 +311,8 @@ nothing checks that spelling, so prefer the def wherever there is one.
311
311
 
312
312
  A container takes time to come up, so `xanots deploy` waits for it: after the import it
313
313
  reads each microservice and reports whether it is ready, still starting, or failed, then
314
- lists them. Skip the wait with `--no-verify`. The same report is available any time from
315
- `xanots ephemeral get <env>`, `xanots sandbox details`, and `xanots workspace details`.
314
+ lists them. Skip the wait with `--skip-liveness`. The same report is available any time from
315
+ `xanots status`, `xanots ephemeral get <env>`, and `xanots workspace details`.
316
316
 
317
317
  Two outcomes, and only one of them is a warning:
318
318
 
@@ -31,7 +31,7 @@ Paths are lower case throughout — an HTTP verb is the one exception, because i
31
31
  the method rather than a word. Bindings keep the object's own casing, so a file name
32
32
  and the symbol it exports can differ.
33
33
 
34
- That is the shape `xanots codegen` writes, and its `index.ts` re-exports every object
34
+ That is the shape `xanots init --from` writes, and its `index.ts` re-exports every object
35
35
  by name — import from the tree's root rather than from a file, since a file path moves
36
36
  when an object's parent or its `_shared.ts` placement changes. Hand-authored projects are
37
37
  free to use any other layout; only `index.ts` registering the objects matters.
@@ -11,10 +11,13 @@ What `xanots init` writes, the two frontend presets, theming, add-ons, and the S
11
11
  (repeatable; writes `CLAUDE.md`/`AGENTS.md`/Cursor rules — none by default),
12
12
  `--marketplace <pkg>` (repeatable, comma-separated; installs add-ons and
13
13
  registers them — below), `--force` (scaffold into a non-empty folder),
14
- `--no-install` (skip `npm install`).
14
+ `--no-install` (skip `npm install`), `--web` (choose all of the above in a
15
+ browser instead — see [Choosing in a browser](#choosing-in-a-browser)),
16
+ `--from <source>` (fill `xano/` from an existing backend rather than the
17
+ starter — see [Pulling an existing workspace](codegen.md)).
15
18
  In a terminal, `init` prompts for the framework and the AI files; every prompt
16
19
  has a default, so pressing enter twice is a valid answer. The look is never
17
- prompted for — it comes from the flags above, or from `xanots onboard`.
20
+ prompted for — it comes from the flags above, or from `--web`.
18
21
  The starter backend is empty but already compiles and deploys — grow it from the
19
22
  walkthrough in `xano/EXAMPLE.md`.
20
23
 
@@ -63,6 +66,26 @@ project — the mistake npm answers by silently writing to the wrong `package.js
63
66
  The package name is passed through exactly as typed, so version specifiers, tags,
64
67
  and third-party packages all work.
65
68
 
69
+ ## Choosing in a browser
70
+
71
+ `--web` collects the same choices against a live preview instead of on the
72
+ command line:
73
+
74
+ ```bash
75
+ xanots init my-app --web
76
+ ```
77
+
78
+ It is a launcher, not a second scaffolder: a configurator is downloaded on
79
+ demand, serves a local page, and finishes by running `init` with the flags your
80
+ choices imply — printing the equivalent command so the project stays
81
+ reproducible from a script. Everything after `--web` is passed to it untouched,
82
+ including `--help`, which is why that one form reaches the network. `xanots help
83
+ init` stays offline, like `init` itself.
84
+
85
+ Because the configurator is fetched at run time, `--web` needs the npm registry
86
+ before it can start. `init` on its own reaches out only to install the new
87
+ project's dependencies, which `--no-install` skips.
88
+
66
89
  ## The frontend preset
67
90
 
68
91
  To point `npm run dev` at a real backend, copy `.env.example` to `.env.local` — both
@@ -168,7 +168,7 @@ is paid by importing any def at all.
168
168
  almost no bundle cost:
169
169
 
170
170
  ```bash
171
- xanots paths ./xano/index.ts --emit xano/routes.gen.ts
171
+ xanots routes ./xano/index.ts --emit xano/routes.gen.ts
172
172
  ```
173
173
 
174
174
  The emitted file is plain data plus one interpolator and imports nothing at all — the same
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Read when the workspace defines an `agent()` or an `mcpServer()`.
4
4
 
5
- - `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral; not in sandbox.
5
+ - `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral.
6
6
  - `tools?`: a `ToolsetToolEntry[]`. Pass the `tool()` HANDLES directly (`tools: [saveNote]`), like every other collection in the SDK; use the `{ tool, enabled?, auth? }` wrapper only when a tool needs `enabled: false` or per-tool `auth`. `auth` names an auth **table** (a `table({ auth: true })` handle or its name) — Xano's ONLY MCP auth surface (per-tool; there is no server-level gate). An entry that names no tool (no handle, no `id`) THROWS at export rather than emitting the `id: 0` null reference it used to; a deliberate raw `id: 0` warns and is carried through, so a pulled workspace still round-trips.
7
7
  - `agent({ name, guid?, description?, docs?, enabled?, canonical?, tags?, history?, llm, tools?, output? })` — an LLM orchestrator. No top-level `instructions`/`prompt`/`spec` — the prompt lives under `llm`. Invoke from a stack with `s.ai.agent.run({ agent, args })`.
8
8
  - `llm` (REQUIRED): typed provider settings, a discriminated union on `type` (`"xano-free" | "anthropic" | "openai" | "google-genai"`). Shared fields: `systemPrompt?`, `maxSteps?` (default `5`), and `prompt?` XOR `messages?` (genuinely exclusive: both is a type error and throws — the engine stores ONE `prompt_type`, so one would be dropped); plus provider fields (`apiKey?`, `model?`, `temperature?`, `reasoningEffort?`, …). String fields accept Twig placeholders — `{{ $args.x }}` for run inputs (the `args` of `s.ai.agent.run`), `{{ $env.NAME }}` for env vars.
@@ -21,7 +21,7 @@ to survive a rename).
21
21
  - ⚠ Under `"custom"`, `allowOrigins` is matched as EXACT strings (scheme+host+port, no wildcard or subdomain expansion) and `"*"` is compared as a literal origin — it matches NOTHING. An unmatched origin gets no `access-control-*` headers at all, so the call fails in the browser on a missing `access-control-allow-origin` while export, deploy and the preflight all look fine. Name each origin, or use `mode: "default"` for any-origin. `allowMethods` gates the REAL response too: a verb left off gets no CORS headers back even though its preflight passes. Export warns on an empty origin list, a `"*"` entry, and a policy with no method enabled.
22
22
  - `defineFunction`/`query`/`apiGroup` above cover the queries+tables core; the four below are the "reach past that" primitives (tasks, workflow tests, middleware, tools). Agents and MCP servers are the same family and live in `llms/kinds-agent-mcp.md`. Same envelope conventions (`guid?`, `description?`, `docs?`, `tags?`, `history?`) unless noted.
23
23
  - `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` — a scheduled background job (function-like `stack`, no `input`/`response`).
24
- - `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in seconds** (default `86400` = daily); `endsOn` present ⇒ the schedule has an end. `endsEnabled?` defaults to that and is recovery-only — state it to reproduce a stored schedule that remembers an end date with the gate OFF. Fires on an ephemeral; does NOT fire in the sandbox (see Gotchas).
24
+ - `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in seconds** (default `86400` = daily); `endsOn` present ⇒ the schedule has an end. `endsEnabled?` defaults to that and is recovery-only — state it to reproduce a stored schedule that remembers an end date with the gate OFF. Fires on an ephemeral (see Gotchas).
25
25
  - `workflowTest({ name, guid?, description?, docs?, datasource?, active?, tags?, stack? })` — an end-to-end test. NO `input`/`response`: `.call` something with an `as`, then assert on that var — `s.function.call({ fn, input, as: "r" })`, `s.expect.to_equal({ expr: ref("r"), value: c.int(42) })`. `s.expect.*` belongs here — it is not inert elsewhere (a failure 500s the request), so treat one in a query/function/task as a mistake to remove. `active?` defaults `true`; chain tests with `s.workflow_test.call({ workflowTest: <def handle> })`.
26
26
  - `datasource?`: **the trap.** Default `""` is an EMPTY datasource (recommended), not "no datasource". Any non-empty name makes the engine CLONE it before EVERY run — against production-sized data, slow enough to fail the run. `"live"` warns at compile time; other names don't.
27
27
  - `middleware({ name, guid?, description?, docs?, resultStrategy?, exceptionPolicy?, tags?, history?, input?, stack?, response?, responseShape?, tests? })` — a pre/post interceptor (function-like `stack`); attach it via a host's `middleware: { pre, post }`. ⚠ `input` ENCODES but an ATTACHED middleware never has it bound — the host request binds its own inputs, so `inp()` inside pre/post fails at runtime with `Unable to locate input` and a declared default does not stand in (`export()` warns). Read the request body with `s.util.get_all_input` instead; it yields a `{ type, vars }` envelope. `s.middleware.call` is the one path that DOES bind the declared map.
@@ -46,7 +46,7 @@
46
46
  - **Tenant instances (isolated DB):** a tenant's realtime objects live in the TENANT's database, so BOTH halves of a client must name the tenant.
47
47
  - Socket: `server.getUrl(base, { tenant })` → `/ws/<tenant>:<canonical>`. ⚠ A bare canonical on a tenant host resolves against the INSTANCE workspace instead.
48
48
  - That colon form is PECULIAR TO THE SOCKET. Every other tenant URL gives the tenant its OWN segment — the HTTP half of the same client is `https://<host>/tenant/<tenant>/api:<canonical>/…`. NO request header is required for either.
49
- - Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `sandbox details` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
49
+ - Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `xanots status` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
50
50
  - ⚠ `getUrl`/`socketUrl` are NOT idempotent — a `baseUrl` that already carries a `/ws/<…>` path (an earlier result of either) THROWS. Resolve ONCE from the http(s) base; pass that result to `new WebSocket`, never back in as a base.
51
51
  - Still pass `{ tenant }` explicitly for a tenant on its OWN DOMAIN — the hostname carries it for HTTP, but there is nothing in the URL for the socket to lift.
52
52
  - ⚠ Tokens are tenant-scoped (audience `<tenant>:<license>`, not the bare license), so one minted through the instance workspace is REJECTED by a tenant's realtime server — authenticate and dial through the same tenant.
package/llms/lock.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lock file
2
2
 
3
- > Read when a `xano.lock` exists or should — renaming/pruning/adopting identities, seeding the lock programmatically, or asking which commands write it.
3
+ > Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
4
4
 
5
5
  `xano.lock` pins each object's guid and each api-group/toolset canonical, so renames
6
6
  stay renames (guids otherwise derive from `(type, name)`; a query's from `(api group,
@@ -13,11 +13,11 @@ old key drops automatically once its guid re-lands under the composed one).
13
13
  - `xanots lock rename <kind> <old> <new>` — `kind` is the payload key (or `table`/`api_group`).
14
14
  Run it after renaming in code; the next export emits the original guid under the new name.
15
15
  - `xanots lock prune <entry-file> [keys…] --yes` — drops orphaned entries. Finding orphans
16
- RUNS the entry's module scope (env assertions included); `--no-verify --yes <kind:name>…`
16
+ RUNS the entry's module scope (env assertions included); `--identity-only --yes <kind:name>…`
17
17
  prunes named keys with no evaluation and no orphan check.
18
- - `xanots lock adopt <live-bundle.json> [--yes]` — seed the lock from an engine
18
+ - `xanots lock import <live-bundle.json> [--yes]` — seed the lock from an engine
19
19
  packageExport when taking over an existing workspace.
20
- - Every lock subcommand accepts `--lock=<path>`. `rename`/`adopt` take no entry file, so
20
+ - Every lock subcommand accepts `--lock=<path>`. `rename`/`import` take no entry file, so
21
21
  from outside the lock's directory pass `--lock` (or `--entry=<entry-file>` to derive it).
22
22
  - Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing any def
23
23
  module — references bake guids at import time, so late seeding is a silent no-op
@@ -25,7 +25,7 @@ old key drops automatically once its guid re-lands under the composed one).
25
25
 
26
26
  What writes the lock: `export`/`deploy` of an ENTRY FILE update it via the shared compile
27
27
  step — only when a lock exists or `--lock` is passed. Nothing from a DEPLOY is written
28
- back beyond that (an ephemeral/sandbox is a separate workspace, so its identities must
28
+ back beyond that (an ephemeral is a separate workspace, so its identities must
29
29
  not pollute yours). The one write-back is `release --replace`, which mints fresh
30
30
  identities in the workspace the lock describes: it re-pins the lock from the rebuilt
31
31
  workspace, because otherwise the next release matches nothing and duplicates every
package/llms/tests.md CHANGED
@@ -51,7 +51,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
51
51
 
52
52
  `xanots test run-all` runs the unit tests AND the `workflowTest()` objects an environment carries. It takes no entry file and compiles nothing: it runs what is DEPLOYED, so deploy before testing.
53
53
 
54
- - `--dest ephemeral` (DEFAULT, `--name <env>` to pick one), `--dest sandbox`, or `--dest workspace`. Unlike `deploy`, `workspace` is allowed here running a test reads.
54
+ - `--env ephemeral` (DEFAULT the one this project last deployed to), `--env ephemeral:<name>`, or `--env workspace`. Same grammar as `init --from`. `deploy` takes no `--env` at all; `test` does, and `workspace` is allowed here because running a test only reads.
55
55
  - `xanots test list` shows what is there without running it; `xanots test run "<name>"` runs one. When a name is ambiguous the error prints the qualified `function:math/happy path` form, which `run` also accepts.
56
56
  - `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
57
57
  - A failing suite exits 5, distinct from a crash. Tests that could not be REACHED exit 6 — retry that one, investigate the other. An environment with no tests is success, not failure.
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # xanots v0.0.9
1
+ # xanots v0.0.10
2
2
 
3
3
  > TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
4
4
 
@@ -50,7 +50,7 @@ installed), so a plain file read resolves them at the version you have.
50
50
  - [Column and input types](llms/fields.md): Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and accessor methods, and the `s.precondition` error/status contract that rides the same catalog.
51
51
  - [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
52
52
  - [Lambda bodies (JavaScript)](llms/lambda.md): Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`. Lambda workers are a bounded shared resource, and each surface binds a different set of identifiers.
53
- - [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/adopting identities, seeding the lock programmatically, or asking which commands write it.
53
+ - [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
54
54
  - [Legacy paradigms and retired statements](llms/legacy.md): Read when the code was PULLED from an existing Xano instance rather than authored here — how a codegen'd tree reads and the three shapes in it that must not be "fixed", plus a `raw({ name: "mvp:…" })` statement, a `realtimeTrigger()`, or any name the catalogs do not list.
55
55
  - [Statement catalog](llms/statements-catalog.md): Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace. Look here after the control-flow core in the router does not cover what you need.
56
56
 
@@ -115,7 +115,7 @@ which fails with a "must be ES modules" error until you switch it to module.
115
115
 
116
116
  Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
117
117
  without one a group's client paths are unresolvable until a lock exists: the bundle
118
- exports fine and `xanots paths` / `getPath()` then fail on the very queries it just
118
+ exports fine and `xanots routes` / `getPath()` then fail on the very queries it just
119
119
  built. An explicit `canonical` resolves them from the source alone.
120
120
 
121
121
  Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
@@ -134,7 +134,7 @@ api-group/toolset canonical in a lock file written BESIDE THE ENTRY FILE —
134
134
  To rename an object: rename in code, export (stderr prints the exact fix-up), run
135
135
  `xanots lock rename <kind> <old> <new>`, export again — the original guid is emitted
136
136
  under the new name, so the engine renames in place instead of delete+create. Taking
137
- over an existing workspace: `xanots lock adopt <its-packageExport.json>` first, then
137
+ over an existing workspace: `xanots lock import <its-packageExport.json>` first, then
138
138
  export. Pruning, programmatic seeding, and which commands write the lock:
139
139
  `llms/lock.md`.
140
140
 
@@ -147,19 +147,19 @@ environment and prints its URL.
147
147
  AND records — before importing. The blast radius is a disposable environment, not a
148
148
  production workspace, but confirm with the user before the first run.
149
149
 
150
- **Two destinations, and the choice changes more than the target.**
150
+ **One destination, and no flag for it.**
151
151
 
152
- - `--dest ephemeral` (DEFAULT) a NAMED, workspace-scoped, auto-expiring tenant
153
- (~1h; `--expires-hours` 1–72 at create time). The active one is tracked in
152
+ - `xanots deploy` writes to a NAMED, workspace-scoped, auto-expiring ephemeral tenant
153
+ (~1h; `--expires-hours` 1–72 at create time), and to nothing else an `--env` here
154
+ is a usage error, not a choice. The active env is tracked in
154
155
  `./.xano/ephemeral.json`, so deploying again REFRESHES it and the URL is unchanged;
155
156
  if it expired or was swept, a fresh one is created and the new URL is called out.
156
157
  `--static` puts the frontend ON THE EPHEMERAL, so backend and frontend share one
157
158
  disposable environment.
158
159
  ⚠ Only the BACKEND URL survives a refresh: the replace clears static hosting too,
159
160
  so `--static` publishes a NEW host every run and the previous URL stops serving.
160
- - `--dest sandbox` your single throwaway tenant, no expiry. `--static` puts the
161
- frontend on your OWN (parent) workspace instead, because the sandbox tenant does
162
- not serve static hosting.
161
+ - `xanots status` names the env this project last deployed to, its URL and its expiry,
162
+ without your having to remember which one it was.
163
163
  - `xanots release` promotes to your INSTANCE workspace and MERGES, not replaces:
164
164
  adds/updates what you define, deletes nothing, writes no rows. Destruction is
165
165
  opt-in per flag, previewed + confirmed, and can drop a table WITH its rows.
@@ -182,7 +182,7 @@ host falls back to '', and every call 404s off the dev server.
182
182
  ⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
183
183
  by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
184
184
  bare `XANO_HOST` token.
185
- ⚠ `xanots validate` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
185
+ ⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
186
186
  `XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
187
187
  **Displaying a stored file.** A file column comes back as `{ path, name, type, size,
188
188
  meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
@@ -332,7 +332,7 @@ Non-obvious authoring rules:
332
332
  never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
333
333
  one, adds ~1 kB. So the cost is paid by importing ANY def at all, and reducing what a
334
334
  def does will not reduce it.
335
- Fix: `xanots paths <entry> --emit xano/routes.gen.ts` (`routes` is an accepted alias) — verbs, paths, and sockets as
335
+ Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
336
336
  plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
337
337
  `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
338
338
  URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
@@ -419,19 +419,10 @@ Non-obvious authoring rules:
419
419
  credential the runner has. As a file that triple is `{ "type": "token",
420
420
  "instance_base_url": …, "workspace_id": <n>, "meta_api_token": … }`. The older
421
421
  `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works but ROTATES: single-use.
422
- - **Event-driven objects fire on an EPHEMERAL, not in the sandbox.** A `task`
423
- (scheduled), an `mcpServer`, and every trigger — `tableTrigger` included — run normally
424
- on an ephemeral env, which is `deploy`'s DEFAULT destination. So test an event-driven
425
- design (screen-on-insert, cron cleanup, MCP tool call) by deploying it and letting it
426
- run.
427
- ⚠ Under `--dest sandbox` they import cleanly but their stacks NEVER execute, and there
428
- is no way to fire one manually — an insert on a bound table does not run its
429
- `tableTrigger`, and the design silently does nothing. Only synchronously-invoked objects
430
- (queries, functions, and the agents an endpoint calls with `s.ai.agent.run`) run there.
431
- If you must stay on the sandbox, verify the logic out of band: factor the body into a
432
- `defineFunction` (or a callable `query`) and invoke it directly — a `tableTrigger` that
433
- screens a row on insert should delegate to a function a `query` can also call via
434
- `s.function.run`, and you assert against that.
422
+ - **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
423
+ and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
424
+ is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
425
+ cleanup, MCP tool call) by deploying it and letting it run.
435
426
  - **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
436
427
  `["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
437
428
  `0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
@@ -539,7 +530,7 @@ to survive a rename).
539
530
  - ⚠ Under `"custom"`, `allowOrigins` is matched as EXACT strings (scheme+host+port, no wildcard or subdomain expansion) and `"*"` is compared as a literal origin — it matches NOTHING. An unmatched origin gets no `access-control-*` headers at all, so the call fails in the browser on a missing `access-control-allow-origin` while export, deploy and the preflight all look fine. Name each origin, or use `mode: "default"` for any-origin. `allowMethods` gates the REAL response too: a verb left off gets no CORS headers back even though its preflight passes. Export warns on an empty origin list, a `"*"` entry, and a policy with no method enabled.
540
531
  - `defineFunction`/`query`/`apiGroup` above cover the queries+tables core; the four below are the "reach past that" primitives (tasks, workflow tests, middleware, tools). Agents and MCP servers are the same family and live in `llms/kinds-agent-mcp.md`. Same envelope conventions (`guid?`, `description?`, `docs?`, `tags?`, `history?`) unless noted.
541
532
  - `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` — a scheduled background job (function-like `stack`, no `input`/`response`).
542
- - `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in seconds** (default `86400` = daily); `endsOn` present ⇒ the schedule has an end. `endsEnabled?` defaults to that and is recovery-only — state it to reproduce a stored schedule that remembers an end date with the gate OFF. Fires on an ephemeral; does NOT fire in the sandbox (see Gotchas).
533
+ - `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in seconds** (default `86400` = daily); `endsOn` present ⇒ the schedule has an end. `endsEnabled?` defaults to that and is recovery-only — state it to reproduce a stored schedule that remembers an end date with the gate OFF. Fires on an ephemeral (see Gotchas).
543
534
  - `workflowTest({ name, guid?, description?, docs?, datasource?, active?, tags?, stack? })` — an end-to-end test. NO `input`/`response`: `.call` something with an `as`, then assert on that var — `s.function.call({ fn, input, as: "r" })`, `s.expect.to_equal({ expr: ref("r"), value: c.int(42) })`. `s.expect.*` belongs here — it is not inert elsewhere (a failure 500s the request), so treat one in a query/function/task as a mistake to remove. `active?` defaults `true`; chain tests with `s.workflow_test.call({ workflowTest: <def handle> })`.
544
535
  - `datasource?`: **the trap.** Default `""` is an EMPTY datasource (recommended), not "no datasource". Any non-empty name makes the engine CLONE it before EVERY run — against production-sized data, slow enough to fail the run. `"live"` warns at compile time; other names don't.
545
536
  - `middleware({ name, guid?, description?, docs?, resultStrategy?, exceptionPolicy?, tags?, history?, input?, stack?, response?, responseShape?, tests? })` — a pre/post interceptor (function-like `stack`); attach it via a host's `middleware: { pre, post }`. ⚠ `input` ENCODES but an ATTACHED middleware never has it bound — the host request binds its own inputs, so `inp()` inside pre/post fails at runtime with `Unable to locate input` and a declared default does not stand in (`export()` warns). Read the request body with `s.util.get_all_input` instead; it yields a `{ type, vars }` envelope. `s.middleware.call` is the one path that DOES bind the declared map.
@@ -639,7 +630,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
639
630
 
640
631
  `xanots test run-all` runs the unit tests AND the `workflowTest()` objects an environment carries. It takes no entry file and compiles nothing: it runs what is DEPLOYED, so deploy before testing.
641
632
 
642
- - `--dest ephemeral` (DEFAULT, `--name <env>` to pick one), `--dest sandbox`, or `--dest workspace`. Unlike `deploy`, `workspace` is allowed here running a test reads.
633
+ - `--env ephemeral` (DEFAULT the one this project last deployed to), `--env ephemeral:<name>`, or `--env workspace`. Same grammar as `init --from`. `deploy` takes no `--env` at all; `test` does, and `workspace` is allowed here because running a test only reads.
643
634
  - `xanots test list` shows what is there without running it; `xanots test run "<name>"` runs one. When a name is ambiguous the error prints the qualified `function:math/happy path` form, which `run` also accepts.
644
635
  - `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
645
636
  - A failing suite exits 5, distinct from a crash. Tests that could not be REACHED exit 6 — retry that one, investigate the other. An environment with no tests is success, not failure.
@@ -650,7 +641,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
650
641
 
651
642
  > Read when the workspace defines an `agent()` or an `mcpServer()`.
652
643
 
653
- - `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral; not in sandbox.
644
+ - `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral.
654
645
  - `tools?`: a `ToolsetToolEntry[]`. Pass the `tool()` HANDLES directly (`tools: [saveNote]`), like every other collection in the SDK; use the `{ tool, enabled?, auth? }` wrapper only when a tool needs `enabled: false` or per-tool `auth`. `auth` names an auth **table** (a `table({ auth: true })` handle or its name) — Xano's ONLY MCP auth surface (per-tool; there is no server-level gate). An entry that names no tool (no handle, no `id`) THROWS at export rather than emitting the `id: 0` null reference it used to; a deliberate raw `id: 0` warns and is carried through, so a pulled workspace still round-trips.
655
646
  - `agent({ name, guid?, description?, docs?, enabled?, canonical?, tags?, history?, llm, tools?, output? })` — an LLM orchestrator. No top-level `instructions`/`prompt`/`spec` — the prompt lives under `llm`. Invoke from a stack with `s.ai.agent.run({ agent, args })`.
656
647
  - `llm` (REQUIRED): typed provider settings, a discriminated union on `type` (`"xano-free" | "anthropic" | "openai" | "google-genai"`). Shared fields: `systemPrompt?`, `maxSteps?` (default `5`), and `prompt?` XOR `messages?` (genuinely exclusive: both is a type error and throws — the engine stores ONE `prompt_type`, so one would be dropped); plus provider fields (`apiKey?`, `model?`, `temperature?`, `reasoningEffort?`, …). String fields accept Twig placeholders — `{{ $args.x }}` for run inputs (the `args` of `s.ai.agent.run`), `{{ $env.NAME }}` for env vars.
@@ -728,7 +719,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
728
719
  - **Tenant instances (isolated DB):** a tenant's realtime objects live in the TENANT's database, so BOTH halves of a client must name the tenant.
729
720
  - Socket: `server.getUrl(base, { tenant })` → `/ws/<tenant>:<canonical>`. ⚠ A bare canonical on a tenant host resolves against the INSTANCE workspace instead.
730
721
  - That colon form is PECULIAR TO THE SOCKET. Every other tenant URL gives the tenant its OWN segment — the HTTP half of the same client is `https://<host>/tenant/<tenant>/api:<canonical>/…`. NO request header is required for either.
731
- - Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `sandbox details` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
722
+ - Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `xanots status` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
732
723
  - ⚠ `getUrl`/`socketUrl` are NOT idempotent — a `baseUrl` that already carries a `/ws/<…>` path (an earlier result of either) THROWS. Resolve ONCE from the http(s) base; pass that result to `new WebSocket`, never back in as a base.
733
724
  - Still pass `{ tenant }` explicitly for a tenant on its OWN DOMAIN — the hostname carries it for HTTP, but there is nothing in the URL for the socket to lift.
734
725
  - ⚠ Tokens are tenant-scoped (audience `<tenant>:<license>`, not the bare license), so one minted through the instance workspace is REJECTED by a tenant's realtime server — authenticate and dial through the same tenant.
@@ -1367,7 +1358,7 @@ TypeScript annotations survive in the body, and top-level `await` works.
1367
1358
 
1368
1359
  # Lock file
1369
1360
 
1370
- > Read when a `xano.lock` exists or should — renaming/pruning/adopting identities, seeding the lock programmatically, or asking which commands write it.
1361
+ > Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
1371
1362
 
1372
1363
  `xano.lock` pins each object's guid and each api-group/toolset canonical, so renames
1373
1364
  stay renames (guids otherwise derive from `(type, name)`; a query's from `(api group,
@@ -1380,11 +1371,11 @@ old key drops automatically once its guid re-lands under the composed one).
1380
1371
  - `xanots lock rename <kind> <old> <new>` — `kind` is the payload key (or `table`/`api_group`).
1381
1372
  Run it after renaming in code; the next export emits the original guid under the new name.
1382
1373
  - `xanots lock prune <entry-file> [keys…] --yes` — drops orphaned entries. Finding orphans
1383
- RUNS the entry's module scope (env assertions included); `--no-verify --yes <kind:name>…`
1374
+ RUNS the entry's module scope (env assertions included); `--identity-only --yes <kind:name>…`
1384
1375
  prunes named keys with no evaluation and no orphan check.
1385
- - `xanots lock adopt <live-bundle.json> [--yes]` — seed the lock from an engine
1376
+ - `xanots lock import <live-bundle.json> [--yes]` — seed the lock from an engine
1386
1377
  packageExport when taking over an existing workspace.
1387
- - Every lock subcommand accepts `--lock=<path>`. `rename`/`adopt` take no entry file, so
1378
+ - Every lock subcommand accepts `--lock=<path>`. `rename`/`import` take no entry file, so
1388
1379
  from outside the lock's directory pass `--lock` (or `--entry=<entry-file>` to derive it).
1389
1380
  - Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing any def
1390
1381
  module — references bake guids at import time, so late seeding is a silent no-op
@@ -1392,7 +1383,7 @@ old key drops automatically once its guid re-lands under the composed one).
1392
1383
 
1393
1384
  What writes the lock: `export`/`deploy` of an ENTRY FILE update it via the shared compile
1394
1385
  step — only when a lock exists or `--lock` is passed. Nothing from a DEPLOY is written
1395
- back beyond that (an ephemeral/sandbox is a separate workspace, so its identities must
1386
+ back beyond that (an ephemeral is a separate workspace, so its identities must
1396
1387
  not pollute yours). The one write-back is `release --replace`, which mints fresh
1397
1388
  identities in the workspace the lock describes: it re-pins the lock from the rebuilt
1398
1389
  workspace, because otherwise the next release matches nothing and duplicates every