@xano/cli 1.2.0 → 1.3.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 (116) hide show
  1. package/README.md +112 -3
  2. package/dist/base-command.d.ts +43 -36
  3. package/dist/base-command.js +223 -160
  4. package/dist/commands/auth/index.d.ts +1 -1
  5. package/dist/commands/auth/index.js +7 -7
  6. package/dist/commands/branch/create/index.d.ts +1 -1
  7. package/dist/commands/branch/create/index.js +1 -1
  8. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  9. package/dist/commands/ephemeral/create/index.js +103 -0
  10. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/delete/index.js +99 -0
  12. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  13. package/dist/commands/ephemeral/edit/index.js +109 -0
  14. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  15. package/dist/commands/ephemeral/get/index.js +125 -0
  16. package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
  17. package/dist/commands/ephemeral/impersonate/index.js +120 -0
  18. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  19. package/dist/commands/ephemeral/list/index.js +123 -0
  20. package/dist/commands/ephemeral/pull/index.d.ts +19 -0
  21. package/dist/commands/ephemeral/pull/index.js +163 -0
  22. package/dist/commands/ephemeral/push/index.d.ts +35 -0
  23. package/dist/commands/ephemeral/push/index.js +223 -0
  24. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  25. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  26. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  27. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  28. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  29. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  30. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  31. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  32. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  33. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  34. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  35. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  36. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  37. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  38. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  39. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  40. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  41. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  42. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  43. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  44. package/dist/commands/flatten/index.d.ts +34 -0
  45. package/dist/commands/flatten/index.js +140 -0
  46. package/dist/commands/function/create/index.js +1 -1
  47. package/dist/commands/function/edit/index.js +26 -32
  48. package/dist/commands/function/get/index.js +2 -3
  49. package/dist/commands/function/run/index.d.ts +1 -0
  50. package/dist/commands/function/run/index.js +19 -6
  51. package/dist/commands/profile/create/index.d.ts +1 -1
  52. package/dist/commands/profile/create/index.js +7 -7
  53. package/dist/commands/profile/me/index.js +1 -1
  54. package/dist/commands/profile/use/index.js +0 -4
  55. package/dist/commands/profile/wizard/index.d.ts +1 -1
  56. package/dist/commands/profile/wizard/index.js +12 -10
  57. package/dist/commands/profile/workspace/set/index.js +1 -1
  58. package/dist/commands/release/edit/index.js +2 -2
  59. package/dist/commands/release/export/index.js +1 -2
  60. package/dist/commands/release/import/index.js +1 -1
  61. package/dist/commands/release/pull/index.d.ts +0 -6
  62. package/dist/commands/release/pull/index.js +13 -99
  63. package/dist/commands/release/push/index.js +2 -2
  64. package/dist/commands/sandbox/env/get_all/index.js +1 -1
  65. package/dist/commands/sandbox/env/set_all/index.js +1 -1
  66. package/dist/commands/sandbox/get/index.js +1 -1
  67. package/dist/commands/sandbox/license/get/index.js +1 -1
  68. package/dist/commands/sandbox/license/set/index.js +1 -1
  69. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  70. package/dist/commands/sandbox/pull/index.js +13 -81
  71. package/dist/commands/sandbox/push/index.d.ts +1 -1
  72. package/dist/commands/sandbox/push/index.js +5 -5
  73. package/dist/commands/static_host/build/create/index.d.ts +1 -1
  74. package/dist/commands/static_host/build/create/index.js +2 -2
  75. package/dist/commands/static_host/build/pull/index.js +1 -1
  76. package/dist/commands/static_host/build/push/index.js +1 -1
  77. package/dist/commands/static_host/edit/index.js +1 -1
  78. package/dist/commands/tenant/backup/export/index.js +1 -2
  79. package/dist/commands/tenant/backup/import/index.js +1 -1
  80. package/dist/commands/tenant/cluster/create/index.js +1 -1
  81. package/dist/commands/tenant/cluster/license/get/index.js +1 -1
  82. package/dist/commands/tenant/cluster/license/set/index.js +1 -1
  83. package/dist/commands/tenant/create/index.d.ts +1 -1
  84. package/dist/commands/tenant/create/index.js +6 -6
  85. package/dist/commands/tenant/deploy_platform/index.js +1 -1
  86. package/dist/commands/tenant/edit/index.js +7 -7
  87. package/dist/commands/tenant/env/get_all/index.js +1 -1
  88. package/dist/commands/tenant/env/set_all/index.js +1 -1
  89. package/dist/commands/tenant/impersonate/index.d.ts +1 -1
  90. package/dist/commands/tenant/impersonate/index.js +14 -14
  91. package/dist/commands/tenant/license/get/index.js +1 -1
  92. package/dist/commands/tenant/license/set/index.js +1 -1
  93. package/dist/commands/tenant/pull/index.d.ts +0 -6
  94. package/dist/commands/tenant/pull/index.js +14 -100
  95. package/dist/commands/workspace/git/pull/index.d.ts +4 -4
  96. package/dist/commands/workspace/git/pull/index.js +158 -97
  97. package/dist/commands/workspace/pull/index.d.ts +1 -7
  98. package/dist/commands/workspace/pull/index.js +17 -103
  99. package/dist/help.js +4 -2
  100. package/dist/update-check.d.ts +1 -1
  101. package/dist/update-check.js +1 -1
  102. package/dist/utils/document-parser.d.ts +116 -0
  103. package/dist/utils/document-parser.js +311 -4
  104. package/dist/utils/flatten.d.ts +36 -0
  105. package/dist/utils/flatten.js +68 -0
  106. package/dist/utils/knowledge-sync.d.ts +1 -0
  107. package/dist/utils/knowledge-sync.js +11 -6
  108. package/dist/utils/microservice-wait.d.ts +53 -0
  109. package/dist/utils/microservice-wait.js +84 -0
  110. package/dist/utils/multidoc-push.d.ts +88 -0
  111. package/dist/utils/multidoc-push.js +378 -15
  112. package/dist/utils/paging.d.ts +2 -1
  113. package/dist/utils/paging.js +0 -1
  114. package/dist/utils/reference-checker.js +14 -3
  115. package/oclif.manifest.json +5264 -3170
  116. package/package.json +10 -1
@@ -0,0 +1,123 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
4
+ export default class EphemeralList extends BaseCommand {
5
+ static description = 'List ephemeral tenants in a workspace, or across the whole instance with --global';
6
+ static examples = [
7
+ `$ xano ephemeral list
8
+ Ephemeral tenants in workspace 5:
9
+ - PR preview (e4f2-9ab1-...) [ok] - expires in 1h 0m
10
+ - Demo (e7a3-2c10-...) [ok] - expires in 23h 12m
11
+ `,
12
+ `$ xano ephemeral list --global
13
+ Ephemeral tenants (all workspaces):
14
+ - PR preview (e4f2-9ab1-...) [ok] - expires in 1h 0m
15
+ `,
16
+ `$ xano ephemeral list -w 5 -o json`,
17
+ ];
18
+ static flags = {
19
+ ...BaseCommand.baseFlags,
20
+ global: Flags.boolean({
21
+ default: false,
22
+ description: 'List ephemeral tenants across the whole instance (all workspaces)',
23
+ required: false,
24
+ }),
25
+ output: Flags.string({
26
+ char: 'o',
27
+ default: 'summary',
28
+ description: 'Output format',
29
+ options: ['summary', 'json'],
30
+ required: false,
31
+ }),
32
+ workspace: Flags.string({
33
+ char: 'w',
34
+ description: 'Workspace ID (uses profile workspace if not provided)',
35
+ required: false,
36
+ }),
37
+ };
38
+ async run() {
39
+ const { flags } = await this.parse(EphemeralList);
40
+ const { profile } = this.resolveProfile(flags);
41
+ let workspaceId;
42
+ let apiUrl;
43
+ if (flags.global) {
44
+ apiUrl = `${profile.instance_origin}/api:meta/ephemeral`;
45
+ }
46
+ else {
47
+ workspaceId = flags.workspace || profile.workspace;
48
+ if (!workspaceId) {
49
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
50
+ }
51
+ apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/ephemeral`;
52
+ }
53
+ try {
54
+ const response = await this.verboseFetch(apiUrl, {
55
+ headers: {
56
+ accept: 'application/json',
57
+ Authorization: `Bearer ${profile.access_token}`,
58
+ },
59
+ method: 'GET',
60
+ }, flags.verbose, profile.access_token);
61
+ if (!response.ok) {
62
+ const errorText = await response.text();
63
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
64
+ }
65
+ const data = (await response.json());
66
+ let tenants;
67
+ if (Array.isArray(data)) {
68
+ tenants = data;
69
+ }
70
+ else if (data && typeof data === 'object' && Array.isArray(data.items)) {
71
+ tenants = data.items;
72
+ }
73
+ else {
74
+ this.error('Unexpected API response format');
75
+ }
76
+ if (flags.output === 'json') {
77
+ this.log(JSON.stringify(buildPagingJson({ items: tenants }, { tier: 'none' }), null, 2));
78
+ }
79
+ else if (tenants.length === 0) {
80
+ this.log('No ephemeral tenants found');
81
+ }
82
+ else {
83
+ this.log(flags.global ? 'Ephemeral tenants (all workspaces):' : `Ephemeral tenants in workspace ${workspaceId}:`);
84
+ for (const tenant of tenants) {
85
+ const state = tenant.state ? ` [${tenant.state}]` : '';
86
+ const expires = ` - expires ${this.formatExpiration(tenant.ephemeral_expires_at)}`;
87
+ const workspaceLabel = flags.global && tenant.workspace_id ? ` (workspace ${tenant.workspace_id})` : '';
88
+ this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${expires}${workspaceLabel}`);
89
+ }
90
+ }
91
+ }
92
+ catch (error) {
93
+ if (error instanceof Error) {
94
+ this.error(`Failed to list ephemeral tenants: ${error.message}`);
95
+ }
96
+ else {
97
+ this.error(`Failed to list ephemeral tenants: ${String(error)}`);
98
+ }
99
+ }
100
+ }
101
+ /**
102
+ * Render a unix-epoch-seconds expiration as a human-readable "in Xh Ym" string,
103
+ * falling back to an ISO timestamp if the value has already passed or is malformed.
104
+ */
105
+ formatExpiration(expiresAt) {
106
+ if (!expiresAt) {
107
+ return '-';
108
+ }
109
+ // The API serializes the timestamp field as a date string
110
+ // ("2026-07-24 19:49:15+0000"); tolerate a raw unix-epoch number too.
111
+ const expiresMs = typeof expiresAt === 'number' ? expiresAt * 1000 : Date.parse(String(expiresAt).replace(' ', 'T'));
112
+ if (Number.isNaN(expiresMs)) {
113
+ return String(expiresAt);
114
+ }
115
+ const diffSeconds = (expiresMs - Date.now()) / 1000;
116
+ if (diffSeconds <= 0) {
117
+ return `${new Date(expiresMs).toISOString()} (expired)`;
118
+ }
119
+ const hours = Math.floor(diffSeconds / 3600);
120
+ const minutes = Math.floor((diffSeconds % 3600) / 60);
121
+ return `in ${hours}h ${minutes}m`;
122
+ }
123
+ }
@@ -0,0 +1,19 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralPull 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
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ run(): Promise<void>;
19
+ }
@@ -0,0 +1,163 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import snakeCase from 'lodash.snakecase';
3
+ import * as fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import BaseCommand from '../../../base-command.js';
6
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
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
+ };
14
+ static description = 'Pull an ephemeral tenant multidoc from the Xano Metadata API and split into individual files';
15
+ static examples = [
16
+ `$ xano ephemeral pull e4f2-9ab1-xyz1
17
+ Pulled 42 documents from tenant e4f2-9ab1-xyz1 to current directory
18
+ `,
19
+ `$ xano ephemeral pull e4f2-9ab1-xyz1 -d ./my-tenant
20
+ Pulled 42 documents from tenant e4f2-9ab1-xyz1 to ./my-tenant
21
+ `,
22
+ `$ xano ephemeral pull e4f2-9ab1-xyz1 -d ./output -w 40
23
+ Pulled 15 documents from tenant e4f2-9ab1-xyz1 to ./output
24
+ `,
25
+ `$ xano ephemeral pull e4f2-9ab1-xyz1 --profile production --env --records
26
+ Pulled 58 documents from tenant e4f2-9ab1-xyz1
27
+ `,
28
+ `$ xano ephemeral pull e4f2-9ab1-xyz1 --draft`,
29
+ ];
30
+ static flags = {
31
+ ...BaseCommand.baseFlags,
32
+ directory: Flags.string({
33
+ char: 'd',
34
+ default: '.',
35
+ description: 'Output directory for pulled documents (defaults to current directory)',
36
+ required: false,
37
+ }),
38
+ draft: Flags.boolean({
39
+ default: false,
40
+ description: 'Include draft versions',
41
+ required: false,
42
+ }),
43
+ env: Flags.boolean({
44
+ default: false,
45
+ description: 'Include environment variables',
46
+ required: false,
47
+ }),
48
+ records: Flags.boolean({
49
+ default: false,
50
+ description: 'Include records',
51
+ required: false,
52
+ }),
53
+ workspace: Flags.string({
54
+ char: 'w',
55
+ description: 'Workspace ID (optional if set in profile)',
56
+ required: false,
57
+ }),
58
+ };
59
+ async run() {
60
+ const { args, flags } = await this.parse(EphemeralPull);
61
+ const { profile, profileName } = this.resolveProfile(flags);
62
+ // Determine workspace_id from flag or profile
63
+ let workspaceId;
64
+ if (flags.workspace) {
65
+ workspaceId = flags.workspace;
66
+ }
67
+ else if (profile.workspace) {
68
+ workspaceId = profile.workspace;
69
+ }
70
+ else {
71
+ this.error(`Workspace ID is required. Either:\n` +
72
+ ` 1. Provide it as a flag: xano ephemeral pull <tenant_name> -w <workspace_id>\n` +
73
+ ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
74
+ }
75
+ const tenantName = args.tenant_name;
76
+ // Build query parameters
77
+ const queryParams = new URLSearchParams({
78
+ env: flags.env.toString(),
79
+ include_draft: flags.draft.toString(),
80
+ records: flags.records.toString(),
81
+ });
82
+ // Construct the API URL
83
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/multidoc?${queryParams.toString()}`;
84
+ // Fetch multidoc from the API
85
+ let responseText;
86
+ const requestHeaders = {
87
+ accept: 'application/json',
88
+ Authorization: `Bearer ${profile.access_token}`,
89
+ };
90
+ try {
91
+ const response = await this.verboseFetch(apiUrl, {
92
+ headers: requestHeaders,
93
+ method: 'GET',
94
+ }, flags.verbose, profile.access_token);
95
+ if (!response.ok) {
96
+ const errorText = await response.text();
97
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
98
+ }
99
+ responseText = await response.text();
100
+ }
101
+ catch (error) {
102
+ if (error instanceof Error) {
103
+ this.error(`Failed to fetch multidoc: ${error.message}`);
104
+ }
105
+ else {
106
+ this.error(`Failed to fetch multidoc: ${String(error)}`);
107
+ }
108
+ }
109
+ // Split the response into individual documents
110
+ const rawDocuments = responseText.split('\n---\n');
111
+ // Parse each document
112
+ const documents = [];
113
+ for (const raw of rawDocuments) {
114
+ const trimmed = raw.trim();
115
+ if (!trimmed) {
116
+ continue;
117
+ }
118
+ const parsed = parseDocument(trimmed);
119
+ if (parsed) {
120
+ documents.push(parsed);
121
+ }
122
+ }
123
+ if (documents.length === 0) {
124
+ this.log('No documents found in response');
125
+ return;
126
+ }
127
+ // Resolve the output directory
128
+ const outputDir = path.resolve(flags.directory);
129
+ // Create the output directory if it doesn't exist
130
+ fs.mkdirSync(outputDir, { recursive: true });
131
+ // Resolve api_group names to unique folder names, disambiguating collisions
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);
136
+ // Track filenames per type to handle duplicates
137
+ const filenameCounters = new Map();
138
+ let writtenCount = 0;
139
+ for (const doc of documents) {
140
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
141
+ getApiGroupFolder,
142
+ getChannelServer,
143
+ join: path.join,
144
+ snakeCase,
145
+ });
146
+ fs.mkdirSync(typeDir, { recursive: true });
147
+ // Track duplicates per directory
148
+ const dirKey = path.relative(outputDir, typeDir);
149
+ if (!filenameCounters.has(dirKey)) {
150
+ filenameCounters.set(dirKey, new Map());
151
+ }
152
+ const typeCounters = filenameCounters.get(dirKey);
153
+ const count = typeCounters.get(baseName) || 0;
154
+ typeCounters.set(baseName, count + 1);
155
+ // Append numeric suffix for duplicates
156
+ const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
157
+ const filePath = path.join(typeDir, filename);
158
+ fs.writeFileSync(filePath, doc.content, 'utf8');
159
+ writtenCount++;
160
+ }
161
+ this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
162
+ }
163
+ }
@@ -0,0 +1,35 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class EphemeralPush 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
+ delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
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>;
21
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ };
26
+ /** Number of lines the last progress render wrote (for in-place TTY updates). */
27
+ private lastRenderLines;
28
+ run(): Promise<void>;
29
+ /**
30
+ * Poll GET .../tenant/{name}/microservice until auto microservices settle.
31
+ * Renders a one-line-per-microservice progress view that updates in place, then
32
+ * exits non-zero if any auto microservice failed or the wait timed out.
33
+ */
34
+ private waitForDeploy;
35
+ }
@@ -0,0 +1,223 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import BaseCommand from '../../../base-command.js';
5
+ import { isAwaited, waitForMicroservices, } from '../../../utils/microservice-wait.js';
6
+ import { executePush } from '../../../utils/multidoc-push.js';
7
+ /** Status glyph for one microservice row in the `--wait` progress block. */
8
+ const icon = (e) => {
9
+ if (!isAwaited(e))
10
+ return '➖';
11
+ switch (e.status) {
12
+ case 'error': {
13
+ return '❌';
14
+ }
15
+ case 'ok': {
16
+ return '✅';
17
+ }
18
+ default: {
19
+ return '⏳';
20
+ }
21
+ }
22
+ };
23
+ /** One rendered microservice row: glyph, padded name, status detail. */
24
+ const line = (e) => {
25
+ const detail = isAwaited(e) ? e.detail || e.status : `skipped (${e.tenant_deploy ?? 'manual'})`;
26
+ return ` ${icon(e)} ${e.name.padEnd(24)} ${detail}`;
27
+ };
28
+ export default class EphemeralPush extends BaseCommand {
29
+ static args = {
30
+ tenant_name: Args.string({
31
+ description: 'Ephemeral tenant name to push to',
32
+ required: true,
33
+ }),
34
+ };
35
+ static description = '[IMPORTANT] ALWAYS run --dry-run first and show the user the output before pushing. Push local documents to an ephemeral tenant via multidoc import. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only.';
36
+ static examples = [
37
+ `$ xano ephemeral push e4f2-9ab1-xyz1
38
+ Push from current directory (default partial mode)
39
+ `,
40
+ `$ xano ephemeral push e4f2-9ab1-xyz1 -d ./my-workspace
41
+ Push from a specific directory
42
+ `,
43
+ `$ xano ephemeral push e4f2-9ab1-xyz1 --sync
44
+ Push all files to the ephemeral tenant
45
+ `,
46
+ `$ xano ephemeral push e4f2-9ab1-xyz1 --sync --delete
47
+ Push all files and delete remote objects not included
48
+ `,
49
+ `$ xano ephemeral push e4f2-9ab1-xyz1 --dry-run
50
+ Preview changes without pushing
51
+ `,
52
+ `$ xano ephemeral push e4f2-9ab1-xyz1 --force
53
+ Skip preview and push immediately
54
+ `,
55
+ `$ xano ephemeral push e4f2-9ab1-xyz1 --records --env`,
56
+ `$ xano ephemeral push e4f2-9ab1-xyz1 --truncate`,
57
+ ];
58
+ static flags = {
59
+ ...BaseCommand.baseFlags,
60
+ delete: Flags.boolean({
61
+ default: false,
62
+ description: 'Delete remote objects not included in the push (requires --sync)',
63
+ required: false,
64
+ }),
65
+ directory: Flags.string({
66
+ char: 'd',
67
+ default: '.',
68
+ description: 'Directory containing documents to push (defaults to current directory)',
69
+ required: false,
70
+ }),
71
+ 'dry-run': Flags.boolean({
72
+ default: false,
73
+ description: 'Show preview of changes without pushing (exit after preview)',
74
+ required: false,
75
+ }),
76
+ env: Flags.boolean({
77
+ default: false,
78
+ description: 'Include environment variables in import',
79
+ required: false,
80
+ }),
81
+ force: Flags.boolean({
82
+ default: false,
83
+ description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
84
+ required: false,
85
+ }),
86
+ guids: Flags.boolean({
87
+ allowNo: true,
88
+ default: true,
89
+ description: 'Write server-assigned GUIDs back to local files (use --no-guids to skip)',
90
+ required: false,
91
+ }),
92
+ records: Flags.boolean({
93
+ default: false,
94
+ description: 'Include records in import',
95
+ required: false,
96
+ }),
97
+ sync: Flags.boolean({
98
+ default: false,
99
+ description: 'Full push — send all files, not just changed ones. Required for --delete.',
100
+ required: false,
101
+ }),
102
+ transaction: Flags.boolean({
103
+ allowNo: true,
104
+ default: true,
105
+ description: '[CRITICAL] DO NOT run with --no-transaction without explicit user confirmation; this disables rollback. Wraps import in a database transaction (use --no-transaction for debugging purposes).',
106
+ required: false,
107
+ }),
108
+ truncate: Flags.boolean({
109
+ default: false,
110
+ description: '[CRITICAL] STOP and confirm with the user; this truncates live tables before importing.',
111
+ required: false,
112
+ }),
113
+ wait: Flags.boolean({
114
+ default: false,
115
+ description: 'After pushing, wait for auto-deployed microservices (tenant_deploy="auto") to become ready. Exits non-zero if any fails to deploy or the wait times out. Ignored with --dry-run.',
116
+ required: false,
117
+ }),
118
+ 'wait-timeout': Flags.integer({
119
+ default: 300,
120
+ description: 'Seconds to wait for microservices to become ready when --wait is set (default 300).',
121
+ min: 1,
122
+ required: false,
123
+ }),
124
+ workspace: Flags.string({
125
+ char: 'w',
126
+ description: 'Workspace ID (uses profile workspace if not provided)',
127
+ required: false,
128
+ }),
129
+ };
130
+ /** Number of lines the last progress render wrote (for in-place TTY updates). */
131
+ lastRenderLines = 0;
132
+ async run() {
133
+ const { args, flags } = await this.parse(EphemeralPush);
134
+ const { profile } = this.resolveProfile(flags);
135
+ const workspaceId = flags.workspace || profile.workspace;
136
+ if (!workspaceId) {
137
+ this.error('No workspace ID provided. Ephemeral tenants require a workspace — use --workspace or set one in your profile.');
138
+ }
139
+ const inputDir = resolve(flags.directory);
140
+ if (!fs.existsSync(inputDir)) {
141
+ this.error(`Directory not found: ${inputDir}`);
142
+ }
143
+ if (!fs.statSync(inputDir).isDirectory()) {
144
+ this.error(`Not a directory: ${inputDir}`);
145
+ }
146
+ const tenantName = args.tenant_name;
147
+ const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
148
+ const target = {
149
+ buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
150
+ buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
151
+ cliVersion: this.config.version,
152
+ instanceOrigin: profile.instance_origin,
153
+ label: `ephemeral tenant ${tenantName}`,
154
+ sourceWorkspaceId: profile.workspace,
155
+ supportsBranches: false,
156
+ supportsPartial: true,
157
+ warnOnWorkspaceMismatch: false,
158
+ };
159
+ const pushFlags = {
160
+ delete: flags.delete,
161
+ 'dry-run': flags['dry-run'],
162
+ env: flags.env,
163
+ force: flags.force,
164
+ guids: flags.guids,
165
+ records: flags.records,
166
+ sync: flags.sync,
167
+ transaction: flags.transaction,
168
+ truncate: flags.truncate,
169
+ verbose: flags.verbose,
170
+ };
171
+ await executePush({
172
+ accessToken: profile.access_token,
173
+ branch: '',
174
+ command: this,
175
+ inputDir,
176
+ verboseFetch: this.verboseFetch.bind(this),
177
+ }, target, pushFlags);
178
+ // --wait: after a real push, poll the tenant's live microservice status until
179
+ // every auto-deployed microservice (tenant_deploy="auto") is ready or the
180
+ // wait times out. Skipped on --dry-run (nothing was deployed).
181
+ if (flags.wait && !flags['dry-run']) {
182
+ await this.waitForDeploy(baseUrl, profile.access_token, tenantName, flags['wait-timeout'], flags.verbose);
183
+ }
184
+ }
185
+ /**
186
+ * Poll GET .../tenant/{name}/microservice until auto microservices settle.
187
+ * Renders a one-line-per-microservice progress view that updates in place, then
188
+ * exits non-zero if any auto microservice failed or the wait timed out.
189
+ */
190
+ async waitForDeploy(baseUrl, accessToken, tenantName, timeoutSeconds, verbose) {
191
+ this.log('');
192
+ this.log(`Waiting for microservices to deploy (timeout ${timeoutSeconds}s)…`);
193
+ const result = await waitForMicroservices({
194
+ accessToken,
195
+ onPoll: (entries) => {
196
+ // Re-render the block: move the cursor up over the previous render (if any)
197
+ // and rewrite. Falls back to plain appends when not a TTY.
198
+ if (process.stdout.isTTY && this.lastRenderLines > 0) {
199
+ process.stdout.write(`[${this.lastRenderLines}A`);
200
+ }
201
+ const lines = entries.map((e) => line(e));
202
+ for (const l of lines)
203
+ this.log(l);
204
+ this.lastRenderLines = process.stdout.isTTY ? lines.length : 0;
205
+ },
206
+ statusUrl: `${baseUrl}/microservice`,
207
+ timeoutMs: timeoutSeconds * 1000,
208
+ verbose,
209
+ verboseFetch: this.verboseFetch.bind(this),
210
+ });
211
+ const awaited = result.entries.filter((e) => isAwaited(e));
212
+ const ready = awaited.filter((e) => e.status === 'ok').length;
213
+ if (result.timedOut) {
214
+ this.error(`Timed out after ${timeoutSeconds}s waiting for microservices (${ready}/${awaited.length} ready). ` +
215
+ `Check status with the Xano dashboard or re-run with a larger --wait-timeout.`);
216
+ }
217
+ if (result.hadError) {
218
+ const failed = awaited.filter((e) => e.status === 'error').map((e) => `${e.name} (${e.detail || 'error'})`);
219
+ this.error(`Microservice deploy failed: ${failed.join(', ')}`);
220
+ }
221
+ this.log(`All microservices ready (${ready}/${awaited.length}).`);
222
+ }
223
+ }
@@ -0,0 +1,20 @@
1
+ import BaseCommand from '../../../../../base-command.js';
2
+ export default class EphemeralStaticHostBuildDelete 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
+ build_id: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'static-host': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ run(): Promise<void>;
19
+ private confirm;
20
+ }