@uptimerobot/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +179 -0
  3. package/bin/run.js +43 -0
  4. package/dist/api/client.js +95 -0
  5. package/dist/api/errors.js +26 -0
  6. package/dist/commands/alert-contacts/create.js +10 -0
  7. package/dist/commands/alert-contacts/delete.js +10 -0
  8. package/dist/commands/alert-contacts/get.js +10 -0
  9. package/dist/commands/alert-contacts/list.js +10 -0
  10. package/dist/commands/alert-contacts/update.js +10 -0
  11. package/dist/commands/auth/login.js +51 -0
  12. package/dist/commands/auth/logout.js +33 -0
  13. package/dist/commands/auth/status.js +38 -0
  14. package/dist/commands/auth/whoami.js +6 -0
  15. package/dist/commands/incidents/activity-log.js +10 -0
  16. package/dist/commands/incidents/alerts.js +10 -0
  17. package/dist/commands/incidents/comments/create.js +10 -0
  18. package/dist/commands/incidents/comments/delete.js +10 -0
  19. package/dist/commands/incidents/comments/list.js +10 -0
  20. package/dist/commands/incidents/comments/update.js +10 -0
  21. package/dist/commands/incidents/get.js +10 -0
  22. package/dist/commands/incidents/list.js +10 -0
  23. package/dist/commands/integrations/create.js +10 -0
  24. package/dist/commands/integrations/delete.js +10 -0
  25. package/dist/commands/integrations/get.js +10 -0
  26. package/dist/commands/integrations/list.js +10 -0
  27. package/dist/commands/integrations/update.js +10 -0
  28. package/dist/commands/maintenance-windows/create.js +10 -0
  29. package/dist/commands/maintenance-windows/delete.js +10 -0
  30. package/dist/commands/maintenance-windows/get.js +10 -0
  31. package/dist/commands/maintenance-windows/list.js +10 -0
  32. package/dist/commands/maintenance-windows/update.js +10 -0
  33. package/dist/commands/monitor-groups/create.js +10 -0
  34. package/dist/commands/monitor-groups/delete.js +10 -0
  35. package/dist/commands/monitor-groups/get.js +10 -0
  36. package/dist/commands/monitor-groups/list.js +10 -0
  37. package/dist/commands/monitor-groups/update.js +10 -0
  38. package/dist/commands/monitors/bulk/pause.js +10 -0
  39. package/dist/commands/monitors/bulk/start.js +10 -0
  40. package/dist/commands/monitors/bulk/update.js +10 -0
  41. package/dist/commands/monitors/create/api.js +10 -0
  42. package/dist/commands/monitors/create/dns.js +10 -0
  43. package/dist/commands/monitors/create/heartbeat.js +10 -0
  44. package/dist/commands/monitors/create/http.js +10 -0
  45. package/dist/commands/monitors/create/keyword.js +10 -0
  46. package/dist/commands/monitors/create/ping.js +10 -0
  47. package/dist/commands/monitors/create/port.js +10 -0
  48. package/dist/commands/monitors/create/udp.js +10 -0
  49. package/dist/commands/monitors/create/visual-comparison.js +10 -0
  50. package/dist/commands/monitors/delete.js +10 -0
  51. package/dist/commands/monitors/get.js +10 -0
  52. package/dist/commands/monitors/list.js +10 -0
  53. package/dist/commands/monitors/pause.js +10 -0
  54. package/dist/commands/monitors/reset.js +10 -0
  55. package/dist/commands/monitors/schema.js +57 -0
  56. package/dist/commands/monitors/start.js +10 -0
  57. package/dist/commands/monitors/stats/response-time/all.js +10 -0
  58. package/dist/commands/monitors/stats/response-time.js +10 -0
  59. package/dist/commands/monitors/stats/uptime.js +10 -0
  60. package/dist/commands/monitors/update.js +10 -0
  61. package/dist/commands/monitors/uptime-stats.js +10 -0
  62. package/dist/commands/status-pages/announcements/create.js +10 -0
  63. package/dist/commands/status-pages/announcements/get.js +10 -0
  64. package/dist/commands/status-pages/announcements/list.js +10 -0
  65. package/dist/commands/status-pages/announcements/pin.js +10 -0
  66. package/dist/commands/status-pages/announcements/unpin.js +10 -0
  67. package/dist/commands/status-pages/announcements/update.js +10 -0
  68. package/dist/commands/status-pages/create.js +10 -0
  69. package/dist/commands/status-pages/delete.js +10 -0
  70. package/dist/commands/status-pages/get.js +10 -0
  71. package/dist/commands/status-pages/list.js +10 -0
  72. package/dist/commands/status-pages/update.js +10 -0
  73. package/dist/commands/tags/delete.js +10 -0
  74. package/dist/commands/tags/list.js +10 -0
  75. package/dist/commands/user/alert-contacts.js +10 -0
  76. package/dist/commands/user/all-alert-contacts.js +10 -0
  77. package/dist/commands/user/get.js +5 -0
  78. package/dist/commands/user/me.js +10 -0
  79. package/dist/generated/operations.js +7631 -0
  80. package/dist/lib/auth.js +60 -0
  81. package/dist/lib/base-command.js +21 -0
  82. package/dist/lib/cli-errors.js +80 -0
  83. package/dist/lib/command-suggestions.js +44 -0
  84. package/dist/lib/credential-store.js +152 -0
  85. package/dist/lib/flag-name.js +11 -0
  86. package/dist/lib/invocation.js +74 -0
  87. package/dist/lib/objects.js +34 -0
  88. package/dist/lib/operation-command.js +486 -0
  89. package/dist/lib/operation-parser-error.js +28 -0
  90. package/dist/lib/request-curation.js +76 -0
  91. package/dist/lib/request-example.js +82 -0
  92. package/dist/lib/request-preview.js +25 -0
  93. package/dist/lib/request.js +301 -0
  94. package/dist/lib/sensitive-fields.js +19 -0
  95. package/dist/lib/types.js +1 -0
  96. package/dist/output/columns.js +221 -0
  97. package/dist/output/display-width.js +34 -0
  98. package/dist/output/normalize-result.js +28 -0
  99. package/dist/output/pagination-notice.js +27 -0
  100. package/dist/output/renderer.js +145 -0
  101. package/dist/output/request-progress.js +15 -0
  102. package/dist/output/resolve-format.js +32 -0
  103. package/dist/output/style.js +57 -0
  104. package/dist/runtime/parse-input.js +35 -0
  105. package/dist/runtime/types.js +1 -0
  106. package/dist/runtime/validate-input.js +63 -0
  107. package/openapi/openapi.yaml +9034 -0
  108. package/package.json +135 -0
@@ -0,0 +1,486 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { createApiClient } from '../api/client.js';
3
+ import { exitCodeFor, normalizeApiError } from '../api/errors.js';
4
+ import { columnsFor, parseColumnsFlag } from '../output/columns.js';
5
+ import { normalizeResult } from '../output/normalize-result.js';
6
+ import { paginationNotice } from '../output/pagination-notice.js';
7
+ import { formatOutput } from '../output/renderer.js';
8
+ import { withRequestProgress } from '../output/request-progress.js';
9
+ import { resolveFormat } from '../output/resolve-format.js';
10
+ import { parseInput } from '../runtime/parse-input.js';
11
+ import { InputValidationError, validateInput } from '../runtime/validate-input.js';
12
+ import { AuthenticationError, resolveApiKey } from './auth.js';
13
+ import { BaseCommand } from './base-command.js';
14
+ import { flagName } from './flag-name.js';
15
+ import { detectInvocationMode, isCI, requestConfirmation } from './invocation.js';
16
+ import { enrichOperationParserError } from './operation-parser-error.js';
17
+ import { curatedRequestBodyField, curatedRequestExampleBody } from './request-curation.js';
18
+ import { synthesizedRequestExample } from './request-example.js';
19
+ import { buildRequestBody, buildUrl, RequestInputError } from './request.js';
20
+ import { redactRequestPreview } from './request-preview.js';
21
+ export function createOperationCommand(operation, options = {}) {
22
+ const pathParameters = operation.parameters.filter((parameter) => parameter.in === 'path');
23
+ const optionParameters = operation.parameters.filter((parameter) => parameter.in !== 'path');
24
+ const commandArgs = Object.fromEntries(pathParameters.map((parameter) => [
25
+ parameter.name,
26
+ Args.string({ description: parameter.description, required: parameter.required }),
27
+ ]));
28
+ const requestBodyFlags = operation.contentTypes.length > 0
29
+ ? {
30
+ body: Flags.string({
31
+ description: 'JSON request body, @file, or - for stdin',
32
+ helpValue: '<json|@file|->',
33
+ }),
34
+ ...(operation.contentTypes.includes('application/json')
35
+ ? {
36
+ 'dry-run': Flags.boolean({
37
+ description: 'Compile and validate the request as JSON without authentication or networking',
38
+ }),
39
+ }
40
+ : {}),
41
+ ...(operation.contentTypes.includes('multipart/form-data')
42
+ ? {
43
+ file: Flags.string({
44
+ description: 'Multipart file as field=path (repeatable)',
45
+ helpValue: '<field=path>',
46
+ multiple: true,
47
+ }),
48
+ }
49
+ : {}),
50
+ set: Flags.string({
51
+ description: 'Set a request-body field as path=value (repeatable)',
52
+ helpValue: '<path=value>',
53
+ multiple: true,
54
+ }),
55
+ }
56
+ : {};
57
+ const defaultColumns = columnsFor(operation.commandId);
58
+ const bodyFieldFlags = Object.fromEntries((operation.requestBodyFields ?? []).map((field) => [
59
+ field.flag,
60
+ flagForBodyField(curatedRequestBodyField(operation, field)),
61
+ ]));
62
+ const commandFlags = {
63
+ 'api-key': Flags.string({
64
+ description: 'UptimeRobot API key',
65
+ env: 'UPTIMEROBOT_API_KEY',
66
+ helpValue: '<key>',
67
+ }),
68
+ agent: Flags.boolean({ description: 'Mark this invocation as agent-driven' }),
69
+ ...(defaultColumns === undefined
70
+ ? {}
71
+ : {
72
+ all: Flags.boolean({
73
+ description: 'Show every API field in table or plain output; may expose sensitive API fields',
74
+ }),
75
+ columns: Flags.string({
76
+ description: 'Comma-separated columns for table or plain output (dot paths allowed)',
77
+ helpValue: '<a,b.c>',
78
+ }),
79
+ }),
80
+ ...(operation.destructive
81
+ ? { confirm: Flags.boolean({ description: 'Explicitly confirm a destructive action' }) }
82
+ : {}),
83
+ json: Flags.boolean({
84
+ description: 'Shorthand for --format json; emit normalized structured JSON',
85
+ }),
86
+ format: Flags.string({
87
+ description: 'Output format; json is normalized and jsonl emits one resource per line',
88
+ options: ['json', 'jsonl', 'table', 'plain'],
89
+ }),
90
+ raw: Flags.boolean({
91
+ description: 'Emit the untouched API response as JSON; implies --format json',
92
+ }),
93
+ ...requestBodyFlags,
94
+ ...bodyFieldFlags,
95
+ ...Object.fromEntries(optionParameters.map((parameter) => [flagName(parameter.name), flagForParameter(parameter)])),
96
+ };
97
+ class OperationCommand extends BaseCommand {
98
+ static args = commandArgs;
99
+ static description = options.canonicalCommand
100
+ ? `${operation.description ?? operation.summary}
101
+
102
+ Canonical command: ${options.canonicalCommand}`
103
+ : (operation.description ?? operation.summary);
104
+ static examples = examplesFor(operation);
105
+ static flags = commandFlags;
106
+ static summary = operation.summary;
107
+ async catch(error) {
108
+ enrichOperationParserError(error, operation);
109
+ return super.catch(error);
110
+ }
111
+ async run() {
112
+ const parsed = await this.parse(OperationCommand);
113
+ const flags = parsed.flags;
114
+ const apiUrl = new URL(process.env.UPTIMEROBOT_DEV_API_URL ?? operation.defaultApiUrl);
115
+ const mode = detectInvocationMode(flags.agent === true);
116
+ const ci = isCI();
117
+ const outputFormat = resolveFormat(flags, mode);
118
+ const jsonOutput = outputFormat === 'json' || outputFormat === 'jsonl';
119
+ if (flags.json === true && flags.format !== undefined) {
120
+ return this.fail({ code: 'INVALID_INPUT', message: '--json cannot be combined with --format.' }, 2);
121
+ }
122
+ if (flags.raw === true && flags.format !== undefined && flags.format !== 'json') {
123
+ return this.fail({ code: 'INVALID_INPUT', message: '--raw can only be used with JSON output.' }, 2);
124
+ }
125
+ if (flags['dry-run'] === true &&
126
+ ((flags.format !== undefined && flags.format !== 'json') || flags.raw === true)) {
127
+ return this.fail({
128
+ code: 'INVALID_INPUT',
129
+ message: '--dry-run emits a JSON request preview and cannot use this output option.',
130
+ }, 2);
131
+ }
132
+ if (flags.columns !== undefined && flags.all === true) {
133
+ return this.fail({ code: 'INVALID_INPUT', message: '--columns cannot be combined with --all.' }, 2);
134
+ }
135
+ if ((flags.columns !== undefined || flags.all === true) && jsonOutput) {
136
+ return this.fail({
137
+ code: 'INVALID_INPUT',
138
+ message: '--columns and --all only apply to table or plain output.',
139
+ }, 2);
140
+ }
141
+ let requestedColumns;
142
+ try {
143
+ requestedColumns =
144
+ typeof flags.columns === 'string' ? parseColumnsFlag(flags.columns) : undefined;
145
+ }
146
+ catch (error) {
147
+ return this.fail({
148
+ code: 'INVALID_INPUT',
149
+ message: error instanceof Error ? error.message : String(error),
150
+ }, 2);
151
+ }
152
+ let input;
153
+ try {
154
+ input = validateInput(operation, parseInput(operation, parsed.args, flags));
155
+ }
156
+ catch (error) {
157
+ if (error instanceof InputValidationError) {
158
+ return this.fail({
159
+ code: 'INVALID_INPUT',
160
+ expected: error.expected,
161
+ message: error.message,
162
+ path: error.path,
163
+ }, 2);
164
+ }
165
+ throw error;
166
+ }
167
+ const headers = new Headers({ accept: 'application/json' });
168
+ let body;
169
+ try {
170
+ body = await buildRequestBody(operation, flags, headers);
171
+ }
172
+ catch (error) {
173
+ if (error instanceof RequestInputError) {
174
+ return this.fail({
175
+ code: error.code,
176
+ expected: error.expected,
177
+ message: error.message,
178
+ path: error.path,
179
+ }, error.exitCode);
180
+ }
181
+ return this.fail({
182
+ code: 'INVALID_INPUT',
183
+ message: error instanceof Error ? error.message : String(error),
184
+ }, 2);
185
+ }
186
+ if (flags['dry-run'] === true) {
187
+ if (typeof body !== 'string') {
188
+ return this.fail({ code: 'INVALID_INPUT', message: '--dry-run only supports JSON request bodies.' }, 2);
189
+ }
190
+ const url = buildUrl(operation, input, apiUrl);
191
+ const preview = redactRequestPreview(JSON.parse(body));
192
+ this.log(JSON.stringify({
193
+ body: preview.body,
194
+ command: operation.commandId.replaceAll(':', ' '),
195
+ contentType: headers.get('content-type'),
196
+ dryRun: true,
197
+ method: operation.method,
198
+ path: `${url.pathname}${url.search}`,
199
+ ...(preview.redacted.length > 0 ? { redacted: preview.redacted } : {}),
200
+ }, null, 2));
201
+ return;
202
+ }
203
+ if (operation.destructive && flags.confirm !== true) {
204
+ const confirmed = await requestConfirmation(operation, mode);
205
+ if (!confirmed) {
206
+ const error = {
207
+ code: 'CONFIRMATION_REQUIRED',
208
+ command: operation.commandId.replaceAll(':', ' '),
209
+ message: 'Explicit confirmation is required. Re-run with --confirm.',
210
+ };
211
+ return this.fail(error, 2);
212
+ }
213
+ }
214
+ let apiKey;
215
+ try {
216
+ apiKey = await resolveApiKey(flags['api-key'], apiUrl.toString());
217
+ }
218
+ catch (error) {
219
+ if (error instanceof AuthenticationError) {
220
+ return this.fail({ code: error.code, message: error.message }, error.exitCode);
221
+ }
222
+ throw error;
223
+ }
224
+ const url = buildUrl(operation, input, apiUrl);
225
+ const client = createApiClient({
226
+ apiKey,
227
+ apiUrl,
228
+ environment: ci ? 'ci' : 'local',
229
+ mode,
230
+ version: this.config.pjson.version,
231
+ });
232
+ let result;
233
+ try {
234
+ const showProgress = mode === 'human' &&
235
+ (outputFormat === 'plain' || outputFormat === 'table') &&
236
+ process.stdout.isTTY === true &&
237
+ process.stderr.isTTY === true &&
238
+ !ci;
239
+ result = await withRequestProgress(showProgress, () => client.request(url, { body, headers, method: operation.method }));
240
+ }
241
+ catch (error) {
242
+ return this.fail({
243
+ code: 'NETWORK_ERROR',
244
+ message: error instanceof Error ? error.message : String(error),
245
+ }, 1);
246
+ }
247
+ const { payload, response } = result;
248
+ if (!response.ok) {
249
+ const apiError = normalizeApiError(response.status, payload);
250
+ return this.fail(apiError, exitCodeFor(response.status));
251
+ }
252
+ const normalized = flags.raw === true ? payload : normalizeResult(payload);
253
+ const output = formatOutput(outputFormat, normalized, {
254
+ allColumns: flags.all === true,
255
+ columns: requestedColumns ?? (flags.all === true ? undefined : defaultColumns),
256
+ });
257
+ if (output !== undefined)
258
+ this.log(output);
259
+ const notice = paginationNotice(outputFormat, normalized);
260
+ if (notice)
261
+ this.logToStderr(notice.message);
262
+ }
263
+ }
264
+ return OperationCommand;
265
+ }
266
+ function flagForBodyField(field) {
267
+ const options = {
268
+ ...(field.aliases ? { aliases: [...field.aliases] } : {}),
269
+ ...(field.deprecated ? { deprecated: true } : {}),
270
+ description: bodyFieldDescription(field),
271
+ ...(field.hidden ? { hidden: true } : {}),
272
+ };
273
+ if (field.type === 'boolean')
274
+ return Flags.boolean({
275
+ ...options,
276
+ allowNo: true,
277
+ ...(typeof field.default === 'boolean' ? { defaultHelp: field.default } : {}),
278
+ });
279
+ if (field.type === 'array') {
280
+ const itemOptions = field.items?.enum?.every((value) => typeof value === 'string' || typeof value === 'number')
281
+ ? { options: field.items.enum.map(String) }
282
+ : {};
283
+ return Flags.string({
284
+ ...options,
285
+ ...itemOptions,
286
+ ...(Array.isArray(field.default)
287
+ ? { defaultHelp: field.default.map((value) => String(value)) }
288
+ : {}),
289
+ helpValue: field.items?.enum?.every((value) => typeof value === 'string' || typeof value === 'number')
290
+ ? '<option>'
291
+ : field.items?.type === 'object' || field.items?.type === 'array'
292
+ ? '<json>'
293
+ : scalarHelpValue(field.items?.type),
294
+ multiple: true,
295
+ });
296
+ }
297
+ if ((field.type === 'string' || field.type === 'number' || field.type === 'integer') &&
298
+ field.enum?.every((value) => typeof value === 'string' || typeof value === 'number')) {
299
+ return Flags.string({
300
+ ...options,
301
+ ...(field.default !== undefined ? { defaultHelp: displayValue(field.default) } : {}),
302
+ options: field.enum.map(String),
303
+ });
304
+ }
305
+ return Flags.string({
306
+ ...options,
307
+ ...(field.default !== undefined ? { defaultHelp: displayValue(field.default) } : {}),
308
+ helpValue: field.type === 'object' || field.type === 'json'
309
+ ? '<json>'
310
+ : field.type === 'number' || field.type === 'integer'
311
+ ? '<number>'
312
+ : '<value>',
313
+ });
314
+ }
315
+ function flagForParameter(parameter) {
316
+ const common = {
317
+ description: parameterDescription(parameter),
318
+ required: parameter.required,
319
+ };
320
+ if (parameter.type === 'boolean') {
321
+ return Flags.boolean({
322
+ ...common,
323
+ allowNo: true,
324
+ ...(typeof parameter.default === 'boolean' ? { defaultHelp: parameter.default } : {}),
325
+ });
326
+ }
327
+ if (parameter.type === 'array') {
328
+ return Flags.string({
329
+ ...common,
330
+ ...(Array.isArray(parameter.default)
331
+ ? { defaultHelp: parameter.default.map((value) => String(value)) }
332
+ : {}),
333
+ helpValue: scalarHelpValue(parameter.itemType),
334
+ multiple: true,
335
+ });
336
+ }
337
+ const hasPrimitiveEnum = parameter.enum?.every((value) => typeof value === 'string' || typeof value === 'number') ===
338
+ true;
339
+ return Flags.string({
340
+ ...common,
341
+ ...(hasPrimitiveEnum ? { options: parameter.enum.map(String) } : {}),
342
+ ...(parameter.default !== undefined ? { defaultHelp: displayValue(parameter.default) } : {}),
343
+ helpValue: hasPrimitiveEnum
344
+ ? '<option>'
345
+ : parameter.type === 'number' || parameter.type === 'integer'
346
+ ? '<number>'
347
+ : '<value>',
348
+ });
349
+ }
350
+ function bodyFieldDescription(field) {
351
+ const constraints = [];
352
+ if (field.required)
353
+ constraints.push('required');
354
+ if (field.minimum !== undefined)
355
+ constraints.push(`min: ${field.minimum}`);
356
+ if (field.maximum !== undefined)
357
+ constraints.push(`max: ${field.maximum}`);
358
+ if (field.minLength !== undefined)
359
+ constraints.push(`min length: ${field.minLength}`);
360
+ if (field.maxLength !== undefined)
361
+ constraints.push(`max length: ${field.maxLength}`);
362
+ if (field.minItems !== undefined)
363
+ constraints.push(`min items: ${field.minItems}`);
364
+ if (field.maxItems !== undefined)
365
+ constraints.push(`max items: ${field.maxItems}`);
366
+ if (field.requiredWhenParentPresent)
367
+ constraints.push('required when parent object is provided');
368
+ if (field.type === 'boolean') {
369
+ constraints.push(typeof field.default === 'boolean'
370
+ ? `not sent when omitted; API default: ${field.default ? 'on' : 'off'}`
371
+ : 'not sent when omitted; API default not documented');
372
+ }
373
+ const suffix = constraints.length > 0 ? ` (${constraints.join(', ')})` : '';
374
+ const itemShape = field.type === 'array' && field.items && schemaHasDisplayDetails(field.items)
375
+ ? schemaShape(field.items)
376
+ : undefined;
377
+ const shapeSuffix = itemShape
378
+ ? ` Item shape: ${itemShape}.${field.items?.requiredProperties?.length ? ' An asterisk marks a required property.' : ''}`
379
+ : '';
380
+ const convenience = field.path === 'assignedAlertContacts'
381
+ ? ' A bare numeric contact ID is also accepted and uses threshold 0 and recurrence 0.'
382
+ : '';
383
+ return `${field.description ?? `Set ${field.path}`}${suffix}.${shapeSuffix}${convenience}`.replace('..', '.');
384
+ }
385
+ function parameterDescription(parameter) {
386
+ const description = parameter.description ?? `Set ${parameter.name}`;
387
+ if (parameter.type !== 'boolean')
388
+ return description;
389
+ const omission = typeof parameter.default === 'boolean'
390
+ ? `not sent when omitted; API default: ${parameter.default ? 'on' : 'off'}`
391
+ : 'not sent when omitted; API default not documented';
392
+ return `${description.replace(/[.]$/, '')} (${omission}).`;
393
+ }
394
+ function schemaShape(schema) {
395
+ if (schema.type === 'object' && schema.properties) {
396
+ const required = new Set(schema.requiredProperties ?? []);
397
+ const properties = Object.entries(schema.properties).map(([name, property]) => `${name}${required.has(name) ? '*' : ''}:${schemaShape(property)}`);
398
+ return `{${properties.join(', ')}}${schemaConstraints(schema)}`;
399
+ }
400
+ if (schema.type === 'array') {
401
+ return `array<${schema.items ? schemaShape(schema.items) : 'value'}>${schemaConstraints(schema)}`;
402
+ }
403
+ return `${schema.type}${schemaConstraints(schema)}`;
404
+ }
405
+ function schemaConstraints(schema) {
406
+ const constraints = [];
407
+ if (schema.enum?.length)
408
+ constraints.push(`one of: ${schema.enum.map(String).join(', ')}`);
409
+ if (schema.minimum !== undefined)
410
+ constraints.push(`min: ${schema.minimum}`);
411
+ if (schema.maximum !== undefined)
412
+ constraints.push(`max: ${schema.maximum}`);
413
+ if (schema.minLength !== undefined)
414
+ constraints.push(`min length: ${schema.minLength}`);
415
+ if (schema.maxLength !== undefined)
416
+ constraints.push(`max length: ${schema.maxLength}`);
417
+ if (schema.minItems !== undefined)
418
+ constraints.push(`min items: ${schema.minItems}`);
419
+ if (schema.maxItems !== undefined)
420
+ constraints.push(`max items: ${schema.maxItems}`);
421
+ if (schema.example !== undefined)
422
+ constraints.push(`example: ${displayValue(schema.example)}`);
423
+ return constraints.length > 0 ? ` (${constraints.join('; ')})` : '';
424
+ }
425
+ function schemaHasDisplayDetails(schema) {
426
+ return (schema.type === 'object' ||
427
+ schema.type === 'array' ||
428
+ schema.enum !== undefined ||
429
+ schema.minimum !== undefined ||
430
+ schema.maximum !== undefined ||
431
+ schema.minLength !== undefined ||
432
+ schema.maxLength !== undefined ||
433
+ schema.minItems !== undefined ||
434
+ schema.maxItems !== undefined ||
435
+ schema.example !== undefined);
436
+ }
437
+ function examplesFor(operation) {
438
+ const command = commandForExample(operation);
439
+ const authoredExamples = operation.requestBodyExamples?.map((example) => curatedRequestExampleBody(operation, example.body));
440
+ const synthesized = authoredExamples?.length ? undefined : synthesizedRequestExample(operation);
441
+ const requestExamples = authoredExamples?.length
442
+ ? authoredExamples
443
+ : synthesized === undefined
444
+ ? []
445
+ : [synthesized];
446
+ if (requestExamples.length > 0) {
447
+ return requestExamples.map((body) => `${command} --body \\\n ${shellQuote(JSON.stringify(body))}`);
448
+ }
449
+ return operation.contentTypes.length === 0 ? [command] : [];
450
+ }
451
+ function commandForExample(operation) {
452
+ const parts = [`<%= config.bin %>`, operation.commandId.replaceAll(':', ' ')];
453
+ for (const parameter of operation.parameters.filter((candidate) => candidate.in === 'path')) {
454
+ parts.push(exampleParameterValue(parameter));
455
+ }
456
+ for (const parameter of operation.parameters.filter((candidate) => candidate.in === 'query' && candidate.required)) {
457
+ const flag = `--${flagName(parameter.name)}`;
458
+ if (parameter.type === 'boolean')
459
+ parts.push(flag);
460
+ else
461
+ parts.push(flag, exampleParameterValue(parameter));
462
+ }
463
+ return parts.join(' ');
464
+ }
465
+ function exampleParameterValue(parameter) {
466
+ const value = parameter.example ??
467
+ parameter.enum?.[0] ??
468
+ (parameter.type === 'number' || parameter.type === 'integer' ? 123 : 'example');
469
+ const rendered = String(value);
470
+ return /\s/.test(rendered) ? shellQuote(rendered) : rendered;
471
+ }
472
+ function shellQuote(value) {
473
+ return `'${value.replaceAll("'", `'\\''`)}'`;
474
+ }
475
+ function scalarHelpValue(type) {
476
+ if (type === 'number' || type === 'integer')
477
+ return '<number>';
478
+ if (type === 'boolean')
479
+ return '<boolean>';
480
+ if (type === 'json')
481
+ return '<json>';
482
+ return '<value>';
483
+ }
484
+ function displayValue(value) {
485
+ return typeof value === 'string' ? value : JSON.stringify(value);
486
+ }
@@ -0,0 +1,28 @@
1
+ import { flagName } from './flag-name.js';
2
+ export function enrichOperationParserError(error, operation) {
3
+ const enumFailure = /^Expected --([^=]+)=.* to be one of: ([^\n]+)/.exec(error.message);
4
+ if (enumFailure) {
5
+ const [, flag, options] = enumFailure;
6
+ const path = pathForFlag(operation, flag);
7
+ if (path)
8
+ Object.assign(error, { expected: `one of: ${options}`, path });
9
+ return;
10
+ }
11
+ const missingArgument = /^Missing 1 required arg:\n([^\n]+)/.exec(error.message);
12
+ if (missingArgument?.[1]) {
13
+ Object.assign(error, { expected: 'a value', path: missingArgument[1] });
14
+ return;
15
+ }
16
+ const missingFlag = /Missing required flag ([^\s\n]+)/.exec(error.message);
17
+ if (missingFlag?.[1]) {
18
+ const path = pathForFlag(operation, missingFlag[1]);
19
+ if (path)
20
+ Object.assign(error, { expected: 'a value', path });
21
+ }
22
+ }
23
+ function pathForFlag(operation, flag) {
24
+ const bodyField = operation.requestBodyFields?.find((field) => field.flag === flag || field.aliases?.includes(flag));
25
+ if (bodyField)
26
+ return bodyField.path;
27
+ return operation.parameters.find((parameter) => parameter.in === 'query' && flagName(parameter.name) === flag)?.name;
28
+ }
@@ -0,0 +1,76 @@
1
+ import { assignPath, isPlainRecord, valueAtPath } from './objects.js';
2
+ const REQUEST_CURATIONS = {
3
+ 'monitors:create:api': {
4
+ field: (field) => curateApiAssertionTarget(field, field.path),
5
+ },
6
+ 'monitors:create:keyword': {
7
+ defaults: { httpMethodType: 'GET' },
8
+ field: (field) => field.path === 'httpMethodType'
9
+ ? {
10
+ ...field,
11
+ default: 'GET',
12
+ description: 'HTTP method to use. The CLI defaults to GET; HEAD is invalid for Keyword monitors',
13
+ enum: field.enum?.filter((value) => value !== 'HEAD'),
14
+ example: 'GET',
15
+ }
16
+ : field,
17
+ issue: (body) => body.httpMethodType === 'HEAD'
18
+ ? {
19
+ expected: 'a method other than HEAD for Keyword monitors',
20
+ path: 'httpMethodType',
21
+ }
22
+ : undefined,
23
+ },
24
+ };
25
+ export function cliRequestDefaults(operation) {
26
+ return curationFor(operation)?.defaults ?? {};
27
+ }
28
+ export function curatedRequestBodyField(operation, field) {
29
+ return curationFor(operation)?.field?.(field) ?? field;
30
+ }
31
+ export function curatedRequestBodyFields(operation) {
32
+ return (operation.requestBodyFields ?? []).map((field) => curatedRequestBodyField(operation, field));
33
+ }
34
+ export function curatedRequestExampleBody(operation, body) {
35
+ if (!isPlainRecord(body))
36
+ return body;
37
+ const curated = structuredClone(body);
38
+ for (const [path, value] of Object.entries(cliRequestDefaults(operation))) {
39
+ if (valueAtPath(curated, path) === undefined)
40
+ assignPath(curated, path, value);
41
+ }
42
+ return curated;
43
+ }
44
+ export function curatedRequestExamples(operation) {
45
+ return (operation.requestBodyExamples ?? []).map((example) => ({
46
+ ...example,
47
+ body: curatedRequestExampleBody(operation, example.body),
48
+ }));
49
+ }
50
+ export function requestCurationIssue(operation, body) {
51
+ return curationFor(operation)?.issue?.(body);
52
+ }
53
+ function curationFor(operation) {
54
+ return REQUEST_CURATIONS[operation.commandId];
55
+ }
56
+ function curateApiAssertionTarget(schema, path) {
57
+ const curated = {
58
+ ...schema,
59
+ ...(schema.items ? { items: curateApiAssertionTarget(schema.items, `${path}[]`) } : {}),
60
+ ...(schema.properties
61
+ ? {
62
+ properties: Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => [
63
+ name,
64
+ curateApiAssertionTarget(property, `${path}.${name}`),
65
+ ])),
66
+ }
67
+ : {}),
68
+ };
69
+ return path === 'config.apiAssertions.checks[].target' && curated.type === 'object'
70
+ ? {
71
+ ...curated,
72
+ description: 'Temporarily treated as arbitrary JSON because the published schema conflicts with its examples',
73
+ type: 'json',
74
+ }
75
+ : curated;
76
+ }
@@ -0,0 +1,82 @@
1
+ import { assignPath, valueAtPath } from './objects.js';
2
+ import { cliRequestDefaults, curatedRequestBodyFields } from './request-curation.js';
3
+ import { isSensitiveFieldName } from './sensitive-fields.js';
4
+ const UNAVAILABLE = Symbol('unavailable request example');
5
+ export function synthesizedRequestExample(operation) {
6
+ const fields = curatedRequestBodyFields(operation);
7
+ if (fields.length === 0)
8
+ return undefined;
9
+ const body = {};
10
+ let suppliedField = false;
11
+ for (const field of fields.filter((candidate) => candidate.required)) {
12
+ const value = exampleValue(field);
13
+ if (value === UNAVAILABLE)
14
+ return undefined;
15
+ assignPath(body, field.path, value);
16
+ suppliedField = true;
17
+ }
18
+ if (!suppliedField) {
19
+ const candidate = fields.find((field) => !field.deprecated &&
20
+ !field.hidden &&
21
+ !field.path.includes('.') &&
22
+ !isSensitiveFieldName(field.path) &&
23
+ exampleValue(field) !== UNAVAILABLE);
24
+ if (candidate) {
25
+ assignPath(body, candidate.path, exampleValue(candidate));
26
+ suppliedField = true;
27
+ }
28
+ }
29
+ for (const [path, value] of Object.entries(cliRequestDefaults(operation))) {
30
+ if (valueAtPath(body, path) === undefined)
31
+ assignPath(body, path, structuredClone(value));
32
+ }
33
+ for (const [path, value] of Object.entries(operation.requestBodyDefaults ?? {})) {
34
+ assignPath(body, path, structuredClone(value));
35
+ }
36
+ return suppliedField || Object.keys(body).length > 0 ? body : undefined;
37
+ }
38
+ function exampleValue(schema) {
39
+ if (schema.example !== undefined)
40
+ return structuredClone(schema.example);
41
+ if (schema.enum?.length)
42
+ return structuredClone(schema.enum[0]);
43
+ switch (schema.type) {
44
+ case 'array': {
45
+ if (!schema.items)
46
+ return [];
47
+ const item = exampleValue(schema.items);
48
+ if (item === UNAVAILABLE)
49
+ return UNAVAILABLE;
50
+ return Array.from({ length: Math.max(1, schema.minItems ?? 0) }, () => structuredClone(item));
51
+ }
52
+ case 'boolean':
53
+ return typeof schema.default === 'boolean' ? schema.default : false;
54
+ case 'integer':
55
+ return Math.ceil(exampleNumber(schema));
56
+ case 'json':
57
+ return {};
58
+ case 'number':
59
+ return exampleNumber(schema);
60
+ case 'object': {
61
+ const value = {};
62
+ for (const property of schema.requiredProperties ?? []) {
63
+ const propertySchema = schema.properties?.[property];
64
+ if (!propertySchema)
65
+ return UNAVAILABLE;
66
+ const propertyValue = exampleValue(propertySchema);
67
+ if (propertyValue === UNAVAILABLE)
68
+ return UNAVAILABLE;
69
+ value[property] = propertyValue;
70
+ }
71
+ return value;
72
+ }
73
+ case 'string': {
74
+ const minimum = schema.minLength ?? 0;
75
+ return minimum > 7 ? 'x'.repeat(minimum) : 'example';
76
+ }
77
+ }
78
+ }
79
+ function exampleNumber(schema) {
80
+ const minimum = schema.minimum ?? 1;
81
+ return schema.maximum !== undefined ? Math.min(minimum, schema.maximum) : minimum;
82
+ }