@xano/cli 1.0.6-beta.2 → 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 +10 -106
- package/dist/commands/workspace/git/pull/index.js +48 -7
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -115
- package/dist/utils/document-parser.d.ts +59 -0
- package/dist/utils/document-parser.js +177 -1
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +2573 -2573
- 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,
|
|
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 = [
|
|
@@ -129,107 +129,19 @@ Pulled 58 documents from tenant my-tenant
|
|
|
129
129
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
130
130
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
131
131
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
132
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
133
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
134
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
132
135
|
// Track filenames per type to handle duplicates
|
|
133
136
|
const filenameCounters = new Map();
|
|
134
137
|
let writtenCount = 0;
|
|
135
138
|
for (const doc of documents) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 === 'channel') {
|
|
194
|
-
// Realtime v2 — see workspace/pull for the full rationale.
|
|
195
|
-
// channel "rooms/{room_id}" → channel/rooms/[room_id]/_channel.xs
|
|
196
|
-
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
197
|
-
baseName = '_channel';
|
|
198
|
-
}
|
|
199
|
-
else if (doc.type === 'message' && doc.channel) {
|
|
200
|
-
// Realtime v2 message → channel/{channel_path}/{message_name}.xs
|
|
201
|
-
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
202
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
203
|
-
}
|
|
204
|
-
else if (doc.type === 'api_group') {
|
|
205
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
206
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
207
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
208
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
209
|
-
}
|
|
210
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
211
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
212
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
213
|
-
const nameParts = doc.name.split('/');
|
|
214
|
-
const leafName = nameParts.pop();
|
|
215
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
216
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
217
|
-
baseName = this.sanitizeFilename(leafName);
|
|
218
|
-
if (doc.verb) {
|
|
219
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
// Default: split folder path from name
|
|
224
|
-
const nameParts = doc.name.split('/');
|
|
225
|
-
const leafName = nameParts.pop();
|
|
226
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
227
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
228
|
-
baseName = this.sanitizeFilename(leafName);
|
|
229
|
-
if (doc.verb) {
|
|
230
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
139
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
140
|
+
getApiGroupFolder,
|
|
141
|
+
getChannelServer,
|
|
142
|
+
join: path.join,
|
|
143
|
+
snakeCase,
|
|
144
|
+
});
|
|
233
145
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
234
146
|
// Track duplicates per directory
|
|
235
147
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -248,12 +160,4 @@ Pulled 58 documents from tenant my-tenant
|
|
|
248
160
|
}
|
|
249
161
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
250
162
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Sanitize a document name for use as a filename.
|
|
253
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
254
|
-
* characters that are unsafe in filenames.
|
|
255
|
-
*/
|
|
256
|
-
sanitizeFilename(name) {
|
|
257
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
258
|
-
}
|
|
259
163
|
}
|
|
@@ -5,7 +5,7 @@ import * as os from 'node:os';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import snakeCase from 'lodash.snakecase';
|
|
7
7
|
import BaseCommand, { buildUserAgent } from '../../../../base-command.js';
|
|
8
|
-
import { buildApiGroupFolderResolver, channelPathSegments, parseDocument, } from '../../../../utils/document-parser.js';
|
|
8
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, channelPathSegments, parseDocument, } from '../../../../utils/document-parser.js';
|
|
9
9
|
export default class GitPull extends BaseCommand {
|
|
10
10
|
static description = 'Pull XanoScript files from a git repository into a local directory';
|
|
11
11
|
static examples = [
|
|
@@ -97,10 +97,13 @@ export default class GitPull extends BaseCommand {
|
|
|
97
97
|
// Write documents to output directory using the same file tree logic as workspace pull
|
|
98
98
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
99
99
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
100
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
101
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
102
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
100
103
|
const filenameCounters = new Map();
|
|
101
104
|
let writtenCount = 0;
|
|
102
105
|
for (const doc of documents) {
|
|
103
|
-
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder);
|
|
106
|
+
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer);
|
|
104
107
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
105
108
|
// Track duplicates per directory
|
|
106
109
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -325,7 +328,7 @@ export default class GitPull extends BaseCommand {
|
|
|
325
328
|
* Resolve the output directory and base filename for a parsed document.
|
|
326
329
|
* Uses the same type-to-directory mapping as workspace pull.
|
|
327
330
|
*/
|
|
328
|
-
resolveOutputPath(outputDir, doc, getApiGroupFolder) {
|
|
331
|
+
resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer) {
|
|
329
332
|
let typeDir;
|
|
330
333
|
let baseName;
|
|
331
334
|
if (doc.type === 'workspace') {
|
|
@@ -373,13 +376,51 @@ export default class GitPull extends BaseCommand {
|
|
|
373
376
|
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
374
377
|
baseName = this.sanitizeFilename(doc.name);
|
|
375
378
|
}
|
|
379
|
+
else if (doc.type === 'realtime_server') {
|
|
380
|
+
// Realtime v2 — see workspace/pull for the full rationale. Its own
|
|
381
|
+
// document is named after itself, mirroring api_group.
|
|
382
|
+
// realtime_server "chat" → realtime/server/chat/chat.xs
|
|
383
|
+
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.name));
|
|
384
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
385
|
+
}
|
|
376
386
|
else if (doc.type === 'channel') {
|
|
377
|
-
// Realtime v2
|
|
378
|
-
|
|
379
|
-
|
|
387
|
+
// Realtime v2. The channel owns a directory named after itself (the full
|
|
388
|
+
// channel name, snake_cased into a single flat segment — like every other
|
|
389
|
+
// object), and its messages live in a message/ subfolder inside it. It
|
|
390
|
+
// nests under its owning realtime_server (from `server = "..."`):
|
|
391
|
+
// channel "rooms/{room_id}" (server "chat")
|
|
392
|
+
// → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
|
|
393
|
+
//
|
|
394
|
+
// The channel name is snake_cased as a whole (rooms/{room_id} →
|
|
395
|
+
// rooms_room_id), so there is no path nesting and no bracket escaping —
|
|
396
|
+
// the on-disk name is a display form; the .xs content keeps the real
|
|
397
|
+
// name. A channel with no resolvable server (e.g. a pre-server export)
|
|
398
|
+
// falls back to the legacy flat channel/<path>/_channel.xs layout.
|
|
399
|
+
if (doc.server) {
|
|
400
|
+
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.server), 'channel', snakeCase(doc.name));
|
|
401
|
+
baseName = snakeCase(doc.name);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
405
|
+
baseName = '_channel';
|
|
406
|
+
}
|
|
380
407
|
}
|
|
381
408
|
else if (doc.type === 'message' && doc.channel) {
|
|
382
|
-
|
|
409
|
+
// Realtime v2 message → nests in a message/ subfolder under its channel,
|
|
410
|
+
// under that channel's server. The message names only its channel; the
|
|
411
|
+
// server is resolved by looking that channel up in the same multidoc
|
|
412
|
+
// (two-pass resolve).
|
|
413
|
+
// message "post" on channel "rooms/{room_id}" (server "chat")
|
|
414
|
+
// → realtime/server/chat/channel/rooms_room_id/message/post.xs
|
|
415
|
+
//
|
|
416
|
+
// Nesting matters beyond tidiness: message names are unique only WITHIN
|
|
417
|
+
// a channel, so a flat message/ directory would collide when two
|
|
418
|
+
// channels both define e.g. "say". A channel whose server can't be
|
|
419
|
+
// resolved falls back to the legacy flat channel/ layout.
|
|
420
|
+
const messageServer = getChannelServer(doc.channel);
|
|
421
|
+
typeDir = messageServer
|
|
422
|
+
? path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(messageServer), 'channel', snakeCase(doc.channel), 'message')
|
|
423
|
+
: path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
383
424
|
baseName = this.sanitizeFilename(doc.name);
|
|
384
425
|
}
|
|
385
426
|
else if (doc.type === 'api_group') {
|
|
@@ -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
|
}
|