@siteqwality/cli 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @siteqwality/cli
2
2
 
3
- Version 0.1.2, Node.js 22+. Install:
3
+ Version 0.1.3, 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. Acknowledgement 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.
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
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, InvalidArgumentError } from 'commander';
3
- import { ApiClient, IncidentClient, injectDirectory, uploadDirectory } from '../src/index.js';
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.2')
8
- .description('Site Qwality source maps, releases and incidents');
13
+ .version('0.1.3')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteqwality/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Site Qwality source maps, releases and incident tooling",
5
5
  "type": "module",
6
6
  "engines": {
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
+ }