@xano/cli 1.0.6-beta.1 → 1.0.6-beta.11
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 +138 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
- package/dist/commands/ephemeral/impersonate/index.js +11 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/ephemeral/push/index.d.ts +10 -0
- package/dist/commands/ephemeral/push/index.js +77 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
- package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/list/index.js +20 -40
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +76 -38
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +74 -36
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -95
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +80 -38
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +78 -36
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +81 -41
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +82 -39
- package/dist/commands/workspace/git/pull/index.js +53 -3
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -95
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +274 -1
- 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 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +12 -0
- package/dist/utils/multidoc-push.js +84 -0
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +3420 -3208
- package/package.json +1 -1
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { formatPagingFooter } from '../../../../utils/paging.js';
|
|
3
4
|
export default class SandboxEnvList extends BaseCommand {
|
|
4
5
|
static description = 'List environment variable keys for a sandbox environment';
|
|
5
6
|
static examples = [
|
|
@@ -51,6 +52,9 @@ Environment variables for sandbox environment:
|
|
|
51
52
|
this.log(` - ${envVar.name}`);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
const footer = formatPagingFooter({ items: envVars }, { noun: 'environment variable', tier: 'none' });
|
|
56
|
+
if (footer)
|
|
57
|
+
this.log(footer);
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
catch (error) {
|
|
@@ -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
|
}
|
|
@@ -3,6 +3,8 @@ export default class SandboxUnitTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
8
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
9
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
3
4
|
export default class SandboxUnitTestList extends BaseCommand {
|
|
4
5
|
static description = 'List all unit tests for a sandbox environment';
|
|
5
6
|
static examples = [
|
|
@@ -28,12 +29,12 @@ Unit tests:
|
|
|
28
29
|
options: ['summary', 'json'],
|
|
29
30
|
required: false,
|
|
30
31
|
}),
|
|
32
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
31
33
|
};
|
|
32
34
|
async run() {
|
|
33
35
|
const { flags } = await this.parse(SandboxUnitTestList);
|
|
34
36
|
const { profile } = this.resolveProfile(flags);
|
|
35
|
-
const params = new URLSearchParams();
|
|
36
|
-
params.set('per_page', '10000');
|
|
37
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
37
38
|
if (flags.branch)
|
|
38
39
|
params.set('branch', flags.branch);
|
|
39
40
|
if (flags['obj-type'])
|
|
@@ -51,19 +52,10 @@ Unit tests:
|
|
|
51
52
|
const message = await this.parseApiError(response, 'API request failed');
|
|
52
53
|
this.error(message);
|
|
53
54
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
if (Array.isArray(data)) {
|
|
57
|
-
tests = data;
|
|
58
|
-
}
|
|
59
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
60
|
-
tests = data.items;
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
this.error('Unexpected API response format');
|
|
64
|
-
}
|
|
55
|
+
const list = normalizeListResponse(await response.json());
|
|
56
|
+
const tests = list.items;
|
|
65
57
|
if (flags.output === 'json') {
|
|
66
|
-
this.log(JSON.stringify(
|
|
58
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
67
59
|
}
|
|
68
60
|
else {
|
|
69
61
|
if (tests.length === 0) {
|
|
@@ -75,6 +67,9 @@ Unit tests:
|
|
|
75
67
|
this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
|
|
76
68
|
}
|
|
77
69
|
}
|
|
70
|
+
const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
|
|
71
|
+
if (footer)
|
|
72
|
+
this.log(footer);
|
|
78
73
|
}
|
|
79
74
|
}
|
|
80
75
|
catch (error) {
|
|
@@ -46,24 +46,30 @@ Result: PASS
|
|
|
46
46
|
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
50
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
51
|
+
const passed = result.status === 'ok';
|
|
49
52
|
if (flags.output === 'json') {
|
|
50
53
|
this.log(JSON.stringify(result, null, 2));
|
|
51
54
|
}
|
|
55
|
+
else if (passed) {
|
|
56
|
+
this.log('Result: PASS');
|
|
57
|
+
}
|
|
52
58
|
else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
59
|
-
for (const expect of failedExpects) {
|
|
60
|
-
if (expect.message) {
|
|
61
|
-
this.log(` Error: ${expect.message}`);
|
|
62
|
-
}
|
|
59
|
+
this.log('Result: FAIL');
|
|
60
|
+
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
61
|
+
for (const expect of failedExpects) {
|
|
62
|
+
if (expect.message) {
|
|
63
|
+
this.log(` Error: ${expect.message}`);
|
|
63
64
|
}
|
|
64
|
-
this.exit(1);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
68
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
69
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
70
|
+
if (!passed) {
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
67
73
|
}
|
|
68
74
|
catch (error) {
|
|
69
75
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -4,6 +4,7 @@ export default class SandboxUnitTestRunAll extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../../utils/paging.js';
|
|
3
5
|
export default class SandboxUnitTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all unit tests for a sandbox environment';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 4 passed, 1 failed
|
|
|
21
23
|
description: 'Filter by branch name',
|
|
22
24
|
required: false,
|
|
23
25
|
}),
|
|
26
|
+
concurrency: Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
24
31
|
'obj-type': Flags.string({
|
|
25
32
|
description: 'Filter by object type',
|
|
26
33
|
options: ['function', 'query', 'middleware'],
|
|
@@ -40,36 +47,46 @@ Results: 4 passed, 1 failed
|
|
|
40
47
|
const baseUrl = `${profile.instance_origin}/api:meta/sandbox/unit_test`;
|
|
41
48
|
try {
|
|
42
49
|
// Step 1: List all unit tests
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
51
|
+
// request is plenty for typical suites and does not make the instance
|
|
52
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
53
|
+
// never "the page came back full".
|
|
54
|
+
const branchValue = flags.branch;
|
|
55
|
+
const tests = await collectAllPages(async (page) => {
|
|
56
|
+
const listParams = new URLSearchParams();
|
|
57
|
+
listParams.set('page', String(page));
|
|
58
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
59
|
+
if (branchValue)
|
|
60
|
+
listParams.set('branch', branchValue);
|
|
61
|
+
if (flags['obj-type'])
|
|
62
|
+
listParams.set('obj_type', flags['obj-type']);
|
|
63
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
64
|
+
headers: {
|
|
65
|
+
accept: 'application/json',
|
|
66
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
67
|
+
},
|
|
68
|
+
method: 'GET',
|
|
69
|
+
}, flags.verbose, profile.access_token);
|
|
70
|
+
if (!listResponse.ok) {
|
|
71
|
+
const errorText = await listResponse.text();
|
|
72
|
+
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
73
|
+
}
|
|
74
|
+
return normalizeListResponse(await listResponse.json());
|
|
75
|
+
}, {
|
|
76
|
+
onTruncate: (collected) => {
|
|
77
|
+
this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
|
|
53
78
|
},
|
|
54
|
-
|
|
55
|
-
}, flags.verbose, profile.access_token);
|
|
56
|
-
if (!listResponse.ok) {
|
|
57
|
-
const errorText = await listResponse.text();
|
|
58
|
-
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
59
|
-
}
|
|
60
|
-
const data = (await listResponse.json());
|
|
61
|
-
let tests;
|
|
62
|
-
if (Array.isArray(data)) {
|
|
63
|
-
tests = data;
|
|
64
|
-
}
|
|
65
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
66
|
-
tests = data.items;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
this.error('Unexpected API response format');
|
|
70
|
-
}
|
|
79
|
+
});
|
|
71
80
|
if (tests.length === 0) {
|
|
72
|
-
|
|
81
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
82
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
83
|
+
// with the same keys the populated path emits below.
|
|
84
|
+
if (flags.output === 'json') {
|
|
85
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.log('No unit tests found');
|
|
89
|
+
}
|
|
73
90
|
return;
|
|
74
91
|
}
|
|
75
92
|
if (flags.output === 'summary') {
|
|
@@ -77,7 +94,15 @@ Results: 4 passed, 1 failed
|
|
|
77
94
|
}
|
|
78
95
|
// Step 2: Run each test
|
|
79
96
|
const results = [];
|
|
80
|
-
|
|
97
|
+
const runOne = async (test) => {
|
|
98
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
99
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
100
|
+
// run can still emit in input order.
|
|
101
|
+
const results = [];
|
|
102
|
+
const lines = [];
|
|
103
|
+
const log = (message) => {
|
|
104
|
+
lines.push(message);
|
|
105
|
+
};
|
|
81
106
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
82
107
|
try {
|
|
83
108
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -98,10 +123,10 @@ Results: 4 passed, 1 failed
|
|
|
98
123
|
status: 'fail',
|
|
99
124
|
});
|
|
100
125
|
if (flags.output === 'summary') {
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
127
|
+
log(` Error: API error ${runResponse.status}`);
|
|
103
128
|
}
|
|
104
|
-
|
|
129
|
+
return { lines, results };
|
|
105
130
|
}
|
|
106
131
|
const runResult = (await runResponse.json());
|
|
107
132
|
const passed = runResult.status === 'ok';
|
|
@@ -115,13 +140,13 @@ Results: 4 passed, 1 failed
|
|
|
115
140
|
});
|
|
116
141
|
if (flags.output === 'summary') {
|
|
117
142
|
if (passed) {
|
|
118
|
-
|
|
143
|
+
log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
119
144
|
}
|
|
120
145
|
else {
|
|
121
|
-
|
|
146
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
122
147
|
for (const expect of failedExpects) {
|
|
123
148
|
if (expect.message) {
|
|
124
|
-
|
|
149
|
+
log(` Error: ${expect.message}`);
|
|
125
150
|
}
|
|
126
151
|
}
|
|
127
152
|
}
|
|
@@ -139,11 +164,24 @@ Results: 4 passed, 1 failed
|
|
|
139
164
|
status: 'fail',
|
|
140
165
|
});
|
|
141
166
|
if (flags.output === 'summary') {
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
168
|
+
log(` Error: ${message}`);
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
|
-
|
|
171
|
+
return { lines, results };
|
|
172
|
+
};
|
|
173
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
174
|
+
// diffable against a sequential run.
|
|
175
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
176
|
+
for (const line of settled.lines)
|
|
177
|
+
this.log(line);
|
|
178
|
+
});
|
|
179
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
180
|
+
const settled = await runOne(test);
|
|
181
|
+
emitter.settle(index, settled);
|
|
182
|
+
return settled;
|
|
183
|
+
});
|
|
184
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
147
185
|
// Step 3: Summary
|
|
148
186
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
149
187
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
@@ -3,6 +3,8 @@ export default class SandboxWorkflowTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
8
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
3
4
|
export default class SandboxWorkflowTestList extends BaseCommand {
|
|
4
5
|
static description = 'List workflow tests for a sandbox environment';
|
|
5
6
|
static examples = [
|
|
@@ -23,12 +24,12 @@ Workflow tests:
|
|
|
23
24
|
options: ['summary', 'json'],
|
|
24
25
|
required: false,
|
|
25
26
|
}),
|
|
27
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
26
28
|
};
|
|
27
29
|
async run() {
|
|
28
30
|
const { flags } = await this.parse(SandboxWorkflowTestList);
|
|
29
31
|
const { profile } = this.resolveProfile(flags);
|
|
30
|
-
const params = new URLSearchParams();
|
|
31
|
-
params.set('per_page', '10000');
|
|
32
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
32
33
|
if (flags.branch)
|
|
33
34
|
params.set('branch', flags.branch);
|
|
34
35
|
const apiUrl = `${profile.instance_origin}/api:meta/sandbox/workflow_test?${params}`;
|
|
@@ -44,19 +45,10 @@ Workflow tests:
|
|
|
44
45
|
const message = await this.parseApiError(response, 'API request failed');
|
|
45
46
|
this.error(message);
|
|
46
47
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (Array.isArray(data)) {
|
|
50
|
-
tests = data;
|
|
51
|
-
}
|
|
52
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
53
|
-
tests = data.items;
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
this.error('Unexpected API response format');
|
|
57
|
-
}
|
|
48
|
+
const list = normalizeListResponse(await response.json());
|
|
49
|
+
const tests = list.items;
|
|
58
50
|
if (flags.output === 'json') {
|
|
59
|
-
this.log(JSON.stringify(
|
|
51
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
60
52
|
}
|
|
61
53
|
else {
|
|
62
54
|
if (tests.length === 0) {
|
|
@@ -68,6 +60,9 @@ Workflow tests:
|
|
|
68
60
|
this.log(` - ${test.name} (ID: ${test.id})`);
|
|
69
61
|
}
|
|
70
62
|
}
|
|
63
|
+
const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
|
|
64
|
+
if (footer)
|
|
65
|
+
this.log(footer);
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
68
|
catch (error) {
|
|
@@ -46,22 +46,25 @@ Result: PASS (0.25s)
|
|
|
46
46
|
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
50
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
51
|
+
const passed = result.status === 'ok';
|
|
49
52
|
if (flags.output === 'json') {
|
|
50
53
|
this.log(JSON.stringify(result, null, 2));
|
|
51
54
|
}
|
|
52
55
|
else {
|
|
53
56
|
const timing = result.timing ? ` (${result.timing}s)` : '';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else {
|
|
58
|
-
this.log(`Result: FAIL${timing}`);
|
|
59
|
-
if (result.message) {
|
|
60
|
-
this.log(` Error: ${result.message}`);
|
|
61
|
-
}
|
|
62
|
-
this.exit(1);
|
|
57
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
58
|
+
if (!passed && result.message) {
|
|
59
|
+
this.log(` Error: ${result.message}`);
|
|
63
60
|
}
|
|
64
61
|
}
|
|
62
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
63
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
64
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
65
|
+
if (!passed) {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
70
|
if (error instanceof Error && 'oclif' in error)
|