@xano/cli 1.0.5 → 1.0.6-beta.1

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 (41) hide show
  1. package/README.md +57 -0
  2. package/dist/base-command.d.ts +2 -0
  3. package/dist/base-command.js +10 -4
  4. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  5. package/dist/commands/ephemeral/create/index.js +103 -0
  6. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  7. package/dist/commands/ephemeral/delete/index.js +99 -0
  8. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  9. package/dist/commands/ephemeral/edit/index.js +109 -0
  10. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  11. package/dist/commands/ephemeral/get/index.js +125 -0
  12. package/dist/commands/ephemeral/impersonate/index.d.ts +19 -0
  13. package/dist/commands/ephemeral/impersonate/index.js +112 -0
  14. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  15. package/dist/commands/ephemeral/list/index.js +122 -0
  16. package/dist/commands/ephemeral/pull/index.d.ts +25 -0
  17. package/dist/commands/ephemeral/pull/index.js +249 -0
  18. package/dist/commands/ephemeral/push/index.d.ts +25 -0
  19. package/dist/commands/ephemeral/push/index.js +144 -0
  20. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  21. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  22. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  23. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  24. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +19 -0
  25. package/dist/commands/ephemeral/static_host/build/list/index.js +158 -0
  26. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  27. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  28. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  29. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  30. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  31. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  32. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  33. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  34. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  35. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  36. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  37. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  38. package/dist/commands/ephemeral/static_host/list/index.d.ts +18 -0
  39. package/dist/commands/ephemeral/static_host/list/index.js +152 -0
  40. package/oclif.manifest.json +4369 -2352
  41. package/package.json +10 -1
package/README.md CHANGED
@@ -564,6 +564,63 @@ xano tenant cluster license set <cluster_id>
564
564
  xano tenant cluster license set <cluster_id> --file ./kubeconfig.yaml
565
565
  ```
566
566
 
567
+ ### Ephemeral Tenants
568
+
569
+ Manage ephemeral tenants — short-lived, auto-expiring tenants scoped to a workspace (default TTL 1h, max 72h). Unlike a sandbox, an ephemeral tenant requires a workspace.
570
+
571
+ ```bash
572
+ # List ephemeral tenants in the current workspace
573
+ xano ephemeral list
574
+ xano ephemeral list -w 5
575
+
576
+ # List ephemeral tenants across every workspace you can access
577
+ xano ephemeral list --global
578
+
579
+ # Create an ephemeral tenant (workspace required)
580
+ xano ephemeral create "PR preview"
581
+ xano ephemeral create "Demo" --expires-hours 24 -w 5
582
+ xano ephemeral create "Load test" -d "48h soak" --expires-hours 48
583
+
584
+ # Get / edit / delete
585
+ xano ephemeral get <tenant_name>
586
+ xano ephemeral edit <tenant_name> --display "New Name" -d "New description"
587
+ xano ephemeral delete <tenant_name> --force
588
+
589
+ # Pull to / push from local files (multidoc)
590
+ xano ephemeral pull <tenant_name> -d ./my-ephemeral
591
+ xano ephemeral push <tenant_name> -d ./my-ephemeral --dry-run # preview first
592
+ xano ephemeral push <tenant_name> -d ./my-ephemeral
593
+
594
+ # Open in the browser (or print the URL)
595
+ xano ephemeral impersonate <tenant_name>
596
+ xano ephemeral impersonate <tenant_name> --url-only
597
+ ```
598
+
599
+ #### Static hosting for an ephemeral tenant
600
+
601
+ An ephemeral tenant can host static sites, scoped to that tenant. These commands mirror
602
+ `xano static_host *` but take the tenant name as the first argument (the tenant's static
603
+ hosting is isolated inside the tenant's own database).
604
+
605
+ ```bash
606
+ # List / create / inspect a tenant's static hosts
607
+ xano ephemeral static_host list <tenant_name>
608
+ xano ephemeral static_host create <tenant_name> --name marketing --description "Marketing site"
609
+ xano ephemeral static_host get <tenant_name> -H marketing
610
+ xano ephemeral static_host edit <tenant_name> -H marketing --description "Updated"
611
+
612
+ # Builds: push a directory, list, inspect, deploy to an env, pull, delete
613
+ xano ephemeral static_host build push <tenant_name> -H default -f ./site
614
+ xano ephemeral static_host build list <tenant_name> -H default
615
+ xano ephemeral static_host build get <tenant_name> -H default --build_id 52
616
+ xano ephemeral static_host deploy <tenant_name> -H default --build_id 52 --env prod
617
+ xano ephemeral static_host build pull <tenant_name> -H default --latest
618
+ xano ephemeral static_host build delete <tenant_name> -H default --build_id 52
619
+ ```
620
+
621
+ > Static hosting is currently available for **local** tenants. Remote (tier2/tier3)
622
+ > tenants are not yet supported and will return an error.
623
+
567
624
  ### Sandbox
568
625
 
569
626
  Manage your sandbox tenant. Each user has a single sandbox tenant that is auto-provisioned on first use.
@@ -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;
@@ -0,0 +1,18 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralCreate extends BaseCommand {
3
+ static args: {
4
+ display: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'expires-hours': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,103 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class EphemeralCreate extends BaseCommand {
4
+ static args = {
5
+ display: Args.string({
6
+ description: 'Display name for the ephemeral tenant',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Create a new ephemeral (short-lived, auto-expiring) tenant in a workspace';
11
+ static examples = [
12
+ `$ xano ephemeral create "PR preview"
13
+ Created ephemeral tenant: PR preview (e4f2-9ab1-...) - ID: 42
14
+ Expires: in 1 hour
15
+ `,
16
+ `$ xano ephemeral create "Demo" --expires-hours 24 -w 114`,
17
+ `$ xano ephemeral create "Load test" --description "48h soak" --expires-hours 48 -o json`,
18
+ ];
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ description: Flags.string({
22
+ char: 'd',
23
+ description: 'Ephemeral tenant description',
24
+ required: false,
25
+ }),
26
+ 'expires-hours': Flags.integer({
27
+ default: 1,
28
+ description: 'Hours until the tenant auto-expires (1-72)',
29
+ max: 72,
30
+ min: 1,
31
+ required: false,
32
+ }),
33
+ output: Flags.string({
34
+ char: 'o',
35
+ default: 'summary',
36
+ description: 'Output format',
37
+ options: ['summary', 'json'],
38
+ required: false,
39
+ }),
40
+ workspace: Flags.string({
41
+ char: 'w',
42
+ description: 'Workspace ID (uses profile workspace if not provided)',
43
+ required: false,
44
+ }),
45
+ };
46
+ async run() {
47
+ const { args, flags } = await this.parse(EphemeralCreate);
48
+ const { profile } = this.resolveProfile(flags);
49
+ // Ephemeral tenants REQUIRE a workspace (unlike sandbox, which is workspace-agnostic).
50
+ const workspaceId = flags.workspace || profile.workspace;
51
+ if (!workspaceId) {
52
+ this.error('No workspace ID provided. Ephemeral tenants require a workspace — use --workspace or set one in your profile.');
53
+ }
54
+ const body = {
55
+ display: args.display,
56
+ expires_hours: flags['expires-hours'],
57
+ tag: [],
58
+ };
59
+ if (flags.description)
60
+ body.description = flags.description;
61
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/ephemeral`;
62
+ try {
63
+ const response = await this.verboseFetch(apiUrl, {
64
+ body: JSON.stringify(body),
65
+ headers: {
66
+ accept: 'application/json',
67
+ Authorization: `Bearer ${profile.access_token}`,
68
+ 'Content-Type': 'application/json',
69
+ },
70
+ method: 'POST',
71
+ }, flags.verbose, profile.access_token);
72
+ if (!response.ok) {
73
+ const errorText = await response.text();
74
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
75
+ }
76
+ const tenant = (await response.json());
77
+ if (flags.output === 'json') {
78
+ this.log(JSON.stringify(tenant, null, 2));
79
+ }
80
+ else {
81
+ this.log(`Created ephemeral tenant: ${tenant.display || tenant.name} (${tenant.name}) - ID: ${tenant.id}`);
82
+ if (tenant.state) {
83
+ this.log(` State: ${tenant.state}`);
84
+ }
85
+ if (tenant.ephemeral_expires_at) {
86
+ // The API serializes the timestamp field as a date string
87
+ // ("2026-07-24 19:49:15+0000"); tolerate a raw unix-epoch number too.
88
+ const raw = tenant.ephemeral_expires_at;
89
+ const expiresMs = typeof raw === 'number' ? raw * 1000 : Date.parse(String(raw).replace(' ', 'T'));
90
+ this.log(` Expires: ${Number.isNaN(expiresMs) ? String(raw) : new Date(expiresMs).toISOString()}`);
91
+ }
92
+ }
93
+ }
94
+ catch (error) {
95
+ if (error instanceof Error) {
96
+ this.error(`Failed to create ephemeral tenant: ${error.message}`);
97
+ }
98
+ else {
99
+ this.error(`Failed to create ephemeral tenant: ${String(error)}`);
100
+ }
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,18 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralDelete extends BaseCommand {
3
+ static args: {
4
+ tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ private confirm;
18
+ }
@@ -0,0 +1,99 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class EphemeralDelete extends BaseCommand {
4
+ static args = {
5
+ tenant_name: Args.string({
6
+ description: 'Ephemeral tenant name to delete',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Delete an ephemeral tenant permanently. This destroys all associated infrastructure and cannot be undone.';
11
+ static examples = [
12
+ `$ xano ephemeral delete e4f2-9ab1-xyz1
13
+ Are you sure you want to delete tenant e4f2-9ab1-xyz1? This action cannot be undone. (y/N) y
14
+ Deleted tenant e4f2-9ab1-xyz1
15
+ `,
16
+ `$ xano ephemeral delete e4f2-9ab1-xyz1 --force
17
+ Deleted tenant e4f2-9ab1-xyz1
18
+ `,
19
+ `$ xano ephemeral delete e4f2-9ab1-xyz1 -f -o json`,
20
+ ];
21
+ static flags = {
22
+ ...BaseCommand.baseFlags,
23
+ force: Flags.boolean({
24
+ char: 'f',
25
+ default: false,
26
+ description: '[CRITICAL] NEVER run without explicit user confirmation. Skips the confirmation prompt.',
27
+ required: false,
28
+ }),
29
+ output: Flags.string({
30
+ char: 'o',
31
+ default: 'summary',
32
+ description: 'Output format',
33
+ options: ['summary', 'json'],
34
+ required: false,
35
+ }),
36
+ workspace: Flags.string({
37
+ char: 'w',
38
+ description: 'Workspace ID (uses profile workspace if not provided)',
39
+ required: false,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(EphemeralDelete);
44
+ const { profile } = this.resolveProfile(flags);
45
+ const workspaceId = flags.workspace || profile.workspace;
46
+ if (!workspaceId) {
47
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
48
+ }
49
+ const tenantName = args.tenant_name;
50
+ if (!flags.force) {
51
+ const confirmed = await this.confirm(`Are you sure you want to delete tenant ${tenantName}? This action cannot be undone.`);
52
+ if (!confirmed) {
53
+ this.log('Deletion cancelled.');
54
+ return;
55
+ }
56
+ }
57
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
58
+ try {
59
+ const response = await this.verboseFetch(apiUrl, {
60
+ headers: {
61
+ 'accept': 'application/json',
62
+ 'Authorization': `Bearer ${profile.access_token}`,
63
+ },
64
+ method: 'DELETE',
65
+ }, flags.verbose, profile.access_token);
66
+ if (!response.ok) {
67
+ const errorText = await response.text();
68
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
69
+ }
70
+ if (flags.output === 'json') {
71
+ this.log(JSON.stringify({ deleted: true, tenant_name: tenantName }, null, 2));
72
+ }
73
+ else {
74
+ this.log(`Deleted tenant ${tenantName}`);
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (error instanceof Error) {
79
+ this.error(`Failed to delete ephemeral tenant: ${error.message}`);
80
+ }
81
+ else {
82
+ this.error(`Failed to delete ephemeral tenant: ${String(error)}`);
83
+ }
84
+ }
85
+ }
86
+ async confirm(message) {
87
+ const readline = await import('node:readline');
88
+ const rl = readline.createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ });
92
+ return new Promise((resolve) => {
93
+ rl.question(`${message} (y/N) `, (answer) => {
94
+ rl.close();
95
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
96
+ });
97
+ });
98
+ }
99
+ }
@@ -0,0 +1,18 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralEdit extends BaseCommand {
3
+ static args: {
4
+ tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,109 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class EphemeralEdit extends BaseCommand {
4
+ static args = {
5
+ tenant_name: Args.string({
6
+ description: 'Ephemeral tenant name to edit',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Edit an existing ephemeral tenant';
11
+ static examples = [
12
+ `$ xano ephemeral edit e4f2-9ab1-xyz1 --display "New Name" --description "Updated description"
13
+ Updated tenant: New Name (e4f2-9ab1-xyz1) - ID: 42
14
+ `,
15
+ `$ xano ephemeral edit e4f2-9ab1-xyz1 -o json`,
16
+ ];
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ description: Flags.string({
20
+ char: 'd',
21
+ description: 'New description',
22
+ required: false,
23
+ }),
24
+ display: Flags.string({
25
+ description: 'New display name',
26
+ required: false,
27
+ }),
28
+ output: Flags.string({
29
+ char: 'o',
30
+ default: 'summary',
31
+ description: 'Output format',
32
+ options: ['summary', 'json'],
33
+ required: false,
34
+ }),
35
+ workspace: Flags.string({
36
+ char: 'w',
37
+ description: 'Workspace ID (uses profile workspace if not provided)',
38
+ required: false,
39
+ }),
40
+ };
41
+ async run() {
42
+ const { args, flags } = await this.parse(EphemeralEdit);
43
+ const { profile } = this.resolveProfile(flags);
44
+ const workspaceId = flags.workspace || profile.workspace;
45
+ if (!workspaceId) {
46
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
47
+ }
48
+ const tenantName = args.tenant_name;
49
+ const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
50
+ const headers = {
51
+ 'accept': 'application/json',
52
+ 'Authorization': `Bearer ${profile.access_token}`,
53
+ 'Content-Type': 'application/json',
54
+ };
55
+ try {
56
+ // Fetch current tenant state (PUT requires all fields)
57
+ const getResponse = await this.verboseFetch(baseUrl, {
58
+ headers: {
59
+ 'accept': 'application/json',
60
+ 'Authorization': `Bearer ${profile.access_token}`,
61
+ },
62
+ method: 'GET',
63
+ }, flags.verbose, profile.access_token);
64
+ if (!getResponse.ok) {
65
+ const errorText = await getResponse.text();
66
+ this.error(`Failed to fetch tenant: ${getResponse.status} ${getResponse.statusText}\n${errorText}`);
67
+ }
68
+ const current = await getResponse.json();
69
+ // Merge in user-provided values
70
+ const body = {
71
+ description: flags.description !== undefined ? flags.description : (current.description ?? ''),
72
+ display: flags.display !== undefined ? flags.display : (current.display ?? current.name),
73
+ domain: current.domain ?? '',
74
+ ingress: current.ingress ?? false,
75
+ proxy: current.proxy ?? '',
76
+ rbac: {
77
+ enabled: current.rbac?.enabled ?? false,
78
+ },
79
+ tag: current.tag ?? [],
80
+ tasks: current.tasks ?? false,
81
+ };
82
+ // Update tenant
83
+ const putResponse = await this.verboseFetch(baseUrl, {
84
+ body: JSON.stringify(body),
85
+ headers,
86
+ method: 'PUT',
87
+ }, flags.verbose, profile.access_token);
88
+ if (!putResponse.ok) {
89
+ const errorText = await putResponse.text();
90
+ this.error(`API request failed with status ${putResponse.status}: ${putResponse.statusText}\n${errorText}`);
91
+ }
92
+ const tenant = await putResponse.json();
93
+ if (flags.output === 'json') {
94
+ this.log(JSON.stringify(tenant, null, 2));
95
+ }
96
+ else {
97
+ this.log(`Updated tenant: ${tenant.display || tenant.name} (${tenant.name}) - ID: ${tenant.id}`);
98
+ }
99
+ }
100
+ catch (error) {
101
+ if (error instanceof Error) {
102
+ this.error(`Failed to edit ephemeral tenant: ${error.message}`);
103
+ }
104
+ else {
105
+ this.error(`Failed to edit ephemeral tenant: ${String(error)}`);
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,21 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralGet extends BaseCommand {
3
+ static args: {
4
+ tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ run(): Promise<void>;
16
+ /**
17
+ * Render a unix-epoch-seconds expiration as a human-readable "in Xh Ym" string,
18
+ * falling back to an ISO timestamp if the value has already passed or is absent.
19
+ */
20
+ private formatExpiration;
21
+ }
@@ -0,0 +1,125 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class EphemeralGet extends BaseCommand {
4
+ static args = {
5
+ tenant_name: Args.string({
6
+ description: 'Ephemeral tenant name to retrieve',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Get details of a specific ephemeral tenant';
11
+ static examples = [
12
+ `$ xano ephemeral get e4f2-9ab1-xyz1
13
+ Tenant: PR preview (e4f2-9ab1-xyz1)
14
+ State: ok
15
+ License: tier1
16
+ Expires: in 1h 0m
17
+ `,
18
+ `$ xano ephemeral get e4f2-9ab1-xyz1 -w 5 -o json`,
19
+ ];
20
+ static flags = {
21
+ ...BaseCommand.baseFlags,
22
+ output: Flags.string({
23
+ char: 'o',
24
+ default: 'summary',
25
+ description: 'Output format',
26
+ options: ['summary', 'json'],
27
+ required: false,
28
+ }),
29
+ workspace: Flags.string({
30
+ char: 'w',
31
+ description: 'Workspace ID (uses profile workspace if not provided)',
32
+ required: false,
33
+ }),
34
+ };
35
+ async run() {
36
+ const { args, flags } = await this.parse(EphemeralGet);
37
+ const { profile } = this.resolveProfile(flags);
38
+ const workspaceId = flags.workspace || profile.workspace;
39
+ if (!workspaceId) {
40
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
41
+ }
42
+ const tenantName = args.tenant_name;
43
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
44
+ try {
45
+ const response = await this.verboseFetch(apiUrl, {
46
+ headers: {
47
+ accept: 'application/json',
48
+ Authorization: `Bearer ${profile.access_token}`,
49
+ },
50
+ method: 'GET',
51
+ }, flags.verbose, profile.access_token);
52
+ if (!response.ok) {
53
+ const errorText = await response.text();
54
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
55
+ }
56
+ const tenant = (await response.json());
57
+ if (flags.output === 'json') {
58
+ this.log(JSON.stringify(tenant, null, 2));
59
+ }
60
+ else {
61
+ this.log(`Tenant: ${tenant.display || tenant.name} (${tenant.name})`);
62
+ if (tenant.state)
63
+ this.log(` State: ${tenant.state}`);
64
+ if (tenant.license)
65
+ this.log(` License: ${tenant.license}`);
66
+ if (tenant.xano_domain)
67
+ this.log(` Domain: ${tenant.xano_domain}`);
68
+ if (tenant.domain)
69
+ this.log(` Custom Domain: ${tenant.domain}`);
70
+ if (tenant.cluster?.name)
71
+ this.log(` Cluster: ${tenant.cluster.name}`);
72
+ const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
73
+ const releaseId = typeof tenant.release === 'object' ? tenant.release?.id : undefined;
74
+ if (releaseName)
75
+ this.log(` Release: ${releaseName} (ID: ${releaseId})`);
76
+ if (tenant.platform?.name)
77
+ this.log(` Platform: ${tenant.platform.name}`);
78
+ if (tenant.version !== undefined)
79
+ this.log(` Version: ${tenant.version}`);
80
+ if (tenant.tasks !== undefined)
81
+ this.log(` Tasks: ${tenant.tasks}`);
82
+ if (tenant.ingress !== undefined)
83
+ this.log(` Ingress: ${tenant.ingress}`);
84
+ if (tenant.type)
85
+ this.log(` Type: ${tenant.type}`);
86
+ this.log(` Expires: ${this.formatExpiration(tenant.ephemeral_expires_at)}`);
87
+ if (tenant.deployed_at) {
88
+ const d = new Date(tenant.deployed_at);
89
+ const deployedDate = Number.isNaN(d.getTime()) ? tenant.deployed_at : d.toISOString().split('T')[0];
90
+ this.log(` Deployed: ${deployedDate}`);
91
+ }
92
+ }
93
+ }
94
+ catch (error) {
95
+ if (error instanceof Error) {
96
+ this.error(`Failed to get ephemeral tenant: ${error.message}`);
97
+ }
98
+ else {
99
+ this.error(`Failed to get ephemeral tenant: ${String(error)}`);
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * Render a unix-epoch-seconds expiration as a human-readable "in Xh Ym" string,
105
+ * falling back to an ISO timestamp if the value has already passed or is absent.
106
+ */
107
+ formatExpiration(expiresAt) {
108
+ if (!expiresAt) {
109
+ return '-';
110
+ }
111
+ // The API serializes the timestamp field as a date string
112
+ // ("2026-07-24 19:49:15+0000"); tolerate a raw unix-epoch number too.
113
+ const expiresMs = typeof expiresAt === 'number' ? expiresAt * 1000 : Date.parse(String(expiresAt).replace(' ', 'T'));
114
+ if (Number.isNaN(expiresMs)) {
115
+ return String(expiresAt);
116
+ }
117
+ const diffSeconds = (expiresMs - Date.now()) / 1000;
118
+ if (diffSeconds <= 0) {
119
+ return `${new Date(expiresMs).toISOString()} (expired)`;
120
+ }
121
+ const hours = Math.floor(diffSeconds / 3600);
122
+ const minutes = Math.floor((diffSeconds % 3600) / 60);
123
+ return `in ${hours}h ${minutes}m`;
124
+ }
125
+ }
@@ -0,0 +1,19 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralImpersonate extends BaseCommand {
3
+ static args: {
4
+ tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'url-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ private getImpersonateResponse;
18
+ private getFrontendUrl;
19
+ }