@xano/cli 1.0.4 → 1.0.6-beta.0

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 (81) hide show
  1. package/README.md +139 -4
  2. package/dist/base-command.d.ts +27 -0
  3. package/dist/base-command.js +124 -1
  4. package/dist/commands/auth/index.d.ts +45 -1
  5. package/dist/commands/auth/index.js +197 -29
  6. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  7. package/dist/commands/ephemeral/create/index.js +103 -0
  8. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  9. package/dist/commands/ephemeral/delete/index.js +99 -0
  10. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/edit/index.js +109 -0
  12. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  13. package/dist/commands/ephemeral/get/index.js +125 -0
  14. package/dist/commands/ephemeral/impersonate/index.d.ts +19 -0
  15. package/dist/commands/ephemeral/impersonate/index.js +112 -0
  16. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  17. package/dist/commands/ephemeral/list/index.js +122 -0
  18. package/dist/commands/ephemeral/pull/index.d.ts +23 -0
  19. package/dist/commands/ephemeral/pull/index.js +248 -0
  20. package/dist/commands/{knowledge → ephemeral}/push/index.d.ts +8 -5
  21. package/dist/commands/ephemeral/push/index.js +143 -0
  22. package/dist/commands/function/run/index.d.ts +40 -0
  23. package/dist/commands/function/run/index.js +244 -0
  24. package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
  25. package/dist/commands/knowledge/get/index.js +126 -0
  26. package/dist/commands/knowledge/list/index.d.ts +18 -0
  27. package/dist/commands/knowledge/list/index.js +135 -0
  28. package/dist/commands/sandbox/pull/index.js +12 -2
  29. package/dist/commands/sandbox/push/index.d.ts +1 -1
  30. package/dist/commands/sandbox/push/index.js +19 -14
  31. package/dist/commands/sandbox/reset/index.js +12 -0
  32. package/dist/commands/static_host/build/create/index.d.ts +9 -1
  33. package/dist/commands/static_host/build/create/index.js +54 -4
  34. package/dist/commands/static_host/build/delete/index.d.ts +19 -0
  35. package/dist/commands/static_host/build/delete/index.js +114 -0
  36. package/dist/commands/static_host/build/get/index.d.ts +1 -1
  37. package/dist/commands/static_host/build/get/index.js +16 -10
  38. package/dist/commands/static_host/build/pull/index.d.ts +52 -0
  39. package/dist/commands/static_host/build/pull/index.js +300 -0
  40. package/dist/commands/static_host/build/push/index.d.ts +23 -0
  41. package/dist/commands/static_host/build/push/index.js +228 -0
  42. package/dist/commands/static_host/create/index.d.ts +17 -0
  43. package/dist/commands/static_host/create/index.js +86 -0
  44. package/dist/commands/static_host/deploy/index.d.ts +18 -0
  45. package/dist/commands/static_host/deploy/index.js +105 -0
  46. package/dist/commands/static_host/edit/index.d.ts +23 -0
  47. package/dist/commands/static_host/edit/index.js +151 -0
  48. package/dist/commands/static_host/get/index.d.ts +18 -0
  49. package/dist/commands/static_host/get/index.js +94 -0
  50. package/dist/commands/static_host/migrate/index.d.ts +44 -0
  51. package/dist/commands/static_host/migrate/index.js +205 -0
  52. package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
  53. package/dist/commands/tenant/snapshot/create/index.js +78 -0
  54. package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
  55. package/dist/commands/tenant/snapshot/delete/index.js +102 -0
  56. package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
  57. package/dist/commands/tenant/snapshot/list/index.js +96 -0
  58. package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
  59. package/dist/commands/tenant/snapshot/swap/index.js +103 -0
  60. package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
  61. package/dist/commands/tenant/unit_test/run/index.js +7 -0
  62. package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
  63. package/dist/commands/unit_test/run/index.d.ts +1 -0
  64. package/dist/commands/unit_test/run/index.js +8 -0
  65. package/dist/commands/unit_test/run_all/index.js +7 -3
  66. package/dist/commands/workspace/pull/index.js +13 -2
  67. package/dist/commands/workspace/push/index.d.ts +1 -1
  68. package/dist/commands/workspace/push/index.js +15 -5
  69. package/dist/utils/function-input.d.ts +77 -0
  70. package/dist/utils/function-input.js +148 -0
  71. package/dist/utils/knowledge-sync.d.ts +84 -79
  72. package/dist/utils/knowledge-sync.js +376 -323
  73. package/dist/utils/multidoc-push.d.ts +30 -18
  74. package/dist/utils/multidoc-push.js +284 -148
  75. package/dist/utils/reference-checker.js +2 -2
  76. package/dist/utils/static-host-files.d.ts +21 -0
  77. package/dist/utils/static-host-files.js +41 -0
  78. package/oclif.manifest.json +5046 -2691
  79. package/package.json +10 -1
  80. package/dist/commands/knowledge/pull/index.js +0 -86
  81. package/dist/commands/knowledge/push/index.js +0 -126
@@ -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
+ }
@@ -0,0 +1,112 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import open from 'open';
3
+ import BaseCommand from '../../../base-command.js';
4
+ export default class EphemeralImpersonate extends BaseCommand {
5
+ static args = {
6
+ tenant_name: Args.string({
7
+ description: 'Ephemeral tenant name to impersonate',
8
+ required: true,
9
+ }),
10
+ };
11
+ static description = 'Impersonate an ephemeral tenant and open it in the browser';
12
+ static examples = [
13
+ `$ xano ephemeral impersonate e4f2-9ab1-xyz1
14
+ Opening browser...
15
+ Impersonation successful!
16
+ `,
17
+ `$ xano ephemeral impersonate e4f2-9ab1-xyz1 -o json`,
18
+ ];
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ output: Flags.string({
22
+ char: 'o',
23
+ default: 'summary',
24
+ description: 'Output format',
25
+ options: ['summary', 'json'],
26
+ required: false,
27
+ }),
28
+ 'url-only': Flags.boolean({
29
+ char: 'u',
30
+ default: false,
31
+ description: 'Print the URL without opening the browser',
32
+ required: false,
33
+ }),
34
+ workspace: Flags.string({
35
+ char: 'w',
36
+ description: 'Workspace ID (uses profile workspace if not provided)',
37
+ required: false,
38
+ }),
39
+ };
40
+ async run() {
41
+ const { args, flags } = await this.parse(EphemeralImpersonate);
42
+ const { profile } = this.resolveProfile(flags);
43
+ const workspaceId = flags.workspace || profile.workspace;
44
+ if (!workspaceId) {
45
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
46
+ }
47
+ const tenantName = args.tenant_name;
48
+ try {
49
+ const response = await this.getImpersonateResponse(profile, workspaceId, tenantName);
50
+ if (flags.output === 'json') {
51
+ this.log(JSON.stringify(response, null, 2));
52
+ }
53
+ else {
54
+ const frontendUrl = this.getFrontendUrl(profile.instance_origin);
55
+ const params = new URLSearchParams({
56
+ _ti: response._ti,
57
+ });
58
+ const impersonateUrl = `${frontendUrl}/impersonate?${params.toString()}`;
59
+ if (flags['url-only']) {
60
+ this.log(impersonateUrl);
61
+ }
62
+ else {
63
+ this.log('Opening browser...');
64
+ await open(impersonateUrl);
65
+ this.log('Impersonation successful!');
66
+ }
67
+ }
68
+ process.exit(0);
69
+ }
70
+ catch (error) {
71
+ if (error instanceof Error) {
72
+ this.error(`Failed to impersonate tenant: ${error.message}`);
73
+ }
74
+ else {
75
+ this.error(`Failed to impersonate tenant: ${String(error)}`);
76
+ }
77
+ }
78
+ }
79
+ async getImpersonateResponse(profile, workspaceId, tenantName) {
80
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(tenantName)}/impersonate`;
81
+ const { verbose } = await this.parse(EphemeralImpersonate).then((r) => r.flags);
82
+ const response = await this.verboseFetch(apiUrl, {
83
+ headers: {
84
+ Authorization: `Bearer ${profile.access_token}`,
85
+ accept: 'application/json',
86
+ },
87
+ method: 'GET',
88
+ }, verbose, profile.access_token);
89
+ if (!response.ok) {
90
+ const errorText = await response.text();
91
+ throw new Error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
92
+ }
93
+ const result = (await response.json());
94
+ if (!result._ti) {
95
+ throw new Error('No one-time token returned from impersonate API');
96
+ }
97
+ return result;
98
+ }
99
+ getFrontendUrl(instanceOrigin) {
100
+ try {
101
+ const url = new URL(instanceOrigin);
102
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
103
+ url.port = '4200';
104
+ return url.origin;
105
+ }
106
+ }
107
+ catch {
108
+ // fall through
109
+ }
110
+ return instanceOrigin;
111
+ }
112
+ }
@@ -0,0 +1,19 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralList extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ /**
15
+ * Render a unix-epoch-seconds expiration as a human-readable "in Xh Ym" string,
16
+ * falling back to an ISO timestamp if the value has already passed or is malformed.
17
+ */
18
+ private formatExpiration;
19
+ }
@@ -0,0 +1,122 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class EphemeralList extends BaseCommand {
4
+ static description = 'List ephemeral tenants in a workspace, or across the whole instance with --global';
5
+ static examples = [
6
+ `$ xano ephemeral list
7
+ Ephemeral tenants in workspace 5:
8
+ - PR preview (e4f2-9ab1-...) [ok] - expires in 1h 0m
9
+ - Demo (e7a3-2c10-...) [ok] - expires in 23h 12m
10
+ `,
11
+ `$ xano ephemeral list --global
12
+ Ephemeral tenants (all workspaces):
13
+ - PR preview (e4f2-9ab1-...) [ok] - expires in 1h 0m
14
+ `,
15
+ `$ xano ephemeral list -w 5 -o json`,
16
+ ];
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ global: Flags.boolean({
20
+ default: false,
21
+ description: 'List ephemeral tenants across the whole instance (all workspaces)',
22
+ required: false,
23
+ }),
24
+ output: Flags.string({
25
+ char: 'o',
26
+ default: 'summary',
27
+ description: 'Output format',
28
+ options: ['summary', 'json'],
29
+ required: false,
30
+ }),
31
+ workspace: Flags.string({
32
+ char: 'w',
33
+ description: 'Workspace ID (uses profile workspace if not provided)',
34
+ required: false,
35
+ }),
36
+ };
37
+ async run() {
38
+ const { flags } = await this.parse(EphemeralList);
39
+ const { profile } = this.resolveProfile(flags);
40
+ let workspaceId;
41
+ let apiUrl;
42
+ if (flags.global) {
43
+ apiUrl = `${profile.instance_origin}/api:meta/ephemeral`;
44
+ }
45
+ else {
46
+ workspaceId = flags.workspace || profile.workspace;
47
+ if (!workspaceId) {
48
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
49
+ }
50
+ apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/ephemeral`;
51
+ }
52
+ try {
53
+ const response = await this.verboseFetch(apiUrl, {
54
+ headers: {
55
+ accept: 'application/json',
56
+ Authorization: `Bearer ${profile.access_token}`,
57
+ },
58
+ method: 'GET',
59
+ }, flags.verbose, profile.access_token);
60
+ if (!response.ok) {
61
+ const errorText = await response.text();
62
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
63
+ }
64
+ const data = (await response.json());
65
+ let tenants;
66
+ if (Array.isArray(data)) {
67
+ tenants = data;
68
+ }
69
+ else if (data && typeof data === 'object' && Array.isArray(data.items)) {
70
+ tenants = data.items;
71
+ }
72
+ else {
73
+ this.error('Unexpected API response format');
74
+ }
75
+ if (flags.output === 'json') {
76
+ this.log(JSON.stringify(tenants, null, 2));
77
+ }
78
+ else if (tenants.length === 0) {
79
+ this.log('No ephemeral tenants found');
80
+ }
81
+ else {
82
+ this.log(flags.global ? 'Ephemeral tenants (all workspaces):' : `Ephemeral tenants in workspace ${workspaceId}:`);
83
+ for (const tenant of tenants) {
84
+ const state = tenant.state ? ` [${tenant.state}]` : '';
85
+ const expires = ` - expires ${this.formatExpiration(tenant.ephemeral_expires_at)}`;
86
+ const workspaceLabel = flags.global && tenant.workspace_id ? ` (workspace ${tenant.workspace_id})` : '';
87
+ this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${expires}${workspaceLabel}`);
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ if (error instanceof Error) {
93
+ this.error(`Failed to list ephemeral tenants: ${error.message}`);
94
+ }
95
+ else {
96
+ this.error(`Failed to list ephemeral tenants: ${String(error)}`);
97
+ }
98
+ }
99
+ }
100
+ /**
101
+ * Render a unix-epoch-seconds expiration as a human-readable "in Xh Ym" string,
102
+ * falling back to an ISO timestamp if the value has already passed or is malformed.
103
+ */
104
+ formatExpiration(expiresAt) {
105
+ if (!expiresAt) {
106
+ return '-';
107
+ }
108
+ // The API serializes the timestamp field as a date string
109
+ // ("2026-07-24 19:49:15+0000"); tolerate a raw unix-epoch number too.
110
+ const expiresMs = typeof expiresAt === 'number' ? expiresAt * 1000 : Date.parse(String(expiresAt).replace(' ', 'T'));
111
+ if (Number.isNaN(expiresMs)) {
112
+ return String(expiresAt);
113
+ }
114
+ const diffSeconds = (expiresMs - Date.now()) / 1000;
115
+ if (diffSeconds <= 0) {
116
+ return `${new Date(expiresMs).toISOString()} (expired)`;
117
+ }
118
+ const hours = Math.floor(diffSeconds / 3600);
119
+ const minutes = Math.floor((diffSeconds % 3600) / 60);
120
+ return `in ${hours}h ${minutes}m`;
121
+ }
122
+ }
@@ -0,0 +1,23 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralPull extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ tenant: 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
+ /**
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
+ }