uptimer-cli 0.0.0-stage → 0.2.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # uptimer-cli
2
+
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bc089e6: Publish through the CI trusted publisher with npm provenance attestations, so the released tarball is verifiably built from this repository by the tagged workflow run.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Add `uptimer skill install`, which copies the bundled agent skill into a local directory (`./skills` by default, `--dir` to override, `--force` to replace) without requiring an instance URL or token.
14
+
15
+ ### Patch Changes
16
+
17
+ - d44b001: Package the Uptimer CLI as a standalone JavaScript executable and automate npm publishing and GitHub Releases with Changesets.
18
+ - Fix configuration output safety and input handling: redact `http_headers_json` and notification `config_json` payloads and reflected token keys, stop echoing payload fragments and transport messages in errors, bound timeout cleanup, accept a null `group_name` from group unassignment, and reject repeated options. Help payload examples now match the Admin API fields.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vrian Cao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # Uptimer CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Manage monitors, notification channels, status pages, and global settings through the Uptimer Admin API. Requires Node.js >=22.14.0.
4
+
5
+ ## Install
6
+
7
+ After the first npm release:
8
+
9
+ ```sh
10
+ npm install --global uptimer-cli
11
+ uptimer --help
12
+ ```
13
+
14
+ To run a pinned version without a global install:
15
+
16
+ ```sh
17
+ npx --yes uptimer-cli@<version> --help
18
+ ```
19
+
20
+ ## Connect
21
+
22
+ Set `UPTIMER_URL` to the instance origin, such as `https://your-worker.workers.dev`, without `/api/v1`. Supply `UPTIMER_ADMIN_TOKEN` through the invoking process environment or your agent credential configuration. No credential file is created and `.env` is not loaded automatically. HTTPS is required except for explicit loopback HTTP development origins.
23
+
24
+ ```sh
25
+ uptimer settings get
26
+ uptimer monitors list --limit 20
27
+ uptimer status-pages list
28
+ ```
29
+
30
+ Write commands accept a UTF-8 JSON object using `--file path.json` or `--file -` for stdin. Payloads use the existing Admin API fields. For example:
31
+
32
+ ```sh
33
+ printf '%s\n' '{"name":"API","type":"http","target":"https://api.example.com/health","interval_sec":60}' | uptimer monitors create --file -
34
+ ```
35
+
36
+ Successful commands write JSON to stdout. Errors write JSON to stderr and exit with 1 for request failures or 2 for local input/configuration failures. Help exits with 0. Monitor and notification-channel lists are bounded to 50 items by default, or 1–200 with `--limit`; they are not complete exports.
37
+
38
+ The Admin Token has full management permissions. Treat all returned configuration as administrative data and keep it out of shared logs. A failed or timed-out write may already have reached the server; query before retrying.
39
+
40
+ ## Use from a coding agent
41
+
42
+ This package ships an agent skill covering install checks, connection setup, payload field references, and a write-then-verify sequence. Install it locally without leaving the CLI:
43
+
44
+ ```sh
45
+ uptimer skill install # ./skills/uptimer-cli/
46
+ uptimer skill install --dir ~/.claude # ~/.claude/uptimer-cli/
47
+ uptimer skill install --force # replace an existing copy
48
+ ```
49
+
50
+ The command needs no instance URL or token and prints the installed path and file list as JSON. Alternatively, `npx skills add nexttylabs/Uptimer --skill uptimer-cli` auto-detects installed agents (`-g` for global, `-a <agent>` for one target). The skill source lives in [`skills/uptimer-cli/`](../../skills/uptimer-cli/SKILL.md); its [command reference](../../skills/uptimer-cli/references/commands.md) adds field detail beyond the inline `--help` examples.
51
+
52
+ ## Develop and release
53
+
54
+ From the repository root:
55
+
56
+ ```sh
57
+ pnpm --filter uptimer-cli start --help
58
+ pnpm --filter uptimer-cli test
59
+ pnpm --filter uptimer-cli test:package
60
+ pnpm changeset
61
+ ```
62
+
63
+ See [CLI release maintenance](https://github.com/nexttylabs/Uptimer/blob/master/docs/cli-releases.md) for the version PR, npm publishing, and GitHub Release workflow.
@@ -0,0 +1,441 @@
1
+ import { z } from 'zod';
2
+ import { CliError } from "./errors.js";
3
+ import { readJsonPayload } from "./input.js";
4
+ import { installSkill } from "./skill.js";
5
+ import { executeApiRequest } from "./transport.js";
6
+ const monitorsListEnvelope = z.object({
7
+ monitors: z.array(z.record(z.unknown())),
8
+ });
9
+ const monitorEnvelope = z.object({
10
+ monitor: z.record(z.unknown()),
11
+ });
12
+ const deletedEnvelope = z.object({
13
+ deleted: z.boolean(),
14
+ });
15
+ const monitorGroupsReorderEnvelope = z.object({
16
+ updated_groups: z.number(),
17
+ affected_monitors: z.number(),
18
+ });
19
+ const monitorGroupsAssignEnvelope = z.object({
20
+ // The server returns null when monitors are removed from a group.
21
+ group_name: z.string().nullable(),
22
+ group_sort_order: z.number(),
23
+ updated_monitors: z.number(),
24
+ });
25
+ const notificationChannelsListEnvelope = z.object({
26
+ notification_channels: z.array(z.record(z.unknown())),
27
+ });
28
+ const notificationChannelEnvelope = z.object({
29
+ notification_channel: z.record(z.unknown()),
30
+ });
31
+ const statusPagesListEnvelope = z.object({
32
+ status_pages: z.array(z.record(z.unknown())),
33
+ });
34
+ const statusPageEnvelope = z.object({
35
+ status_page: z.record(z.unknown()),
36
+ });
37
+ const settingsEnvelope = z.object({
38
+ settings: z.record(z.unknown()),
39
+ });
40
+ const uptimeRatingEnvelope = z.object({
41
+ uptime_rating_level: z.number(),
42
+ });
43
+ // A repeated option silently changes the request (last value wins), so reject it.
44
+ function assertUnset(value, option) {
45
+ if (value !== undefined) {
46
+ throw new CliError(2, 'INVALID_ARGUMENT', `Option ${option} may only be given once`);
47
+ }
48
+ }
49
+ export function parseArgs(argv) {
50
+ const positionals = [];
51
+ const options = { force: false };
52
+ let isHelp = false;
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const arg = argv[i];
55
+ if (arg === '--help' || arg === '-h') {
56
+ isHelp = true;
57
+ continue;
58
+ }
59
+ if (arg === '--force') {
60
+ options.force = true;
61
+ continue;
62
+ }
63
+ if (arg.startsWith('--dir=')) {
64
+ assertUnset(options.dir, '--dir');
65
+ options.dir = arg.slice(6);
66
+ continue;
67
+ }
68
+ if (arg === '--dir') {
69
+ assertUnset(options.dir, '--dir');
70
+ i++;
71
+ if (i >= argv.length) {
72
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --dir requires a value');
73
+ }
74
+ options.dir = argv[i];
75
+ continue;
76
+ }
77
+ if (arg.startsWith('--limit=')) {
78
+ assertUnset(options.limit, '--limit');
79
+ options.limit = arg.slice(8);
80
+ continue;
81
+ }
82
+ if (arg === '--limit') {
83
+ assertUnset(options.limit, '--limit');
84
+ i++;
85
+ if (i >= argv.length || argv[i].startsWith('--')) {
86
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit requires a value');
87
+ }
88
+ options.limit = argv[i];
89
+ continue;
90
+ }
91
+ if (arg.startsWith('--file=')) {
92
+ assertUnset(options.file, '--file');
93
+ options.file = arg.slice(7);
94
+ continue;
95
+ }
96
+ if (arg === '--file') {
97
+ assertUnset(options.file, '--file');
98
+ i++;
99
+ if (i >= argv.length) {
100
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file requires a value');
101
+ }
102
+ options.file = argv[i];
103
+ continue;
104
+ }
105
+ if (arg.startsWith('--') || /^-[a-zA-Z]/.test(arg)) {
106
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown option: "${arg}"`);
107
+ }
108
+ positionals.push(arg);
109
+ }
110
+ return { positionals, options, isHelp };
111
+ }
112
+ function parseId(raw) {
113
+ if (!raw) {
114
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Resource ID is required');
115
+ }
116
+ if (!/^[1-9]\d*$/.test(raw)) {
117
+ throw new CliError(2, 'INVALID_ARGUMENT', `Invalid ID: "${raw}". ID must be a positive integer.`);
118
+ }
119
+ const n = Number(raw);
120
+ if (!Number.isSafeInteger(n) || n <= 0) {
121
+ throw new CliError(2, 'INVALID_ARGUMENT', `Invalid ID: "${raw}". ID must be a positive safe integer.`);
122
+ }
123
+ return n;
124
+ }
125
+ function parseLimit(raw) {
126
+ if (raw === undefined) {
127
+ return 50;
128
+ }
129
+ if (!/^\d+$/.test(raw)) {
130
+ throw new CliError(2, 'INVALID_ARGUMENT', `Invalid limit: "${raw}". Limit must be an integer between 1 and 200.`);
131
+ }
132
+ const n = Number(raw);
133
+ if (n < 1 || n > 200 || !Number.isSafeInteger(n)) {
134
+ throw new CliError(2, 'INVALID_ARGUMENT', `Invalid limit: "${raw}". Limit must be between 1 and 200.`);
135
+ }
136
+ return n;
137
+ }
138
+ export async function executeCommand(parsed, transportOptions, stdin) {
139
+ const { positionals, options } = parsed;
140
+ if (positionals.length === 0) {
141
+ throw new CliError(2, 'INVALID_ARGUMENT', 'No command provided. Run with --help for usage.');
142
+ }
143
+ const family = positionals[0];
144
+ if (family === 'skill') {
145
+ if (positionals[1] !== 'install') {
146
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Use "skill install" to install the agent skill locally');
147
+ }
148
+ if (positionals.length > 2) {
149
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
150
+ }
151
+ if (options.file !== undefined || options.limit !== undefined) {
152
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options --file and --limit are not supported for skill install');
153
+ }
154
+ return await installSkill({ dir: options.dir ?? 'skills', force: options.force });
155
+ }
156
+ if (family === 'monitors') {
157
+ const action = positionals[1];
158
+ if (!action) {
159
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing subcommand for monitors. Run with --help for usage.');
160
+ }
161
+ if (action === 'groups') {
162
+ const subAction = positionals[2];
163
+ if (!subAction) {
164
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing subcommand for monitors groups. Use "reorder" or "assign".');
165
+ }
166
+ if (positionals.length > 3) {
167
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
168
+ }
169
+ if (options.limit !== undefined) {
170
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for monitors groups');
171
+ }
172
+ if (!options.file) {
173
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for monitors groups operations');
174
+ }
175
+ if (subAction === 'reorder') {
176
+ const payload = await readJsonPayload(options.file, stdin);
177
+ return await executeApiRequest('POST', '/monitors/groups/reorder', payload, monitorGroupsReorderEnvelope, transportOptions);
178
+ }
179
+ if (subAction === 'assign') {
180
+ const payload = await readJsonPayload(options.file, stdin);
181
+ return await executeApiRequest('POST', '/monitors/groups/assign', payload, monitorGroupsAssignEnvelope, transportOptions);
182
+ }
183
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown subcommand for monitors groups: "${subAction}"`);
184
+ }
185
+ if (action === 'list') {
186
+ if (positionals.length > 2) {
187
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
188
+ }
189
+ if (options.file !== undefined) {
190
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is not supported for monitors list');
191
+ }
192
+ const limit = parseLimit(options.limit);
193
+ return await executeApiRequest('GET', `/monitors?limit=${limit}`, undefined, monitorsListEnvelope, transportOptions);
194
+ }
195
+ if (action === 'create') {
196
+ if (positionals.length > 2) {
197
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
198
+ }
199
+ if (options.limit !== undefined) {
200
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for monitors create');
201
+ }
202
+ if (!options.file) {
203
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for monitors create');
204
+ }
205
+ const payload = await readJsonPayload(options.file, stdin);
206
+ return await executeApiRequest('POST', '/monitors', payload, monitorEnvelope, transportOptions);
207
+ }
208
+ if (action === 'update') {
209
+ if (positionals.length < 3) {
210
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing monitor ID for update');
211
+ }
212
+ if (positionals.length > 3) {
213
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
214
+ }
215
+ const id = parseId(positionals[2]);
216
+ if (options.limit !== undefined) {
217
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for monitors update');
218
+ }
219
+ if (!options.file) {
220
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for monitors update');
221
+ }
222
+ const payload = await readJsonPayload(options.file, stdin);
223
+ return await executeApiRequest('PATCH', `/monitors/${id}`, payload, monitorEnvelope, transportOptions);
224
+ }
225
+ if (action === 'delete') {
226
+ if (positionals.length < 3) {
227
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing monitor ID for delete');
228
+ }
229
+ if (positionals.length > 3) {
230
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
231
+ }
232
+ const id = parseId(positionals[2]);
233
+ if (options.limit !== undefined || options.file !== undefined) {
234
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for monitors delete');
235
+ }
236
+ return await executeApiRequest('DELETE', `/monitors/${id}`, undefined, deletedEnvelope, transportOptions);
237
+ }
238
+ if (action === 'pause') {
239
+ if (positionals.length < 3) {
240
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing monitor ID for pause');
241
+ }
242
+ if (positionals.length > 3) {
243
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
244
+ }
245
+ const id = parseId(positionals[2]);
246
+ if (options.limit !== undefined || options.file !== undefined) {
247
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for monitors pause');
248
+ }
249
+ return await executeApiRequest('POST', `/monitors/${id}/pause`, undefined, monitorEnvelope, transportOptions);
250
+ }
251
+ if (action === 'resume') {
252
+ if (positionals.length < 3) {
253
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing monitor ID for resume');
254
+ }
255
+ if (positionals.length > 3) {
256
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
257
+ }
258
+ const id = parseId(positionals[2]);
259
+ if (options.limit !== undefined || options.file !== undefined) {
260
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for monitors resume');
261
+ }
262
+ return await executeApiRequest('POST', `/monitors/${id}/resume`, undefined, monitorEnvelope, transportOptions);
263
+ }
264
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown action for monitors: "${action}"`);
265
+ }
266
+ if (family === 'notification-channels') {
267
+ const action = positionals[1];
268
+ if (!action) {
269
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing subcommand for notification-channels');
270
+ }
271
+ if (action === 'list') {
272
+ if (positionals.length > 2) {
273
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
274
+ }
275
+ if (options.file !== undefined) {
276
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is not supported for notification-channels list');
277
+ }
278
+ const limit = parseLimit(options.limit);
279
+ return await executeApiRequest('GET', `/notification-channels?limit=${limit}`, undefined, notificationChannelsListEnvelope, transportOptions);
280
+ }
281
+ if (action === 'create') {
282
+ if (positionals.length > 2) {
283
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
284
+ }
285
+ if (options.limit !== undefined) {
286
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for notification-channels create');
287
+ }
288
+ if (!options.file) {
289
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for notification-channels create');
290
+ }
291
+ const payload = await readJsonPayload(options.file, stdin);
292
+ return await executeApiRequest('POST', '/notification-channels', payload, notificationChannelEnvelope, transportOptions);
293
+ }
294
+ if (action === 'update') {
295
+ if (positionals.length < 3) {
296
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing notification-channel ID for update');
297
+ }
298
+ if (positionals.length > 3) {
299
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
300
+ }
301
+ const id = parseId(positionals[2]);
302
+ if (options.limit !== undefined) {
303
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for notification-channels update');
304
+ }
305
+ if (!options.file) {
306
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for notification-channels update');
307
+ }
308
+ const payload = await readJsonPayload(options.file, stdin);
309
+ return await executeApiRequest('PATCH', `/notification-channels/${id}`, payload, notificationChannelEnvelope, transportOptions);
310
+ }
311
+ if (action === 'delete') {
312
+ if (positionals.length < 3) {
313
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing notification-channel ID for delete');
314
+ }
315
+ if (positionals.length > 3) {
316
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
317
+ }
318
+ const id = parseId(positionals[2]);
319
+ if (options.limit !== undefined || options.file !== undefined) {
320
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for notification-channels delete');
321
+ }
322
+ return await executeApiRequest('DELETE', `/notification-channels/${id}`, undefined, deletedEnvelope, transportOptions);
323
+ }
324
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown action for notification-channels: "${action}"`);
325
+ }
326
+ if (family === 'status-pages') {
327
+ const action = positionals[1];
328
+ if (!action) {
329
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing subcommand for status-pages');
330
+ }
331
+ if (action === 'list') {
332
+ if (positionals.length > 2) {
333
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
334
+ }
335
+ if (options.limit !== undefined || options.file !== undefined) {
336
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for status-pages list');
337
+ }
338
+ return await executeApiRequest('GET', '/status-pages', undefined, statusPagesListEnvelope, transportOptions);
339
+ }
340
+ if (action === 'create') {
341
+ if (positionals.length > 2) {
342
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
343
+ }
344
+ if (options.limit !== undefined) {
345
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for status-pages create');
346
+ }
347
+ if (!options.file) {
348
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for status-pages create');
349
+ }
350
+ const payload = await readJsonPayload(options.file, stdin);
351
+ return await executeApiRequest('POST', '/status-pages', payload, statusPageEnvelope, transportOptions);
352
+ }
353
+ if (action === 'update') {
354
+ if (positionals.length < 3) {
355
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing status-page ID for update');
356
+ }
357
+ if (positionals.length > 3) {
358
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
359
+ }
360
+ const id = parseId(positionals[2]);
361
+ if (options.limit !== undefined) {
362
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for status-pages update');
363
+ }
364
+ if (!options.file) {
365
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for status-pages update');
366
+ }
367
+ const payload = await readJsonPayload(options.file, stdin);
368
+ return await executeApiRequest('PATCH', `/status-pages/${id}`, payload, statusPageEnvelope, transportOptions);
369
+ }
370
+ if (action === 'delete') {
371
+ if (positionals.length < 3) {
372
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing status-page ID for delete');
373
+ }
374
+ if (positionals.length > 3) {
375
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
376
+ }
377
+ const id = parseId(positionals[2]);
378
+ if (options.limit !== undefined || options.file !== undefined) {
379
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for status-pages delete');
380
+ }
381
+ return await executeApiRequest('DELETE', `/status-pages/${id}`, undefined, deletedEnvelope, transportOptions);
382
+ }
383
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown action for status-pages: "${action}"`);
384
+ }
385
+ if (family === 'settings') {
386
+ const action = positionals[1];
387
+ if (!action) {
388
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing subcommand for settings');
389
+ }
390
+ if (action === 'uptime-rating') {
391
+ const subAction = positionals[2];
392
+ if (!subAction) {
393
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Missing subcommand for settings uptime-rating. Use "get" or "update".');
394
+ }
395
+ if (positionals.length > 3) {
396
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[3]}"`);
397
+ }
398
+ if (subAction === 'get') {
399
+ if (options.limit !== undefined || options.file !== undefined) {
400
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for settings uptime-rating get');
401
+ }
402
+ return await executeApiRequest('GET', '/settings/uptime-rating', undefined, uptimeRatingEnvelope, transportOptions);
403
+ }
404
+ if (subAction === 'update') {
405
+ if (options.limit !== undefined) {
406
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for settings uptime-rating update');
407
+ }
408
+ if (!options.file) {
409
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for settings uptime-rating update');
410
+ }
411
+ const payload = await readJsonPayload(options.file, stdin);
412
+ return await executeApiRequest('PUT', '/settings/uptime-rating', payload, uptimeRatingEnvelope, transportOptions);
413
+ }
414
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown subcommand for settings uptime-rating: "${subAction}"`);
415
+ }
416
+ if (action === 'get') {
417
+ if (positionals.length > 2) {
418
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
419
+ }
420
+ if (options.limit !== undefined || options.file !== undefined) {
421
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Options are not supported for settings get');
422
+ }
423
+ return await executeApiRequest('GET', '/settings', undefined, settingsEnvelope, transportOptions);
424
+ }
425
+ if (action === 'update') {
426
+ if (positionals.length > 2) {
427
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unexpected positional argument: "${positionals[2]}"`);
428
+ }
429
+ if (options.limit !== undefined) {
430
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --limit is not supported for settings update');
431
+ }
432
+ if (!options.file) {
433
+ throw new CliError(2, 'INVALID_ARGUMENT', 'Option --file is required for settings update');
434
+ }
435
+ const payload = await readJsonPayload(options.file, stdin);
436
+ return await executeApiRequest('PATCH', '/settings', payload, settingsEnvelope, transportOptions);
437
+ }
438
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown action for settings: "${action}"`);
439
+ }
440
+ throw new CliError(2, 'INVALID_ARGUMENT', `Unknown command family: "${family}"`);
441
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ import { redactCredentials } from "./redact.js";
2
+ export class CliError extends Error {
3
+ exitCode;
4
+ code;
5
+ status;
6
+ constructor(exitCode, code, message, status) {
7
+ super(message);
8
+ this.name = 'CliError';
9
+ this.exitCode = exitCode;
10
+ this.code = code;
11
+ this.status = status;
12
+ }
13
+ }
14
+ export function formatErrorEnvelope(error, adminToken) {
15
+ if (error instanceof CliError) {
16
+ const envelope = {
17
+ error: {
18
+ code: error.code,
19
+ message: error.message,
20
+ ...(error.status !== undefined ? { status: error.status } : {}),
21
+ },
22
+ };
23
+ return redactCredentials(envelope, adminToken);
24
+ }
25
+ const rawMessage = error instanceof Error ? error.message : String(error);
26
+ const envelope = {
27
+ error: {
28
+ code: 'INTERNAL_ERROR',
29
+ message: rawMessage,
30
+ },
31
+ };
32
+ return redactCredentials(envelope, adminToken);
33
+ }