@siteqwality/cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/bin/siteqwality.js +37 -3
- package/package.json +1 -1
- package/src/index.d.ts +38 -0
- package/src/index.js +1 -0
- package/src/oncall.js +41 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @siteqwality/cli
|
|
2
2
|
|
|
3
|
-
Version 0.1.
|
|
3
|
+
Version 0.1.4, Node.js 22+. Install:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install --save-dev @siteqwality/cli
|
|
@@ -42,10 +42,24 @@ npx siteqwality incidents update "$INCIDENT_ID" --clear-service
|
|
|
42
42
|
|
|
43
43
|
`list` defaults to active incidents and returns `{ data, total }` for one page. Status is `active`, `resolved` or `all`; limit is 1 to 100. Increase `--offset` to fetch later pages.
|
|
44
44
|
|
|
45
|
-
`ack` requires `--by` because an API key does not identify a member. The server checks that the member belongs to the account.
|
|
45
|
+
`ack` requires `--by` because an API key does not identify a member. The server checks that the member belongs to the account. Acknowledgment stops escalation. `resolve` always posts a public resolution for published incidents and emails their subscribers by default; `--no-notify-subscribers` suppresses the email. `update` changes only the supplied title, severity or service, without setting lifecycle status. Moving an incident to a service can start its escalation policy.
|
|
46
46
|
|
|
47
47
|
All four commands accept `--api-url`, `--timeout` and `--retries`. Only reads retry transient failures. Incident mutations make one request even if `--retries` is set, so an ambiguous failure cannot repeat a page or public update. Inspect the incident before retrying a failed mutation. HTTP 400, 403, 404 and 409 errors return a nonzero exit code without printing response bodies or the API key.
|
|
48
48
|
|
|
49
|
+
## On-call
|
|
50
|
+
|
|
51
|
+
On-call commands read schedules with an account API key that has `read:incidents`. They never change a schedule.
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
export SITEQWALITY_API_KEY='your-incident-api-key'
|
|
55
|
+
npx siteqwality oncall now
|
|
56
|
+
npx siteqwality oncall now --schedule "$SCHEDULE_ID" --at 2026-10-21T09:00:00Z
|
|
57
|
+
npx siteqwality oncall shifts "$SCHEDULE_ID" --since 2026-10-21T00:00:00Z --until 2026-11-04T00:00:00Z
|
|
58
|
+
npx siteqwality oncall mine --user "$MEMBER_ID"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`now` lists who is on call on every schedule, or, with `--schedule`, one schedule's current shift with who is next. `shifts` returns a schedule's timeline with overrides applied and gaps included, 14 days by default and at most 90. `mine` lists one member's shifts across every schedule for the next 14 days; `--user` is required because an API key does not identify a member. Times are RFC 3339.
|
|
62
|
+
|
|
49
63
|
Upload creates the release idempotently before presigning. With no release it uses the reserved `__debug_id__` release. `releases deploy <version>` requires an existing release and records server time. It does not deploy application files or configure the browser SDK's release value.
|
|
50
64
|
|
|
51
65
|
## Injection
|
|
@@ -58,7 +72,7 @@ Unchanged injected pairs can be processed repeatedly. If injected JavaScript cha
|
|
|
58
72
|
|
|
59
73
|
## Upload and deletion
|
|
60
74
|
|
|
61
|
-
|
|
75
|
+
Each upload runs these steps:
|
|
62
76
|
|
|
63
77
|
1. Create the app-scoped release.
|
|
64
78
|
2. Presign at most 50 maps per batch, with unique filenames and Debug IDs. Declare exact gzip byte sizes and JSON/gzip headers.
|
|
@@ -68,7 +82,7 @@ Uploads follow the Phase 4 API contract:
|
|
|
68
82
|
|
|
69
83
|
Both raw JSON and gzip output are limited to 32 MiB per map. The client retries network failures, timeouts, HTTP 408/429 and 5xx responses with bounded backoff. It respects `Retry-After` up to 30 seconds and fails for longer requested waits. A storage 403 triggers a fresh presign and re-upload, bounded by `--retries`. Other permanent failures stop the command. An incomplete or mismatched presign/confirmation response fails closed. Failed uploads may leave pending server records; retrying gets new IDs. Earlier confirmed batches may remain ready, but no local map is removed when a later batch fails.
|
|
70
84
|
|
|
71
|
-
Confirmation
|
|
85
|
+
Confirmation means the map is stored and its metadata accepted. To check symbolication end to end, open a new error from that release in Site Qwality.
|
|
72
86
|
|
|
73
87
|
## JavaScript API
|
|
74
88
|
|
package/bin/siteqwality.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, InvalidArgumentError } from 'commander';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ApiClient,
|
|
5
|
+
IncidentClient,
|
|
6
|
+
OnCallClient,
|
|
7
|
+
injectDirectory,
|
|
8
|
+
uploadDirectory,
|
|
9
|
+
} from '../src/index.js';
|
|
4
10
|
|
|
5
11
|
const program = new Command()
|
|
6
12
|
.name('siteqwality')
|
|
7
|
-
.version('0.1.
|
|
8
|
-
.description('Site Qwality source maps, releases and
|
|
13
|
+
.version('0.1.4')
|
|
14
|
+
.description('Site Qwality source maps, releases, incidents and on-call');
|
|
9
15
|
const integer = (value) => {
|
|
10
16
|
if (!/^\d+$/.test(value)) throw new InvalidArgumentError('Expected an integer');
|
|
11
17
|
return Number(value);
|
|
@@ -120,6 +126,34 @@ accountAuthenticated(
|
|
|
120
126
|
}),
|
|
121
127
|
);
|
|
122
128
|
});
|
|
129
|
+
const oncall = program.command('oncall').description('See who is on call and when');
|
|
130
|
+
accountAuthenticated(
|
|
131
|
+
oncall
|
|
132
|
+
.command('now')
|
|
133
|
+
.description('Who is on call now: every schedule, or one with its next shift'),
|
|
134
|
+
)
|
|
135
|
+
.option('--schedule <uuid>', 'Only this schedule, with who is next')
|
|
136
|
+
.option('--at <time>', 'RFC 3339 instant instead of now')
|
|
137
|
+
.action(async (opts) =>
|
|
138
|
+
print(
|
|
139
|
+
await new OnCallClient(options(opts)).onCallNow({ scheduleId: opts.schedule, at: opts.at }),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
accountAuthenticated(
|
|
143
|
+
oncall.command('shifts <schedule>').description("A schedule's shifts, gaps included"),
|
|
144
|
+
)
|
|
145
|
+
.option('--since <time>', 'RFC 3339 start; defaults to now')
|
|
146
|
+
.option('--until <time>', 'RFC 3339 end; defaults to 14 days later, at most 90')
|
|
147
|
+
.action(async (schedule, opts) =>
|
|
148
|
+
print(await new OnCallClient(options(opts)).listShifts(schedule, opts)),
|
|
149
|
+
);
|
|
150
|
+
accountAuthenticated(oncall.command('mine').description("A member's shifts across every schedule"))
|
|
151
|
+
.requiredOption('--user <uuid>', 'Member UUID, required for API keys')
|
|
152
|
+
.option('--since <time>', 'RFC 3339 start; defaults to now')
|
|
153
|
+
.option('--until <time>', 'RFC 3339 end; defaults to 14 days later, at most 90')
|
|
154
|
+
.action(async (opts) =>
|
|
155
|
+
print(await new OnCallClient(options(opts)).listMemberShifts(opts.user, opts)),
|
|
156
|
+
);
|
|
123
157
|
try {
|
|
124
158
|
await program.parseAsync();
|
|
125
159
|
} catch (error) {
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -126,4 +126,42 @@ export class IncidentClient {
|
|
|
126
126
|
): Promise<Incident>;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export interface OnCallShift {
|
|
130
|
+
schedule_id?: string;
|
|
131
|
+
schedule_name?: string;
|
|
132
|
+
user_id?: string;
|
|
133
|
+
source: 'override' | 'layer' | 'gap';
|
|
134
|
+
layer_id?: string;
|
|
135
|
+
layer_number?: number;
|
|
136
|
+
override_id?: string;
|
|
137
|
+
starts_at?: string;
|
|
138
|
+
ends_at?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface OnCallNow {
|
|
141
|
+
schedule_id: string;
|
|
142
|
+
at: string;
|
|
143
|
+
timezone: string;
|
|
144
|
+
on_call: OnCallShift;
|
|
145
|
+
next?: OnCallShift;
|
|
146
|
+
}
|
|
147
|
+
export class OnCallClient {
|
|
148
|
+
constructor(options: AccountApiOptions);
|
|
149
|
+
onCallNow(options: { scheduleId: string; at?: string }): Promise<OnCallNow>;
|
|
150
|
+
onCallNow(options?: { at?: string }): Promise<OnCallShift[]>;
|
|
151
|
+
listShifts(
|
|
152
|
+
scheduleId: string,
|
|
153
|
+
range?: { since?: string; until?: string },
|
|
154
|
+
): Promise<{
|
|
155
|
+
schedule_id: string;
|
|
156
|
+
since: string;
|
|
157
|
+
until: string;
|
|
158
|
+
timezone: string;
|
|
159
|
+
shifts: OnCallShift[];
|
|
160
|
+
}>;
|
|
161
|
+
listMemberShifts(
|
|
162
|
+
userId: string,
|
|
163
|
+
range?: { since?: string; until?: string },
|
|
164
|
+
): Promise<OnCallShift[]>;
|
|
165
|
+
}
|
|
166
|
+
|
|
129
167
|
export function sourceMapReference(code: string): string | undefined;
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { inject, validateMap } from './inject.js';
|
|
|
2
2
|
export { injectDirectory, collectPairs } from './files.js';
|
|
3
3
|
export { ApiClient } from './api.js';
|
|
4
4
|
export { IncidentClient } from './incidents.js';
|
|
5
|
+
export { OnCallClient } from './oncall.js';
|
|
5
6
|
export { uploadMaps, uploadDirectory } from './upload.js';
|
|
6
7
|
|
|
7
8
|
export { sourceMapReference } from './comments.js';
|
package/src/oncall.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AuthenticatedClient, isUuid } from './api.js';
|
|
2
|
+
|
|
3
|
+
const DAY_MS = 86_400_000;
|
|
4
|
+
const uuid = (id, name) => {
|
|
5
|
+
if (!isUuid(id)) throw new Error(`A valid ${name} UUID is required`);
|
|
6
|
+
return id;
|
|
7
|
+
};
|
|
8
|
+
const instant = (value, name) => {
|
|
9
|
+
if (value === undefined) return undefined;
|
|
10
|
+
const ms = typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
11
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(value) || Number.isNaN(ms))
|
|
12
|
+
throw new Error(`${name} must be an RFC 3339 time, for example 2026-10-21T09:00:00Z`);
|
|
13
|
+
return new Date(ms).toISOString();
|
|
14
|
+
};
|
|
15
|
+
const query = (params) => {
|
|
16
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined);
|
|
17
|
+
return entries.length ? `?${new URLSearchParams(entries)}` : '';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class OnCallClient extends AuthenticatedClient {
|
|
21
|
+
/** Who is on call now (or at `at`): one schedule with its next shift, or every schedule. */
|
|
22
|
+
onCallNow({ scheduleId, at } = {}) {
|
|
23
|
+
const when = instant(at, 'at');
|
|
24
|
+
if (scheduleId === undefined) return this.accountApi(`/on_call${query({ since: when })}`);
|
|
25
|
+
uuid(scheduleId, 'schedule');
|
|
26
|
+
return this.accountApi(`/on_call_schedule/${scheduleId}/on_call${query({ at: when })}`);
|
|
27
|
+
}
|
|
28
|
+
/** A schedule's shifts, gaps included; the API allows at most 90 days. */
|
|
29
|
+
listShifts(scheduleId, { since, until } = {}) {
|
|
30
|
+
uuid(scheduleId, 'schedule');
|
|
31
|
+
const range = { since: instant(since, 'since'), until: instant(until, 'until') };
|
|
32
|
+
return this.accountApi(`/on_call_schedule/${scheduleId}/shifts${query(range)}`);
|
|
33
|
+
}
|
|
34
|
+
/** One member's shifts across every schedule, from now for 14 days unless given. */
|
|
35
|
+
listMemberShifts(userId, { since, until } = {}) {
|
|
36
|
+
uuid(userId, 'member');
|
|
37
|
+
const start = instant(since, 'since') ?? new Date().toISOString();
|
|
38
|
+
const end = instant(until, 'until') ?? new Date(Date.parse(start) + 14 * DAY_MS).toISOString();
|
|
39
|
+
return this.accountApi(`/on_call${query({ user_id: userId, since: start, until: end })}`);
|
|
40
|
+
}
|
|
41
|
+
}
|