@xano/cli 1.0.6-beta.9 → 1.2.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 +46 -62
- package/dist/base-command.d.ts +0 -2
- package/dist/base-command.js +4 -10
- package/dist/commands/release/pull/index.d.ts +6 -0
- package/dist/commands/release/pull/index.js +95 -10
- package/dist/commands/sandbox/pull/index.d.ts +1 -0
- package/dist/commands/sandbox/pull/index.js +77 -10
- 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/tenant/pull/index.d.ts +6 -0
- package/dist/commands/tenant/pull/index.js +95 -10
- 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.js +3 -53
- package/dist/commands/workspace/pull/index.d.ts +6 -0
- package/dist/commands/workspace/pull/index.js +95 -10
- package/dist/utils/document-parser.d.ts +0 -85
- package/dist/utils/document-parser.js +1 -214
- package/dist/utils/knowledge-sync.d.ts +0 -1
- package/dist/utils/knowledge-sync.js +5 -11
- package/dist/utils/paging.d.ts +1 -2
- package/dist/utils/reference-checker.js +0 -13
- package/oclif.manifest.json +2796 -4820
- package/package.json +1 -10
- package/dist/commands/ephemeral/create/index.d.ts +0 -18
- package/dist/commands/ephemeral/create/index.js +0 -103
- package/dist/commands/ephemeral/delete/index.d.ts +0 -18
- package/dist/commands/ephemeral/delete/index.js +0 -99
- package/dist/commands/ephemeral/edit/index.d.ts +0 -18
- package/dist/commands/ephemeral/edit/index.js +0 -109
- package/dist/commands/ephemeral/get/index.d.ts +0 -21
- package/dist/commands/ephemeral/get/index.js +0 -125
- package/dist/commands/ephemeral/impersonate/index.d.ts +0 -20
- package/dist/commands/ephemeral/impersonate/index.js +0 -120
- package/dist/commands/ephemeral/list/index.d.ts +0 -19
- package/dist/commands/ephemeral/list/index.js +0 -123
- package/dist/commands/ephemeral/pull/index.d.ts +0 -19
- package/dist/commands/ephemeral/pull/index.js +0 -164
- package/dist/commands/ephemeral/push/index.d.ts +0 -35
- package/dist/commands/ephemeral/push/index.js +0 -221
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +0 -20
- package/dist/commands/ephemeral/static_host/build/delete/index.js +0 -121
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +0 -18
- package/dist/commands/ephemeral/static_host/build/get/index.js +0 -137
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +0 -18
- package/dist/commands/ephemeral/static_host/build/list/index.js +0 -137
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +0 -53
- package/dist/commands/ephemeral/static_host/build/pull/index.js +0 -308
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +0 -24
- package/dist/commands/ephemeral/static_host/build/push/index.js +0 -250
- package/dist/commands/ephemeral/static_host/create/index.d.ts +0 -18
- package/dist/commands/ephemeral/static_host/create/index.js +0 -92
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +0 -19
- package/dist/commands/ephemeral/static_host/deploy/index.js +0 -112
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +0 -24
- package/dist/commands/ephemeral/static_host/edit/index.js +0 -158
- package/dist/commands/ephemeral/static_host/get/index.d.ts +0 -19
- package/dist/commands/ephemeral/static_host/get/index.js +0 -101
- package/dist/commands/ephemeral/static_host/list/index.d.ts +0 -17
- package/dist/commands/ephemeral/static_host/list/index.js +0 -132
- package/dist/utils/microservice-wait.d.ts +0 -53
- package/dist/utils/microservice-wait.js +0 -83
|
@@ -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, parseDocument } 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,19 +129,96 @@ 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);
|
|
135
132
|
// Track filenames per type to handle duplicates
|
|
136
133
|
const filenameCounters = new Map();
|
|
137
134
|
let writtenCount = 0;
|
|
138
135
|
for (const doc of documents) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
136
|
+
let typeDir;
|
|
137
|
+
let baseName;
|
|
138
|
+
if (doc.type === 'workspace') {
|
|
139
|
+
// workspace → workspace/{name}.xs
|
|
140
|
+
typeDir = path.join(outputDir, 'workspace');
|
|
141
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
142
|
+
}
|
|
143
|
+
else if (doc.type === 'workspace_trigger') {
|
|
144
|
+
// workspace_trigger → workspace/trigger/{name}.xs
|
|
145
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
146
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
147
|
+
}
|
|
148
|
+
else if (doc.type === 'error_trigger') {
|
|
149
|
+
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
150
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
151
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
152
|
+
}
|
|
153
|
+
else if (doc.type === 'agent') {
|
|
154
|
+
// agent → ai/agent/{name}.xs
|
|
155
|
+
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
156
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
157
|
+
}
|
|
158
|
+
else if (doc.type === 'mcp_server') {
|
|
159
|
+
// mcp_server → ai/mcp_server/{name}.xs
|
|
160
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
161
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
162
|
+
}
|
|
163
|
+
else if (doc.type === 'tool') {
|
|
164
|
+
// tool → ai/tool/{name}.xs
|
|
165
|
+
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
166
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
167
|
+
}
|
|
168
|
+
else if (doc.type === 'agent_trigger') {
|
|
169
|
+
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
170
|
+
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
171
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
172
|
+
}
|
|
173
|
+
else if (doc.type === 'mcp_server_trigger') {
|
|
174
|
+
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
175
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
176
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
177
|
+
}
|
|
178
|
+
else if (doc.type === 'table_trigger') {
|
|
179
|
+
// table_trigger → table/trigger/{name}.xs
|
|
180
|
+
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
181
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
182
|
+
}
|
|
183
|
+
else if (doc.type === 'realtime_channel') {
|
|
184
|
+
// realtime_channel → realtime/channel/{name}.xs
|
|
185
|
+
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
186
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
187
|
+
}
|
|
188
|
+
else if (doc.type === 'realtime_trigger') {
|
|
189
|
+
// realtime_trigger → realtime/trigger/{name}.xs
|
|
190
|
+
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
191
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
+
}
|
|
193
|
+
else if (doc.type === 'api_group') {
|
|
194
|
+
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
195
|
+
const groupFolder = getApiGroupFolder(doc.name);
|
|
196
|
+
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
197
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
198
|
+
}
|
|
199
|
+
else if (doc.type === 'query' && doc.apiGroup) {
|
|
200
|
+
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
201
|
+
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
202
|
+
const nameParts = doc.name.split('/');
|
|
203
|
+
const leafName = nameParts.pop();
|
|
204
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
205
|
+
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
206
|
+
baseName = this.sanitizeFilename(leafName);
|
|
207
|
+
if (doc.verb) {
|
|
208
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Default: split folder path from name
|
|
213
|
+
const nameParts = doc.name.split('/');
|
|
214
|
+
const leafName = nameParts.pop();
|
|
215
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
216
|
+
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
217
|
+
baseName = this.sanitizeFilename(leafName);
|
|
218
|
+
if (doc.verb) {
|
|
219
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
145
222
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
146
223
|
// Track duplicates per directory
|
|
147
224
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -160,4 +237,12 @@ Pulled 58 documents from tenant my-tenant
|
|
|
160
237
|
}
|
|
161
238
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
162
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Sanitize a document name for use as a filename.
|
|
242
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
243
|
+
* characters that are unsafe in filenames.
|
|
244
|
+
*/
|
|
245
|
+
sanitizeFilename(name) {
|
|
246
|
+
return snakeCase(name.replaceAll('"', ''));
|
|
247
|
+
}
|
|
163
248
|
}
|
|
@@ -68,26 +68,34 @@ Result: PASS
|
|
|
68
68
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
69
69
|
}
|
|
70
70
|
const result = (await response.json());
|
|
71
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
72
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
73
|
+
const passed = result.status === 'ok';
|
|
71
74
|
if (flags.output === 'json') {
|
|
72
75
|
this.log(JSON.stringify(result, null, 2));
|
|
73
76
|
}
|
|
77
|
+
else if (passed) {
|
|
78
|
+
this.log('Result: PASS');
|
|
79
|
+
}
|
|
74
80
|
else {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
81
|
-
for (const expect of failedExpects) {
|
|
82
|
-
if (expect.message) {
|
|
83
|
-
this.log(` Error: ${expect.message}`);
|
|
84
|
-
}
|
|
81
|
+
this.log('Result: FAIL');
|
|
82
|
+
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
83
|
+
for (const expect of failedExpects) {
|
|
84
|
+
if (expect.message) {
|
|
85
|
+
this.log(` Error: ${expect.message}`);
|
|
85
86
|
}
|
|
86
|
-
this.exit(1);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
90
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
91
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
92
|
+
if (!passed) {
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
89
95
|
}
|
|
90
96
|
catch (error) {
|
|
97
|
+
if (error instanceof Error && 'oclif' in error)
|
|
98
|
+
throw error;
|
|
91
99
|
if (error instanceof Error) {
|
|
92
100
|
this.error(`Failed to run unit test: ${error.message}`);
|
|
93
101
|
}
|
|
@@ -95,7 +95,15 @@ Results: 4 passed, 1 failed
|
|
|
95
95
|
},
|
|
96
96
|
});
|
|
97
97
|
if (tests.length === 0) {
|
|
98
|
-
|
|
98
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
99
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
100
|
+
// with the same keys the populated path emits below.
|
|
101
|
+
if (flags.output === 'json') {
|
|
102
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.log('No unit tests found');
|
|
106
|
+
}
|
|
99
107
|
return;
|
|
100
108
|
}
|
|
101
109
|
if (flags.output === 'summary') {
|
|
@@ -162,6 +170,8 @@ Results: 4 passed, 1 failed
|
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
catch (error) {
|
|
173
|
+
if (error instanceof Error && 'oclif' in error)
|
|
174
|
+
throw error;
|
|
165
175
|
const message = error instanceof Error ? error.message : String(error);
|
|
166
176
|
results.push({
|
|
167
177
|
message,
|
|
@@ -202,6 +212,8 @@ Results: 4 passed, 1 failed
|
|
|
202
212
|
}
|
|
203
213
|
}
|
|
204
214
|
catch (error) {
|
|
215
|
+
if (error instanceof Error && 'oclif' in error)
|
|
216
|
+
throw error;
|
|
205
217
|
if (error instanceof Error) {
|
|
206
218
|
this.error(`Failed to run unit tests: ${error.message}`);
|
|
207
219
|
}
|
|
@@ -61,24 +61,29 @@ Result: PASS (0.25s)
|
|
|
61
61
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
62
62
|
}
|
|
63
63
|
const result = (await response.json());
|
|
64
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
65
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
66
|
+
const passed = result.status === 'ok';
|
|
64
67
|
if (flags.output === 'json') {
|
|
65
68
|
this.log(JSON.stringify(result, null, 2));
|
|
66
69
|
}
|
|
67
70
|
else {
|
|
68
71
|
const timing = result.timing ? ` (${result.timing}s)` : '';
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
else {
|
|
73
|
-
this.log(`Result: FAIL${timing}`);
|
|
74
|
-
if (result.message) {
|
|
75
|
-
this.log(` Error: ${result.message}`);
|
|
76
|
-
}
|
|
77
|
-
this.exit(1);
|
|
72
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
73
|
+
if (!passed && result.message) {
|
|
74
|
+
this.log(` Error: ${result.message}`);
|
|
78
75
|
}
|
|
79
76
|
}
|
|
77
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
78
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
79
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
80
|
+
if (!passed) {
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
80
83
|
}
|
|
81
84
|
catch (error) {
|
|
85
|
+
if (error instanceof Error && 'oclif' in error)
|
|
86
|
+
throw error;
|
|
82
87
|
if (error instanceof Error) {
|
|
83
88
|
this.error(`Failed to run workflow test: ${error.message}`);
|
|
84
89
|
}
|
|
@@ -85,7 +85,15 @@ Results: 2 passed, 1 failed
|
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
87
|
if (tests.length === 0) {
|
|
88
|
-
|
|
88
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
89
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
90
|
+
// with the same keys the populated path emits below.
|
|
91
|
+
if (flags.output === 'json') {
|
|
92
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.log('No workflow tests found');
|
|
96
|
+
}
|
|
89
97
|
return;
|
|
90
98
|
}
|
|
91
99
|
if (flags.output === 'summary') {
|
|
@@ -146,6 +154,8 @@ Results: 2 passed, 1 failed
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
catch (error) {
|
|
157
|
+
if (error instanceof Error && 'oclif' in error)
|
|
158
|
+
throw error;
|
|
149
159
|
const message = error instanceof Error ? error.message : String(error);
|
|
150
160
|
results.push({
|
|
151
161
|
message,
|
|
@@ -184,6 +194,8 @@ Results: 2 passed, 1 failed
|
|
|
184
194
|
}
|
|
185
195
|
}
|
|
186
196
|
catch (error) {
|
|
197
|
+
if (error instanceof Error && 'oclif' in error)
|
|
198
|
+
throw error;
|
|
187
199
|
if (error instanceof Error) {
|
|
188
200
|
this.error(`Failed to run workflow tests: ${error.message}`);
|
|
189
201
|
}
|
|
@@ -63,26 +63,34 @@ Result: PASS
|
|
|
63
63
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
64
64
|
}
|
|
65
65
|
const result = await response.json();
|
|
66
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
67
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
68
|
+
const passed = result.status === 'ok';
|
|
66
69
|
if (flags.output === 'json') {
|
|
67
70
|
this.log(JSON.stringify(result, null, 2));
|
|
68
71
|
}
|
|
72
|
+
else if (passed) {
|
|
73
|
+
this.log('Result: PASS');
|
|
74
|
+
}
|
|
69
75
|
else {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const failedExpects = result.results?.filter(r => r.status === 'fail') ?? [];
|
|
76
|
-
for (const expect of failedExpects) {
|
|
77
|
-
if (expect.message) {
|
|
78
|
-
this.log(` Error: ${expect.message}`);
|
|
79
|
-
}
|
|
76
|
+
this.log('Result: FAIL');
|
|
77
|
+
const failedExpects = result.results?.filter(r => r.status === 'fail') ?? [];
|
|
78
|
+
for (const expect of failedExpects) {
|
|
79
|
+
if (expect.message) {
|
|
80
|
+
this.log(` Error: ${expect.message}`);
|
|
80
81
|
}
|
|
81
|
-
this.exit(1);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
85
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
86
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
87
|
+
if (!passed) {
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
84
90
|
}
|
|
85
91
|
catch (error) {
|
|
92
|
+
if (error instanceof Error && 'oclif' in error)
|
|
93
|
+
throw error;
|
|
86
94
|
if (error instanceof Error) {
|
|
87
95
|
this.error(`Failed to run unit test: ${error.message}`);
|
|
88
96
|
}
|
|
@@ -91,7 +91,15 @@ Results: 2 passed, 1 failed
|
|
|
91
91
|
},
|
|
92
92
|
});
|
|
93
93
|
if (tests.length === 0) {
|
|
94
|
-
|
|
94
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
95
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
96
|
+
// with the same keys the populated path emits below.
|
|
97
|
+
if (flags.output === 'json') {
|
|
98
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.log('No unit tests found');
|
|
102
|
+
}
|
|
95
103
|
return;
|
|
96
104
|
}
|
|
97
105
|
if (flags.output === 'summary') {
|
|
@@ -161,6 +169,8 @@ Results: 2 passed, 1 failed
|
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
catch (error) {
|
|
172
|
+
if (error instanceof Error && 'oclif' in error)
|
|
173
|
+
throw error;
|
|
164
174
|
const message = error instanceof Error ? error.message : String(error);
|
|
165
175
|
results.push({
|
|
166
176
|
message,
|
|
@@ -202,6 +212,8 @@ Results: 2 passed, 1 failed
|
|
|
202
212
|
}
|
|
203
213
|
}
|
|
204
214
|
catch (error) {
|
|
215
|
+
if (error instanceof Error && 'oclif' in error)
|
|
216
|
+
throw error;
|
|
205
217
|
if (error instanceof Error) {
|
|
206
218
|
this.error(`Failed to run unit tests: ${error.message}`);
|
|
207
219
|
}
|
|
@@ -55,24 +55,29 @@ Result: PASS (1.234s)
|
|
|
55
55
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
56
56
|
}
|
|
57
57
|
const result = await response.json();
|
|
58
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
59
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
60
|
+
const passed = result.status === 'ok';
|
|
58
61
|
if (flags.output === 'json') {
|
|
59
62
|
this.log(JSON.stringify(result, null, 2));
|
|
60
63
|
}
|
|
61
64
|
else {
|
|
62
|
-
const timing = `(${result.timing.toFixed(3)}s)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else {
|
|
67
|
-
this.log(`Result: FAIL ${timing}`);
|
|
68
|
-
if (result.message) {
|
|
69
|
-
this.log(` Error: ${result.message}`);
|
|
70
|
-
}
|
|
71
|
-
this.exit(1);
|
|
65
|
+
const timing = typeof result.timing === 'number' ? ` (${result.timing.toFixed(3)}s)` : '';
|
|
66
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
67
|
+
if (!passed && result.message) {
|
|
68
|
+
this.log(` Error: ${result.message}`);
|
|
72
69
|
}
|
|
73
70
|
}
|
|
71
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
72
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
73
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
74
|
+
if (!passed) {
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
74
77
|
}
|
|
75
78
|
catch (error) {
|
|
79
|
+
if (error instanceof Error && 'oclif' in error)
|
|
80
|
+
throw error;
|
|
76
81
|
if (error instanceof Error) {
|
|
77
82
|
this.error(`Failed to run workflow test: ${error.message}`);
|
|
78
83
|
}
|
|
@@ -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
|
}
|
|
@@ -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,
|
|
8
|
+
import { buildApiGroupFolderResolver, 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,13 +97,10 @@ 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);
|
|
103
100
|
const filenameCounters = new Map();
|
|
104
101
|
let writtenCount = 0;
|
|
105
102
|
for (const doc of documents) {
|
|
106
|
-
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder
|
|
103
|
+
const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder);
|
|
107
104
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
108
105
|
// Track duplicates per directory
|
|
109
106
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -328,7 +325,7 @@ export default class GitPull extends BaseCommand {
|
|
|
328
325
|
* Resolve the output directory and base filename for a parsed document.
|
|
329
326
|
* Uses the same type-to-directory mapping as workspace pull.
|
|
330
327
|
*/
|
|
331
|
-
resolveOutputPath(outputDir, doc, getApiGroupFolder
|
|
328
|
+
resolveOutputPath(outputDir, doc, getApiGroupFolder) {
|
|
332
329
|
let typeDir;
|
|
333
330
|
let baseName;
|
|
334
331
|
if (doc.type === 'workspace') {
|
|
@@ -376,53 +373,6 @@ export default class GitPull extends BaseCommand {
|
|
|
376
373
|
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
377
374
|
baseName = this.sanitizeFilename(doc.name);
|
|
378
375
|
}
|
|
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
|
-
}
|
|
386
|
-
else if (doc.type === 'channel') {
|
|
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
|
-
}
|
|
407
|
-
}
|
|
408
|
-
else if (doc.type === 'message' && doc.channel) {
|
|
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));
|
|
424
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
425
|
-
}
|
|
426
376
|
else if (doc.type === 'api_group') {
|
|
427
377
|
const groupFolder = getApiGroupFolder(doc.name);
|
|
428
378
|
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
@@ -14,4 +14,10 @@ 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;
|
|
17
23
|
}
|