@xanots/sdk 0.0.8 → 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.
- package/CHANGELOG.md +29 -0
- package/README.md +28 -21
- package/dist/.build-fingerprint +1 -1
- package/dist/{agent-file-refresh-4W37DGXS.js → agent-file-refresh-QNKN5RYD.js} +3 -3
- package/dist/bin.js +8 -7
- package/dist/{branch-commands-PBQ36L4A.js → branch-commands-2BLOC2GR.js} +13 -11
- package/dist/{capture-7PO6SGB4.js → capture-4WVJY4DQ.js} +2 -2
- package/dist/{chunk-ZOYMZZ3S.js → chunk-3INK4Y4E.js} +1 -1
- package/dist/{chunk-NDZFBZHC.js → chunk-3UABJA4X.js} +1 -1
- package/dist/{chunk-7AXQKABT.js → chunk-5R73LFWK.js} +3 -3
- package/dist/{chunk-CDAQUY5W.js → chunk-5YCQ2QHH.js} +2 -2
- package/dist/{chunk-2ROR3AZC.js → chunk-75Z74TA7.js} +2 -2
- package/dist/{chunk-QTNO2WD6.js → chunk-7ZYW652H.js} +1 -1
- package/dist/{chunk-SNO3KHCY.js → chunk-ACCBOMCB.js} +109 -78
- package/dist/{chunk-4PTSTDIG.js → chunk-AIZKXUNP.js} +2 -2
- package/dist/{chunk-P5C2YKAM.js → chunk-ANUDXFEX.js} +18 -27
- package/dist/chunk-CTD5ZCV6.js +28 -0
- package/dist/{chunk-5HCDZ2XN.js → chunk-DBFU47BJ.js} +2 -2
- package/dist/{chunk-OKNSR7MT.js → chunk-DIA7CT7J.js} +129 -150
- package/dist/{chunk-JQLR64UC.js → chunk-F6CYJ7TN.js} +21 -6
- package/dist/{chunk-BKDJN76G.js → chunk-FE5I6S6N.js} +1 -1
- package/dist/{chunk-EM5QPTXP.js → chunk-I7DQDJAM.js} +30 -48
- package/dist/{chunk-QIWBCS7N.js → chunk-JGCWTCA7.js} +2 -2
- package/dist/{chunk-MDXR5E5Q.js → chunk-K5IOND4K.js} +2 -2
- package/dist/{chunk-4PE4ZAB7.js → chunk-KA6G2L7U.js} +5 -5
- package/dist/{chunk-64QK6JEK.js → chunk-LBYWGMOA.js} +36 -2
- package/dist/{chunk-3MDKV2VA.js → chunk-P6TAVLOX.js} +2 -2
- package/dist/{chunk-3QW5NFZJ.js → chunk-QKM4U5UK.js} +3 -3
- package/dist/{chunk-T4XPCJRF.js → chunk-TCFIPDB3.js} +1 -1
- package/dist/{chunk-FEQTQ6PM.js → chunk-VKSOTZK3.js} +2 -2
- package/dist/{chunk-6DHBYBTO.js → chunk-VNQM3V2C.js} +1 -2
- package/dist/{chunk-PYS7UNNW.js → chunk-W24FJHPD.js} +3 -3
- package/dist/chunk-WGPXT2G2.js +22 -0
- package/dist/{chunk-6AAT2AYQ.js → chunk-WP4OZZV4.js} +2 -2
- package/dist/{chunk-MU3O43L2.js → chunk-YBC3IKMF.js} +2 -2
- package/dist/{chunk-C3M5K4ZH.js → chunk-YYFXVYPX.js} +37 -6
- package/dist/chunk-ZQ2PKR6R.js +40 -0
- package/dist/cli.d.ts +57 -31
- package/dist/cli.js +6 -5
- package/dist/codegen-command-GB3H2KQ7.js +46 -0
- package/dist/{completion-A6BZ3XGU.js → completion-WF46272M.js} +2 -2
- package/dist/{config-NL33PN4D.js → config-476F3PT5.js} +1 -1
- package/dist/{deploy-command-QZ3GNLAD.js → deploy-command-QTIVA22A.js} +68 -100
- package/dist/{ephemeral-command-DL7EY4LL.js → ephemeral-command-U4AQ3TXX.js} +25 -28
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/{init-command-4MG3UKUO.js → init-command-MAXULNAD.js} +13 -10
- package/dist/{onboard-command-EHOHKQQU.js → init-web-3JNFG6GI.js} +10 -10
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +42 -45
- package/dist/{live-diff-QK4KC2FJ.js → live-diff-IXKBVG4K.js} +3 -3
- package/dist/{lock-commands-Y4S4NUCE.js → lock-commands-DQ7CUMIV.js} +20 -19
- package/dist/{login-command-DFCURZLC.js → login-command-Z6CHTA57.js} +7 -7
- package/dist/{logout-command-WXVZQCAV.js → logout-command-ER6IKAYJ.js} +2 -2
- package/dist/{loop-7SAIGRCZ.js → loop-D5NPL4VH.js} +3 -3
- package/dist/{marketplace-command-BCOAAN2I.js → marketplace-command-P4IPLJ6J.js} +6 -6
- package/dist/{meta-client-57ZWVHST.js → meta-client-K2J4XH64.js} +6 -6
- package/dist/node.d.ts +2 -2
- package/dist/node.js +9 -8
- package/dist/{validate-command-GLF5ZOOW.js → preflight-command-GZ2GE5RN.js} +17 -16
- package/dist/{profile-command-QJAAUZIV.js → profile-command-LJSBDV2L.js} +5 -5
- package/dist/{release-command-DN75G5GJ.js → release-command-YUBTNHVX.js} +36 -33
- package/dist/{routes-manifest-SP3ZXLMR.js → routes-manifest-PWZHDOI5.js} +2 -2
- package/dist/scaffold.js +2 -2
- package/dist/{static-host-D6KS7X45.js → static-host-3WMV7IZO.js} +1 -1
- package/dist/status-command-AL47VG7H.js +162 -0
- package/dist/{store-CUCBSYLj.d.ts → store-BG1UPZ3Z.d.ts} +5 -5
- package/dist/{test-command-RRNNJ45I.js → test-command-YAZLKLGQ.js} +46 -29
- package/dist/{upgrade-command-XFOAWDI3.js → upgrade-command-FB5QJ363.js} +13 -12
- package/dist/{workspace-K72NP7SX.js → workspace-2COHDBM3.js} +1 -1
- package/dist/{workspace-command-QCFELEGR.js → workspace-command-43P42FBP.js} +31 -32
- package/dist/{workspace-export-AJMGN3CQ.js → workspace-export-DURY5WYL.js} +2 -2
- package/guides/README.md +2 -2
- package/guides/authoring.md +9 -1
- package/guides/cli.md +17 -19
- package/guides/codegen.md +21 -5
- package/guides/coverage.md +1 -1
- package/guides/deploying.md +31 -34
- package/guides/environment.md +4 -4
- package/guides/object-kinds.md +3 -3
- package/guides/project-structure.md +1 -1
- package/guides/scaffold.md +25 -2
- package/guides/typed-frontend.md +1 -1
- package/llms/fields.md +3 -2
- package/llms/kinds-agent-mcp.md +1 -1
- package/llms/kinds-core.md +1 -1
- package/llms/kinds-realtime.md +1 -1
- package/llms/lock.md +5 -5
- package/llms/statements-calls.md +2 -1
- package/llms/statements-data.md +5 -2
- package/llms/tests.md +3 -2
- package/llms-full.txt +41 -44
- package/llms.txt +20 -29
- package/manifest.json +95 -426
- package/package.json +1 -1
- package/dist/chunk-3EYUR3TX.js +0 -100
- package/dist/chunk-JQJPFUZI.js +0 -118
- package/dist/chunk-XP7S3VWY.js +0 -78
- package/dist/codegen-command-U2W72DLG.js +0 -43
- package/dist/env-target-NAYVEXBH.js +0 -16
- package/dist/sandbox-details-command-EDO56IY3.js +0 -18
- package/dist/sandbox-export-command-MO6DEZ7Z.js +0 -24
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 --
|
|
28
|
-
xanots lock
|
|
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
|
|
47
|
-
xanots
|
|
48
|
-
xanots
|
|
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
|
|
54
|
-
xanots
|
|
55
|
-
xanots
|
|
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
|
|
59
|
-
xanots whoami #
|
|
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
|
|
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
|
|
71
|
-
xanots
|
|
72
|
-
xanots
|
|
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
|
|
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 (`--
|
|
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
|
|
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
|
-
(`--
|
|
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 `
|
|
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
|
|
80
|
-
>
|
|
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
|
---
|
package/guides/coverage.md
CHANGED
|
@@ -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
|
|
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`,
|
package/guides/deploying.md
CHANGED
|
@@ -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`, `
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
101
|
-
|
|
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 **`--
|
|
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
|
|
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
|
|
147
|
-
|
|
148
|
-
(`xanots
|
|
149
|
-
the
|
|
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
|
-
##
|
|
182
|
+
## Checking against a live instance (xanots preflight)
|
|
186
183
|
|
|
187
|
-
`xanots
|
|
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 `
|
|
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
|
|
221
|
-
xanots
|
|
222
|
-
xanots
|
|
223
|
-
xanots
|
|
224
|
-
xanots
|
|
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 **
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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; `--
|
|
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,
|
|
283
|
-
|
|
284
|
-
|
|
279
|
+
Not every instance has ephemeral environments enabled. Where they are off, `deploy` says so
|
|
280
|
+
and names who can turn them on — there 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
|
-
|
|
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
|
|
355
|
+
> before importing. The blast radius is your own disposable ephemeral — but anything
|
package/guides/environment.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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.
|
package/guides/object-kinds.md
CHANGED
|
@@ -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
|
|
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 `--
|
|
315
|
-
`xanots ephemeral get <env>`,
|
|
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
|
|
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.
|
package/guides/scaffold.md
CHANGED
|
@@ -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
|
|
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
|
package/guides/typed-frontend.md
CHANGED
|
@@ -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
|
|
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/fields.md
CHANGED
|
@@ -35,8 +35,9 @@ an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/
|
|
|
35
35
|
`s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather
|
|
36
36
|
than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for
|
|
37
37
|
"not set yet" — `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and
|
|
38
|
-
binds `null`, which is the answer the null was reaching for
|
|
39
|
-
`
|
|
38
|
+
binds `null`, which is the answer the null was reaching for — and never `s.db.get_by_id`,
|
|
39
|
+
which validates `id >= 1` and fails the whole request on the sentinel. `export()` warns on a
|
|
40
|
+
literal `c.null()` in that slot.
|
|
40
41
|
An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through
|
|
41
42
|
any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:
|
|
42
43
|
"vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see
|
package/llms/kinds-agent-mcp.md
CHANGED
|
@@ -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
|
|
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.
|
package/llms/kinds-core.md
CHANGED
|
@@ -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
|
|
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.
|
package/llms/kinds-realtime.md
CHANGED
|
@@ -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 `
|
|
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/
|
|
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); `--
|
|
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
|
|
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`/`
|
|
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
|
|
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/statements-calls.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auth, cross-object calls, and microservices
|
|
2
2
|
|
|
3
|
-
> Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
|
|
3
|
+
> Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
|
|
4
4
|
|
|
5
5
|
Auth & calls:
|
|
6
6
|
|
|
@@ -16,6 +16,7 @@ Auth & calls:
|
|
|
16
16
|
- `s.api.call({ api, input?, headers?, auth?, as? })` — invoke an endpoint. `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) — a bare name is refused, because a query's identity is composed from its api group, verb, and name. `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s — a `{ "Name": value }` record (values may be tagged), a `string[]` of `"Name: value"` lines, or one `Value`. `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING — a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it. Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`) — elsewhere it deploys clean and 500s the first real request.
|
|
17
17
|
- `s.api.request({ url, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` — external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`: `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH), `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter — the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (→ query string for GET/HEAD/OPTIONS, body otherwise), `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines — prefer a header over a `?key=` query param for a credential — a URL travels into access logs, proxies and `Referer`. ⚠ Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request — read `response.result`. A NAME outside the header-token charset is refused, and a LITERAL value carrying a newline is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs lead the emitted array and computed ones follow, so the wire order is not the record's key order, `timeout` a `number` in seconds (1–86400), and `follow_location`/`verify_host`/`verify_peer` booleans. `description` (Settings tab) and `output` filters (Output tab) ride the envelope. SSL cert interdependencies (certificate↔private_key, ca_certificate→verify_peer) are checked at build time when statically provable, else by the engine at runtime. The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.
|
|
18
18
|
- `s.stream.from_request({ url, method?, …tls, as? })` — streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope). `url` is REQUIRED — it shares `api.request`'s engine declaration, which has no default.
|
|
19
|
+
- `s.util.send_email({ to, subject, message, from?, cc?, bcc?, reply_to?, service_provider?, api_key?, scheduled_at?, as? })` — send email from the stack. `service_provider` is `"xano"` (the built-in mailer — needs NO `api_key` and no configuration, and does not require a verified sender) or `"resend"` (pass the key as `api_key: env("RESEND_API_KEY")`). Prefer this over hand-rolling `s.api.request` against a mail provider.
|
|
19
20
|
- `s.webflow.request({ path, method?, …tls, as? })` — Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied), and `path` is REQUIRED — the engine rejects an empty one. No `headers`: the engine builds its own from the workspace's Webflow connection and ignores an authored value.
|
|
20
21
|
- `s.task.call` / `s.tool.call` / `s.trigger.call` / `s.middleware.call` / `s.addon.call` — same `{ <target>, input?, as? }` shape against the named kind. `s.task.call` and `s.trigger.call` are WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`); `s.tool.call`, `s.middleware.call` and `s.addon.call` run from any stack.
|
|
21
22
|
- `s.action.call({ actionId, input?, registry?, as? })` / `s.action.package.call({ traceId, versionId, slug, input?, registry?, as? })` — invoke an installed action. Ids are SUPPLIED, never derived from a name: an action is installed onto the instance, so its identity is assigned at install and differs per instance — read them off a call in a pulled workspace. The package form needs all three parts; they are one composite and two of them address nothing. `registry` is the action's own settings, `input` the per-call arguments.
|
package/llms/statements-data.md
CHANGED
|
@@ -11,7 +11,8 @@ DB reads/writes (`table` is a def handle or name; `fieldName` defaults to the
|
|
|
11
11
|
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
|
-
- `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`
|
|
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
16
|
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
16
17
|
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
17
18
|
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
@@ -19,8 +20,9 @@ primary key `id`):
|
|
|
19
20
|
- `null` is accepted on EVERY column, including ones that refuse every other literal, and encodes `const:null` — a write OF null, not the same as omitting the key. A column's `nullable` is not consulted at encode; the engine refuses a null it forbids.
|
|
20
21
|
- Omitting a key on `add` writes the column's type default — declared `default` if set, `[]` for a list, `{}` for obj/json, else `null`. That `null` is EMITTED, not what the row holds: the engine applies the column's nullability, so a `nullable` column keeps `null` and a non-nullable one lands on its type's zero value (`""`, `0`, `false`). Set the cell when the stored value matters. On `edit` an omitted key keeps its stored value.
|
|
21
22
|
- An `f.password()` cell takes the PLAINTEXT — the column hashes on write, so a pre-hashed value, or a hashing filter on the cell, stores a hash of a hash that `security.check_password` can never match.
|
|
23
|
+
- A `table({ seed })` cell hashes the same way: the import writes the plaintext through the column's own rules, so a seeded credential matches under `security.check_password` exactly as an added one does. Demo accounts work as fixtures — the usual caution about seed data applies, since the plaintext sits in the repo.
|
|
22
24
|
- `s.db.edit({ table, fieldName?, fieldValue, row?, data?, output?, as? })` — update by field match.
|
|
23
|
-
- `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial
|
|
25
|
+
- `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial. ⚠ Unlike `db.edit`'s `row`, `data` is a single object `Value` — write `obj({ unread: c.int(0) })`, not the column-keyed record `row` takes.
|
|
24
26
|
On these three, `output` restricts the columns of the RETURNED row only — it does not change
|
|
25
27
|
what is written. Not offered on `db.del`/`db.has` (their result is a scalar) or on
|
|
26
28
|
`db.add_or_edit` (no output envelope).
|
|
@@ -29,6 +31,7 @@ primary key `id`):
|
|
|
29
31
|
- `where` / `additionalWhere` — `expr(...)`, an `expr[]` (ANDed), or a raw `Value`. Rides `context.search`.
|
|
30
32
|
- ⚠ `ignoreEmpty` DROPS the predicate when the operand is empty — it does not match zero rows. On an `in` comparison an empty list therefore returns the UNFILTERED set, so never use it to scope rows to a permitted-id list: an empty list of permissions returns everything.
|
|
31
33
|
- For the full operator set use `cmp(left, op, right, { ignoreEmpty? })` — `op`: `in`/`not in`/`like`/`ilike`/`between`/`contains`/`includes`/`overlaps`/`@>`/`~`/`search`/… plus the `expr` comparisons. Database-only — a runtime condition takes the `expr` set only.
|
|
34
|
+
- ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
|
|
32
35
|
- Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
|
|
33
36
|
- An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
|
|
34
37
|
- ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
|
package/llms/tests.md
CHANGED
|
@@ -42,7 +42,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
42
42
|
|
|
43
43
|
- The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
|
|
44
44
|
- `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
|
|
45
|
-
- `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is text the raised message must CONTAIN; omit it to accept any error.
|
|
45
|
+
- `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
|
|
46
46
|
- `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` — which reads as a broken auth gate on a gate that works.
|
|
47
47
|
- An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.
|
|
48
48
|
- Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.
|
|
@@ -51,8 +51,9 @@ 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
|
-
- `--
|
|
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.
|
|
58
|
+
- For CI, the exit code says THAT something failed and the JSON says WHICH. Every progress line goes to stderr and stdout carries one JSON document — emitted whenever stdout is not a terminal, or on demand with `--json`. `run-all` and `run`: `{ dest, env, total, passed, failed, tests: [{ kind, name, object?, status: "pass"|"fail", message?, timing? }] }`, with the same keys on an empty suite. `list` is `{ dest, env, total, tests: [...] }` and `deploy --test` nests the run under `testRun`. The per-test array is always `tests`.
|
|
58
59
|
- `xanots deploy ./index.ts --test` deploys and then runs the suite against what it just shipped. A failure exits 5 WITHOUT retracting the deploy — the environment is live either way.
|