@xano/cli 1.0.6-beta.0 → 1.0.6-beta.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/README.md +124 -7
- package/dist/base-command.d.ts +2 -0
- package/dist/base-command.js +10 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
- package/dist/commands/ephemeral/impersonate/index.js +11 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +3 -7
- package/dist/commands/ephemeral/pull/index.js +25 -109
- package/dist/commands/ephemeral/push/index.d.ts +13 -1
- package/dist/commands/ephemeral/push/index.js +94 -16
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
- package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/create/index.js +92 -0
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
- package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/get/index.js +101 -0
- package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
- package/dist/commands/ephemeral/static_host/list/index.js +132 -0
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -95
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +68 -40
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +67 -36
- package/dist/commands/workspace/git/pull/index.js +53 -3
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -95
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +274 -1
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +12 -0
- package/dist/utils/multidoc-push.js +84 -0
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +5038 -3635
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -39,6 +39,38 @@ Destructive commands include imperative safety prefixes in their help text and f
|
|
|
39
39
|
|
|
40
40
|
These warnings are layer 1 of broader push-safety work; ephemeral sandbox environments and push preview remain the structural safeguards.
|
|
41
41
|
|
|
42
|
+
### Paging on list commands
|
|
43
|
+
|
|
44
|
+
The Metadata API pages unevenly, so `list` commands differ in what they expose. Every command reports only what its endpoint actually returns — the CLI never guesses whether more results exist.
|
|
45
|
+
|
|
46
|
+
| Commands | Flags | Footer |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `function list`, `unit_test list`, `workflow_test list` (plus `sandbox` / `tenant` variants) | `--page`, `--per_page` | `Page 2 · 50 shown · next: --page 3` |
|
|
49
|
+
| `static_host list`, `static_host build list` (plus `ephemeral` variants) | `--page` only | `Page 2 · 100 shown · 340 total` |
|
|
50
|
+
| `branch list`, `workspace list`, `tenant snapshot list`, `sandbox env list`, `tenant env list` | none | `12 branches` |
|
|
51
|
+
| `tenant list`, `release list`, `platform list`, `tenant cluster list`, `ephemeral list` | none | none |
|
|
52
|
+
| `tenant backup list` | `--page` only (pre-existing) | none |
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
|
|
56
|
+
- **`--per_page` is only offered where the endpoint accepts it.** Static-host endpoints hardcode 100 items per page server-side, so those commands take `--page` alone. `xano static_host list --per_page 10` previously parsed but did nothing; it is now rejected rather than silently ignored.
|
|
57
|
+
- **All paged commands default `--per_page` to 50.** The test-list commands previously requested 10000 internally so nothing was ever cut off; now that the footer and the JSON envelope both report position, a page that stops at 50 is visible rather than silent. Raise it (up to 10000) when you want everything in one call.
|
|
58
|
+
- **The last group has no CLI paging.** Those endpoints page server-side but return a plain array with no page or total metadata, so the CLI cannot tell you where you are. Rather than show a page number with no context — or infer "more available" from a full page, which is wrong exactly when the result count is a multiple of the page size — these commands are left as-is. **They return at most 25–50 items** (50 for tenants and ephemerals, 25 for releases, platforms, and tenant backups). If you have more than that, query the Metadata API directly until those endpoints return paging metadata.
|
|
59
|
+
- **`--output json` returns an envelope, not a bare array.** Every list command emits `{items, ...}` using the Metadata API's own field names — `curPage`, `nextPage`, `prevPage`, `itemsTotal` — each included only when the endpoint actually reports it. `perPage` is the one field the CLI adds, since the API never echoes it back and it is otherwise invisible at its default. This is a **breaking change** for scripts that parsed the previous bare array — read `.items` instead.
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
// xano function list -o json
|
|
63
|
+
{
|
|
64
|
+
"curPage": 2,
|
|
65
|
+
"perPage": 50, // injected by the CLI; everything else mirrors the API
|
|
66
|
+
"nextPage": 3, // absent on the last page — never inferred
|
|
67
|
+
"prevPage": 1,
|
|
68
|
+
"items": [ /* ... */ ]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The point of the envelope is that a script gets the same honest stop condition a human gets from the footer, in the same shape the raw Metadata API returns. Without it the only signal available is `items.length < perPage`, which is wrong exactly when the result count is a multiple of the page size. Absence of `nextPage` means the server said there is no next page; it is never omitted as a guess.
|
|
73
|
+
|
|
42
74
|
### Authentication
|
|
43
75
|
|
|
44
76
|
```bash
|
|
@@ -197,6 +229,35 @@ xano workspace git pull -r https://github.com/owner/private-repo -t ghp_xxx
|
|
|
197
229
|
xano workspace git pull -r https://github.com/owner/repo --path subdir
|
|
198
230
|
```
|
|
199
231
|
|
|
232
|
+
### Knowledge
|
|
233
|
+
|
|
234
|
+
Knowledge items are user-authored docs and skills (e.g. `CLAUDE.md`, `AGENTS.md`, runbooks)
|
|
235
|
+
attached to a workspace. Each knowledge item's **name is a path** (e.g. `some/thing/CLAUDE.md`):
|
|
236
|
+
`pull` writes its content to that path under the output directory, and `push` turns each local
|
|
237
|
+
file into a knowledge item named by its relative path.
|
|
238
|
+
|
|
239
|
+
Push matches local files to remote items by name. Existing items are updated (content only —
|
|
240
|
+
description, mode, tags, and other metadata are preserved); new files are created. The
|
|
241
|
+
knowledge type for new items is inferred from the filename: `AGENTS.md` → `agents.md`,
|
|
242
|
+
`SKILL.md` → `skill`, everything else → `doc`. Hidden files (dotfiles) and `node_modules`
|
|
243
|
+
are skipped.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# Pull knowledge files to local paths (defaults to current directory)
|
|
247
|
+
xano knowledge pull
|
|
248
|
+
xano knowledge pull -d ./knowledge # Specify output directory
|
|
249
|
+
xano knowledge pull -b dev # Specific branch
|
|
250
|
+
|
|
251
|
+
# Push local files as knowledge (defaults to current directory, only changed files)
|
|
252
|
+
xano knowledge push
|
|
253
|
+
xano knowledge push -d ./knowledge # Push from a specific directory
|
|
254
|
+
xano knowledge push --dry-run # Preview changes without pushing
|
|
255
|
+
xano knowledge push --sync --delete # Full push + delete remote knowledge not included
|
|
256
|
+
xano knowledge push --force # Skip preview and confirmation (for CI/CD)
|
|
257
|
+
xano knowledge push -i "guides/*" # Push only matching files
|
|
258
|
+
xano knowledge push -e "**/README.md" # Push all files except READMEs
|
|
259
|
+
```
|
|
260
|
+
|
|
200
261
|
### Branches
|
|
201
262
|
|
|
202
263
|
All branch commands use **branch labels** (e.g., `v1`, `dev`), not IDs.
|
|
@@ -372,9 +433,10 @@ xano platform get <platform_id>
|
|
|
372
433
|
#### Unit Tests
|
|
373
434
|
|
|
374
435
|
```bash
|
|
375
|
-
# List unit tests
|
|
436
|
+
# List unit tests (returns all tests by default; --per_page narrows the page)
|
|
376
437
|
xano unit_test list
|
|
377
438
|
xano unit_test list --branch dev --obj-type function
|
|
439
|
+
xano unit_test list --page 2 --per_page 25
|
|
378
440
|
|
|
379
441
|
# Run a single unit test
|
|
380
442
|
xano unit_test run <unit_test_id>
|
|
@@ -388,9 +450,10 @@ xano unit_test run_all --branch dev --obj-type function
|
|
|
388
450
|
#### Workflow Tests
|
|
389
451
|
|
|
390
452
|
```bash
|
|
391
|
-
# List workflow tests
|
|
453
|
+
# List workflow tests (returns all tests by default; --per_page narrows the page)
|
|
392
454
|
xano workflow_test list
|
|
393
455
|
xano workflow_test list --branch dev
|
|
456
|
+
xano workflow_test list --page 2 --per_page 25
|
|
394
457
|
|
|
395
458
|
# Get workflow test details
|
|
396
459
|
xano workflow_test get <workflow_test_id>
|
|
@@ -404,10 +467,35 @@ xano workflow_test run <workflow_test_id>
|
|
|
404
467
|
xano workflow_test run_all
|
|
405
468
|
xano workflow_test run_all --branch dev
|
|
406
469
|
|
|
470
|
+
# Run tests in parallel (default is 1 — sequential)
|
|
471
|
+
xano unit_test run_all --concurrency 4
|
|
472
|
+
xano workflow_test run_all --concurrency 4
|
|
473
|
+
|
|
474
|
+
# Sandbox and tenant variants take the same paging flags
|
|
475
|
+
xano sandbox unit_test list --page 2 --per_page 25
|
|
476
|
+
xano sandbox workflow_test list --page 2 --per_page 25
|
|
477
|
+
xano tenant unit_test list my-tenant --page 2 --per_page 25
|
|
478
|
+
xano tenant workflow_test list my-tenant --page 2 --per_page 25
|
|
479
|
+
|
|
407
480
|
# Delete a workflow test
|
|
408
481
|
xano workflow_test delete <workflow_test_id>
|
|
409
482
|
```
|
|
410
483
|
|
|
484
|
+
#### Running tests in parallel
|
|
485
|
+
|
|
486
|
+
Every `run_all` command takes `--concurrency` (default `1`):
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
xano unit_test run_all --concurrency 4
|
|
490
|
+
xano tenant unit_test run_all my-tenant --concurrency 8
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Output stays in test order regardless of concurrency, so a parallel run is still diffable against a sequential one, and the JSON `results` array keeps a stable order.
|
|
494
|
+
|
|
495
|
+
`run_all` discovers the test list by paging at 100 per request rather than asking for the whole suite in one response, and stops when the API reports no next page.
|
|
496
|
+
|
|
497
|
+
**The default is 1 on purpose.** These tests execute against a shared workspace database, so tests that touch the same tables or records can interfere with each other when run at the same time — producing failures that look like flakiness rather than a real regression. Raise `--concurrency` once you know your tests are independent.
|
|
498
|
+
|
|
411
499
|
### Tenants
|
|
412
500
|
|
|
413
501
|
Manage tenants, their environment variables, backups, deployments, and clusters.
|
|
@@ -587,15 +675,41 @@ xano ephemeral edit <tenant_name> --display "New Name" -d "New description"
|
|
|
587
675
|
xano ephemeral delete <tenant_name> --force
|
|
588
676
|
|
|
589
677
|
# Pull to / push from local files (multidoc)
|
|
590
|
-
xano ephemeral pull
|
|
591
|
-
xano ephemeral push
|
|
592
|
-
xano ephemeral push
|
|
678
|
+
xano ephemeral pull <tenant_name> -d ./my-ephemeral
|
|
679
|
+
xano ephemeral push <tenant_name> -d ./my-ephemeral --dry-run # preview first
|
|
680
|
+
xano ephemeral push <tenant_name> -d ./my-ephemeral
|
|
593
681
|
|
|
594
682
|
# Open in the browser (or print the URL)
|
|
595
683
|
xano ephemeral impersonate <tenant_name>
|
|
596
684
|
xano ephemeral impersonate <tenant_name> --url-only
|
|
685
|
+
xano ephemeral impersonate <tenant_name> --guest # read-only session (browse only)
|
|
597
686
|
```
|
|
598
687
|
|
|
688
|
+
#### Static hosting for an ephemeral tenant
|
|
689
|
+
|
|
690
|
+
An ephemeral tenant can host static sites, scoped to that tenant. These commands mirror
|
|
691
|
+
`xano static_host *` but take the tenant name as the first argument (the tenant's static
|
|
692
|
+
hosting is isolated inside the tenant's own database).
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
# List / create / inspect a tenant's static hosts
|
|
696
|
+
xano ephemeral static_host list <tenant_name>
|
|
697
|
+
xano ephemeral static_host create <tenant_name> --name marketing --description "Marketing site"
|
|
698
|
+
xano ephemeral static_host get <tenant_name> -H marketing
|
|
699
|
+
xano ephemeral static_host edit <tenant_name> -H marketing --description "Updated"
|
|
700
|
+
|
|
701
|
+
# Builds: push a directory, list, inspect, deploy to an env, pull, delete
|
|
702
|
+
xano ephemeral static_host build push <tenant_name> -H default -f ./site
|
|
703
|
+
xano ephemeral static_host build list <tenant_name> -H default
|
|
704
|
+
xano ephemeral static_host build get <tenant_name> -H default --build_id 52
|
|
705
|
+
xano ephemeral static_host deploy <tenant_name> -H default --build_id 52 --env prod
|
|
706
|
+
xano ephemeral static_host build pull <tenant_name> -H default --latest
|
|
707
|
+
xano ephemeral static_host build delete <tenant_name> -H default --build_id 52
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
> Static hosting is currently available for **local** tenants. Remote (tier2/tier3)
|
|
711
|
+
> tenants are not yet supported and will return an error.
|
|
712
|
+
|
|
599
713
|
### Sandbox
|
|
600
714
|
|
|
601
715
|
Manage your sandbox tenant. Each user has a single sandbox tenant that is auto-provisioned on first use.
|
|
@@ -638,16 +752,19 @@ xano sandbox reset --force
|
|
|
638
752
|
### Static Hosts
|
|
639
753
|
|
|
640
754
|
```bash
|
|
641
|
-
# List static hosts
|
|
755
|
+
# List static hosts. Page size is fixed at 100 by the API, so there is no
|
|
756
|
+
# --per_page flag; the footer reports the true total.
|
|
642
757
|
xano static_host list
|
|
758
|
+
xano static_host list --page 2
|
|
643
759
|
|
|
644
760
|
# Create / get / edit a static host
|
|
645
761
|
xano static_host create marketing --description "Marketing site"
|
|
646
762
|
xano static_host get marketing
|
|
647
763
|
xano static_host edit marketing --name marketing-v2 --description "Updated"
|
|
648
764
|
|
|
649
|
-
# List builds
|
|
765
|
+
# List builds (--page only; page size is fixed at 100 by the API)
|
|
650
766
|
xano static_host build list default
|
|
767
|
+
xano static_host build list default --page 2
|
|
651
768
|
|
|
652
769
|
# Get build details
|
|
653
770
|
xano static_host build get default --build_id 52
|
package/dist/base-command.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ export default abstract class BaseCommand extends Command {
|
|
|
117
117
|
protected logStaticHostUrls(opts: {
|
|
118
118
|
profile: ProfileConfig;
|
|
119
119
|
staticHost: string;
|
|
120
|
+
tenantName?: string;
|
|
120
121
|
verbose: boolean;
|
|
121
122
|
workspaceId: string;
|
|
122
123
|
}): Promise<void>;
|
|
@@ -126,6 +127,7 @@ export default abstract class BaseCommand extends Command {
|
|
|
126
127
|
profile: ProfileConfig;
|
|
127
128
|
quiet?: boolean;
|
|
128
129
|
staticHost: string;
|
|
130
|
+
tenantName?: string;
|
|
129
131
|
timeoutMs?: number;
|
|
130
132
|
verbose: boolean;
|
|
131
133
|
workspaceId: string;
|
package/dist/base-command.js
CHANGED
|
@@ -370,8 +370,11 @@ export default class BaseCommand extends Command {
|
|
|
370
370
|
* Returns the final status. Resolves to the last-seen status on timeout.
|
|
371
371
|
*/
|
|
372
372
|
async logStaticHostUrls(opts) {
|
|
373
|
-
const { profile, staticHost, verbose, workspaceId } = opts;
|
|
374
|
-
|
|
373
|
+
const { profile, staticHost, tenantName, verbose, workspaceId } = opts;
|
|
374
|
+
// When a tenant is scoped, target the tenant's static hosting; otherwise the
|
|
375
|
+
// workspace-level static host (default, unchanged behavior).
|
|
376
|
+
const scope = tenantName ? `/tenant/${tenantName}` : '';
|
|
377
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}${scope}/static_host/${staticHost}`;
|
|
375
378
|
try {
|
|
376
379
|
const response = await this.verboseFetch(url, {
|
|
377
380
|
headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
|
|
@@ -394,11 +397,14 @@ export default class BaseCommand extends Command {
|
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
async waitForBuild(opts) {
|
|
397
|
-
const { buildId, profile, quiet, staticHost, verbose, workspaceId } = opts;
|
|
400
|
+
const { buildId, profile, quiet, staticHost, tenantName, verbose, workspaceId } = opts;
|
|
398
401
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
399
402
|
const timeoutMs = opts.timeoutMs ?? 600_000; // 10 min
|
|
400
403
|
const terminal = new Set(['error', 'ok']);
|
|
401
|
-
|
|
404
|
+
// When a tenant is scoped, target the tenant's static hosting; otherwise the
|
|
405
|
+
// workspace-level static host (default, unchanged behavior).
|
|
406
|
+
const scope = tenantName ? `/tenant/${tenantName}` : '';
|
|
407
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}${scope}/static_host/${staticHost}/build/${buildId}`;
|
|
402
408
|
// Spinner only on an interactive TTY and when not emitting JSON. Verbose mode
|
|
403
409
|
// also disables it (the spinner would interleave with request/response logs).
|
|
404
410
|
const animate = Boolean(process.stdout.isTTY) && !quiet && !verbose;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, formatPagingFooter } from '../../../utils/paging.js';
|
|
3
4
|
export function filterBackups(branches, includeBackups) {
|
|
4
5
|
return includeBackups ? branches : branches.filter((b) => !b.backup);
|
|
5
6
|
}
|
|
@@ -82,7 +83,7 @@ Available branches:
|
|
|
82
83
|
const branches = filterBackups(allBranches, flags.backups);
|
|
83
84
|
// Output results
|
|
84
85
|
if (flags.output === 'json') {
|
|
85
|
-
this.log(JSON.stringify(branches, null, 2));
|
|
86
|
+
this.log(JSON.stringify(buildPagingJson({ items: branches }, { tier: 'none' }), null, 2));
|
|
86
87
|
}
|
|
87
88
|
else {
|
|
88
89
|
// summary format
|
|
@@ -97,6 +98,9 @@ Available branches:
|
|
|
97
98
|
this.log(` - ${branch.label}${liveIndicator}${backupIndicator}`);
|
|
98
99
|
}
|
|
99
100
|
}
|
|
101
|
+
const footer = formatPagingFooter({ items: branches }, { noun: 'branch', nounPlural: 'branches', tier: 'none' });
|
|
102
|
+
if (footer)
|
|
103
|
+
this.log(footer);
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
catch (error) {
|
|
@@ -6,6 +6,7 @@ export default class EphemeralImpersonate extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
guest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
'url-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -15,9 +15,16 @@ Opening browser...
|
|
|
15
15
|
Impersonation successful!
|
|
16
16
|
`,
|
|
17
17
|
`$ xano ephemeral impersonate e4f2-9ab1-xyz1 -o json`,
|
|
18
|
+
`$ xano ephemeral impersonate e4f2-9ab1-xyz1 --guest --url-only`,
|
|
18
19
|
];
|
|
19
20
|
static flags = {
|
|
20
21
|
...BaseCommand.baseFlags,
|
|
22
|
+
guest: Flags.boolean({
|
|
23
|
+
char: 'g',
|
|
24
|
+
default: false,
|
|
25
|
+
description: 'Mint a read-only guest session (browse only; no schema/data/config changes)',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
21
28
|
output: Flags.string({
|
|
22
29
|
char: 'o',
|
|
23
30
|
default: 'summary',
|
|
@@ -46,7 +53,7 @@ Impersonation successful!
|
|
|
46
53
|
}
|
|
47
54
|
const tenantName = args.tenant_name;
|
|
48
55
|
try {
|
|
49
|
-
const response = await this.getImpersonateResponse(profile, workspaceId, tenantName);
|
|
56
|
+
const response = await this.getImpersonateResponse(profile, workspaceId, tenantName, flags.guest);
|
|
50
57
|
if (flags.output === 'json') {
|
|
51
58
|
this.log(JSON.stringify(response, null, 2));
|
|
52
59
|
}
|
|
@@ -76,8 +83,9 @@ Impersonation successful!
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
|
-
async getImpersonateResponse(profile, workspaceId, tenantName) {
|
|
80
|
-
const
|
|
86
|
+
async getImpersonateResponse(profile, workspaceId, tenantName, guest = false) {
|
|
87
|
+
const query = guest ? '?guest_read_only=true' : '';
|
|
88
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(tenantName)}/impersonate${query}`;
|
|
81
89
|
const { verbose } = await this.parse(EphemeralImpersonate).then((r) => r.flags);
|
|
82
90
|
const response = await this.verboseFetch(apiUrl, {
|
|
83
91
|
headers: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../utils/paging.js';
|
|
3
4
|
export default class EphemeralList extends BaseCommand {
|
|
4
5
|
static description = 'List ephemeral tenants in a workspace, or across the whole instance with --global';
|
|
5
6
|
static examples = [
|
|
@@ -73,7 +74,7 @@ Ephemeral tenants (all workspaces):
|
|
|
73
74
|
this.error('Unexpected API response format');
|
|
74
75
|
}
|
|
75
76
|
if (flags.output === 'json') {
|
|
76
|
-
this.log(JSON.stringify(tenants, null, 2));
|
|
77
|
+
this.log(JSON.stringify(buildPagingJson({ items: tenants }, { tier: 'none' }), null, 2));
|
|
77
78
|
}
|
|
78
79
|
else if (tenants.length === 0) {
|
|
79
80
|
this.log('No ephemeral tenants found');
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
2
|
export default class EphemeralPull extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
3
6
|
static description: string;
|
|
4
7
|
static examples: string[];
|
|
5
8
|
static flags: {
|
|
@@ -7,17 +10,10 @@ export default class EphemeralPull extends BaseCommand {
|
|
|
7
10
|
draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
11
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
12
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
15
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
16
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
};
|
|
16
18
|
run(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Sanitize a document name for use as a filename.
|
|
19
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
-
* characters that are unsafe in filenames.
|
|
21
|
-
*/
|
|
22
|
-
private sanitizeFilename;
|
|
23
19
|
}
|
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class EphemeralPull extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_name: Args.string({
|
|
10
|
+
description: 'Ephemeral tenant name to pull from',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
8
14
|
static description = 'Pull an ephemeral tenant multidoc from the Xano Metadata API and split into individual files';
|
|
9
15
|
static examples = [
|
|
10
|
-
`$ xano ephemeral pull
|
|
16
|
+
`$ xano ephemeral pull e4f2-9ab1-xyz1
|
|
11
17
|
Pulled 42 documents from tenant e4f2-9ab1-xyz1 to current directory
|
|
12
18
|
`,
|
|
13
|
-
`$ xano ephemeral pull -d ./my-tenant
|
|
19
|
+
`$ xano ephemeral pull e4f2-9ab1-xyz1 -d ./my-tenant
|
|
14
20
|
Pulled 42 documents from tenant e4f2-9ab1-xyz1 to ./my-tenant
|
|
15
21
|
`,
|
|
16
|
-
`$ xano ephemeral pull -d ./output -
|
|
22
|
+
`$ xano ephemeral pull e4f2-9ab1-xyz1 -d ./output -w 40
|
|
17
23
|
Pulled 15 documents from tenant e4f2-9ab1-xyz1 to ./output
|
|
18
24
|
`,
|
|
19
|
-
`$ xano ephemeral pull
|
|
25
|
+
`$ xano ephemeral pull e4f2-9ab1-xyz1 --profile production --env --records
|
|
20
26
|
Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
21
27
|
`,
|
|
22
|
-
`$ xano ephemeral pull
|
|
28
|
+
`$ xano ephemeral pull e4f2-9ab1-xyz1 --draft`,
|
|
23
29
|
];
|
|
24
30
|
static flags = {
|
|
25
31
|
...BaseCommand.baseFlags,
|
|
@@ -44,11 +50,6 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
44
50
|
description: 'Include records',
|
|
45
51
|
required: false,
|
|
46
52
|
}),
|
|
47
|
-
tenant: Flags.string({
|
|
48
|
-
char: 't',
|
|
49
|
-
description: 'Ephemeral tenant name to pull from',
|
|
50
|
-
required: true,
|
|
51
|
-
}),
|
|
52
53
|
workspace: Flags.string({
|
|
53
54
|
char: 'w',
|
|
54
55
|
description: 'Workspace ID (optional if set in profile)',
|
|
@@ -56,7 +57,7 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
56
57
|
}),
|
|
57
58
|
};
|
|
58
59
|
async run() {
|
|
59
|
-
const { flags } = await this.parse(EphemeralPull);
|
|
60
|
+
const { args, flags } = await this.parse(EphemeralPull);
|
|
60
61
|
const { profileName, profile } = this.resolveProfile(flags);
|
|
61
62
|
// Determine workspace_id from flag or profile
|
|
62
63
|
let workspaceId;
|
|
@@ -68,10 +69,10 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
68
69
|
}
|
|
69
70
|
else {
|
|
70
71
|
this.error(`Workspace ID is required. Either:\n` +
|
|
71
|
-
` 1. Provide it as a flag: xano ephemeral pull
|
|
72
|
+
` 1. Provide it as a flag: xano ephemeral pull <tenant_name> -w <workspace_id>\n` +
|
|
72
73
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
73
74
|
}
|
|
74
|
-
const tenantName =
|
|
75
|
+
const tenantName = args.tenant_name;
|
|
75
76
|
// Build query parameters
|
|
76
77
|
const queryParams = new URLSearchParams({
|
|
77
78
|
env: flags.env.toString(),
|
|
@@ -129,96 +130,19 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
129
130
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
130
131
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
131
132
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
133
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
134
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
135
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
132
136
|
// Track filenames per type to handle duplicates
|
|
133
137
|
const filenameCounters = new Map();
|
|
134
138
|
let writtenCount = 0;
|
|
135
139
|
for (const doc of documents) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
else if (doc.type === 'workspace_trigger') {
|
|
144
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
145
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
146
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
147
|
-
}
|
|
148
|
-
else if (doc.type === 'error_trigger') {
|
|
149
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
150
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
151
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
152
|
-
}
|
|
153
|
-
else if (doc.type === 'agent') {
|
|
154
|
-
// agent → ai/agent/{name}.xs
|
|
155
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
156
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
157
|
-
}
|
|
158
|
-
else if (doc.type === 'mcp_server') {
|
|
159
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
160
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
161
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
162
|
-
}
|
|
163
|
-
else if (doc.type === 'tool') {
|
|
164
|
-
// tool → ai/tool/{name}.xs
|
|
165
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
166
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
167
|
-
}
|
|
168
|
-
else if (doc.type === 'agent_trigger') {
|
|
169
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
170
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
171
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
172
|
-
}
|
|
173
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
174
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
175
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
176
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
177
|
-
}
|
|
178
|
-
else if (doc.type === 'table_trigger') {
|
|
179
|
-
// table_trigger → table/trigger/{name}.xs
|
|
180
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
181
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
182
|
-
}
|
|
183
|
-
else if (doc.type === 'realtime_channel') {
|
|
184
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
185
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
186
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
187
|
-
}
|
|
188
|
-
else if (doc.type === 'realtime_trigger') {
|
|
189
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
190
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
191
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
-
}
|
|
193
|
-
else if (doc.type === 'api_group') {
|
|
194
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
195
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
196
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
197
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
198
|
-
}
|
|
199
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
200
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
201
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
202
|
-
const nameParts = doc.name.split('/');
|
|
203
|
-
const leafName = nameParts.pop();
|
|
204
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
205
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
206
|
-
baseName = this.sanitizeFilename(leafName);
|
|
207
|
-
if (doc.verb) {
|
|
208
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
// Default: split folder path from name
|
|
213
|
-
const nameParts = doc.name.split('/');
|
|
214
|
-
const leafName = nameParts.pop();
|
|
215
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
216
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
217
|
-
baseName = this.sanitizeFilename(leafName);
|
|
218
|
-
if (doc.verb) {
|
|
219
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
140
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
141
|
+
getApiGroupFolder,
|
|
142
|
+
getChannelServer,
|
|
143
|
+
join: path.join,
|
|
144
|
+
snakeCase,
|
|
145
|
+
});
|
|
222
146
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
223
147
|
// Track duplicates per directory
|
|
224
148
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -237,12 +161,4 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
237
161
|
}
|
|
238
162
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
239
163
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Sanitize a document name for use as a filename.
|
|
242
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
243
|
-
* characters that are unsafe in filenames.
|
|
244
|
-
*/
|
|
245
|
-
sanitizeFilename(name) {
|
|
246
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
247
|
-
}
|
|
248
164
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
2
|
export default class EphemeralPush extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
3
6
|
static description: string;
|
|
4
7
|
static examples: string[];
|
|
5
8
|
static flags: {
|
|
@@ -11,13 +14,22 @@ export default class EphemeralPush extends BaseCommand {
|
|
|
11
14
|
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
15
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
16
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
-
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
17
|
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
18
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
'wait-timeout': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
21
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
22
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
23
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
24
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
25
|
};
|
|
22
26
|
run(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Poll GET .../tenant/{name}/microservice until auto microservices settle.
|
|
29
|
+
* Renders a one-line-per-microservice progress view that updates in place, then
|
|
30
|
+
* exits non-zero if any auto microservice failed or the wait timed out.
|
|
31
|
+
*/
|
|
32
|
+
private waitForDeploy;
|
|
33
|
+
/** Number of lines the last progress render wrote (for in-place TTY updates). */
|
|
34
|
+
private lastRenderLines;
|
|
23
35
|
}
|