@xanots/sdk 0.0.9 → 0.0.11

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 (116) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +66 -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 +10 -8
  6. package/dist/{branch-commands-PBQ36L4A.js → branch-commands-2BLOC2GR.js} +13 -11
  7. package/dist/bundle.d.ts +218 -0
  8. package/dist/bundle.js +143 -0
  9. package/dist/{capture-7PO6SGB4.js → capture-4WVJY4DQ.js} +2 -2
  10. package/dist/{chunk-OW2QZEWL.js → chunk-22TKBSDV.js} +2 -2
  11. package/dist/{chunk-ZOYMZZ3S.js → chunk-3INK4Y4E.js} +1 -1
  12. package/dist/{chunk-NDZFBZHC.js → chunk-3UABJA4X.js} +1 -1
  13. package/dist/{chunk-6Z5CNWFC.js → chunk-4IF54NU5.js} +37 -6
  14. package/dist/{chunk-KSV7VOEX.js → chunk-4Q7ZOHH7.js} +3 -3
  15. package/dist/{chunk-QTNO2WD6.js → chunk-7ZYW652H.js} +1 -1
  16. package/dist/{chunk-4PTSTDIG.js → chunk-AIZKXUNP.js} +2 -2
  17. package/dist/{chunk-P5C2YKAM.js → chunk-ANUDXFEX.js} +18 -27
  18. package/dist/{chunk-4BXJGVZ3.js → chunk-BC2C5GVI.js} +2 -69
  19. package/dist/chunk-BSK7ELHU.js +70 -0
  20. package/dist/chunk-CTD5ZCV6.js +28 -0
  21. package/dist/{chunk-5HCDZ2XN.js → chunk-DBFU47BJ.js} +2 -2
  22. package/dist/{chunk-OKNSR7MT.js → chunk-DIA7CT7J.js} +129 -150
  23. package/dist/chunk-EHP3WPEG.js +21 -0
  24. package/dist/{chunk-JQLR64UC.js → chunk-F6CYJ7TN.js} +21 -6
  25. package/dist/{chunk-BKDJN76G.js → chunk-FE5I6S6N.js} +1 -1
  26. package/dist/{chunk-5YQOIFT4.js → chunk-G4EJMQLD.js} +2 -2
  27. package/dist/{chunk-L6TPHWAU.js → chunk-HYBN4H3F.js} +60 -86
  28. package/dist/{chunk-QIWBCS7N.js → chunk-JGCWTCA7.js} +2 -2
  29. package/dist/{chunk-MDXR5E5Q.js → chunk-K5IOND4K.js} +2 -2
  30. package/dist/{chunk-4PE4ZAB7.js → chunk-KA6G2L7U.js} +5 -5
  31. package/dist/chunk-OHX6MIUZ.js +184 -0
  32. package/dist/{chunk-UDL46O35.js → chunk-OWGCOGKK.js} +155 -3
  33. package/dist/{chunk-QONOJO45.js → chunk-QK7ZQJLP.js} +140 -20
  34. package/dist/{chunk-3QW5NFZJ.js → chunk-QKM4U5UK.js} +3 -3
  35. package/dist/{chunk-7OXFMCTX.js → chunk-QYMAZRAU.js} +7 -7
  36. package/dist/{chunk-K3YSQDXJ.js → chunk-RCT7UX7B.js} +92 -75
  37. package/dist/{chunk-T4XPCJRF.js → chunk-TCFIPDB3.js} +1 -1
  38. package/dist/{chunk-ZZLPTHT5.js → chunk-UOZMSF4C.js} +15 -8
  39. package/dist/{chunk-7JDT4PBU.js → chunk-VAF6A3YD.js} +5 -179
  40. package/dist/{chunk-6DHBYBTO.js → chunk-VNQM3V2C.js} +1 -2
  41. package/dist/chunk-WGPXT2G2.js +22 -0
  42. package/dist/{chunk-6AAT2AYQ.js → chunk-WP4OZZV4.js} +2 -2
  43. package/dist/{chunk-OYMR5AMJ.js → chunk-XEOX6AM7.js} +2 -2
  44. package/dist/{chunk-MU3O43L2.js → chunk-YBC3IKMF.js} +2 -2
  45. package/dist/chunk-ZQ2PKR6R.js +40 -0
  46. package/dist/cli.d.ts +57 -31
  47. package/dist/cli.js +9 -7
  48. package/dist/codegen-command-FUT2KJB6.js +49 -0
  49. package/dist/codegen.d.ts +2 -1
  50. package/dist/{completion-A6BZ3XGU.js → completion-WF46272M.js} +2 -2
  51. package/dist/{config-NL33PN4D.js → config-476F3PT5.js} +1 -1
  52. package/dist/{deploy-command-5TTI4TUP.js → deploy-command-IP7V7GT4.js} +69 -100
  53. package/dist/{ephemeral-command-7TK52YQM.js → ephemeral-command-U4AQ3TXX.js} +25 -28
  54. package/dist/index.d.ts +6 -4
  55. package/dist/index.js +10 -9
  56. package/dist/init-command-NPVL32L6.js +34 -0
  57. package/dist/{onboard-command-EHOHKQQU.js → init-web-3JNFG6GI.js} +10 -10
  58. package/dist/internal.d.ts +4 -4
  59. package/dist/internal.js +86 -73
  60. package/dist/io-P2H75UV2.js +12 -0
  61. package/dist/{live-diff-QK4KC2FJ.js → live-diff-IXKBVG4K.js} +3 -3
  62. package/dist/{lock-GFXD6G2E.js → lock-46FWYE4D.js} +3 -2
  63. package/dist/{lock-commands-ET7NIOLH.js → lock-commands-ZZKZ4LZJ.js} +23 -21
  64. package/dist/{login-command-DFCURZLC.js → login-command-Z6CHTA57.js} +7 -7
  65. package/dist/{logout-command-WXVZQCAV.js → logout-command-ER6IKAYJ.js} +2 -2
  66. package/dist/{loop-7SAIGRCZ.js → loop-D5NPL4VH.js} +3 -3
  67. package/dist/{marketplace-command-BCOAAN2I.js → marketplace-command-P4IPLJ6J.js} +6 -6
  68. package/dist/{meta-client-57ZWVHST.js → meta-client-K2J4XH64.js} +6 -6
  69. package/dist/node.d.ts +5 -4
  70. package/dist/node.js +15 -13
  71. package/dist/{validate-command-SK5FPCV7.js → preflight-command-K346GPTY.js} +20 -18
  72. package/dist/{profile-command-QJAAUZIV.js → profile-command-LJSBDV2L.js} +5 -5
  73. package/dist/{release-command-4EXVLYV2.js → release-command-IMNTIVWK.js} +42 -38
  74. package/dist/response-BQVQ24l1.d.ts +844 -0
  75. package/dist/{routes-manifest-SP3ZXLMR.js → routes-manifest-PWZHDOI5.js} +2 -2
  76. package/dist/scaffold.js +2 -2
  77. package/dist/{static-host-D6KS7X45.js → static-host-3WMV7IZO.js} +1 -1
  78. package/dist/status-command-AL47VG7H.js +162 -0
  79. package/dist/{store-CUCBSYLj.d.ts → store-BLyNeQ8S.d.ts} +21 -8
  80. package/dist/{test-command-2F5LKMI6.js → test-command-YAZLKLGQ.js} +45 -28
  81. package/dist/{upgrade-command-QL523I62.js → upgrade-command-BN3EHAOI.js} +16 -14
  82. package/dist/{workspace-K72NP7SX.js → workspace-2COHDBM3.js} +1 -1
  83. package/dist/{workspace-command-HYA6JEKK.js → workspace-command-MNK7Y7MQ.js} +36 -34
  84. package/dist/{workspace-export-AJMGN3CQ.js → workspace-export-DURY5WYL.js} +2 -2
  85. package/dist/{response-CVAE2kMj.d.ts → xdo-BjJj5W_E.d.ts} +1 -837
  86. package/guides/README.md +2 -2
  87. package/guides/cli.md +17 -19
  88. package/guides/codegen.md +21 -5
  89. package/guides/coverage.md +1 -1
  90. package/guides/deploying.md +31 -34
  91. package/guides/environment.md +4 -4
  92. package/guides/object-kinds.md +3 -3
  93. package/guides/project-structure.md +1 -1
  94. package/guides/scaffold.md +25 -2
  95. package/guides/typed-frontend.md +2 -2
  96. package/llms/filters.md +11 -10
  97. package/llms/kinds-agent-mcp.md +1 -1
  98. package/llms/kinds-core.md +2 -2
  99. package/llms/kinds-realtime.md +1 -1
  100. package/llms/lock.md +5 -5
  101. package/llms/statements-data.md +3 -1
  102. package/llms/tests.md +1 -1
  103. package/llms/values.md +1 -1
  104. package/llms-full.txt +65 -63
  105. package/llms.txt +40 -41
  106. package/manifest.json +96 -427
  107. package/package.json +7 -2
  108. package/dist/chunk-3EYUR3TX.js +0 -100
  109. package/dist/chunk-JQJPFUZI.js +0 -118
  110. package/dist/chunk-YNZFIY4J.js +0 -78
  111. package/dist/codegen-command-YSCSCVT5.js +0 -43
  112. package/dist/env-target-4T7AT347.js +0 -16
  113. package/dist/init-command-OVFFW4QS.js +0 -30
  114. package/dist/io-M7XZEMK7.js +0 -11
  115. package/dist/sandbox-details-command-EDO56IY3.js +0 -18
  116. package/dist/sandbox-export-command-MO6DEZ7Z.js +0 -24
package/guides/README.md CHANGED
@@ -11,10 +11,10 @@ typed, so your editor's autocomplete is usually the faster lookup — tab-comple
11
11
  | [Object kinds](object-kinds.md) | Every authorable kind, and splitting a workspace across microservices |
12
12
  | [Authoring reference](authoring.md) | Tables and fields, statements, values, inputs, middleware, request history |
13
13
  | [CLI](cli.md) | Every command, shell completion, and what failures look like |
14
- | [Signing in & deploying](deploying.md) | Auth, ephemerals, `--static`, releasing to production, `xanots validate` |
14
+ | [Signing in & deploying](deploying.md) | Auth, ephemerals, `--static`, releasing to production, `xanots preflight` |
15
15
  | [Environment & identity](environment.md) | Every environment variable, and how `xano.lock` pins identity |
16
16
  | [The typed frontend surface](typed-frontend.md) | Path resolution, input/response inference, bundle cost, the route manifest |
17
- | [Pulling an existing workspace](codegen.md) | What `xanots codegen` writes, how faithful it is, and how to read its report |
17
+ | [Pulling an existing workspace](codegen.md) | What `xanots init --from` writes, how faithful it is, and how to read its report |
18
18
  | [Coverage & agent grounding](coverage.md) | What's covered, what's out of scope, and the files agents read |
19
19
 
20
20
  ## The agent-facing surface
package/guides/cli.md CHANGED
@@ -24,14 +24,13 @@ xanots export ./xano/index.ts --lock # opt into xano.lock (created beside
24
24
  xanots export ./xano/index.ts --frozen-lock # CI guard: fail if the export would change the lock
25
25
  xanots lock rename --entry=xano/index.ts table users members # move a lock entry after renaming in code
26
26
  xanots lock prune ./xano/index.ts --yes # drop lock entries nothing exports anymore
27
- xanots lock prune --no-verify --entry=xano/index.ts --yes dbo:notes # …or drop named keys without running the workspace
28
- xanots lock adopt live-export.json --entry=xano/index.ts --yes # seed the lock from a live engine export
27
+ xanots lock prune --identity-only --entry=xano/index.ts --yes dbo:notes # …or drop named keys without running the workspace
28
+ xanots lock import live-export.json --entry=xano/index.ts --yes # seed the lock from a live engine export
29
29
 
30
30
  xanots login # OAuth sign-in (once) — pick the instance + workspace at consent
31
31
  xanots login --paste # …when the browser can't reach this machine's 127.0.0.1 (remote shell, container, Codespace)
32
32
  xanots workspace details # which instance/workspace am I bound to, and via which credential?
33
33
  xanots deploy ./xano/index.ts # compile + import into a live ephemeral (the dev loop) → URL
34
- xanots deploy ./xano/index.ts --dest sandbox # …or your throwaway singleton sandbox
35
34
  xanots deploy ./xano/index.ts --static ./frontend/dist # also deploy a static frontend (onto the ephemeral)
36
35
  xanots deploy ./xano/index.ts --static ./frontend/dist --static-env PK=pk_live_1 # + extra public config
37
36
  xanots deploy --bundle ws.json # deploy an already-exported bundle
@@ -43,33 +42,32 @@ xanots ephemeral impersonate <tenant> # open it in the builder (--guest = r
43
42
  xanots release ./xano/index.ts --dry-run # preview what promoting to your main workspace would change
44
43
  xanots release ./xano/index.ts # promote: add + update, never delete, never touch table data
45
44
  xanots release ./xano/index.ts --prune # …also delete objects this project released and no longer defines (needs xano.lock)
46
- xanots sandbox export # export the DEPLOYED sandbox workspace as a JSON bundle → ./sandbox.json
47
- xanots sandbox export --format multidoc --name backend # …or the deployed sandbox as XanoScript → backend.xs
48
- xanots sandbox export --format multidoc --path - # …stream the multidoc to stdout (deploy first)
49
- xanots sandbox details # print the sandbox base URL + tenant details (pretty on a TTY, JSON when piped)
45
+ xanots ephemeral export <tenant> # export a DEPLOYED ephemeral as a JSON bundle → ./<tenant>.json
46
+ xanots ephemeral export <tenant> --format multidoc --name backend # …or as XanoScript → backend.xs
47
+ xanots ephemeral export <tenant> --format multidoc --path - # …stream the multidoc to stdout (deploy first)
50
48
 
51
49
  xanots workspace details # which workspace your token is scoped to (instance, id, name, guid)
52
50
  xanots workspace export --path ws.json # your REAL workspace as a JSON bundle (`--path -` streams to stdout)
53
- xanots workspace codegen my-app # …or as a runnable project (the pull direction)
54
- xanots sandbox codegen my-app # same, from your sandbox
55
- xanots ephemeral codegen <tenant> my-app # same, from an ephemeral (tenant first, path second)
56
- xanots codegen ws.json my-app # …or from a bundle already on disk (offline, no auth)
51
+ xanots init my-app --from workspace # …or as a runnable project (the pull direction)
52
+ xanots init my-app --from ephemeral:<tenant> # same, from an ephemeral (the env is named inside the flag)
53
+ xanots init my-app --from ./ws.json # …or from a bundle already on disk (offline, no auth)
57
54
 
58
- xanots profile me # print the scoped user + instance base URL (pretty on a TTY, JSON when piped)
59
- xanots whoami # alias for `profile me`
55
+ xanots status # who am I, which workspace, and the env this project last deployed to
56
+ xanots whoami # print the scoped user + instance base URL (pretty on a TTY, JSON when piped)
57
+ xanots profile me # alias for `whoami`
60
58
  xanots logout # revoke the refresh token + clear the shared cache (--local for the project one)
61
59
  xanots version # print the installed @xanots/sdk version
62
60
  xanots upgrade --check # is a newer @xanots/sdk published? exits 7 if so, 0 if current
63
61
  xanots upgrade # install it (-g or -D, matching how this CLI is installed)
64
62
  xanots help # grouped command reference (also the no-arg default)
65
63
  xanots <command> --help # that command's usage, subcommands, and flags (`xanots deploy --help`)
66
- xanots <noun> <verb> --help # scoped to one verb (`xanots workspace codegen --help`)
64
+ xanots <noun> <verb> --help # scoped to one verb (`xanots workspace export --help`)
67
65
  xanots <command> --json # force JSON on stdout (otherwise: whenever stdout isn't a terminal)
68
66
  xanots completion zsh # shell completion script (also bash, fish) — see below
69
67
 
70
- xanots validate ./xano/index.ts # import into a live instance, diff each object back
71
- xanots validate ./xano/index.ts --runtime # also run each deployed function on the engine
72
- xanots validate ./xano/index.ts --capture # write the fetched JSON as fixture candidates
68
+ xanots preflight ./xano/index.ts # import into a live instance, diff each object back (needs auth + a throwaway tenant)
69
+ xanots preflight ./xano/index.ts --runtime # also run each deployed function on the engine
70
+ xanots preflight ./xano/index.ts --capture # write the fetched JSON as fixture candidates
73
71
  ```
74
72
 
75
73
  ## Bundle key names vs. the names everything else uses
@@ -97,7 +95,7 @@ correctly looks *missing* if you go looking for `payload.table`. And `agent` and
97
95
  both land in `payload.toolset`, while the kind actually named `toolset` lands in
98
96
  `payload.tool`; matching on the word alone will pick the wrong array.
99
97
 
100
- This is also the format `xanots lock adopt` reads, so both directions are user-facing.
98
+ This is also the format `xanots lock import` reads, so both directions are user-facing.
101
99
 
102
100
 
103
101
  **Build warnings, and `--strict`.** `export`/`deploy` print a `xanots:` warning for the
@@ -113,7 +111,7 @@ The programmatic equivalents are `emitBundle(app, { strict: true })` and
113
111
  ## Shell completion
114
112
 
115
113
  `xanots completion <bash|zsh|fish>` prints a completion script covering every command, verb, flag,
116
- and closed value set (`--dest ephemeral|sandbox`, `--format json|multidoc`, `--ai claude|codex|cursor|none`).
114
+ and closed value set (`--env ephemeral|workspace`, `--format json|multidoc`, `--ai claude|codex|cursor|none`).
117
115
  It is generated from the CLI's own command table, so it never drifts from what the CLI accepts — but it
118
116
  is baked at generation time, so re-run it after upgrading.
119
117
 
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
@@ -94,7 +94,7 @@ on the query and every caller derives from that single source of truth:
94
94
 
95
95
  ```ts
96
96
  const getPost = query({
97
- verb: "GET", apiGroup: blog, name: "get_post",
97
+ verb: "GET", apiGroup: blog, name: "get_post/{id}", // one row → address it in the path
98
98
  input: { id: input.int({ required: true }) },
99
99
  stack: [s.db.query({ table: post, where: expr(col("id"), "=", inp("id")), as: "rows" })],
100
100
  // A filtered response is opaque to the static walk, so derivation is `unknown`.
@@ -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
package/llms/filters.md CHANGED
@@ -39,12 +39,13 @@ database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHER
39
39
  which the DB applies atomically. Reserve the pipeline form for low-contention counters
40
40
  where a rare lost update is acceptable.
41
41
  ⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
42
- does NOT expose: the engine derives a physical name from workspace + table ids (of the
43
- form `x<workspace_id>_<table_id>`, e.g. `x6_203970`), ids assigned at importnot knowable
44
- from a `table()` def (identity is a name + guid, not the numeric id), and `sql_name`
45
- persists empty. So the safe counter drops out of the typed surface: hardcode
46
- the physical name after inspecting the deployed table. A typed atomic path needs an
47
- engine change.
42
+ does NOT expose: the engine derives it from ids assigned at import not knowable from a
43
+ `table()` def (identity is a name + guid, not the numeric id)and `sql_name` persists
44
+ empty. The derived name is also NOT STABLE: a deploy is a full replace, so every table is
45
+ created afresh and the id in its name moves each time, on the same unchanged project. So
46
+ the safe counter drops out of the typed surface: resolve the physical name from
47
+ `information_schema` inside the request that uses it, and never store, cache or hardcode
48
+ one. A typed atomic path needs an engine change.
48
49
 
49
50
  - `fl.add(value: decimal): decimal`
50
51
  - `fl.append(value: <T>, path: text): <T>[]`
@@ -136,11 +137,11 @@ engine change.
136
137
  - `fl.prepend(value: <T>, path: text): <T>[]`
137
138
  - `fl.range(start: int, stop: int): int[]`
138
139
  - `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
139
- - `fl.regex_match(subject: text): text[]`
140
- - `fl.regex_match_all(subject: text): text[]`
140
+ - `fl.regex_match(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
141
+ - `fl.regex_match_all(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
141
142
  - `fl.regex_quote(delimiter?: text): text`
142
- - `fl.regex_replace(replacement: text, subject: text): text`
143
- - `fl.regex_test(subject: text): bool`
143
+ - `fl.regex_replace(replacement: text, subject: text): text` — piped value is the PATTERN, `subject` is the text searched — see `regex_test`. The replacement comes FIRST
144
+ - `fl.regex_test(subject: text): bool` — piped value is the PATTERN (build it with `c.regex(...)`); the arg is the subject — the REVERSE of `contains`/`starts_with`. Swapped, it answers false for every input with no error, so write `withFilters(c.regex("^a+$"), fl.regex_test(inp("s")))` (or name the arg: `fl.regex_test({ subject: inp("s") })`). A pattern found in the subject slot is refused at build time
144
145
  - `fl.round(precision?: int): decimal`
145
146
  - `fl.rtrim(mask?: text): text`
146
147
  - `fl.secureid_decode(salt: text): int`
@@ -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.
@@ -13,7 +13,7 @@ to survive a rename).
13
13
  - `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
14
14
  - `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
15
15
  - `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
16
- - `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
16
+ - `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param. Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
17
17
  - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
18
18
  - `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
19
19
  - `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
@@ -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
@@ -12,7 +12,7 @@ primary key `id`):
12
12
 
13
13
  - `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` — one row by field match; `output` restricts returned columns (and overrides column visibility — it can pull `internal` columns like a password hash).
14
14
  - `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` for an id that names no row. Both spellings are live in pulled workspaces.
15
- - ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`.
15
+ - ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`. `export()` warns when the `id` is statically a `0` — the literal `c.int(0)`, an `inp()` whose declared input default is `0`, or a `ref()` to a column declared `default: 0` — and `--strict` fails the build.
16
16
  - `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
17
17
  - `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
18
18
  - `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
@@ -54,6 +54,8 @@ primary key `id`):
54
54
  - `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
55
55
  - `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
56
56
  - `s.db.direct_query({ sql, responseType?, args?, parser?, as? })` — `sql` is a **raw string** (not a `Value`); binds go in `args: Value[]`. `parser: "template_engine"` renders the body as a template first — how a query interpolates a column or table name a bound arg cannot carry; omit it for the default.
57
+ - Template placeholders are Twig over the request scope: `{{ $input.name }}` for an input, `{{ $var.name }}` for a stack variable. ⚠ A BARE `{{ name }}` renders as the empty string — HTTP 200, no error, a query that silently ran with a blank where the value belonged. A bound `?` arg carries a VALUE without the template at all.
58
+ - ⚠ A table's PHYSICAL name is **not stable across deploys**. A deploy is a full replace, so every table is created afresh and the id in its name moves every time — the same unchanged project redeployed three times gave one table three different names. Never store, cache, hardcode or fixture one: resolve it from `information_schema` inside the same request that uses it.
57
59
  - `s.db.external.<engine>.direct_query({ sql, connectionString, responseType?, args?, parser?, as? })` — same shape against an EXTERNAL database; `<engine>` is `postgres`/`mysql`/`mssql`/`oracle`/`snowflake`. `connectionString` is a `Value` — reach for `env(...)`, not a literal — stored as `context.connection_string_flex`. A bare string stores the older `context.connection_string` instead (an env-var name unless it looks like a URL); each form round-trips as itself.
58
60
  - `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
59
61
  - `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` — `items` is an array `Value`.