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.
Files changed (62) hide show
  1. package/README.md +126 -4
  2. package/examples/openapi/sample-openapi-agent/README.md +12 -0
  3. package/examples/openapi/sample-openapi-agent/agentbridge.manifest.json +85 -0
  4. package/examples/openapi/sample-openapi-agent/cli/README.md +18 -0
  5. package/examples/openapi/sample-openapi-agent/cli/bin/sample-crm-api.js +64 -0
  6. package/examples/openapi/sample-openapi-agent/cli/commands/create-contact.js +59 -0
  7. package/examples/openapi/sample-openapi-agent/cli/commands/delete-contacts-by-contactid.js +45 -0
  8. package/examples/openapi/sample-openapi-agent/cli/commands/get-contacts-by-contactid.js +45 -0
  9. package/examples/openapi/sample-openapi-agent/cli/commands/list-contacts.js +45 -0
  10. package/examples/openapi/sample-openapi-agent/cli/commands/patch-contacts-by-contactid.js +60 -0
  11. package/examples/openapi/sample-openapi-agent/cli/lib/client.js +244 -0
  12. package/examples/openapi/sample-openapi-agent/cli/lib/output.js +21 -0
  13. package/examples/openapi/sample-openapi-agent/cli/package.json +16 -0
  14. package/examples/openapi/sample-openapi-agent/skill/SKILL.md +50 -0
  15. package/examples/openapi/sample-openapi-cli/README.md +18 -0
  16. package/examples/openapi/sample-openapi-cli/bin/sample-crm-api.js +64 -0
  17. package/examples/openapi/sample-openapi-cli/commands/create-contact.js +59 -0
  18. package/examples/openapi/sample-openapi-cli/commands/delete-contacts-by-contactid.js +45 -0
  19. package/examples/openapi/sample-openapi-cli/commands/get-contacts-by-contactid.js +45 -0
  20. package/examples/openapi/sample-openapi-cli/commands/list-contacts.js +45 -0
  21. package/examples/openapi/sample-openapi-cli/commands/patch-contacts-by-contactid.js +60 -0
  22. package/examples/openapi/sample-openapi-cli/lib/client.js +244 -0
  23. package/examples/openapi/sample-openapi-cli/lib/output.js +21 -0
  24. package/examples/openapi/sample-openapi-cli/node_modules/.package-lock.json +15 -0
  25. package/examples/openapi/sample-openapi-cli/node_modules/commander/LICENSE +22 -0
  26. package/examples/openapi/sample-openapi-cli/node_modules/commander/Readme.md +1157 -0
  27. package/examples/openapi/sample-openapi-cli/node_modules/commander/esm.mjs +16 -0
  28. package/examples/openapi/sample-openapi-cli/node_modules/commander/index.js +24 -0
  29. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/argument.js +149 -0
  30. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/command.js +2509 -0
  31. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/error.js +39 -0
  32. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/help.js +520 -0
  33. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/option.js +330 -0
  34. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  35. package/examples/openapi/sample-openapi-cli/node_modules/commander/package-support.json +16 -0
  36. package/examples/openapi/sample-openapi-cli/node_modules/commander/package.json +84 -0
  37. package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/esm.d.mts +3 -0
  38. package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/index.d.ts +969 -0
  39. package/examples/openapi/sample-openapi-cli/package.json +16 -0
  40. package/examples/openapi/sample-openapi.yaml +67 -0
  41. package/examples/trello/trelloapi-agent/README.md +1 -0
  42. package/examples/trello/trelloapi-agent/agentbridge.manifest.json +1 -1
  43. package/examples/trello/trelloapi-agent/cli/commands/get-board.js +4 -0
  44. package/examples/trello/trelloapi-agent/cli/commands/list-board-lists.js +4 -0
  45. package/examples/trello/trelloapi-agent/cli/commands/list-list-cards.js +4 -0
  46. package/examples/trello/trelloapi-agent/cli/lib/client.js +174 -9
  47. package/examples/trello/trelloapi-cli/commands/get-board.js +4 -0
  48. package/examples/trello/trelloapi-cli/commands/list-board-lists.js +4 -0
  49. package/examples/trello/trelloapi-cli/commands/list-list-cards.js +4 -0
  50. package/examples/trello/trelloapi-cli/lib/client.js +174 -9
  51. package/package.json +8 -2
  52. package/src/commands/doctor.js +234 -0
  53. package/src/commands/generate.js +4 -8
  54. package/src/commands/init.js +154 -0
  55. package/src/commands/scaffold.js +9 -9
  56. package/src/commands/validate.js +6 -10
  57. package/src/index.js +21 -5
  58. package/src/lib/generate-cli.js +208 -15
  59. package/src/lib/generate-skill.js +24 -2
  60. package/src/lib/load-config.js +39 -3
  61. package/src/lib/openapi-to-config.js +314 -0
  62. package/src/lib/resolve-config-input.js +50 -0
@@ -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 `async function request(command, options) {
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 value = options[name];
259
+ const raw = readOption(options, name);
97
260
 
98
- if ((value === undefined || value === null || value === '') && schema.required) {
261
+ if ((raw === undefined || raw === null || raw === '') && schema.required) {
99
262
  throw new Error(\`Missing required parameter: --\${name}\`);
100
263
  }
101
264
 
102
- if (value === undefined || value === null || value === '') {
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 body = text;
295
+ let responseBody = text;
126
296
 
127
297
  try {
128
- body = text ? JSON.parse(text) : null;
298
+ responseBody = text ? JSON.parse(text) : null;
129
299
  } catch (_err) {
130
- body = text;
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 = body;
306
+ error.responseBody = responseBody;
137
307
  throw error;
138
308
  }
139
309
 
140
- return body;
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 options = Object.entries(params)
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
- ${options ? `${options}\n` : ''} .option('--pretty', 'Pretty-print JSON')
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
- ensureDir(binDir);
276
- ensureDir(commandsDir);
277
- ensureDir(libDir);
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 flags = Object.entries(command.params || {})
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}${flags ? ` ${Object.keys(command.params || {}).map((p) => `--${toKebab(p)} <value>`).join(' ')}` : ''}`
47
+ ` - example: ${binName} ${command.name}${exampleFlags ? ` ${exampleFlags}` : ''}`
26
48
  ].join('\n');
27
49
  })
28
50
  .join('\n');
@@ -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
- if (command.method !== 'GET') {
64
- fail(`commands[${index}].method must be GET for MVP`);
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
  };