@xano/cli 1.1.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.
- package/README.md +155 -4
- package/dist/base-command.d.ts +43 -36
- package/dist/base-command.js +223 -160
- package/dist/commands/auth/index.d.ts +1 -1
- package/dist/commands/auth/index.js +7 -7
- package/dist/commands/branch/create/index.d.ts +1 -1
- package/dist/commands/branch/create/index.js +1 -1
- 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 +20 -0
- package/dist/commands/ephemeral/impersonate/index.js +120 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +123 -0
- package/dist/commands/ephemeral/pull/index.d.ts +19 -0
- package/dist/commands/ephemeral/pull/index.js +163 -0
- package/dist/commands/ephemeral/push/index.d.ts +35 -0
- package/dist/commands/ephemeral/push/index.js +223 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
- package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/create/index.js +92 -0
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
- package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/get/index.js +101 -0
- package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
- package/dist/commands/ephemeral/static_host/list/index.js +132 -0
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/create/index.js +1 -1
- package/dist/commands/function/edit/index.js +26 -32
- package/dist/commands/function/get/index.js +2 -3
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -6
- package/dist/commands/profile/create/index.d.ts +1 -1
- package/dist/commands/profile/create/index.js +7 -7
- package/dist/commands/profile/me/index.js +1 -1
- package/dist/commands/profile/use/index.js +0 -4
- package/dist/commands/profile/wizard/index.d.ts +1 -1
- package/dist/commands/profile/wizard/index.js +12 -10
- package/dist/commands/profile/workspace/set/index.js +1 -1
- package/dist/commands/release/edit/index.js +2 -2
- package/dist/commands/release/export/index.js +1 -2
- package/dist/commands/release/import/index.js +1 -1
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +13 -99
- package/dist/commands/release/push/index.js +2 -2
- package/dist/commands/sandbox/env/get_all/index.js +1 -1
- package/dist/commands/sandbox/env/set_all/index.js +1 -1
- package/dist/commands/sandbox/get/index.js +1 -1
- package/dist/commands/sandbox/license/get/index.js +1 -1
- package/dist/commands/sandbox/license/set/index.js +1 -1
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +13 -81
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +5 -5
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.js +9 -1
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.js +9 -1
- package/dist/commands/static_host/build/create/index.d.ts +1 -1
- package/dist/commands/static_host/build/create/index.js +2 -2
- package/dist/commands/static_host/build/pull/index.js +1 -1
- package/dist/commands/static_host/build/push/index.js +1 -1
- package/dist/commands/static_host/edit/index.js +1 -1
- package/dist/commands/tenant/backup/export/index.js +1 -2
- package/dist/commands/tenant/backup/import/index.js +1 -1
- package/dist/commands/tenant/cluster/create/index.js +1 -1
- package/dist/commands/tenant/cluster/license/get/index.js +1 -1
- package/dist/commands/tenant/cluster/license/set/index.js +1 -1
- package/dist/commands/tenant/create/index.d.ts +1 -1
- package/dist/commands/tenant/create/index.js +6 -6
- package/dist/commands/tenant/deploy_platform/index.js +1 -1
- package/dist/commands/tenant/edit/index.js +7 -7
- package/dist/commands/tenant/env/get_all/index.js +1 -1
- package/dist/commands/tenant/env/set_all/index.js +1 -1
- package/dist/commands/tenant/impersonate/index.d.ts +1 -1
- package/dist/commands/tenant/impersonate/index.js +14 -14
- package/dist/commands/tenant/license/get/index.js +1 -1
- package/dist/commands/tenant/license/set/index.js +1 -1
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +14 -100
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.js +13 -1
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.js +13 -1
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.js +13 -1
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.js +15 -3
- package/dist/commands/workspace/git/pull/index.d.ts +4 -4
- package/dist/commands/workspace/git/pull/index.js +158 -97
- package/dist/commands/workspace/pull/index.d.ts +1 -7
- package/dist/commands/workspace/pull/index.js +17 -103
- package/dist/help.js +4 -2
- package/dist/update-check.d.ts +1 -1
- package/dist/update-check.js +1 -1
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +311 -4
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -6
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +84 -0
- package/dist/utils/multidoc-push.d.ts +88 -0
- package/dist/utils/multidoc-push.js +378 -15
- package/dist/utils/paging.d.ts +2 -1
- package/dist/utils/paging.js +0 -1
- package/dist/utils/reference-checker.js +14 -3
- package/oclif.manifest.json +5898 -3804
- package/package.json +10 -1
|
@@ -82,7 +82,15 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
82
82
|
},
|
|
83
83
|
});
|
|
84
84
|
if (tests.length === 0) {
|
|
85
|
-
|
|
85
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
86
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
87
|
+
// with the same keys the populated path emits below.
|
|
88
|
+
if (flags.output === 'json') {
|
|
89
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [], total_timing: 0 }, null, 2));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.log('No workflow tests found');
|
|
93
|
+
}
|
|
86
94
|
return;
|
|
87
95
|
}
|
|
88
96
|
if (flags.output === 'summary') {
|
|
@@ -134,9 +142,9 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
134
142
|
timing: runResult.timing,
|
|
135
143
|
};
|
|
136
144
|
results.push(result);
|
|
137
|
-
totalTiming += runResult.timing;
|
|
145
|
+
totalTiming += runResult.timing ?? 0;
|
|
138
146
|
if (flags.output === 'summary') {
|
|
139
|
-
const timing = `(${runResult.timing.toFixed(3)}s)`;
|
|
147
|
+
const timing = `(${(runResult.timing ?? 0).toFixed(3)}s)`;
|
|
140
148
|
if (passed) {
|
|
141
149
|
log(`PASS ${test.name} ${timing}`);
|
|
142
150
|
}
|
|
@@ -149,6 +157,8 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
catch (error) {
|
|
160
|
+
if (error instanceof Error && 'oclif' in error)
|
|
161
|
+
throw error;
|
|
152
162
|
const message = error instanceof Error ? error.message : String(error);
|
|
153
163
|
results.push({
|
|
154
164
|
message,
|
|
@@ -189,6 +199,8 @@ Results: 2 passed, 1 failed (2.691s total)
|
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
catch (error) {
|
|
202
|
+
if (error instanceof Error && 'oclif' in error)
|
|
203
|
+
throw error;
|
|
192
204
|
if (error instanceof Error) {
|
|
193
205
|
this.error(`Failed to run workflow tests: ${error.message}`);
|
|
194
206
|
}
|
|
@@ -13,14 +13,14 @@ export default class GitPull extends BaseCommand {
|
|
|
13
13
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
18
|
-
*/
|
|
19
|
-
private collectFiles;
|
|
20
16
|
/**
|
|
21
17
|
* Clone a git repository using shallow clone.
|
|
22
18
|
*/
|
|
23
19
|
private cloneRepo;
|
|
20
|
+
/**
|
|
21
|
+
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
22
|
+
*/
|
|
23
|
+
private collectFiles;
|
|
24
24
|
/**
|
|
25
25
|
* Fetch a GitHub repository via the tarball API for fast download without requiring git.
|
|
26
26
|
*/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import snakeCase from 'lodash.snakecase';
|
|
2
3
|
import { execSync } from 'node:child_process';
|
|
3
4
|
import * as fs from 'node:fs';
|
|
4
5
|
import * as os from 'node:os';
|
|
5
|
-
import
|
|
6
|
-
import snakeCase from 'lodash.snakecase';
|
|
6
|
+
import path from 'node:path';
|
|
7
7
|
import BaseCommand, { buildUserAgent } from '../../../../base-command.js';
|
|
8
|
-
import { buildApiGroupFolderResolver, 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 = [
|
|
@@ -60,13 +60,7 @@ export default class GitPull extends BaseCommand {
|
|
|
60
60
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xano-git-pull-'));
|
|
61
61
|
try {
|
|
62
62
|
// Fetch repository contents
|
|
63
|
-
|
|
64
|
-
if (repoInfo.host === 'github') {
|
|
65
|
-
repoRoot = await this.fetchGitHubTarball(repoInfo.owner, repoInfo.repo, ref, token, tempDir, flags.verbose);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
repoRoot = this.cloneRepo(repoInfo.url, ref, token, tempDir, flags.verbose);
|
|
69
|
-
}
|
|
63
|
+
const repoRoot = repoInfo.host === 'github' ? (await this.fetchGitHubTarball(repoInfo.owner, repoInfo.repo, ref, token, tempDir, flags.verbose)) : this.cloneRepo(repoInfo.url, ref, token, tempDir, flags.verbose);
|
|
70
64
|
// Determine source directory (optionally scoped to --path or URL path)
|
|
71
65
|
const sourceDir = subPath ? path.join(repoRoot, subPath) : repoRoot;
|
|
72
66
|
if (!fs.existsSync(sourceDir)) {
|
|
@@ -97,10 +91,13 @@ export default class GitPull extends BaseCommand {
|
|
|
97
91
|
// Write documents to output directory using the same file tree logic as workspace pull
|
|
98
92
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
99
93
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
94
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
95
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
96
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
100
97
|
const filenameCounters = new Map();
|
|
101
98
|
let writtenCount = 0;
|
|
102
99
|
for (const doc of documents) {
|
|
103
|
-
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder);
|
|
100
|
+
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer);
|
|
104
101
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
105
102
|
// Track duplicates per directory
|
|
106
103
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -123,23 +120,6 @@ export default class GitPull extends BaseCommand {
|
|
|
123
120
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
128
|
-
*/
|
|
129
|
-
collectFiles(dir) {
|
|
130
|
-
const files = [];
|
|
131
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
const fullPath = path.join(dir, entry.name);
|
|
134
|
-
if (entry.isDirectory()) {
|
|
135
|
-
files.push(...this.collectFiles(fullPath));
|
|
136
|
-
}
|
|
137
|
-
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
138
|
-
files.push(fullPath);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return files.sort();
|
|
142
|
-
}
|
|
143
123
|
/**
|
|
144
124
|
* Clone a git repository using shallow clone.
|
|
145
125
|
*/
|
|
@@ -168,6 +148,23 @@ export default class GitPull extends BaseCommand {
|
|
|
168
148
|
}
|
|
169
149
|
return cloneTarget;
|
|
170
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
153
|
+
*/
|
|
154
|
+
collectFiles(dir) {
|
|
155
|
+
const files = [];
|
|
156
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
const fullPath = path.join(dir, entry.name);
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
files.push(...this.collectFiles(fullPath));
|
|
161
|
+
}
|
|
162
|
+
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
163
|
+
files.push(fullPath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return files.sort();
|
|
167
|
+
}
|
|
171
168
|
/**
|
|
172
169
|
* Fetch a GitHub repository via the tarball API for fast download without requiring git.
|
|
173
170
|
*/
|
|
@@ -325,78 +322,142 @@ export default class GitPull extends BaseCommand {
|
|
|
325
322
|
* Resolve the output directory and base filename for a parsed document.
|
|
326
323
|
* Uses the same type-to-directory mapping as workspace pull.
|
|
327
324
|
*/
|
|
328
|
-
resolveOutputPath(outputDir, doc, getApiGroupFolder) {
|
|
325
|
+
resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer) {
|
|
329
326
|
let typeDir;
|
|
330
327
|
let baseName;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
337
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
338
|
-
}
|
|
339
|
-
else if (doc.type === 'error_trigger') {
|
|
340
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
341
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
342
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
343
|
-
}
|
|
344
|
-
else if (doc.type === 'agent') {
|
|
345
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
346
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
347
|
-
}
|
|
348
|
-
else if (doc.type === 'mcp_server') {
|
|
349
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
350
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
351
|
-
}
|
|
352
|
-
else if (doc.type === 'tool') {
|
|
353
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
354
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
355
|
-
}
|
|
356
|
-
else if (doc.type === 'agent_trigger') {
|
|
357
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
358
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
359
|
-
}
|
|
360
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
361
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
362
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
363
|
-
}
|
|
364
|
-
else if (doc.type === 'table_trigger') {
|
|
365
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
366
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
367
|
-
}
|
|
368
|
-
else if (doc.type === 'realtime_channel') {
|
|
369
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
370
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
371
|
-
}
|
|
372
|
-
else if (doc.type === 'realtime_trigger') {
|
|
373
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
374
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
375
|
-
}
|
|
376
|
-
else if (doc.type === 'api_group') {
|
|
377
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
378
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
379
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
380
|
-
}
|
|
381
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
382
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
383
|
-
const nameParts = doc.name.split('/');
|
|
384
|
-
const leafName = nameParts.pop();
|
|
385
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
386
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
387
|
-
baseName = this.sanitizeFilename(leafName);
|
|
388
|
-
if (doc.verb) {
|
|
389
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
328
|
+
switch (doc.type) {
|
|
329
|
+
case 'agent': {
|
|
330
|
+
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
331
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
332
|
+
break;
|
|
390
333
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
334
|
+
case 'agent_trigger': {
|
|
335
|
+
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
336
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case 'channel': {
|
|
340
|
+
// Realtime v2. The channel owns a directory named after itself (the full
|
|
341
|
+
// channel name, snake_cased into a single flat segment — like every other
|
|
342
|
+
// object), and its messages live in a message/ subfolder inside it. It
|
|
343
|
+
// nests under its owning realtime_server (from `server = "..."`):
|
|
344
|
+
// channel "rooms/{room_id}" (server "chat")
|
|
345
|
+
// → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
|
|
346
|
+
//
|
|
347
|
+
// The channel name is snake_cased as a whole (rooms/{room_id} →
|
|
348
|
+
// rooms_room_id), so there is no path nesting and no bracket escaping —
|
|
349
|
+
// the on-disk name is a display form; the .xs content keeps the real
|
|
350
|
+
// name. A channel with no resolvable server (e.g. a pre-server export)
|
|
351
|
+
// falls back to the legacy flat channel/<path>/_channel.xs layout.
|
|
352
|
+
if (doc.server) {
|
|
353
|
+
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.server), 'channel', snakeCase(doc.name));
|
|
354
|
+
baseName = snakeCase(doc.name);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
358
|
+
baseName = '_channel';
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 'error_trigger': {
|
|
363
|
+
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
364
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
365
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case 'mcp_server': {
|
|
369
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
370
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'mcp_server_trigger': {
|
|
374
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
375
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case 'realtime_channel': {
|
|
379
|
+
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
380
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case 'realtime_server': {
|
|
384
|
+
// Realtime v2 — see workspace/pull for the full rationale. Its own
|
|
385
|
+
// document is named after itself, mirroring api_group.
|
|
386
|
+
// realtime_server "chat" → realtime/server/chat/chat.xs
|
|
387
|
+
typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.name));
|
|
388
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'realtime_trigger': {
|
|
392
|
+
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
393
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case 'table_trigger': {
|
|
397
|
+
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
398
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case 'tool': {
|
|
402
|
+
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
403
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case 'workspace': {
|
|
407
|
+
typeDir = path.join(outputDir, 'workspace');
|
|
408
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case 'workspace_trigger': {
|
|
412
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
413
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
default: {
|
|
417
|
+
if (doc.type === 'message' && doc.channel) {
|
|
418
|
+
// Realtime v2 message → nests in a message/ subfolder under its channel,
|
|
419
|
+
// under that channel's server. The message names only its channel; the
|
|
420
|
+
// server is resolved by looking that channel up in the same multidoc
|
|
421
|
+
// (two-pass resolve).
|
|
422
|
+
// message "post" on channel "rooms/{room_id}" (server "chat")
|
|
423
|
+
// → realtime/server/chat/channel/rooms_room_id/message/post.xs
|
|
424
|
+
//
|
|
425
|
+
// Nesting matters beyond tidiness: message names are unique only WITHIN
|
|
426
|
+
// a channel, so a flat message/ directory would collide when two
|
|
427
|
+
// channels both define e.g. "say". A channel whose server can't be
|
|
428
|
+
// resolved falls back to the legacy flat channel/ layout.
|
|
429
|
+
const messageServer = getChannelServer(doc.channel);
|
|
430
|
+
typeDir = messageServer
|
|
431
|
+
? path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(messageServer), 'channel', snakeCase(doc.channel), 'message')
|
|
432
|
+
: path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
433
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
434
|
+
}
|
|
435
|
+
else if (doc.type === 'api_group') {
|
|
436
|
+
const groupFolder = getApiGroupFolder(doc.name);
|
|
437
|
+
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
438
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
439
|
+
}
|
|
440
|
+
else if (doc.type === 'query' && doc.apiGroup) {
|
|
441
|
+
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
442
|
+
const nameParts = doc.name.split('/');
|
|
443
|
+
const leafName = nameParts.pop();
|
|
444
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
445
|
+
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
446
|
+
baseName = this.sanitizeFilename(leafName);
|
|
447
|
+
if (doc.verb) {
|
|
448
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const nameParts = doc.name.split('/');
|
|
453
|
+
const leafName = nameParts.pop();
|
|
454
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
455
|
+
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
456
|
+
baseName = this.sanitizeFilename(leafName);
|
|
457
|
+
if (doc.verb) {
|
|
458
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
400
461
|
}
|
|
401
462
|
}
|
|
402
463
|
return { baseName, typeDir };
|
|
@@ -5,8 +5,8 @@ export default class Pull extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
8
|
draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -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
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
2
|
import snakeCase from 'lodash.snakecase';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
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
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
8
8
|
export default class Pull extends BaseCommand {
|
|
9
9
|
static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
|
|
@@ -36,14 +36,14 @@ Pulled 58 documents
|
|
|
36
36
|
description: 'Output directory for pulled documents (defaults to current directory)',
|
|
37
37
|
required: false,
|
|
38
38
|
}),
|
|
39
|
-
|
|
39
|
+
draft: Flags.boolean({
|
|
40
40
|
default: false,
|
|
41
|
-
description: 'Include
|
|
41
|
+
description: 'Include draft versions',
|
|
42
42
|
required: false,
|
|
43
43
|
}),
|
|
44
|
-
|
|
44
|
+
env: Flags.boolean({
|
|
45
45
|
default: false,
|
|
46
|
-
description: 'Include
|
|
46
|
+
description: 'Include environment variables',
|
|
47
47
|
required: false,
|
|
48
48
|
}),
|
|
49
49
|
records: Flags.boolean({
|
|
@@ -134,96 +134,19 @@ Pulled 58 documents
|
|
|
134
134
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
135
135
|
// where different names produce the same snakeCase (e.g., "Authentication" vs "authentication")
|
|
136
136
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
137
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
138
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
139
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
137
140
|
// Track filenames per type to handle duplicates
|
|
138
141
|
const filenameCounters = new Map();
|
|
139
142
|
let writtenCount = 0;
|
|
140
143
|
for (const doc of documents) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
else if (doc.type === 'workspace_trigger') {
|
|
149
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
150
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
151
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
152
|
-
}
|
|
153
|
-
else if (doc.type === 'error_trigger') {
|
|
154
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
155
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
156
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
157
|
-
}
|
|
158
|
-
else if (doc.type === 'agent') {
|
|
159
|
-
// agent → ai/agent/{name}.xs
|
|
160
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
161
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
162
|
-
}
|
|
163
|
-
else if (doc.type === 'mcp_server') {
|
|
164
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
165
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
166
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
167
|
-
}
|
|
168
|
-
else if (doc.type === 'tool') {
|
|
169
|
-
// tool → ai/tool/{name}.xs
|
|
170
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
171
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
172
|
-
}
|
|
173
|
-
else if (doc.type === 'agent_trigger') {
|
|
174
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
175
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
176
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
177
|
-
}
|
|
178
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
179
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
180
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
181
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
182
|
-
}
|
|
183
|
-
else if (doc.type === 'table_trigger') {
|
|
184
|
-
// table_trigger → table/trigger/{name}.xs
|
|
185
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
186
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
187
|
-
}
|
|
188
|
-
else if (doc.type === 'realtime_channel') {
|
|
189
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
190
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
191
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
-
}
|
|
193
|
-
else if (doc.type === 'realtime_trigger') {
|
|
194
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
195
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
196
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
197
|
-
}
|
|
198
|
-
else if (doc.type === 'api_group') {
|
|
199
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
200
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
201
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
202
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
203
|
-
}
|
|
204
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
205
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
206
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
207
|
-
const nameParts = doc.name.split('/');
|
|
208
|
-
const leafName = nameParts.pop();
|
|
209
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
210
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
211
|
-
baseName = this.sanitizeFilename(leafName);
|
|
212
|
-
if (doc.verb) {
|
|
213
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
// Default: split folder path from name
|
|
218
|
-
const nameParts = doc.name.split('/');
|
|
219
|
-
const leafName = nameParts.pop();
|
|
220
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
221
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
222
|
-
baseName = this.sanitizeFilename(leafName);
|
|
223
|
-
if (doc.verb) {
|
|
224
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
144
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
145
|
+
getApiGroupFolder,
|
|
146
|
+
getChannelServer,
|
|
147
|
+
join: path.join,
|
|
148
|
+
snakeCase,
|
|
149
|
+
});
|
|
227
150
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
228
151
|
// Track duplicates per directory
|
|
229
152
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -234,8 +157,7 @@ Pulled 58 documents
|
|
|
234
157
|
const count = typeCounters.get(baseName) || 0;
|
|
235
158
|
typeCounters.set(baseName, count + 1);
|
|
236
159
|
// Append numeric suffix for duplicates
|
|
237
|
-
|
|
238
|
-
filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
160
|
+
const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
239
161
|
const filePath = path.join(typeDir, filename);
|
|
240
162
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
241
163
|
writtenCount++;
|
|
@@ -252,12 +174,4 @@ Pulled 58 documents
|
|
|
252
174
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
253
175
|
this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
|
|
254
176
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Sanitize a document name for use as a filename.
|
|
257
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
258
|
-
* characters that are unsafe in filenames.
|
|
259
|
-
*/
|
|
260
|
-
sanitizeFilename(name) {
|
|
261
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
262
|
-
}
|
|
263
177
|
}
|
package/dist/help.js
CHANGED
|
@@ -45,7 +45,9 @@ export default class Help extends BaseHelp {
|
|
|
45
45
|
if (this.config.topicSeparator !== ':')
|
|
46
46
|
c.id = c.id.replaceAll(':', this.config.topicSeparator);
|
|
47
47
|
const summary = this.summary(c);
|
|
48
|
-
|
|
48
|
+
// Strip ANSI colour codes from the summary so the help table aligns.
|
|
49
|
+
// eslint-disable-next-line no-control-regex -- matching the escape char is the point
|
|
50
|
+
return [c.id, summary ? summary.replaceAll(/\u001B\[\d+m/g, '') : ''];
|
|
49
51
|
});
|
|
50
52
|
// Only add promoted commands at the root level, not within a specific topic
|
|
51
53
|
if (isRootHelp) {
|
|
@@ -59,6 +61,6 @@ export default class Help extends BaseHelp {
|
|
|
59
61
|
spacer: '\n',
|
|
60
62
|
stripAnsi: this.opts.stripAnsi,
|
|
61
63
|
});
|
|
62
|
-
return this.section('COMMANDS', body + `\n\n\
|
|
64
|
+
return this.section('COMMANDS', body + `\n\n\u001B[2mSee xano <topic> --help for all commands in a topic.\u001B[0m`);
|
|
63
65
|
}
|
|
64
66
|
}
|
package/dist/update-check.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export declare function clearUpdateCache(): void;
|
|
|
5
5
|
*
|
|
6
6
|
* The check hits npm at most once every 24 hours and caches the result.
|
|
7
7
|
*/
|
|
8
|
-
export declare function checkForUpdate(currentVersion: string, forceCheck?: boolean):
|
|
8
|
+
export declare function checkForUpdate(currentVersion: string, forceCheck?: boolean): null | string;
|
package/dist/update-check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
3
|
import * as os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
const UPDATE_CHECK_FILE = path.join(os.homedir(), '.xano', 'update-check.json');
|
|
6
6
|
const CHECK_INTERVAL_MS = 8 * 60 * 60 * 1000; // 8 hours
|
|
7
7
|
function isBeta(version) {
|