@xano/cli 1.0.6-beta.3 → 1.0.6-beta.5
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/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +7 -142
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +7 -144
- package/dist/utils/document-parser.d.ts +39 -0
- package/dist/utils/document-parser.js +139 -0
- package/oclif.manifest.json +2493 -2493
- package/package.json +1 -1
|
@@ -16,10 +16,4 @@ export default class EphemeralPull extends BaseCommand {
|
|
|
16
16
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
17
|
};
|
|
18
18
|
run(): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* Sanitize a document name for use as a filename.
|
|
21
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
22
|
-
* characters that are unsafe in filenames.
|
|
23
|
-
*/
|
|
24
|
-
private sanitizeFilename;
|
|
25
19
|
}
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class EphemeralPull extends BaseCommand {
|
|
8
8
|
static args = {
|
|
9
9
|
tenant_name: Args.string({
|
|
@@ -130,96 +130,19 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
130
130
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
131
131
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
132
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);
|
|
133
136
|
// Track filenames per type to handle duplicates
|
|
134
137
|
const filenameCounters = new Map();
|
|
135
138
|
let writtenCount = 0;
|
|
136
139
|
for (const doc of documents) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
else if (doc.type === 'workspace_trigger') {
|
|
145
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
146
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
147
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
148
|
-
}
|
|
149
|
-
else if (doc.type === 'error_trigger') {
|
|
150
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
151
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
152
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
153
|
-
}
|
|
154
|
-
else if (doc.type === 'agent') {
|
|
155
|
-
// agent → ai/agent/{name}.xs
|
|
156
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
157
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
158
|
-
}
|
|
159
|
-
else if (doc.type === 'mcp_server') {
|
|
160
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
161
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
162
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
163
|
-
}
|
|
164
|
-
else if (doc.type === 'tool') {
|
|
165
|
-
// tool → ai/tool/{name}.xs
|
|
166
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
167
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
168
|
-
}
|
|
169
|
-
else if (doc.type === 'agent_trigger') {
|
|
170
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
171
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
172
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
173
|
-
}
|
|
174
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
175
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
176
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
177
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
178
|
-
}
|
|
179
|
-
else if (doc.type === 'table_trigger') {
|
|
180
|
-
// table_trigger → table/trigger/{name}.xs
|
|
181
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
182
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
183
|
-
}
|
|
184
|
-
else if (doc.type === 'realtime_channel') {
|
|
185
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
186
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
187
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
188
|
-
}
|
|
189
|
-
else if (doc.type === 'realtime_trigger') {
|
|
190
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
191
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
192
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
193
|
-
}
|
|
194
|
-
else if (doc.type === 'api_group') {
|
|
195
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
196
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
197
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
198
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
199
|
-
}
|
|
200
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
201
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
202
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
203
|
-
const nameParts = doc.name.split('/');
|
|
204
|
-
const leafName = nameParts.pop();
|
|
205
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
206
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
207
|
-
baseName = this.sanitizeFilename(leafName);
|
|
208
|
-
if (doc.verb) {
|
|
209
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
// Default: split folder path from name
|
|
214
|
-
const nameParts = doc.name.split('/');
|
|
215
|
-
const leafName = nameParts.pop();
|
|
216
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
217
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
218
|
-
baseName = this.sanitizeFilename(leafName);
|
|
219
|
-
if (doc.verb) {
|
|
220
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
140
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
141
|
+
getApiGroupFolder,
|
|
142
|
+
getChannelServer,
|
|
143
|
+
join: path.join,
|
|
144
|
+
snakeCase,
|
|
145
|
+
});
|
|
223
146
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
224
147
|
// Track duplicates per directory
|
|
225
148
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -238,12 +161,4 @@ Pulled 58 documents from tenant e4f2-9ab1-xyz1
|
|
|
238
161
|
}
|
|
239
162
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
240
163
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Sanitize a document name for use as a filename.
|
|
243
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
244
|
-
* characters that are unsafe in filenames.
|
|
245
|
-
*/
|
|
246
|
-
sanitizeFilename(name) {
|
|
247
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
248
|
-
}
|
|
249
164
|
}
|
|
@@ -14,10 +14,4 @@ export default class ReleasePull extends BaseCommand {
|
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<void>;
|
|
16
16
|
private resolveReleaseName;
|
|
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
17
|
}
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class ReleasePull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a release multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -123,96 +123,19 @@ Pulled 58 documents from release 'v1.0'
|
|
|
123
123
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
124
124
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
125
125
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
126
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
127
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
128
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
126
129
|
// Track filenames per type to handle duplicates
|
|
127
130
|
const filenameCounters = new Map();
|
|
128
131
|
let writtenCount = 0;
|
|
129
132
|
for (const doc of documents) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
else if (doc.type === 'workspace_trigger') {
|
|
138
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
139
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
140
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
141
|
-
}
|
|
142
|
-
else if (doc.type === 'error_trigger') {
|
|
143
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
144
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
145
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
146
|
-
}
|
|
147
|
-
else if (doc.type === 'agent') {
|
|
148
|
-
// agent → ai/agent/{name}.xs
|
|
149
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
150
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
151
|
-
}
|
|
152
|
-
else if (doc.type === 'mcp_server') {
|
|
153
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
154
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
155
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
156
|
-
}
|
|
157
|
-
else if (doc.type === 'tool') {
|
|
158
|
-
// tool → ai/tool/{name}.xs
|
|
159
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
160
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
161
|
-
}
|
|
162
|
-
else if (doc.type === 'agent_trigger') {
|
|
163
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
164
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
165
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
166
|
-
}
|
|
167
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
168
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
169
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
170
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
171
|
-
}
|
|
172
|
-
else if (doc.type === 'table_trigger') {
|
|
173
|
-
// table_trigger → table/trigger/{name}.xs
|
|
174
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
175
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
176
|
-
}
|
|
177
|
-
else if (doc.type === 'realtime_channel') {
|
|
178
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
179
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
180
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
181
|
-
}
|
|
182
|
-
else if (doc.type === 'realtime_trigger') {
|
|
183
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
184
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
185
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
186
|
-
}
|
|
187
|
-
else if (doc.type === 'api_group') {
|
|
188
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
189
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
190
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
191
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
-
}
|
|
193
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
194
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
195
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
196
|
-
const nameParts = doc.name.split('/');
|
|
197
|
-
const leafName = nameParts.pop();
|
|
198
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
199
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
200
|
-
baseName = this.sanitizeFilename(leafName);
|
|
201
|
-
if (doc.verb) {
|
|
202
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// Default: split folder path from name
|
|
207
|
-
const nameParts = doc.name.split('/');
|
|
208
|
-
const leafName = nameParts.pop();
|
|
209
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
210
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
211
|
-
baseName = this.sanitizeFilename(leafName);
|
|
212
|
-
if (doc.verb) {
|
|
213
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
133
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
134
|
+
getApiGroupFolder,
|
|
135
|
+
getChannelServer,
|
|
136
|
+
join: path.join,
|
|
137
|
+
snakeCase,
|
|
138
|
+
});
|
|
216
139
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
217
140
|
// Track duplicates per directory
|
|
218
141
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -257,12 +180,4 @@ Pulled 58 documents from release 'v1.0'
|
|
|
257
180
|
}
|
|
258
181
|
return match.id;
|
|
259
182
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Sanitize a document name for use as a filename.
|
|
262
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
263
|
-
* characters that are unsafe in filenames.
|
|
264
|
-
*/
|
|
265
|
-
sanitizeFilename(name) {
|
|
266
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
267
|
-
}
|
|
268
183
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import snakeCase from 'lodash.snakecase';
|
|
3
3
|
import BaseCommand from '../../../base-command.js';
|
|
4
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
4
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
5
5
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
@@ -92,82 +92,18 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
92
92
|
const outputDir = path.resolve(flags.directory);
|
|
93
93
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
94
94
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
95
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
96
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
97
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
95
98
|
const filenameCounters = new Map();
|
|
96
99
|
let writtenCount = 0;
|
|
97
100
|
for (const doc of documents) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
else if (doc.type === 'workspace_trigger') {
|
|
105
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
106
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
107
|
-
}
|
|
108
|
-
else if (doc.type === 'error_trigger') {
|
|
109
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
110
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
111
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
112
|
-
}
|
|
113
|
-
else if (doc.type === 'agent') {
|
|
114
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
115
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
116
|
-
}
|
|
117
|
-
else if (doc.type === 'mcp_server') {
|
|
118
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
119
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
120
|
-
}
|
|
121
|
-
else if (doc.type === 'tool') {
|
|
122
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
123
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
124
|
-
}
|
|
125
|
-
else if (doc.type === 'agent_trigger') {
|
|
126
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
127
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
128
|
-
}
|
|
129
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
130
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
131
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
132
|
-
}
|
|
133
|
-
else if (doc.type === 'table_trigger') {
|
|
134
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
135
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
136
|
-
}
|
|
137
|
-
else if (doc.type === 'realtime_channel') {
|
|
138
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
139
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
140
|
-
}
|
|
141
|
-
else if (doc.type === 'realtime_trigger') {
|
|
142
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
143
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
144
|
-
}
|
|
145
|
-
else if (doc.type === 'api_group') {
|
|
146
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
147
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
148
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
149
|
-
}
|
|
150
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
151
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
152
|
-
const nameParts = doc.name.split('/');
|
|
153
|
-
const leafName = nameParts.pop();
|
|
154
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
155
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
156
|
-
baseName = this.sanitizeFilename(leafName);
|
|
157
|
-
if (doc.verb) {
|
|
158
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
const nameParts = doc.name.split('/');
|
|
163
|
-
const leafName = nameParts.pop();
|
|
164
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
165
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
166
|
-
baseName = this.sanitizeFilename(leafName);
|
|
167
|
-
if (doc.verb) {
|
|
168
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
101
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
102
|
+
getApiGroupFolder,
|
|
103
|
+
getChannelServer,
|
|
104
|
+
join: path.join,
|
|
105
|
+
snakeCase,
|
|
106
|
+
});
|
|
171
107
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
172
108
|
const dirKey = path.relative(outputDir, typeDir);
|
|
173
109
|
if (!filenameCounters.has(dirKey)) {
|
|
@@ -194,7 +130,4 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
194
130
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
195
131
|
this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
|
|
196
132
|
}
|
|
197
|
-
sanitizeFilename(name) {
|
|
198
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
199
|
-
}
|
|
200
133
|
}
|
|
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
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
17
|
}
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, buildChannelServerResolver,
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class Pull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -136,139 +136,12 @@ Pulled 58 documents from tenant my-tenant
|
|
|
136
136
|
const filenameCounters = new Map();
|
|
137
137
|
let writtenCount = 0;
|
|
138
138
|
for (const doc of documents) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
else if (doc.type === 'workspace_trigger') {
|
|
147
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
148
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
149
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
150
|
-
}
|
|
151
|
-
else if (doc.type === 'error_trigger') {
|
|
152
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
153
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
154
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
155
|
-
}
|
|
156
|
-
else if (doc.type === 'agent') {
|
|
157
|
-
// agent → ai/agent/{name}.xs
|
|
158
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
159
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
160
|
-
}
|
|
161
|
-
else if (doc.type === 'mcp_server') {
|
|
162
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
163
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
164
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
165
|
-
}
|
|
166
|
-
else if (doc.type === 'tool') {
|
|
167
|
-
// tool → ai/tool/{name}.xs
|
|
168
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
169
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
170
|
-
}
|
|
171
|
-
else if (doc.type === 'agent_trigger') {
|
|
172
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
173
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
174
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
175
|
-
}
|
|
176
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
177
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
178
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
179
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
180
|
-
}
|
|
181
|
-
else if (doc.type === 'table_trigger') {
|
|
182
|
-
// table_trigger → table/trigger/{name}.xs
|
|
183
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
184
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
185
|
-
}
|
|
186
|
-
else if (doc.type === 'realtime_channel') {
|
|
187
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
188
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
189
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
190
|
-
}
|
|
191
|
-
else if (doc.type === 'realtime_trigger') {
|
|
192
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
193
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
194
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
195
|
-
}
|
|
196
|
-
else if (doc.type === 'realtime_server') {
|
|
197
|
-
// Realtime v2 — see workspace/pull for the full rationale. Its own
|
|
198
|
-
// document is named after itself, mirroring api_group.
|
|
199
|
-
// realtime_server "chat" → realtime/server/chat/chat.xs
|
|
200
|
-
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.name));
|
|
201
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
202
|
-
}
|
|
203
|
-
else if (doc.type === 'channel') {
|
|
204
|
-
// Realtime v2. The channel owns a directory named after itself (the full
|
|
205
|
-
// channel name, snake_cased into a single flat segment — like every other
|
|
206
|
-
// object), and its messages live in a message/ subfolder inside it. It
|
|
207
|
-
// nests under its owning realtime_server (from `server = "..."`):
|
|
208
|
-
// channel "rooms/{room_id}" (server "chat")
|
|
209
|
-
// → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
|
|
210
|
-
//
|
|
211
|
-
// The channel name is snake_cased as a whole (rooms/{room_id} →
|
|
212
|
-
// rooms_room_id), so there is no path nesting and no bracket escaping —
|
|
213
|
-
// the on-disk name is a display form; the .xs content keeps the real
|
|
214
|
-
// name. A channel with no resolvable server (e.g. a pre-server export)
|
|
215
|
-
// falls back to the legacy flat channel/<path>/_channel.xs layout.
|
|
216
|
-
if (doc.server) {
|
|
217
|
-
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.server), 'channel', snakeCase(doc.name));
|
|
218
|
-
baseName = snakeCase(doc.name);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
222
|
-
baseName = '_channel';
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
else if (doc.type === 'message' && doc.channel) {
|
|
226
|
-
// Realtime v2 message → nests in a message/ subfolder under its channel,
|
|
227
|
-
// under that channel's server. The message names only its channel; the
|
|
228
|
-
// server is resolved by looking that channel up in the same multidoc
|
|
229
|
-
// (two-pass resolve).
|
|
230
|
-
// message "post" on channel "rooms/{room_id}" (server "chat")
|
|
231
|
-
// → realtime/server/chat/channel/rooms_room_id/message/post.xs
|
|
232
|
-
//
|
|
233
|
-
// Nesting matters beyond tidiness: message names are unique only WITHIN
|
|
234
|
-
// a channel, so a flat message/ directory would collide when two
|
|
235
|
-
// channels both define e.g. "say". A channel whose server can't be
|
|
236
|
-
// resolved falls back to the legacy flat channel/ layout.
|
|
237
|
-
const messageServer = getChannelServer(doc.channel);
|
|
238
|
-
typeDir = messageServer
|
|
239
|
-
? path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(messageServer), 'channel', snakeCase(doc.channel), 'message')
|
|
240
|
-
: path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
241
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
242
|
-
}
|
|
243
|
-
else if (doc.type === 'api_group') {
|
|
244
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
245
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
246
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
247
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
248
|
-
}
|
|
249
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
250
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
251
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
252
|
-
const nameParts = doc.name.split('/');
|
|
253
|
-
const leafName = nameParts.pop();
|
|
254
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
255
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
256
|
-
baseName = this.sanitizeFilename(leafName);
|
|
257
|
-
if (doc.verb) {
|
|
258
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
// Default: split folder path from name
|
|
263
|
-
const nameParts = doc.name.split('/');
|
|
264
|
-
const leafName = nameParts.pop();
|
|
265
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
266
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
267
|
-
baseName = this.sanitizeFilename(leafName);
|
|
268
|
-
if (doc.verb) {
|
|
269
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
139
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
140
|
+
getApiGroupFolder,
|
|
141
|
+
getChannelServer,
|
|
142
|
+
join: path.join,
|
|
143
|
+
snakeCase,
|
|
144
|
+
});
|
|
272
145
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
273
146
|
// Track duplicates per directory
|
|
274
147
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -287,12 +160,4 @@ Pulled 58 documents from tenant my-tenant
|
|
|
287
160
|
}
|
|
288
161
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
289
162
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Sanitize a document name for use as a filename.
|
|
292
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
293
|
-
* characters that are unsafe in filenames.
|
|
294
|
-
*/
|
|
295
|
-
sanitizeFilename(name) {
|
|
296
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
297
|
-
}
|
|
298
163
|
}
|
|
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
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
17
|
}
|