@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.
- package/README.md +139 -4
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +45 -1
- package/dist/commands/auth/index.js +197 -29
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +19 -0
- package/dist/commands/ephemeral/impersonate/index.js +112 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +122 -0
- package/dist/commands/ephemeral/pull/index.d.ts +23 -0
- package/dist/commands/ephemeral/pull/index.js +248 -0
- package/dist/commands/{knowledge → ephemeral}/push/index.d.ts +8 -5
- package/dist/commands/ephemeral/push/index.js +143 -0
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
- package/dist/commands/knowledge/get/index.js +126 -0
- package/dist/commands/knowledge/list/index.d.ts +18 -0
- package/dist/commands/knowledge/list/index.js +135 -0
- package/dist/commands/sandbox/pull/index.js +12 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +19 -14
- package/dist/commands/sandbox/reset/index.js +12 -0
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +228 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- package/dist/commands/workspace/pull/index.js +13 -2
- package/dist/commands/workspace/push/index.d.ts +1 -1
- package/dist/commands/workspace/push/index.js +15 -5
- package/dist/utils/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/dist/utils/knowledge-sync.d.ts +84 -79
- package/dist/utils/knowledge-sync.js +376 -323
- package/dist/utils/multidoc-push.d.ts +30 -18
- package/dist/utils/multidoc-push.js +284 -148
- package/dist/utils/reference-checker.js +2 -2
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +5046 -2691
- package/package.json +10 -1
- package/dist/commands/knowledge/pull/index.js +0 -86
- package/dist/commands/knowledge/push/index.js +0 -126
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import snakeCase from 'lodash.snakecase';
|
|
5
|
+
import BaseCommand from '../../../base-command.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
7
|
+
export default class EphemeralPull extends BaseCommand {
|
|
8
|
+
static description = 'Pull an ephemeral tenant multidoc from the Xano Metadata API and split into individual files';
|
|
9
|
+
static examples = [
|
|
10
|
+
`$ xano ephemeral pull -t e4f2-9ab1-xyz1
|
|
11
|
+
Pulled 42 documents from tenant e4f2-9ab1-xyz1 to current directory
|
|
12
|
+
`,
|
|
13
|
+
`$ xano ephemeral pull -d ./my-tenant -t e4f2-9ab1-xyz1
|
|
14
|
+
Pulled 42 documents from tenant e4f2-9ab1-xyz1 to ./my-tenant
|
|
15
|
+
`,
|
|
16
|
+
`$ xano ephemeral pull -d ./output -t e4f2-9ab1-xyz1 -w 40
|
|
17
|
+
Pulled 15 documents from tenant e4f2-9ab1-xyz1 to ./output
|
|
18
|
+
`,
|
|
19
|
+
`$ xano ephemeral pull -t e4f2-9ab1-xyz1 --profile production --env --records
|
|
20
|
+
Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
21
|
+
`,
|
|
22
|
+
`$ xano ephemeral pull -t e4f2-9ab1-xyz1 --draft`,
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
...BaseCommand.baseFlags,
|
|
26
|
+
directory: Flags.string({
|
|
27
|
+
char: 'd',
|
|
28
|
+
default: '.',
|
|
29
|
+
description: 'Output directory for pulled documents (defaults to current directory)',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
draft: Flags.boolean({
|
|
33
|
+
default: false,
|
|
34
|
+
description: 'Include draft versions',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
env: Flags.boolean({
|
|
38
|
+
default: false,
|
|
39
|
+
description: 'Include environment variables',
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
records: Flags.boolean({
|
|
43
|
+
default: false,
|
|
44
|
+
description: 'Include records',
|
|
45
|
+
required: false,
|
|
46
|
+
}),
|
|
47
|
+
tenant: Flags.string({
|
|
48
|
+
char: 't',
|
|
49
|
+
description: 'Ephemeral tenant name to pull from',
|
|
50
|
+
required: true,
|
|
51
|
+
}),
|
|
52
|
+
workspace: Flags.string({
|
|
53
|
+
char: 'w',
|
|
54
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
55
|
+
required: false,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
async run() {
|
|
59
|
+
const { flags } = await this.parse(EphemeralPull);
|
|
60
|
+
const { profileName, profile } = this.resolveProfile(flags);
|
|
61
|
+
// Determine workspace_id from flag or profile
|
|
62
|
+
let workspaceId;
|
|
63
|
+
if (flags.workspace) {
|
|
64
|
+
workspaceId = flags.workspace;
|
|
65
|
+
}
|
|
66
|
+
else if (profile.workspace) {
|
|
67
|
+
workspaceId = profile.workspace;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
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
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
73
|
+
}
|
|
74
|
+
const tenantName = flags.tenant;
|
|
75
|
+
// Build query parameters
|
|
76
|
+
const queryParams = new URLSearchParams({
|
|
77
|
+
env: flags.env.toString(),
|
|
78
|
+
include_draft: flags.draft.toString(),
|
|
79
|
+
records: flags.records.toString(),
|
|
80
|
+
});
|
|
81
|
+
// Construct the API URL
|
|
82
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/multidoc?${queryParams.toString()}`;
|
|
83
|
+
// Fetch multidoc from the API
|
|
84
|
+
let responseText;
|
|
85
|
+
const requestHeaders = {
|
|
86
|
+
accept: 'application/json',
|
|
87
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
91
|
+
headers: requestHeaders,
|
|
92
|
+
method: 'GET',
|
|
93
|
+
}, flags.verbose, profile.access_token);
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const errorText = await response.text();
|
|
96
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
97
|
+
}
|
|
98
|
+
responseText = await response.text();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
this.error(`Failed to fetch multidoc: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.error(`Failed to fetch multidoc: ${String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Split the response into individual documents
|
|
109
|
+
const rawDocuments = responseText.split('\n---\n');
|
|
110
|
+
// Parse each document
|
|
111
|
+
const documents = [];
|
|
112
|
+
for (const raw of rawDocuments) {
|
|
113
|
+
const trimmed = raw.trim();
|
|
114
|
+
if (!trimmed) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const parsed = parseDocument(trimmed);
|
|
118
|
+
if (parsed) {
|
|
119
|
+
documents.push(parsed);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (documents.length === 0) {
|
|
123
|
+
this.log('No documents found in response');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Resolve the output directory
|
|
127
|
+
const outputDir = path.resolve(flags.directory);
|
|
128
|
+
// Create the output directory if it doesn't exist
|
|
129
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
130
|
+
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
131
|
+
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
132
|
+
// Track filenames per type to handle duplicates
|
|
133
|
+
const filenameCounters = new Map();
|
|
134
|
+
let writtenCount = 0;
|
|
135
|
+
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
|
+
}
|
|
222
|
+
fs.mkdirSync(typeDir, { recursive: true });
|
|
223
|
+
// Track duplicates per directory
|
|
224
|
+
const dirKey = path.relative(outputDir, typeDir);
|
|
225
|
+
if (!filenameCounters.has(dirKey)) {
|
|
226
|
+
filenameCounters.set(dirKey, new Map());
|
|
227
|
+
}
|
|
228
|
+
const typeCounters = filenameCounters.get(dirKey);
|
|
229
|
+
const count = typeCounters.get(baseName) || 0;
|
|
230
|
+
typeCounters.set(baseName, count + 1);
|
|
231
|
+
// Append numeric suffix for duplicates
|
|
232
|
+
let filename;
|
|
233
|
+
filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
234
|
+
const filePath = path.join(typeDir, filename);
|
|
235
|
+
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
236
|
+
writtenCount++;
|
|
237
|
+
}
|
|
238
|
+
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
239
|
+
}
|
|
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
|
+
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
|
-
export default class
|
|
2
|
+
export default class EphemeralPush extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
6
|
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
8
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
|
|
9
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
-
|
|
11
|
+
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
13
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
17
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
18
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
19
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { 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 { executePush } from '../../../utils/multidoc-push.js';
|
|
6
|
+
export default class EphemeralPush extends BaseCommand {
|
|
7
|
+
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.';
|
|
8
|
+
static examples = [
|
|
9
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1
|
|
10
|
+
Push from current directory (default partial mode)
|
|
11
|
+
`,
|
|
12
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 -d ./my-workspace
|
|
13
|
+
Push from a specific directory
|
|
14
|
+
`,
|
|
15
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 --sync
|
|
16
|
+
Push all files to the ephemeral tenant
|
|
17
|
+
`,
|
|
18
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 --sync --delete
|
|
19
|
+
Push all files and delete remote objects not included
|
|
20
|
+
`,
|
|
21
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 --dry-run
|
|
22
|
+
Preview changes without pushing
|
|
23
|
+
`,
|
|
24
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 --force
|
|
25
|
+
Skip preview and push immediately
|
|
26
|
+
`,
|
|
27
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 --records --env`,
|
|
28
|
+
`$ xano ephemeral push -t e4f2-9ab1-xyz1 --truncate`,
|
|
29
|
+
];
|
|
30
|
+
static flags = {
|
|
31
|
+
...BaseCommand.baseFlags,
|
|
32
|
+
directory: Flags.string({
|
|
33
|
+
char: 'd',
|
|
34
|
+
default: '.',
|
|
35
|
+
description: 'Directory containing documents to push (defaults to current directory)',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
delete: Flags.boolean({
|
|
39
|
+
default: false,
|
|
40
|
+
description: 'Delete remote objects not included in the push (requires --sync)',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
'dry-run': Flags.boolean({
|
|
44
|
+
default: false,
|
|
45
|
+
description: 'Show preview of changes without pushing (exit after preview)',
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
env: Flags.boolean({
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Include environment variables in import',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
53
|
+
force: Flags.boolean({
|
|
54
|
+
default: false,
|
|
55
|
+
description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
|
|
56
|
+
required: false,
|
|
57
|
+
}),
|
|
58
|
+
guids: Flags.boolean({
|
|
59
|
+
allowNo: true,
|
|
60
|
+
default: true,
|
|
61
|
+
description: 'Write server-assigned GUIDs back to local files (use --no-guids to skip)',
|
|
62
|
+
required: false,
|
|
63
|
+
}),
|
|
64
|
+
records: Flags.boolean({
|
|
65
|
+
default: false,
|
|
66
|
+
description: 'Include records in import',
|
|
67
|
+
required: false,
|
|
68
|
+
}),
|
|
69
|
+
sync: Flags.boolean({
|
|
70
|
+
default: false,
|
|
71
|
+
description: 'Full push — send all files, not just changed ones. Required for --delete.',
|
|
72
|
+
required: false,
|
|
73
|
+
}),
|
|
74
|
+
tenant: Flags.string({
|
|
75
|
+
char: 't',
|
|
76
|
+
description: 'Ephemeral tenant name to push to',
|
|
77
|
+
required: true,
|
|
78
|
+
}),
|
|
79
|
+
transaction: Flags.boolean({
|
|
80
|
+
allowNo: true,
|
|
81
|
+
default: true,
|
|
82
|
+
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).',
|
|
83
|
+
required: false,
|
|
84
|
+
}),
|
|
85
|
+
truncate: Flags.boolean({
|
|
86
|
+
default: false,
|
|
87
|
+
description: '[CRITICAL] STOP and confirm with the user; this truncates live tables before importing.',
|
|
88
|
+
required: false,
|
|
89
|
+
}),
|
|
90
|
+
workspace: Flags.string({
|
|
91
|
+
char: 'w',
|
|
92
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
93
|
+
required: false,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
async run() {
|
|
97
|
+
const { flags } = await this.parse(EphemeralPush);
|
|
98
|
+
const { profile } = this.resolveProfile(flags);
|
|
99
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
100
|
+
if (!workspaceId) {
|
|
101
|
+
this.error('No workspace ID provided. Ephemeral tenants require a workspace — use --workspace or set one in your profile.');
|
|
102
|
+
}
|
|
103
|
+
const inputDir = resolve(flags.directory);
|
|
104
|
+
if (!fs.existsSync(inputDir)) {
|
|
105
|
+
this.error(`Directory not found: ${inputDir}`);
|
|
106
|
+
}
|
|
107
|
+
if (!fs.statSync(inputDir).isDirectory()) {
|
|
108
|
+
this.error(`Not a directory: ${inputDir}`);
|
|
109
|
+
}
|
|
110
|
+
const tenantName = flags.tenant;
|
|
111
|
+
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
|
|
112
|
+
const target = {
|
|
113
|
+
buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
|
|
114
|
+
buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
|
|
115
|
+
cliVersion: this.config.version,
|
|
116
|
+
instanceOrigin: profile.instance_origin,
|
|
117
|
+
label: `ephemeral tenant ${tenantName}`,
|
|
118
|
+
sourceWorkspaceId: profile.workspace,
|
|
119
|
+
supportsBranches: false,
|
|
120
|
+
supportsPartial: true,
|
|
121
|
+
warnOnWorkspaceMismatch: false,
|
|
122
|
+
};
|
|
123
|
+
const pushFlags = {
|
|
124
|
+
delete: flags.delete,
|
|
125
|
+
'dry-run': flags['dry-run'],
|
|
126
|
+
env: flags.env,
|
|
127
|
+
force: flags.force,
|
|
128
|
+
guids: flags.guids,
|
|
129
|
+
records: flags.records,
|
|
130
|
+
sync: flags.sync,
|
|
131
|
+
transaction: flags.transaction,
|
|
132
|
+
truncate: flags.truncate,
|
|
133
|
+
verbose: flags.verbose,
|
|
134
|
+
};
|
|
135
|
+
await executePush({
|
|
136
|
+
accessToken: profile.access_token,
|
|
137
|
+
branch: '',
|
|
138
|
+
command: this,
|
|
139
|
+
inputDir,
|
|
140
|
+
verboseFetch: this.verboseFetch.bind(this),
|
|
141
|
+
}, target, pushFlags);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class FunctionRun extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
data: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
logs: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'no-input-check': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
};
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
private emitResult;
|
|
24
|
+
/**
|
|
25
|
+
* Fetch a single function record by name via the list endpoint's search,
|
|
26
|
+
* returning the exact-name match (so `calc` doesn't accidentally match
|
|
27
|
+
* `calcScore`). Best-effort: returns undefined and warns on any failure so a
|
|
28
|
+
* transient introspection problem never blocks an otherwise-valid run.
|
|
29
|
+
*/
|
|
30
|
+
private fetchFunctionByName;
|
|
31
|
+
/**
|
|
32
|
+
* Prompt for each missing required input on an interactive TTY. Values are
|
|
33
|
+
* read as raw JSON when they parse as such (so numbers/bools/objects work),
|
|
34
|
+
* otherwise treated as a string — mirroring the `key:=`/`key=` distinction.
|
|
35
|
+
* Non-TTY: fail fast with the list of missing params (never hang on a prompt).
|
|
36
|
+
*/
|
|
37
|
+
private promptForMissing;
|
|
38
|
+
private readStdinSync;
|
|
39
|
+
private resolveWorkspaceId;
|
|
40
|
+
}
|