@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.
- package/LICENSE +201 -0
- package/README.md +179 -0
- package/bin/run.js +43 -0
- package/dist/api/client.js +95 -0
- package/dist/api/errors.js +26 -0
- package/dist/commands/alert-contacts/create.js +10 -0
- package/dist/commands/alert-contacts/delete.js +10 -0
- package/dist/commands/alert-contacts/get.js +10 -0
- package/dist/commands/alert-contacts/list.js +10 -0
- package/dist/commands/alert-contacts/update.js +10 -0
- package/dist/commands/auth/login.js +51 -0
- package/dist/commands/auth/logout.js +33 -0
- package/dist/commands/auth/status.js +38 -0
- package/dist/commands/auth/whoami.js +6 -0
- package/dist/commands/incidents/activity-log.js +10 -0
- package/dist/commands/incidents/alerts.js +10 -0
- package/dist/commands/incidents/comments/create.js +10 -0
- package/dist/commands/incidents/comments/delete.js +10 -0
- package/dist/commands/incidents/comments/list.js +10 -0
- package/dist/commands/incidents/comments/update.js +10 -0
- package/dist/commands/incidents/get.js +10 -0
- package/dist/commands/incidents/list.js +10 -0
- package/dist/commands/integrations/create.js +10 -0
- package/dist/commands/integrations/delete.js +10 -0
- package/dist/commands/integrations/get.js +10 -0
- package/dist/commands/integrations/list.js +10 -0
- package/dist/commands/integrations/update.js +10 -0
- package/dist/commands/maintenance-windows/create.js +10 -0
- package/dist/commands/maintenance-windows/delete.js +10 -0
- package/dist/commands/maintenance-windows/get.js +10 -0
- package/dist/commands/maintenance-windows/list.js +10 -0
- package/dist/commands/maintenance-windows/update.js +10 -0
- package/dist/commands/monitor-groups/create.js +10 -0
- package/dist/commands/monitor-groups/delete.js +10 -0
- package/dist/commands/monitor-groups/get.js +10 -0
- package/dist/commands/monitor-groups/list.js +10 -0
- package/dist/commands/monitor-groups/update.js +10 -0
- package/dist/commands/monitors/bulk/pause.js +10 -0
- package/dist/commands/monitors/bulk/start.js +10 -0
- package/dist/commands/monitors/bulk/update.js +10 -0
- package/dist/commands/monitors/create/api.js +10 -0
- package/dist/commands/monitors/create/dns.js +10 -0
- package/dist/commands/monitors/create/heartbeat.js +10 -0
- package/dist/commands/monitors/create/http.js +10 -0
- package/dist/commands/monitors/create/keyword.js +10 -0
- package/dist/commands/monitors/create/ping.js +10 -0
- package/dist/commands/monitors/create/port.js +10 -0
- package/dist/commands/monitors/create/udp.js +10 -0
- package/dist/commands/monitors/create/visual-comparison.js +10 -0
- package/dist/commands/monitors/delete.js +10 -0
- package/dist/commands/monitors/get.js +10 -0
- package/dist/commands/monitors/list.js +10 -0
- package/dist/commands/monitors/pause.js +10 -0
- package/dist/commands/monitors/reset.js +10 -0
- package/dist/commands/monitors/schema.js +57 -0
- package/dist/commands/monitors/start.js +10 -0
- package/dist/commands/monitors/stats/response-time/all.js +10 -0
- package/dist/commands/monitors/stats/response-time.js +10 -0
- package/dist/commands/monitors/stats/uptime.js +10 -0
- package/dist/commands/monitors/update.js +10 -0
- package/dist/commands/monitors/uptime-stats.js +10 -0
- package/dist/commands/status-pages/announcements/create.js +10 -0
- package/dist/commands/status-pages/announcements/get.js +10 -0
- package/dist/commands/status-pages/announcements/list.js +10 -0
- package/dist/commands/status-pages/announcements/pin.js +10 -0
- package/dist/commands/status-pages/announcements/unpin.js +10 -0
- package/dist/commands/status-pages/announcements/update.js +10 -0
- package/dist/commands/status-pages/create.js +10 -0
- package/dist/commands/status-pages/delete.js +10 -0
- package/dist/commands/status-pages/get.js +10 -0
- package/dist/commands/status-pages/list.js +10 -0
- package/dist/commands/status-pages/update.js +10 -0
- package/dist/commands/tags/delete.js +10 -0
- package/dist/commands/tags/list.js +10 -0
- package/dist/commands/user/alert-contacts.js +10 -0
- package/dist/commands/user/all-alert-contacts.js +10 -0
- package/dist/commands/user/get.js +5 -0
- package/dist/commands/user/me.js +10 -0
- package/dist/generated/operations.js +7631 -0
- package/dist/lib/auth.js +60 -0
- package/dist/lib/base-command.js +21 -0
- package/dist/lib/cli-errors.js +80 -0
- package/dist/lib/command-suggestions.js +44 -0
- package/dist/lib/credential-store.js +152 -0
- package/dist/lib/flag-name.js +11 -0
- package/dist/lib/invocation.js +74 -0
- package/dist/lib/objects.js +34 -0
- package/dist/lib/operation-command.js +486 -0
- package/dist/lib/operation-parser-error.js +28 -0
- package/dist/lib/request-curation.js +76 -0
- package/dist/lib/request-example.js +82 -0
- package/dist/lib/request-preview.js +25 -0
- package/dist/lib/request.js +301 -0
- package/dist/lib/sensitive-fields.js +19 -0
- package/dist/lib/types.js +1 -0
- package/dist/output/columns.js +221 -0
- package/dist/output/display-width.js +34 -0
- package/dist/output/normalize-result.js +28 -0
- package/dist/output/pagination-notice.js +27 -0
- package/dist/output/renderer.js +145 -0
- package/dist/output/request-progress.js +15 -0
- package/dist/output/resolve-format.js +32 -0
- package/dist/output/style.js +57 -0
- package/dist/runtime/parse-input.js +35 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/validate-input.js +63 -0
- package/openapi/openapi.yaml +9034 -0
- package/package.json +135 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isRecord } from './objects.js';
|
|
2
|
+
import { isSensitiveFieldName } from './sensitive-fields.js';
|
|
3
|
+
const REDACTED = '[REDACTED]';
|
|
4
|
+
export function redactRequestPreview(body) {
|
|
5
|
+
const redacted = [];
|
|
6
|
+
return { body: redactValue(body, [], redacted), redacted };
|
|
7
|
+
}
|
|
8
|
+
function redactValue(value, path, redacted) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((item, index) => redactValue(item, [...path, String(index)], redacted));
|
|
11
|
+
}
|
|
12
|
+
if (!isRecord(value))
|
|
13
|
+
return value;
|
|
14
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
15
|
+
const childPath = [...path, key];
|
|
16
|
+
if (isSensitiveFieldName(key)) {
|
|
17
|
+
redacted.push(formatPath(childPath));
|
|
18
|
+
return [key, REDACTED];
|
|
19
|
+
}
|
|
20
|
+
return [key, redactValue(child, childPath, redacted)];
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function formatPath(segments) {
|
|
24
|
+
return segments.reduce((path, segment) => /^\d+$/.test(segment) ? `${path}[${segment}]` : path ? `${path}.${segment}` : segment, '');
|
|
25
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { assignPath, isPlainRecord, valueAtPath } from './objects.js';
|
|
4
|
+
import { cliRequestDefaults, curatedRequestBodyFields, requestCurationIssue, } from './request-curation.js';
|
|
5
|
+
export class RequestInputError extends Error {
|
|
6
|
+
path;
|
|
7
|
+
expected;
|
|
8
|
+
code = 'INVALID_INPUT';
|
|
9
|
+
exitCode = 2;
|
|
10
|
+
constructor(path, expected, message = `Invalid ${path}: expected ${expected}.`) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.path = path;
|
|
13
|
+
this.expected = expected;
|
|
14
|
+
this.name = 'RequestInputError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function buildUrl(operation, input, apiUrl) {
|
|
18
|
+
let path = operation.path;
|
|
19
|
+
for (const parameter of operation.parameters.filter((candidate) => candidate.in === 'path')) {
|
|
20
|
+
const value = input.path[parameter.name];
|
|
21
|
+
if (value !== undefined)
|
|
22
|
+
path = path.replace(`{${parameter.name}}`, encodeURIComponent(String(value)));
|
|
23
|
+
}
|
|
24
|
+
const base = apiUrl.toString().replace(/\/$/, '');
|
|
25
|
+
const url = new URL(`${base}${path}`);
|
|
26
|
+
for (const parameter of operation.parameters.filter((candidate) => candidate.in === 'query')) {
|
|
27
|
+
const value = input.query[parameter.name];
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
value.forEach((item) => url.searchParams.append(parameter.name, item));
|
|
30
|
+
else if (value !== undefined)
|
|
31
|
+
url.searchParams.set(parameter.name, String(value));
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
export async function buildRequestBody(operation, flags, headers) {
|
|
36
|
+
const bodyValue = typeof flags.body === 'string' ? await readBodyFlag(flags.body) : undefined;
|
|
37
|
+
const assignments = Array.isArray(flags.set) ? flags.set : [];
|
|
38
|
+
const files = Array.isArray(flags.file) ? flags.file : [];
|
|
39
|
+
const bodyFields = curatedRequestBodyFields(operation);
|
|
40
|
+
const hasNamedFields = bodyFields.some((field) => flags[field.flag] !== undefined);
|
|
41
|
+
const hasDefaults = Object.keys(operation.requestBodyDefaults ?? {}).length > 0;
|
|
42
|
+
const hasBodyInput = bodyValue !== undefined ||
|
|
43
|
+
assignments.length > 0 ||
|
|
44
|
+
files.length > 0 ||
|
|
45
|
+
hasNamedFields ||
|
|
46
|
+
hasDefaults;
|
|
47
|
+
if (!hasBodyInput) {
|
|
48
|
+
if (operation.requestBodyRequired)
|
|
49
|
+
throw new Error('A request body is required. Pass --body or --set.');
|
|
50
|
+
if (['PATCH', 'POST', 'PUT'].includes(operation.method)) {
|
|
51
|
+
headers.set('content-type', 'application/json');
|
|
52
|
+
return '{}';
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const objectBody = toObject(bodyValue);
|
|
57
|
+
for (const field of bodyFields) {
|
|
58
|
+
const value = flags[field.flag];
|
|
59
|
+
if (value !== undefined)
|
|
60
|
+
assignPath(objectBody, field.path, parseBodyField(field, value));
|
|
61
|
+
}
|
|
62
|
+
for (const assignment of assignments)
|
|
63
|
+
assignValue(objectBody, assignment);
|
|
64
|
+
for (const [path, value] of Object.entries(operation.requestBodyDefaults ?? {})) {
|
|
65
|
+
const supplied = valueAtPath(objectBody, path);
|
|
66
|
+
if (supplied !== undefined && supplied !== value) {
|
|
67
|
+
throw new RequestInputError(path, String(value), `${path} is fixed to ${String(value)} for this command.`);
|
|
68
|
+
}
|
|
69
|
+
assignPath(objectBody, path, value);
|
|
70
|
+
}
|
|
71
|
+
applyRequestCuration(operation, objectBody);
|
|
72
|
+
validateBodyFields(bodyFields, objectBody);
|
|
73
|
+
if (operation.contentTypes.includes('multipart/form-data')) {
|
|
74
|
+
const form = new FormData();
|
|
75
|
+
appendFormValues(form, objectBody);
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const [field, path] = splitAssignment(file, '--file');
|
|
78
|
+
const bytes = await readFile(path);
|
|
79
|
+
form.append(field, new Blob([bytes]), basename(path));
|
|
80
|
+
}
|
|
81
|
+
return form;
|
|
82
|
+
}
|
|
83
|
+
headers.set('content-type', 'application/json');
|
|
84
|
+
return JSON.stringify(objectBody);
|
|
85
|
+
}
|
|
86
|
+
function applyRequestCuration(operation, body) {
|
|
87
|
+
for (const [path, value] of Object.entries(cliRequestDefaults(operation))) {
|
|
88
|
+
if (valueAtPath(body, path) === undefined)
|
|
89
|
+
assignPath(body, path, value);
|
|
90
|
+
}
|
|
91
|
+
const issue = requestCurationIssue(operation, body);
|
|
92
|
+
if (issue)
|
|
93
|
+
throw new RequestInputError(issue.path, issue.expected);
|
|
94
|
+
}
|
|
95
|
+
function appendFormValues(form, values) {
|
|
96
|
+
for (const [key, value] of Object.entries(values)) {
|
|
97
|
+
if (Array.isArray(value))
|
|
98
|
+
value.forEach((item) => form.append(key, stringifyFormValue(item)));
|
|
99
|
+
else if (value !== undefined && value !== null)
|
|
100
|
+
form.append(key, stringifyFormValue(value));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function stringifyFormValue(value) {
|
|
104
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
105
|
+
}
|
|
106
|
+
async function readBodyFlag(value) {
|
|
107
|
+
const source = value === '-'
|
|
108
|
+
? await readStandardInput()
|
|
109
|
+
: value.startsWith('@')
|
|
110
|
+
? await readFile(value.slice(1), 'utf8')
|
|
111
|
+
: value;
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(source);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error('Request body must be valid JSON.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function readStandardInput() {
|
|
120
|
+
const chunks = [];
|
|
121
|
+
for await (const chunk of process.stdin)
|
|
122
|
+
chunks.push(Buffer.from(chunk));
|
|
123
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
124
|
+
}
|
|
125
|
+
function toObject(value) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return {};
|
|
128
|
+
if (!isPlainRecord(value)) {
|
|
129
|
+
throw new Error('Request body must be a JSON object.');
|
|
130
|
+
}
|
|
131
|
+
return structuredClone(value);
|
|
132
|
+
}
|
|
133
|
+
function assignValue(target, assignment) {
|
|
134
|
+
const [path, rawValue] = splitAssignment(assignment, '--set');
|
|
135
|
+
assignPath(target, path, parseValue(rawValue));
|
|
136
|
+
}
|
|
137
|
+
function splitAssignment(value, flag) {
|
|
138
|
+
const separator = value.indexOf('=');
|
|
139
|
+
if (separator <= 0 || separator === value.length - 1)
|
|
140
|
+
throw new Error(`${flag} expects name=value.`);
|
|
141
|
+
return [value.slice(0, separator), value.slice(separator + 1)];
|
|
142
|
+
}
|
|
143
|
+
function parseValue(value) {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(value);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function parseBodyField(field, rawValue) {
|
|
152
|
+
if (field.type === 'array') {
|
|
153
|
+
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
154
|
+
const itemSchema = field.items ?? { type: 'string' };
|
|
155
|
+
return values.map((value, index) => {
|
|
156
|
+
if (field.path === 'assignedAlertContacts' && itemSchema.type === 'object') {
|
|
157
|
+
const contactId = numericId(value);
|
|
158
|
+
if (contactId !== undefined) {
|
|
159
|
+
return { alertContactId: contactId, recurrence: 0, threshold: 0 };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return parseTypedValue(itemSchema, value, `${field.path}[${index}]`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return parseTypedValue(field, rawValue, field.path);
|
|
166
|
+
}
|
|
167
|
+
function parseTypedValue(schema, rawValue, path) {
|
|
168
|
+
if (schema.type === 'boolean') {
|
|
169
|
+
if (rawValue === true || rawValue === false)
|
|
170
|
+
return rawValue;
|
|
171
|
+
if (rawValue === 'true')
|
|
172
|
+
return true;
|
|
173
|
+
if (rawValue === 'false')
|
|
174
|
+
return false;
|
|
175
|
+
return rawValue;
|
|
176
|
+
}
|
|
177
|
+
if (schema.type === 'integer' || schema.type === 'number')
|
|
178
|
+
return Number(rawValue);
|
|
179
|
+
if (schema.type === 'object') {
|
|
180
|
+
const value = typeof rawValue === 'string' ? parseJsonValue(rawValue, path) : rawValue;
|
|
181
|
+
if (!isPlainRecord(value))
|
|
182
|
+
throw new RequestInputError(path, 'an object');
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
if (schema.type === 'array') {
|
|
186
|
+
const value = typeof rawValue === 'string' ? parseJsonValue(rawValue, path) : rawValue;
|
|
187
|
+
if (!Array.isArray(value))
|
|
188
|
+
throw new RequestInputError(path, 'an array');
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
if (schema.type === 'json' && typeof rawValue === 'string')
|
|
192
|
+
return parseValue(rawValue);
|
|
193
|
+
return String(rawValue);
|
|
194
|
+
}
|
|
195
|
+
function parseJsonValue(value, path) {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(value);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
throw new RequestInputError(path, 'valid JSON');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function validateBodyFields(fields, body) {
|
|
204
|
+
for (const field of fields) {
|
|
205
|
+
const value = valueAtPath(body, field.path);
|
|
206
|
+
if (value === undefined) {
|
|
207
|
+
if (field.required) {
|
|
208
|
+
throw new RequestInputError(field.path, 'a value', `${field.path} is required. Pass --${field.flag} or --set.`);
|
|
209
|
+
}
|
|
210
|
+
if (field.requiredWhenParentPresent) {
|
|
211
|
+
const parentPath = field.path.split('.').slice(0, -1).join('.');
|
|
212
|
+
if (parentPath && valueAtPath(body, parentPath) !== undefined) {
|
|
213
|
+
throw new RequestInputError(field.path, 'a value because its parent object is present');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
validateSchemaValue(field, value, field.path);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function validateSchemaValue(schema, value, path) {
|
|
222
|
+
if (value === null && schema.nullable)
|
|
223
|
+
return;
|
|
224
|
+
if (schema.type === 'json')
|
|
225
|
+
return;
|
|
226
|
+
if (schema.type === 'array') {
|
|
227
|
+
if (!Array.isArray(value))
|
|
228
|
+
throw new RequestInputError(path, 'an array');
|
|
229
|
+
if (schema.minItems !== undefined && value.length < schema.minItems)
|
|
230
|
+
throw new RequestInputError(path, `at least ${schema.minItems} items`);
|
|
231
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems)
|
|
232
|
+
throw new RequestInputError(path, `at most ${schema.maxItems} items`);
|
|
233
|
+
if (schema.items) {
|
|
234
|
+
value.forEach((item, index) => validateSchemaValue(schema.items, item, `${path}[${index}]`));
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (schema.type === 'object') {
|
|
239
|
+
if (!isPlainRecord(value))
|
|
240
|
+
throw new RequestInputError(path, 'an object');
|
|
241
|
+
const properties = schema.properties ?? {};
|
|
242
|
+
for (const property of schema.requiredProperties ?? []) {
|
|
243
|
+
if (value[property] === undefined) {
|
|
244
|
+
throw new RequestInputError(`${path}.${property}`, `required property ${property}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const [name, propertySchema] of Object.entries(properties)) {
|
|
248
|
+
if (value[name] !== undefined)
|
|
249
|
+
validateSchemaValue(propertySchema, value[name], `${path}.${name}`);
|
|
250
|
+
}
|
|
251
|
+
if (schema.additionalProperties === false) {
|
|
252
|
+
const unknownProperty = Object.keys(value).find((name) => !(name in properties));
|
|
253
|
+
if (unknownProperty !== undefined) {
|
|
254
|
+
throw new RequestInputError(`${path}.${unknownProperty}`, 'a documented property');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (typeof schema.additionalProperties === 'object') {
|
|
258
|
+
for (const [name, propertyValue] of Object.entries(value)) {
|
|
259
|
+
if (!(name in properties)) {
|
|
260
|
+
validateSchemaValue(schema.additionalProperties, propertyValue, `${path}.${name}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (schema.type === 'boolean' && typeof value !== 'boolean')
|
|
267
|
+
throw new RequestInputError(path, 'a boolean');
|
|
268
|
+
if ((schema.type === 'integer' || schema.type === 'number') && !isFiniteNumber(value))
|
|
269
|
+
throw new RequestInputError(path, 'a number');
|
|
270
|
+
if (schema.type === 'integer' && typeof value === 'number' && !Number.isInteger(value))
|
|
271
|
+
throw new RequestInputError(path, 'an integer');
|
|
272
|
+
if (schema.type === 'string' && typeof value !== 'string')
|
|
273
|
+
throw new RequestInputError(path, 'a string');
|
|
274
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
275
|
+
throw new RequestInputError(path, `one of: ${schema.enum.map(String).join(', ')}`);
|
|
276
|
+
}
|
|
277
|
+
if (typeof value === 'number') {
|
|
278
|
+
if (schema.minimum !== undefined && value < schema.minimum)
|
|
279
|
+
throw new RequestInputError(path, `at least ${schema.minimum}`);
|
|
280
|
+
if (schema.maximum !== undefined && value > schema.maximum)
|
|
281
|
+
throw new RequestInputError(path, `at most ${schema.maximum}`);
|
|
282
|
+
}
|
|
283
|
+
if (typeof value === 'string') {
|
|
284
|
+
const length = [...value].length;
|
|
285
|
+
if (schema.minLength !== undefined && length < schema.minLength)
|
|
286
|
+
throw new RequestInputError(path, `at least ${schema.minLength} characters`);
|
|
287
|
+
if (schema.maxLength !== undefined && length > schema.maxLength)
|
|
288
|
+
throw new RequestInputError(path, `at most ${schema.maxLength} characters`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function numericId(value) {
|
|
292
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
|
|
293
|
+
return value;
|
|
294
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value))
|
|
295
|
+
return undefined;
|
|
296
|
+
const id = Number(value);
|
|
297
|
+
return Number.isSafeInteger(id) ? id : undefined;
|
|
298
|
+
}
|
|
299
|
+
function isFiniteNumber(value) {
|
|
300
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
301
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field names that look like they carry credentials. Used to redact dry-run
|
|
3
|
+
* previews and to avoid picking secret-looking fields in synthesized examples.
|
|
4
|
+
*/
|
|
5
|
+
const SENSITIVE_FIELD_SUFFIXES = [
|
|
6
|
+
'apikey',
|
|
7
|
+
'authorization',
|
|
8
|
+
'cookie',
|
|
9
|
+
'credential',
|
|
10
|
+
'password',
|
|
11
|
+
'passphrase',
|
|
12
|
+
'privatekey',
|
|
13
|
+
'secret',
|
|
14
|
+
'token',
|
|
15
|
+
];
|
|
16
|
+
export function isSensitiveFieldName(name) {
|
|
17
|
+
const normalized = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
18
|
+
return SENSITIVE_FIELD_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { isRecord } from '../lib/objects.js';
|
|
2
|
+
export const COLLECTION_COMMAND_IDS = [
|
|
3
|
+
'alert-contacts:list',
|
|
4
|
+
'incidents:activity-log',
|
|
5
|
+
'incidents:alerts',
|
|
6
|
+
'incidents:comments:list',
|
|
7
|
+
'incidents:list',
|
|
8
|
+
'integrations:list',
|
|
9
|
+
'maintenance-windows:list',
|
|
10
|
+
'monitor-groups:list',
|
|
11
|
+
'monitors:list',
|
|
12
|
+
'status-pages:announcements:list',
|
|
13
|
+
'status-pages:list',
|
|
14
|
+
'tags:list',
|
|
15
|
+
'user:alert-contacts',
|
|
16
|
+
'user:all-alert-contacts',
|
|
17
|
+
];
|
|
18
|
+
const ALERT_CONTACT_COLUMNS = [
|
|
19
|
+
{ key: 'id' },
|
|
20
|
+
{ key: 'status' },
|
|
21
|
+
{ header: 'NAME', key: 'friendlyName', maxWidth: 28 },
|
|
22
|
+
{ key: 'type' },
|
|
23
|
+
{ key: 'value', maxWidth: 36 },
|
|
24
|
+
{ header: 'NOTIFY FOR', key: 'enableNotificationsFor' },
|
|
25
|
+
];
|
|
26
|
+
export const COMMAND_COLUMNS = {
|
|
27
|
+
'alert-contacts:list': ALERT_CONTACT_COLUMNS,
|
|
28
|
+
'incidents:activity-log': [
|
|
29
|
+
{ header: 'TIME', key: 'date' },
|
|
30
|
+
{ format: activityEvent, header: 'EVENT', key: 'type' },
|
|
31
|
+
{ format: activityDetail, header: 'DETAIL', key: 'reason', maxWidth: 48 },
|
|
32
|
+
{ format: activityRegion, header: 'REGION', key: 'region' },
|
|
33
|
+
{ format: activityResult, header: 'RESULT', key: 'notificationStatus' },
|
|
34
|
+
],
|
|
35
|
+
'incidents:alerts': [
|
|
36
|
+
{ key: 'timestamp' },
|
|
37
|
+
{ key: 'status' },
|
|
38
|
+
{ header: 'RECIPIENT', key: 'recipientName', maxWidth: 28 },
|
|
39
|
+
{ header: 'CONTACT', key: 'recipientValue', maxWidth: 36 },
|
|
40
|
+
{ header: 'CHANNEL', key: 'channelType' },
|
|
41
|
+
],
|
|
42
|
+
'incidents:comments:list': [
|
|
43
|
+
{ key: 'id' },
|
|
44
|
+
{ key: 'created' },
|
|
45
|
+
{ header: 'AUTHOR', key: 'user.fullName', maxWidth: 28 },
|
|
46
|
+
{ key: 'comment', maxWidth: 48 },
|
|
47
|
+
],
|
|
48
|
+
'incidents:list': [
|
|
49
|
+
{ key: 'id' },
|
|
50
|
+
{ key: 'status' },
|
|
51
|
+
{ header: 'MONITOR', key: 'monitor.friendlyName', maxWidth: 28 },
|
|
52
|
+
{ key: 'type' },
|
|
53
|
+
{ format: rootCause, header: 'ROOT CAUSE', key: 'reason', maxWidth: 48 },
|
|
54
|
+
{ header: 'STARTED', key: 'startedAt' },
|
|
55
|
+
{ format: durationSeconds, key: 'duration' },
|
|
56
|
+
{ header: 'COMMENTS', key: 'commentsCount' },
|
|
57
|
+
],
|
|
58
|
+
'integrations:list': [
|
|
59
|
+
{ key: 'id' },
|
|
60
|
+
{ key: 'status' },
|
|
61
|
+
{ header: 'NAME', key: 'friendlyName', maxWidth: 28 },
|
|
62
|
+
{ key: 'type' },
|
|
63
|
+
{ header: 'NOTIFY FOR', key: 'enableNotificationsFor' },
|
|
64
|
+
],
|
|
65
|
+
'maintenance-windows:list': [
|
|
66
|
+
{ key: 'id' },
|
|
67
|
+
{ key: 'status' },
|
|
68
|
+
{ key: 'name', maxWidth: 28 },
|
|
69
|
+
{ header: 'REPEAT', key: 'interval' },
|
|
70
|
+
{ key: 'date' },
|
|
71
|
+
{ key: 'time' },
|
|
72
|
+
{ header: 'DURATION (MIN)', key: 'duration' },
|
|
73
|
+
],
|
|
74
|
+
'monitor-groups:list': [
|
|
75
|
+
{ key: 'id' },
|
|
76
|
+
{ key: 'name', maxWidth: 28 },
|
|
77
|
+
{ key: 'createdAt' },
|
|
78
|
+
{ key: 'updatedAt' },
|
|
79
|
+
],
|
|
80
|
+
'monitors:list': [
|
|
81
|
+
{ key: 'id' },
|
|
82
|
+
{ key: 'status' },
|
|
83
|
+
{ format: nameOrUrl, header: 'NAME', key: 'friendlyName', maxWidth: 28 },
|
|
84
|
+
{ key: 'type' },
|
|
85
|
+
{ header: 'TARGET', key: 'url', maxWidth: 40 },
|
|
86
|
+
{ format: durationSeconds, key: 'interval' },
|
|
87
|
+
{ format: stateAge, header: 'IN STATE', key: 'currentStateDuration' },
|
|
88
|
+
{ format: joinNames, key: 'tags', maxWidth: 32 },
|
|
89
|
+
],
|
|
90
|
+
'status-pages:announcements:list': [
|
|
91
|
+
{ key: 'id' },
|
|
92
|
+
{ key: 'title', maxWidth: 40 },
|
|
93
|
+
{ key: 'status' },
|
|
94
|
+
{ key: 'type' },
|
|
95
|
+
{ key: 'startDate' },
|
|
96
|
+
{ key: 'endDate' },
|
|
97
|
+
{ key: 'deliveryStatus' },
|
|
98
|
+
],
|
|
99
|
+
'status-pages:list': [
|
|
100
|
+
{ key: 'id' },
|
|
101
|
+
{ key: 'status' },
|
|
102
|
+
{ header: 'NAME', key: 'friendlyName', maxWidth: 28 },
|
|
103
|
+
{ key: 'urlKey' },
|
|
104
|
+
{ key: 'customDomain', maxWidth: 36 },
|
|
105
|
+
{ header: 'MONITORS', key: 'monitorsCount' },
|
|
106
|
+
{ format: accessLevel, header: 'ACCESS', key: 'isPasswordSet' },
|
|
107
|
+
],
|
|
108
|
+
'tags:list': [{ key: 'id' }, { key: 'name', maxWidth: 28 }],
|
|
109
|
+
'user:alert-contacts': ALERT_CONTACT_COLUMNS,
|
|
110
|
+
'user:all-alert-contacts': [
|
|
111
|
+
{ header: 'USER ID', key: 'user.id' },
|
|
112
|
+
{ header: 'USER', key: 'user.name', maxWidth: 28 },
|
|
113
|
+
{ key: 'notifyOnly' },
|
|
114
|
+
{ format: joinNames, header: 'CONTACTS', key: 'alertContacts', maxWidth: 32 },
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
export function columnsFor(commandId) {
|
|
118
|
+
if (!Object.hasOwn(COMMAND_COLUMNS, commandId))
|
|
119
|
+
return undefined;
|
|
120
|
+
return COMMAND_COLUMNS[commandId];
|
|
121
|
+
}
|
|
122
|
+
export function parseColumnsFlag(value) {
|
|
123
|
+
const keys = value
|
|
124
|
+
.split(',')
|
|
125
|
+
.map((key) => key.trim())
|
|
126
|
+
.filter((key) => key.length > 0);
|
|
127
|
+
if (keys.length === 0)
|
|
128
|
+
throw new Error('--columns must contain at least one column name.');
|
|
129
|
+
return keys.map((key) => ({ key }));
|
|
130
|
+
}
|
|
131
|
+
function nameOrUrl(value, row) {
|
|
132
|
+
return value ?? row.url;
|
|
133
|
+
}
|
|
134
|
+
function rootCause(value, row) {
|
|
135
|
+
return value ?? row.cause;
|
|
136
|
+
}
|
|
137
|
+
function activityEvent(value, row) {
|
|
138
|
+
if (value === 'STATUS_UPDATE') {
|
|
139
|
+
return firstString(row.incidentStatus, row.alertLogType) ?? 'Status update';
|
|
140
|
+
}
|
|
141
|
+
if (value === 'COMMENT')
|
|
142
|
+
return 'Comment';
|
|
143
|
+
if (value === 'NOTIFICATION')
|
|
144
|
+
return firstString(row.notificationType) ?? 'Notification';
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function activityDetail(_value, row) {
|
|
148
|
+
if (row.type === 'STATUS_UPDATE')
|
|
149
|
+
return row.reason;
|
|
150
|
+
if (row.type === 'COMMENT')
|
|
151
|
+
return firstString(row.commentFullName, row.commentEmail);
|
|
152
|
+
if (row.type === 'NOTIFICATION')
|
|
153
|
+
return firstString(row.sentToFullName, row.sentToValue);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
function activityRegion(value, row) {
|
|
157
|
+
const region = firstString(value);
|
|
158
|
+
if (region !== undefined)
|
|
159
|
+
return region;
|
|
160
|
+
if (!isRecord(row.remoteNode))
|
|
161
|
+
return undefined;
|
|
162
|
+
const location = [row.remoteNode.city, row.remoteNode.country].filter((part) => typeof part === 'string' && part.length > 0);
|
|
163
|
+
return location.length > 0 ? location.join(', ') : undefined;
|
|
164
|
+
}
|
|
165
|
+
function activityResult(value, row) {
|
|
166
|
+
if (row.type === 'STATUS_UPDATE') {
|
|
167
|
+
return typeof row.responseTime === 'number' && Number.isFinite(row.responseTime)
|
|
168
|
+
? `${row.responseTime} ms`
|
|
169
|
+
: undefined;
|
|
170
|
+
}
|
|
171
|
+
if (row.type === 'NOTIFICATION')
|
|
172
|
+
return value;
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
function firstString(...values) {
|
|
176
|
+
return values.find((value) => typeof value === 'string' && value.length > 0);
|
|
177
|
+
}
|
|
178
|
+
function durationSeconds(value) {
|
|
179
|
+
return humanizeDuration(value, 4);
|
|
180
|
+
}
|
|
181
|
+
// State ages reach hundreds of days, where trailing minutes and seconds are
|
|
182
|
+
// noise in a scannable column.
|
|
183
|
+
function stateAge(value) {
|
|
184
|
+
return humanizeDuration(value, 2);
|
|
185
|
+
}
|
|
186
|
+
function humanizeDuration(value, maxUnits) {
|
|
187
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
188
|
+
return undefined;
|
|
189
|
+
const units = [
|
|
190
|
+
['d', 86_400],
|
|
191
|
+
['h', 3600],
|
|
192
|
+
['m', 60],
|
|
193
|
+
['s', 1],
|
|
194
|
+
];
|
|
195
|
+
const parts = [];
|
|
196
|
+
let remaining = Math.floor(value);
|
|
197
|
+
for (const [suffix, size] of units) {
|
|
198
|
+
const amount = Math.floor(remaining / size);
|
|
199
|
+
remaining %= size;
|
|
200
|
+
if (amount > 0)
|
|
201
|
+
parts.push(`${amount}${suffix}`);
|
|
202
|
+
if (parts.length === maxUnits)
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
return parts.length > 0 ? parts.join(' ') : '0s';
|
|
206
|
+
}
|
|
207
|
+
function joinNames(value) {
|
|
208
|
+
if (!Array.isArray(value))
|
|
209
|
+
return undefined;
|
|
210
|
+
const names = value
|
|
211
|
+
.map((item) => (isRecord(item) ? item.name : undefined))
|
|
212
|
+
.filter((name) => typeof name === 'string');
|
|
213
|
+
return names.length > 0 ? names.join(', ') : undefined;
|
|
214
|
+
}
|
|
215
|
+
function accessLevel(value) {
|
|
216
|
+
if (value === true)
|
|
217
|
+
return 'password';
|
|
218
|
+
if (value === false)
|
|
219
|
+
return 'public';
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal display width for table layout. Grapheme clusters are the unit of
|
|
3
|
+
* truncation so surrogate pairs, combining marks, and ZWJ sequences are never
|
|
4
|
+
* split; East Asian wide/fullwidth characters and emoji count as two columns.
|
|
5
|
+
*/
|
|
6
|
+
const graphemeSegmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
7
|
+
// Wide/fullwidth ranges per wcwidth, plus emoji blocks. The variation
|
|
8
|
+
// selector U+FE0F upgrades a cluster to emoji presentation (width 2);
|
|
9
|
+
// it is intentionally matched as a combining mark inside a grapheme cluster.
|
|
10
|
+
const WIDE_OR_EMOJI =
|
|
11
|
+
// oxlint-disable-next-line no-misleading-character-class
|
|
12
|
+
/[\u1100-\u115F\u2329\u232A\u2E80-\uA4CF\uA960-\uA97F\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6\uFE0F\u{1F000}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u;
|
|
13
|
+
export function displayWidth(text) {
|
|
14
|
+
let width = 0;
|
|
15
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
16
|
+
width += WIDE_OR_EMOJI.test(segment) ? 2 : 1;
|
|
17
|
+
}
|
|
18
|
+
return width;
|
|
19
|
+
}
|
|
20
|
+
export function truncateToWidth(text, maxWidth) {
|
|
21
|
+
if (displayWidth(text) <= maxWidth)
|
|
22
|
+
return text;
|
|
23
|
+
const budget = maxWidth - 1;
|
|
24
|
+
let width = 0;
|
|
25
|
+
let kept = '';
|
|
26
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
27
|
+
const segmentWidth = WIDE_OR_EMOJI.test(segment) ? 2 : 1;
|
|
28
|
+
if (width + segmentWidth > budget)
|
|
29
|
+
break;
|
|
30
|
+
width += segmentWidth;
|
|
31
|
+
kept += segment;
|
|
32
|
+
}
|
|
33
|
+
return `${kept}…`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isRecord } from '../lib/objects.js';
|
|
2
|
+
export function normalizeResult(payload) {
|
|
3
|
+
if (Array.isArray(payload))
|
|
4
|
+
return { items: payload, nextCursor: null };
|
|
5
|
+
if (!isRecord(payload) || !Array.isArray(payload.data))
|
|
6
|
+
return payload;
|
|
7
|
+
return {
|
|
8
|
+
items: payload.data,
|
|
9
|
+
nextCursor: cursorFromNextLink(payload.nextLink) ??
|
|
10
|
+
cursorFromValue(payload.nextCursor) ??
|
|
11
|
+
cursorFromValue(payload.nextCursorId),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function cursorFromNextLink(nextLink) {
|
|
15
|
+
if (typeof nextLink !== 'string')
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
return new URL(nextLink, 'https://api.uptimerobot.com').searchParams.get('cursor');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function cursorFromValue(value) {
|
|
25
|
+
if (typeof value === 'string')
|
|
26
|
+
return value.length > 0 ? value : null;
|
|
27
|
+
return typeof value === 'number' && Number.isFinite(value) ? String(value) : null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isRecord } from '../lib/objects.js';
|
|
2
|
+
export function paginationNotice(format, normalizedResult) {
|
|
3
|
+
if ((format !== 'table' && format !== 'plain') || !isRecord(normalizedResult))
|
|
4
|
+
return undefined;
|
|
5
|
+
const cursor = normalizedResult.nextCursor;
|
|
6
|
+
if (typeof cursor !== 'string' || cursor.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
return {
|
|
9
|
+
message: `More results are available. Next cursor: ${printableCursor(cursor)}`,
|
|
10
|
+
stream: 'stderr',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function printableCursor(cursor) {
|
|
14
|
+
return Array.from(cursor, (character) => {
|
|
15
|
+
if (character === '\n')
|
|
16
|
+
return '\\n';
|
|
17
|
+
if (character === '\r')
|
|
18
|
+
return '\\r';
|
|
19
|
+
if (character === '\t')
|
|
20
|
+
return '\\t';
|
|
21
|
+
const codePoint = character.codePointAt(0);
|
|
22
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
23
|
+
return `\\u${codePoint.toString(16).padStart(4, '0')}`;
|
|
24
|
+
}
|
|
25
|
+
return character;
|
|
26
|
+
}).join('');
|
|
27
|
+
}
|