api-to-cli 0.1.2 → 0.1.3
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 +126 -4
- package/examples/openapi/sample-openapi-agent/README.md +12 -0
- package/examples/openapi/sample-openapi-agent/agentbridge.manifest.json +85 -0
- package/examples/openapi/sample-openapi-agent/cli/README.md +18 -0
- package/examples/openapi/sample-openapi-agent/cli/bin/sample-crm-api.js +64 -0
- package/examples/openapi/sample-openapi-agent/cli/commands/create-contact.js +59 -0
- package/examples/openapi/sample-openapi-agent/cli/commands/delete-contacts-by-contactid.js +45 -0
- package/examples/openapi/sample-openapi-agent/cli/commands/get-contacts-by-contactid.js +45 -0
- package/examples/openapi/sample-openapi-agent/cli/commands/list-contacts.js +45 -0
- package/examples/openapi/sample-openapi-agent/cli/commands/patch-contacts-by-contactid.js +60 -0
- package/examples/openapi/sample-openapi-agent/cli/lib/client.js +244 -0
- package/examples/openapi/sample-openapi-agent/cli/lib/output.js +21 -0
- package/examples/openapi/sample-openapi-agent/cli/package.json +16 -0
- package/examples/openapi/sample-openapi-agent/skill/SKILL.md +50 -0
- package/examples/openapi/sample-openapi-cli/README.md +18 -0
- package/examples/openapi/sample-openapi-cli/bin/sample-crm-api.js +64 -0
- package/examples/openapi/sample-openapi-cli/commands/create-contact.js +59 -0
- package/examples/openapi/sample-openapi-cli/commands/delete-contacts-by-contactid.js +45 -0
- package/examples/openapi/sample-openapi-cli/commands/get-contacts-by-contactid.js +45 -0
- package/examples/openapi/sample-openapi-cli/commands/list-contacts.js +45 -0
- package/examples/openapi/sample-openapi-cli/commands/patch-contacts-by-contactid.js +60 -0
- package/examples/openapi/sample-openapi-cli/lib/client.js +244 -0
- package/examples/openapi/sample-openapi-cli/lib/output.js +21 -0
- package/examples/openapi/sample-openapi-cli/node_modules/.package-lock.json +15 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/LICENSE +22 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/Readme.md +1157 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/esm.mjs +16 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/index.js +24 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/argument.js +149 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/command.js +2509 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/error.js +39 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/help.js +520 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/option.js +330 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/package-support.json +16 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/package.json +84 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/esm.d.mts +3 -0
- package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/index.d.ts +969 -0
- package/examples/openapi/sample-openapi-cli/package.json +16 -0
- package/examples/openapi/sample-openapi.yaml +67 -0
- package/examples/trello/trelloapi-agent/README.md +1 -0
- package/examples/trello/trelloapi-agent/agentbridge.manifest.json +1 -1
- package/examples/trello/trelloapi-agent/cli/commands/get-board.js +4 -0
- package/examples/trello/trelloapi-agent/cli/commands/list-board-lists.js +4 -0
- package/examples/trello/trelloapi-agent/cli/commands/list-list-cards.js +4 -0
- package/examples/trello/trelloapi-agent/cli/lib/client.js +174 -9
- package/examples/trello/trelloapi-cli/commands/get-board.js +4 -0
- package/examples/trello/trelloapi-cli/commands/list-board-lists.js +4 -0
- package/examples/trello/trelloapi-cli/commands/list-list-cards.js +4 -0
- package/examples/trello/trelloapi-cli/lib/client.js +174 -9
- package/package.json +8 -2
- package/src/commands/doctor.js +234 -0
- package/src/commands/generate.js +4 -8
- package/src/commands/init.js +154 -0
- package/src/commands/scaffold.js +9 -9
- package/src/commands/validate.js +6 -10
- package/src/index.js +21 -5
- package/src/lib/generate-cli.js +208 -15
- package/src/lib/generate-skill.js +24 -2
- package/src/lib/load-config.js +39 -3
- package/src/lib/openapi-to-config.js +314 -0
- package/src/lib/resolve-config-input.js +50 -0
package/src/lib/generate-cli.js
CHANGED
|
@@ -6,6 +6,11 @@ function ensureDir(dirPath) {
|
|
|
6
6
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function resetDir(dirPath) {
|
|
10
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
11
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
function commandToMethodName(commandName) {
|
|
10
15
|
return String(commandName)
|
|
11
16
|
.split(/[^a-zA-Z0-9]/)
|
|
@@ -66,7 +71,165 @@ module.exports = {
|
|
|
66
71
|
function renderClientLib(config) {
|
|
67
72
|
const authConfig = JSON.stringify(config.auth || { credentials: [] }, null, 2);
|
|
68
73
|
|
|
69
|
-
return `
|
|
74
|
+
return `const fs = require('fs');
|
|
75
|
+
|
|
76
|
+
function toKebab(name) {
|
|
77
|
+
return String(name)
|
|
78
|
+
.trim()
|
|
79
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
82
|
+
.replace(/^-+|-+$/g, '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toCamelCase(name) {
|
|
86
|
+
return String(name).replace(/[-_]+([a-zA-Z0-9])/g, (_m, g1) => g1.toUpperCase());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readOption(options, name) {
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(options, name)) {
|
|
91
|
+
return options[name];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const camel = toCamelCase(name);
|
|
95
|
+
if (Object.prototype.hasOwnProperty.call(options, camel)) {
|
|
96
|
+
return options[camel];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function coerceValue(value, type) {
|
|
103
|
+
if (value === undefined || value === null || value === '') {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (type === 'number') {
|
|
108
|
+
const parsed = Number(value);
|
|
109
|
+
if (Number.isNaN(parsed)) {
|
|
110
|
+
throw new Error(\`Expected number but received: \${value}\`);
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (type === 'boolean') {
|
|
116
|
+
if (typeof value === 'boolean') {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const normalized = String(value).toLowerCase();
|
|
121
|
+
if (normalized === 'true' || normalized === '1') {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (normalized === 'false' || normalized === '0') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(\`Expected boolean but received: \${value}\`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return String(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseJsonText(raw, label) {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(raw);
|
|
138
|
+
} catch (_error) {
|
|
139
|
+
throw new Error(\`Invalid JSON for \${label}\`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseJsonBody(rawBody) {
|
|
144
|
+
if (rawBody === undefined || rawBody === null || rawBody === '') {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parseJsonText(rawBody, '--body');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseBodyFromStdin(enabled) {
|
|
152
|
+
if (!enabled) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
157
|
+
if (!raw) {
|
|
158
|
+
throw new Error('Expected JSON on stdin because --body-stdin was provided');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return parseJsonText(raw, '--body-stdin');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildRequestBody(command, options) {
|
|
165
|
+
const requestBody = command.requestBody || null;
|
|
166
|
+
if (!requestBody) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const direct = parseJsonBody(readOption(options, 'body'));
|
|
171
|
+
const stdin = parseBodyFromStdin(Boolean(readOption(options, 'body-stdin')));
|
|
172
|
+
|
|
173
|
+
if (direct !== null && stdin !== null) {
|
|
174
|
+
throw new Error('Use either --body or --body-stdin, not both');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const properties = requestBody.properties || {};
|
|
178
|
+
const hasBodyProps = Object.keys(properties).length > 0;
|
|
179
|
+
|
|
180
|
+
let payload = direct !== null ? direct : stdin !== null ? stdin : hasBodyProps ? {} : null;
|
|
181
|
+
if (payload !== null && (typeof payload !== 'object' || Array.isArray(payload))) {
|
|
182
|
+
throw new Error('Request body must be a JSON object');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (payload === null && requestBody.required) {
|
|
186
|
+
throw new Error('Missing required request body. Provide --body, --body-stdin, or body field flags.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let hadBodyFlag = false;
|
|
190
|
+
Object.entries(properties).forEach(([propName, schema]) => {
|
|
191
|
+
const optionName = \`body-\${toKebab(propName)}\`;
|
|
192
|
+
const raw = readOption(options, optionName);
|
|
193
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (payload === null) {
|
|
198
|
+
payload = {};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
payload[propName] = coerceValue(raw, schema.type);
|
|
202
|
+
hadBodyFlag = true;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (hasBodyProps) {
|
|
206
|
+
if (payload === null) {
|
|
207
|
+
payload = {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
Object.entries(properties).forEach(([propName, schema]) => {
|
|
211
|
+
if (!schema.required) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!Object.prototype.hasOwnProperty.call(payload, propName) || payload[propName] === undefined || payload[propName] === null || payload[propName] === '') {
|
|
216
|
+
const optionName = \`--body-\${toKebab(propName)}\`;
|
|
217
|
+
throw new Error(\`Missing required request body field: \${optionName}\`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (payload !== null) {
|
|
223
|
+
const isEmptyObject = typeof payload === 'object' && !Array.isArray(payload) && Object.keys(payload).length === 0;
|
|
224
|
+
if (isEmptyObject && !requestBody.required && !hadBodyFlag && direct === null && stdin === null) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return payload;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function request(command, options) {
|
|
70
233
|
const auth = ${authConfig};
|
|
71
234
|
const params = new URLSearchParams();
|
|
72
235
|
const commandParams = command.params || {};
|
|
@@ -93,16 +256,17 @@ function renderClientLib(config) {
|
|
|
93
256
|
});
|
|
94
257
|
|
|
95
258
|
Object.entries(commandParams).forEach(([name, schema]) => {
|
|
96
|
-
const
|
|
259
|
+
const raw = readOption(options, name);
|
|
97
260
|
|
|
98
|
-
if ((
|
|
261
|
+
if ((raw === undefined || raw === null || raw === '') && schema.required) {
|
|
99
262
|
throw new Error(\`Missing required parameter: --\${name}\`);
|
|
100
263
|
}
|
|
101
264
|
|
|
102
|
-
if (
|
|
265
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
103
266
|
return;
|
|
104
267
|
}
|
|
105
268
|
|
|
269
|
+
const value = coerceValue(raw, schema.type);
|
|
106
270
|
const token = \`{\${name}}\`;
|
|
107
271
|
|
|
108
272
|
if (resolvedPath.includes(token)) {
|
|
@@ -115,29 +279,35 @@ function renderClientLib(config) {
|
|
|
115
279
|
|
|
116
280
|
const query = params.toString();
|
|
117
281
|
const url = '${config.apiBase}' + resolvedPath + (query ? '?' + query : '');
|
|
282
|
+
const requestBody = buildRequestBody(command, options);
|
|
283
|
+
|
|
284
|
+
if (requestBody !== null) {
|
|
285
|
+
headers['content-type'] = 'application/json';
|
|
286
|
+
}
|
|
118
287
|
|
|
119
288
|
const response = await fetch(url, {
|
|
120
289
|
method: command.method,
|
|
121
|
-
headers
|
|
290
|
+
headers,
|
|
291
|
+
body: requestBody !== null ? JSON.stringify(requestBody) : undefined
|
|
122
292
|
});
|
|
123
293
|
|
|
124
294
|
const text = await response.text();
|
|
125
|
-
let
|
|
295
|
+
let responseBody = text;
|
|
126
296
|
|
|
127
297
|
try {
|
|
128
|
-
|
|
298
|
+
responseBody = text ? JSON.parse(text) : null;
|
|
129
299
|
} catch (_err) {
|
|
130
|
-
|
|
300
|
+
responseBody = text;
|
|
131
301
|
}
|
|
132
302
|
|
|
133
303
|
if (!response.ok) {
|
|
134
304
|
const error = new Error(\`HTTP \${response.status}\`);
|
|
135
305
|
error.statusCode = response.status;
|
|
136
|
-
error.responseBody =
|
|
306
|
+
error.responseBody = responseBody;
|
|
137
307
|
throw error;
|
|
138
308
|
}
|
|
139
309
|
|
|
140
|
-
return
|
|
310
|
+
return responseBody;
|
|
141
311
|
}
|
|
142
312
|
|
|
143
313
|
module.exports = {
|
|
@@ -157,6 +327,10 @@ const command = ${serializedCommand};
|
|
|
157
327
|
|
|
158
328
|
async function ${functionName}(options) {
|
|
159
329
|
try {
|
|
330
|
+
if (command.method !== 'GET' && !options.yes) {
|
|
331
|
+
throw new Error('This operation changes state. Re-run with --yes to confirm.');
|
|
332
|
+
}
|
|
333
|
+
|
|
160
334
|
const data = await request(command, options);
|
|
161
335
|
output.json(data, Boolean(options.pretty));
|
|
162
336
|
} catch (error) {
|
|
@@ -194,7 +368,7 @@ function renderBinFile(config) {
|
|
|
194
368
|
.map((command, index) => {
|
|
195
369
|
const varName = `cmd${index}`;
|
|
196
370
|
const params = command.params || {};
|
|
197
|
-
const
|
|
371
|
+
const paramOptions = Object.entries(params)
|
|
198
372
|
.map(([paramName, schema]) => {
|
|
199
373
|
const flagName = `--${toKebab(paramName)} <value>`;
|
|
200
374
|
const desc = schema.description || `${paramName} parameter`;
|
|
@@ -202,10 +376,29 @@ function renderBinFile(config) {
|
|
|
202
376
|
})
|
|
203
377
|
.join('\n');
|
|
204
378
|
|
|
379
|
+
const bodyProps = (command.requestBody && command.requestBody.properties) || {};
|
|
380
|
+
const bodyOptions = Object.entries(bodyProps)
|
|
381
|
+
.map(([propName, schema]) => {
|
|
382
|
+
const flagName = `--body-${toKebab(propName)} <value>`;
|
|
383
|
+
const req = schema.required ? 'required' : 'optional';
|
|
384
|
+
const desc = schema.description || `${propName} body field`;
|
|
385
|
+
return ` .option('${flagName}', '${desc} (${req})')`;
|
|
386
|
+
})
|
|
387
|
+
.join('\n');
|
|
388
|
+
|
|
389
|
+
const mutationOptions = command.method !== 'GET'
|
|
390
|
+
? [
|
|
391
|
+
` .option('--yes', 'Confirm non-GET operation')`,
|
|
392
|
+
command.requestBody ? ` .option('--body <json>', 'Raw JSON request body')` : null,
|
|
393
|
+
command.requestBody ? ` .option('--body-stdin', 'Read JSON body from stdin')` : null,
|
|
394
|
+
bodyOptions || null
|
|
395
|
+
].filter(Boolean).join('\n')
|
|
396
|
+
: '';
|
|
397
|
+
|
|
205
398
|
return `program
|
|
206
399
|
.command('${command.name}')
|
|
207
400
|
.description('${command.description.replace(/'/g, "\\'")}')
|
|
208
|
-
${
|
|
401
|
+
${paramOptions ? `${paramOptions}\n` : ''}${mutationOptions ? `${mutationOptions}\n` : ''} .option('--pretty', 'Pretty-print JSON')
|
|
209
402
|
.action((options) => ${varName}.run(options));`;
|
|
210
403
|
})
|
|
211
404
|
.join('\n\n');
|
|
@@ -272,9 +465,9 @@ function generateCliProject({ config, outputPath }) {
|
|
|
272
465
|
const libDir = path.join(outputPath, 'lib');
|
|
273
466
|
|
|
274
467
|
ensureDir(outputPath);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
468
|
+
resetDir(binDir);
|
|
469
|
+
resetDir(commandsDir);
|
|
470
|
+
resetDir(libDir);
|
|
278
471
|
|
|
279
472
|
writeFile(path.join(outputPath, 'package.json'), renderPackageJson(config));
|
|
280
473
|
writeFile(path.join(outputPath, 'README.md'), renderReadme(config));
|
|
@@ -11,18 +11,40 @@ function renderSkill(config, cliProjectPath) {
|
|
|
11
11
|
|
|
12
12
|
const commandDocs = config.commands
|
|
13
13
|
.map((command) => {
|
|
14
|
-
const
|
|
14
|
+
const paramFlags = Object.entries(command.params || {})
|
|
15
15
|
.map(([name, schema]) => {
|
|
16
16
|
const flag = `--${toKebab(name)} <value>`;
|
|
17
17
|
const req = schema.required ? 'required' : 'optional';
|
|
18
18
|
return ` - ${flag} (${req})`;
|
|
19
19
|
})
|
|
20
20
|
.join('\n');
|
|
21
|
+
const mutationFlags = command.method !== 'GET'
|
|
22
|
+
? [
|
|
23
|
+
' - --yes (required for non-GET operations)',
|
|
24
|
+
command.requestBody ? ' - --body <json> (raw JSON body fallback)' : null,
|
|
25
|
+
command.requestBody ? ' - --body-stdin (read JSON body from stdin)' : null,
|
|
26
|
+
...(command.requestBody && command.requestBody.properties
|
|
27
|
+
? Object.entries(command.requestBody.properties).map(([propName, schema]) => {
|
|
28
|
+
const req = schema.required ? 'required' : 'optional';
|
|
29
|
+
return ` - --body-${toKebab(propName)} <value> (${req})`;
|
|
30
|
+
})
|
|
31
|
+
: [])
|
|
32
|
+
].filter(Boolean).join('\n')
|
|
33
|
+
: '';
|
|
34
|
+
const flags = [paramFlags, mutationFlags].filter(Boolean).join('\n');
|
|
35
|
+
const exampleFlags = [
|
|
36
|
+
...Object.keys(command.params || {}).map((p) => `--${toKebab(p)} <value>`),
|
|
37
|
+
...(command.method !== 'GET' ? ['--yes'] : []),
|
|
38
|
+
...(command.requestBody && command.requestBody.properties
|
|
39
|
+
? Object.keys(command.requestBody.properties).map((p) => `--body-${toKebab(p)} <value>`)
|
|
40
|
+
: []),
|
|
41
|
+
...(command.requestBody ? ["--body '{\"key\":\"value\"}'"] : [])
|
|
42
|
+
].join(' ');
|
|
21
43
|
|
|
22
44
|
return [
|
|
23
45
|
`- ${command.name}: ${command.description}`,
|
|
24
46
|
flags || ' - no params',
|
|
25
|
-
` - example: ${binName} ${command.name}${
|
|
47
|
+
` - example: ${binName} ${command.name}${exampleFlags ? ` ${exampleFlags}` : ''}`
|
|
26
48
|
].join('\n');
|
|
27
49
|
})
|
|
28
50
|
.join('\n');
|
package/src/lib/load-config.js
CHANGED
|
@@ -60,8 +60,9 @@ function validateCommand(command, index) {
|
|
|
60
60
|
fail(`commands[${index}].description must be a non-empty string`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const allowedMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
64
|
+
if (!allowedMethods.has(command.method)) {
|
|
65
|
+
fail(`commands[${index}].method must be one of: GET, POST, PUT, PATCH, DELETE`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
if (typeof command.path !== 'string' || !command.path.startsWith('/')) {
|
|
@@ -71,6 +72,32 @@ function validateCommand(command, index) {
|
|
|
71
72
|
if (command.params !== undefined && !isObject(command.params)) {
|
|
72
73
|
fail(`commands[${index}].params must be an object when provided`);
|
|
73
74
|
}
|
|
75
|
+
|
|
76
|
+
if (command.requestBody !== undefined) {
|
|
77
|
+
if (!isObject(command.requestBody)) {
|
|
78
|
+
fail(`commands[${index}].requestBody must be an object when provided`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (command.requestBody.required !== undefined && typeof command.requestBody.required !== 'boolean') {
|
|
82
|
+
fail(`commands[${index}].requestBody.required must be a boolean when provided`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (command.requestBody.properties !== undefined) {
|
|
86
|
+
if (!isObject(command.requestBody.properties)) {
|
|
87
|
+
fail(`commands[${index}].requestBody.properties must be an object when provided`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Object.entries(command.requestBody.properties).forEach(([propName, propSchema]) => {
|
|
91
|
+
if (!isObject(propSchema)) {
|
|
92
|
+
fail(`commands[${index}].requestBody.properties.${propName} must be an object`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (propSchema.required !== undefined && typeof propSchema.required !== 'boolean') {
|
|
96
|
+
fail(`commands[${index}].requestBody.properties.${propName}.required must be boolean`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
function validateConfig(config) {
|
|
@@ -98,6 +125,14 @@ function validateConfig(config) {
|
|
|
98
125
|
validateAuth(config.auth);
|
|
99
126
|
}
|
|
100
127
|
|
|
128
|
+
const commandNames = new Set();
|
|
129
|
+
config.commands.forEach((command, index) => {
|
|
130
|
+
if (commandNames.has(command.name)) {
|
|
131
|
+
fail(`commands[${index}].name duplicates an existing command: ${command.name}`);
|
|
132
|
+
}
|
|
133
|
+
commandNames.add(command.name);
|
|
134
|
+
});
|
|
135
|
+
|
|
101
136
|
config.commands.forEach(validateCommand);
|
|
102
137
|
}
|
|
103
138
|
|
|
@@ -116,5 +151,6 @@ function loadConfig(configPath) {
|
|
|
116
151
|
}
|
|
117
152
|
|
|
118
153
|
module.exports = {
|
|
119
|
-
loadConfig
|
|
154
|
+
loadConfig,
|
|
155
|
+
validateConfig
|
|
120
156
|
};
|