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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const { validateConfig } = require('./load-config');
|
|
5
|
+
const { toKebab } = require('./config-utils');
|
|
6
|
+
|
|
7
|
+
function isUrl(value) {
|
|
8
|
+
return /^https?:\/\//i.test(String(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readLocalFile(specPath) {
|
|
12
|
+
const resolved = path.resolve(process.cwd(), specPath);
|
|
13
|
+
if (!fs.existsSync(resolved)) {
|
|
14
|
+
throw new Error(`Spec file not found: ${resolved}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return fs.readFileSync(resolved, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readSpecText(specInput) {
|
|
21
|
+
if (isUrl(specInput)) {
|
|
22
|
+
const response = await fetch(String(specInput));
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Unable to fetch spec: HTTP ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return response.text();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return readLocalFile(String(specInput));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseSpec(text) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
} catch (_error) {
|
|
37
|
+
return yaml.load(text);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveParameter(spec, parameter) {
|
|
42
|
+
if (!parameter || typeof parameter !== 'object') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!parameter.$ref) {
|
|
47
|
+
return parameter;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const match = String(parameter.$ref).match(/^#\/components\/parameters\/([^/]+)$/);
|
|
51
|
+
if (!match) {
|
|
52
|
+
throw new Error(`Unsupported parameter $ref: ${parameter.$ref}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const key = match[1];
|
|
56
|
+
const resolved = spec.components && spec.components.parameters && spec.components.parameters[key];
|
|
57
|
+
|
|
58
|
+
if (!resolved) {
|
|
59
|
+
throw new Error(`Unable to resolve parameter ref: ${parameter.$ref}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveSchema(spec, schema) {
|
|
66
|
+
if (!schema || typeof schema !== 'object') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!schema.$ref) {
|
|
71
|
+
return schema;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const match = String(schema.$ref).match(/^#\/components\/schemas\/([^/]+)$/);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const key = match[1];
|
|
80
|
+
return spec.components && spec.components.schemas && spec.components.schemas[key];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveRequestBody(spec, requestBody) {
|
|
84
|
+
if (!requestBody || typeof requestBody !== 'object') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!requestBody.$ref) {
|
|
89
|
+
return requestBody;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const match = String(requestBody.$ref).match(/^#\/components\/requestBodies\/([^/]+)$/);
|
|
93
|
+
if (!match) {
|
|
94
|
+
throw new Error(`Unsupported requestBody $ref: ${requestBody.$ref}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const key = match[1];
|
|
98
|
+
const resolved = spec.components && spec.components.requestBodies && spec.components.requestBodies[key];
|
|
99
|
+
|
|
100
|
+
if (!resolved) {
|
|
101
|
+
throw new Error(`Unable to resolve requestBody ref: ${requestBody.$ref}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dedupeParameters(parameters) {
|
|
108
|
+
const map = new Map();
|
|
109
|
+
|
|
110
|
+
parameters.forEach((parameter) => {
|
|
111
|
+
if (!parameter || !parameter.name) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const key = `${parameter.in}:${parameter.name}`;
|
|
116
|
+
map.set(key, parameter);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return [...map.values()];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inferTypeFromSchema(schema) {
|
|
123
|
+
const resolved = schema && typeof schema === 'object' ? schema : {};
|
|
124
|
+
if (resolved.type === 'integer' || resolved.type === 'number') {
|
|
125
|
+
return 'number';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (resolved.type === 'boolean') {
|
|
129
|
+
return 'boolean';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return 'string';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inferType(parameter) {
|
|
136
|
+
return inferTypeFromSchema(parameter.schema || {});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildCommandName(method, routePath, operation) {
|
|
140
|
+
if (operation.operationId && String(operation.operationId).trim()) {
|
|
141
|
+
return toKebab(String(operation.operationId).trim());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const parts = routePath
|
|
145
|
+
.split('/')
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.map((part) => {
|
|
148
|
+
if (part.startsWith('{') && part.endsWith('}')) {
|
|
149
|
+
return `by-${part.slice(1, -1)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return part;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return [method.toLowerCase(), ...parts]
|
|
156
|
+
.join('-')
|
|
157
|
+
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
158
|
+
.replace(/-+/g, '-')
|
|
159
|
+
.replace(/^-+|-+$/g, '')
|
|
160
|
+
.toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function extractRequestBody(spec, requestBody) {
|
|
164
|
+
const resolved = resolveRequestBody(spec, requestBody);
|
|
165
|
+
if (!resolved) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const content = resolved.content || {};
|
|
170
|
+
const jsonContent = content['application/json'];
|
|
171
|
+
if (!jsonContent || !jsonContent.schema) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const schema = resolveSchema(spec, jsonContent.schema) || jsonContent.schema;
|
|
176
|
+
const request = {
|
|
177
|
+
required: Boolean(resolved.required)
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object') {
|
|
181
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
182
|
+
const properties = {};
|
|
183
|
+
|
|
184
|
+
Object.entries(schema.properties).forEach(([name, propSchema]) => {
|
|
185
|
+
const resolvedProp = resolveSchema(spec, propSchema) || propSchema;
|
|
186
|
+
properties[name] = {
|
|
187
|
+
type: inferTypeFromSchema(resolvedProp),
|
|
188
|
+
required: required.has(name),
|
|
189
|
+
description: (resolvedProp && resolvedProp.description) || `${name} field`
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
request.properties = properties;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return request;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function operationToCommand(spec, method, routePath, operation) {
|
|
200
|
+
const combinedParameters = dedupeParameters([
|
|
201
|
+
...((operation.__pathParameters || []).map((parameter) => resolveParameter(spec, parameter))),
|
|
202
|
+
...((operation.parameters || []).map((parameter) => resolveParameter(spec, parameter)))
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const params = {};
|
|
206
|
+
|
|
207
|
+
combinedParameters.forEach((parameter) => {
|
|
208
|
+
if (!parameter || !parameter.name) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (parameter.in !== 'path' && parameter.in !== 'query') {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
params[parameter.name] = {
|
|
217
|
+
type: inferType(parameter),
|
|
218
|
+
required: parameter.in === 'path' ? true : Boolean(parameter.required),
|
|
219
|
+
description: parameter.description || `${parameter.name} (${parameter.in})`
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
name: buildCommandName(method, routePath, operation),
|
|
225
|
+
description: operation.summary || operation.description || `${method.toUpperCase()} ${routePath}`,
|
|
226
|
+
method: method.toUpperCase(),
|
|
227
|
+
path: routePath,
|
|
228
|
+
params,
|
|
229
|
+
requestBody: extractRequestBody(spec, operation.requestBody)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeName(rawName) {
|
|
234
|
+
return String(rawName)
|
|
235
|
+
.trim()
|
|
236
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
237
|
+
.replace(/^-+|-+$/g, '')
|
|
238
|
+
.toLowerCase();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toConfigFromSpec(spec, overrides = {}) {
|
|
242
|
+
if (!spec || typeof spec !== 'object') {
|
|
243
|
+
throw new Error('Invalid OpenAPI: spec must be an object');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const title = (spec.info && spec.info.title) || overrides.name;
|
|
247
|
+
const version = overrides.version || (spec.info && spec.info.version) || '1.0.0';
|
|
248
|
+
const server = (spec.servers && spec.servers[0] && spec.servers[0].url) || '';
|
|
249
|
+
|
|
250
|
+
if (!title) {
|
|
251
|
+
throw new Error('Missing CLI name: provide --name or include info.title in spec');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!server || !/^https?:\/\//.test(server)) {
|
|
255
|
+
throw new Error('OpenAPI spec must include servers[0].url with http(s) base URL');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const paths = spec.paths && typeof spec.paths === 'object' ? spec.paths : {};
|
|
259
|
+
const commands = [];
|
|
260
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete'];
|
|
261
|
+
|
|
262
|
+
Object.entries(paths).forEach(([routePath, pathItem]) => {
|
|
263
|
+
if (!pathItem || typeof pathItem !== 'object') {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
methods.forEach((method) => {
|
|
268
|
+
if (!pathItem[method] || typeof pathItem[method] !== 'object') {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const operation = {
|
|
273
|
+
...pathItem[method],
|
|
274
|
+
__pathParameters: Array.isArray(pathItem.parameters) ? pathItem.parameters : []
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
commands.push(operationToCommand(spec, method, routePath, operation));
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (commands.length === 0) {
|
|
282
|
+
throw new Error('No supported operations found in OpenAPI spec');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const config = {
|
|
286
|
+
name: normalizeName(title),
|
|
287
|
+
version: String(version),
|
|
288
|
+
apiBase: String(server).replace(/\/$/, ''),
|
|
289
|
+
commands
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
validateConfig(config);
|
|
293
|
+
|
|
294
|
+
return config;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function loadConfigFromOpenApi({ specInput, name, version }) {
|
|
298
|
+
if (!specInput) {
|
|
299
|
+
throw new Error('Missing required flag: --spec <path-or-url>');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const text = await readSpecText(specInput);
|
|
303
|
+
const spec = parseSpec(text);
|
|
304
|
+
|
|
305
|
+
if (!spec || typeof spec !== 'object') {
|
|
306
|
+
throw new Error('Failed to parse OpenAPI spec');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return toConfigFromSpec(spec, { name, version });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
loadConfigFromOpenApi
|
|
314
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { loadConfig } = require('./load-config');
|
|
3
|
+
const { loadConfigFromOpenApi } = require('./openapi-to-config');
|
|
4
|
+
|
|
5
|
+
function hasFlag(flags, key) {
|
|
6
|
+
return Object.prototype.hasOwnProperty.call(flags, key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function resolveConfigInput(flags) {
|
|
10
|
+
const hasConfig = hasFlag(flags, 'config');
|
|
11
|
+
const hasSpec = hasFlag(flags, 'spec');
|
|
12
|
+
|
|
13
|
+
if (!hasConfig && !hasSpec) {
|
|
14
|
+
throw new Error('Provide one input source: --config <path> or --spec <path-or-url>');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (hasConfig && hasSpec) {
|
|
18
|
+
throw new Error('Use either --config or --spec, not both');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (hasConfig) {
|
|
22
|
+
const configPath = path.resolve(process.cwd(), String(flags.config));
|
|
23
|
+
const config = loadConfig(configPath);
|
|
24
|
+
return {
|
|
25
|
+
config,
|
|
26
|
+
source: {
|
|
27
|
+
type: 'config',
|
|
28
|
+
value: configPath
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const config = await loadConfigFromOpenApi({
|
|
34
|
+
specInput: String(flags.spec),
|
|
35
|
+
name: flags.name ? String(flags.name) : undefined,
|
|
36
|
+
version: flags.version ? String(flags.version) : undefined
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
config,
|
|
41
|
+
source: {
|
|
42
|
+
type: 'spec',
|
|
43
|
+
value: String(flags.spec)
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
resolveConfigInput
|
|
50
|
+
};
|