@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.
Files changed (101) hide show
  1. package/README.md +124 -7
  2. package/dist/base-command.d.ts +2 -0
  3. package/dist/base-command.js +10 -4
  4. package/dist/commands/branch/list/index.js +5 -1
  5. package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
  6. package/dist/commands/ephemeral/impersonate/index.js +11 -3
  7. package/dist/commands/ephemeral/list/index.js +2 -1
  8. package/dist/commands/ephemeral/pull/index.d.ts +3 -7
  9. package/dist/commands/ephemeral/pull/index.js +25 -109
  10. package/dist/commands/ephemeral/push/index.d.ts +13 -1
  11. package/dist/commands/ephemeral/push/index.js +94 -16
  12. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  13. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  14. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  15. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  16. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  17. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  18. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  19. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  20. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  21. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  22. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  23. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  24. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  25. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  26. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  27. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  28. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  29. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  30. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  31. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  32. package/dist/commands/flatten/index.d.ts +34 -0
  33. package/dist/commands/flatten/index.js +140 -0
  34. package/dist/commands/function/list/index.d.ts +4 -4
  35. package/dist/commands/function/list/index.js +30 -38
  36. package/dist/commands/knowledge/list/index.js +2 -1
  37. package/dist/commands/platform/list/index.js +13 -14
  38. package/dist/commands/release/list/index.js +13 -14
  39. package/dist/commands/release/pull/index.d.ts +0 -6
  40. package/dist/commands/release/pull/index.js +10 -95
  41. package/dist/commands/sandbox/env/list/index.js +4 -0
  42. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  43. package/dist/commands/sandbox/pull/index.js +10 -77
  44. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  45. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  46. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  47. package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
  48. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  49. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  50. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  51. package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
  52. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  53. package/dist/commands/static_host/build/list/index.js +19 -40
  54. package/dist/commands/static_host/build/pull/index.js +12 -9
  55. package/dist/commands/static_host/list/index.d.ts +2 -3
  56. package/dist/commands/static_host/list/index.js +20 -40
  57. package/dist/commands/static_host/migrate/index.js +14 -10
  58. package/dist/commands/tenant/backup/list/index.js +13 -14
  59. package/dist/commands/tenant/cluster/list/index.js +9 -10
  60. package/dist/commands/tenant/env/list/index.js +4 -0
  61. package/dist/commands/tenant/list/index.js +19 -20
  62. package/dist/commands/tenant/pull/index.d.ts +0 -6
  63. package/dist/commands/tenant/pull/index.js +10 -95
  64. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  65. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  66. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  67. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  68. package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
  69. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  70. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  71. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  72. package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
  73. package/dist/commands/unit_test/list/index.d.ts +3 -1
  74. package/dist/commands/unit_test/list/index.js +9 -14
  75. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  76. package/dist/commands/unit_test/run_all/index.js +68 -40
  77. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  78. package/dist/commands/workflow_test/list/index.js +10 -13
  79. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  80. package/dist/commands/workflow_test/run_all/index.js +67 -36
  81. package/dist/commands/workspace/git/pull/index.js +53 -3
  82. package/dist/commands/workspace/list/index.js +7 -13
  83. package/dist/commands/workspace/pull/index.d.ts +0 -6
  84. package/dist/commands/workspace/pull/index.js +10 -95
  85. package/dist/utils/concurrency.d.ts +29 -0
  86. package/dist/utils/concurrency.js +54 -0
  87. package/dist/utils/document-parser.d.ts +116 -0
  88. package/dist/utils/document-parser.js +274 -1
  89. package/dist/utils/flatten.d.ts +36 -0
  90. package/dist/utils/flatten.js +68 -0
  91. package/dist/utils/knowledge-sync.d.ts +1 -0
  92. package/dist/utils/knowledge-sync.js +11 -5
  93. package/dist/utils/microservice-wait.d.ts +53 -0
  94. package/dist/utils/microservice-wait.js +83 -0
  95. package/dist/utils/multidoc-push.d.ts +12 -0
  96. package/dist/utils/multidoc-push.js +84 -0
  97. package/dist/utils/paging.d.ts +145 -0
  98. package/dist/utils/paging.js +199 -0
  99. package/dist/utils/reference-checker.js +13 -0
  100. package/oclif.manifest.json +5038 -3635
  101. 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 -t <tenant_name> -d ./my-ephemeral
591
- xano ephemeral push -t <tenant_name> -d ./my-ephemeral --dry-run # preview first
592
- xano ephemeral push -t <tenant_name> -d ./my-ephemeral
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
@@ -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;
@@ -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
- const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}`;
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
- const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build/${buildId}`;
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 apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(tenantName)}/impersonate`;
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 -t e4f2-9ab1-xyz1
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 -t e4f2-9ab1-xyz1
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 -t e4f2-9ab1-xyz1 -w 40
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 -t e4f2-9ab1-xyz1 --profile production --env --records
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 -t e4f2-9ab1-xyz1 --draft`,
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 -t <tenant_name> -w <workspace_id>\n` +
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 = flags.tenant;
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
- let typeDir;
137
- let baseName;
138
- if (doc.type === 'workspace') {
139
- // workspace → workspace/{name}.xs
140
- typeDir = path.join(outputDir, 'workspace');
141
- baseName = this.sanitizeFilename(doc.name);
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
  }