@zeegme/cli 0.4.1 → 0.6.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/README.md +2 -0
- package/dist/commands/bookings.js +12 -5
- package/dist/commands/eventTypes.js +38 -1
- package/dist/commands/util.js +13 -0
- package/dist/errors.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ Plain `http` is accepted for `localhost` only. Each origin keeps its own login.
|
|
|
47
47
|
| Command | What it does |
|
|
48
48
|
|---|---|
|
|
49
49
|
| `zeeg event-types list` / `get <uuid>` | Your event types (scheduling pages) |
|
|
50
|
+
| `zeeg event-types create --type --title [--language --data]` | Create an event type |
|
|
51
|
+
| `zeeg event-types update <uuid> [--title --language --data]` | Change an event type, its language included (`de-du` is German addressing the invitee as du) |
|
|
50
52
|
| `zeeg availability <eventTypeUuid> [--from --to --tz]` | Open start times, by day |
|
|
51
53
|
| `zeeg schedules list [--email]` | Availability schedules |
|
|
52
54
|
| `zeeg time-off add --start --end --title [--type]` | Block days as time off |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
1
2
|
import { unwrap } from '../api/client.js';
|
|
2
3
|
import { UsageError } from '../errors.js';
|
|
3
|
-
import { asJson, clockTime, collect, isoDate, localTimeZone, positiveInt } from './util.js';
|
|
4
|
+
import { asJson, clockTime, collect, isoDate, localTimeZone, overrideLevelOption, positiveInt } from './util.js';
|
|
4
5
|
const inviteeNames = (row) => (row.invitees ?? []).map((invitee) => invitee.fullName ?? invitee.email);
|
|
5
6
|
export function registerBookings(program, context) {
|
|
6
7
|
const bookings = program.command('bookings').description('Scheduled events (bookings)');
|
|
@@ -67,6 +68,8 @@ export function registerBookings(program, context) {
|
|
|
67
68
|
.option('--guest <email>', 'add a guest, repeatable', collect)
|
|
68
69
|
.option('--location <type>', 'location type, when the event type offers several')
|
|
69
70
|
.option('--location-key <key>', 'exact location key from the event type')
|
|
71
|
+
.option('--host <email>', 'book with this host (Round Robin event types only)')
|
|
72
|
+
.addOption(overrideLevelOption())
|
|
70
73
|
.option('-y, --yes', 'book without asking')
|
|
71
74
|
.action(async (uuid, options) => {
|
|
72
75
|
const ctx = context();
|
|
@@ -85,6 +88,8 @@ export function registerBookings(program, context) {
|
|
|
85
88
|
guests: options.guest,
|
|
86
89
|
location: options.location,
|
|
87
90
|
locationKey: options.locationKey,
|
|
91
|
+
hostEmail: options.host,
|
|
92
|
+
overrideLevel: options.overrideLevel,
|
|
88
93
|
},
|
|
89
94
|
})));
|
|
90
95
|
ctx.print.record(data, `Booked: ${data.title ?? 'meeting'} at ${data.startTime ?? `${options.date} ${options.time}`} (${data.uuid}).`);
|
|
@@ -111,13 +116,14 @@ export function registerBookings(program, context) {
|
|
|
111
116
|
.option('--tz <zone>', 'IANA time zone of --date and --time (default: the invitee’s)')
|
|
112
117
|
.option('--duration <minutes>', 'new length', positiveInt('--duration'))
|
|
113
118
|
.option('--reason <text>', 'shown to the invitees')
|
|
119
|
+
.addOption(overrideLevelOption())
|
|
114
120
|
.option('-y, --yes', 'reschedule without asking')
|
|
115
121
|
.action(async (uuid, options) => {
|
|
116
122
|
const ctx = context();
|
|
117
123
|
await ctx.confirm(`Move booking ${uuid} to ${options.date} ${options.time}? The invitees are notified.`, options.yes);
|
|
118
124
|
const data = asJson(await unwrap((await ctx.api()).PUT('/scheduled-events/{uuid}/reschedule', {
|
|
119
125
|
params: { path: { uuid } },
|
|
120
|
-
body: { date: options.date, start: options.time, timeZone: options.tz, duration: options.duration, reason: options.reason },
|
|
126
|
+
body: { date: options.date, start: options.time, timeZone: options.tz, duration: options.duration, reason: options.reason, overrideLevel: options.overrideLevel },
|
|
121
127
|
})));
|
|
122
128
|
ctx.print.record(data.resource, `Rescheduled ${uuid} to ${options.date} ${options.time}.`);
|
|
123
129
|
});
|
|
@@ -142,8 +148,9 @@ export function registerBookings(program, context) {
|
|
|
142
148
|
.command('hand-over <uuid>')
|
|
143
149
|
.description('Hand a round-robin booking to another host')
|
|
144
150
|
.option('--to <email>', 'the new host (default: Zeeg picks the next available host)')
|
|
145
|
-
.
|
|
146
|
-
.option('--fallback-to-reschedule', '
|
|
151
|
+
.addOption(new Option('--require-availability', 'accepted and ignored').hideHelp())
|
|
152
|
+
.option('--fallback-to-reschedule', 'when the booking cannot move in place, reschedule it to the new host at the same time; the booking gets a new uuid')
|
|
153
|
+
.addOption(overrideLevelOption())
|
|
147
154
|
.option('-y, --yes', 'hand over without asking')
|
|
148
155
|
.action(async (uuid, options) => {
|
|
149
156
|
const ctx = context();
|
|
@@ -152,8 +159,8 @@ export function registerBookings(program, context) {
|
|
|
152
159
|
params: { path: { uuid } },
|
|
153
160
|
body: {
|
|
154
161
|
newHostEmail: options.to,
|
|
155
|
-
requireHostAvailability: options.requireAvailability,
|
|
156
162
|
fallbackToReschedule: options.fallbackToReschedule,
|
|
163
|
+
overrideLevel: options.overrideLevel,
|
|
157
164
|
},
|
|
158
165
|
}));
|
|
159
166
|
ctx.print.record(asJson(data).resource, `Handed over ${uuid}.`);
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { unwrap } from '../api/client.js';
|
|
2
|
+
import { UsageError } from '../errors.js';
|
|
2
3
|
import { renderTable } from '../output.js';
|
|
3
|
-
import { addDays, asJson, isoDate, localTimeZone, positiveInt } from './util.js';
|
|
4
|
+
import { addDays, asJson, isoDate, jsonArgument, localTimeZone, positiveInt } from './util.js';
|
|
5
|
+
/** The API answers 422 to any other code, so the CLI passes the value through unchecked. */
|
|
6
|
+
const LANGUAGE_HELP = 'language of the booking page and of invitee emails: en, de (German, formal), de-du (German, informal), nl, fi, es, fr, pl, da, sv or it';
|
|
7
|
+
const DATA_HELP = 'any other field of the API body, as a JSON object inline or as @file.json; the other flags win over it';
|
|
8
|
+
/** The `--data` object with the flags laid over it, leaving out the flags not given. */
|
|
9
|
+
async function writeBody(options, flags) {
|
|
10
|
+
const given = Object.fromEntries(Object.entries({ title: options.title, language: options.language, ...flags }).filter(([, value]) => value !== undefined));
|
|
11
|
+
return { ...(options.data === undefined ? {} : await jsonArgument(options.data)), ...given };
|
|
12
|
+
}
|
|
4
13
|
export function registerEventTypes(program, context) {
|
|
5
14
|
const eventTypes = program.command('event-types').description('Scheduling pages (event types)');
|
|
6
15
|
eventTypes
|
|
@@ -31,6 +40,34 @@ export function registerEventTypes(program, context) {
|
|
|
31
40
|
const data = asJson(await unwrap((await ctx.api()).GET('/event-types/{uuid}', { params: { path: { uuid } } })));
|
|
32
41
|
ctx.print.record(data.resource);
|
|
33
42
|
});
|
|
43
|
+
eventTypes
|
|
44
|
+
.command('create')
|
|
45
|
+
.description('Create an event type; it is bookable through its link at once')
|
|
46
|
+
.option('--type <type>', 'ONE_ON_ONE, GROUP, ROUND_ROBIN or COLLECTIVE')
|
|
47
|
+
.option('--title <title>', 'the name invitees see')
|
|
48
|
+
.option('--language <code>', `${LANGUAGE_HELP} (default: your own)`)
|
|
49
|
+
.option('--data <json>', DATA_HELP)
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
const ctx = context();
|
|
52
|
+
const body = await writeBody(options, { type: options.type });
|
|
53
|
+
const data = asJson(await unwrap((await ctx.api()).POST('/event-types', { body: body })));
|
|
54
|
+
ctx.print.record(data.resource);
|
|
55
|
+
});
|
|
56
|
+
eventTypes
|
|
57
|
+
.command('update <uuid>')
|
|
58
|
+
.description('Change an event type; a field left out keeps its value')
|
|
59
|
+
.option('--title <title>', 'the name invitees see')
|
|
60
|
+
.option('--language <code>', LANGUAGE_HELP)
|
|
61
|
+
.option('--data <json>', DATA_HELP)
|
|
62
|
+
.action(async (uuid, options) => {
|
|
63
|
+
const ctx = context();
|
|
64
|
+
const body = await writeBody(options, {});
|
|
65
|
+
if (Object.keys(body).length === 0) {
|
|
66
|
+
throw new UsageError('Pass what to change: --title, --language or --data.');
|
|
67
|
+
}
|
|
68
|
+
const data = asJson(await unwrap((await ctx.api()).PATCH('/event-types/{uuid}', { params: { path: { uuid } }, body: body })));
|
|
69
|
+
ctx.print.record(data.resource);
|
|
70
|
+
});
|
|
34
71
|
program
|
|
35
72
|
.command('availability <eventTypeUuid>')
|
|
36
73
|
.description('Available start times for an event type (needs a paid plan)')
|
package/dist/commands/util.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Option } from 'commander';
|
|
2
3
|
import { UsageError } from '../errors.js';
|
|
3
4
|
export function asJson(value) {
|
|
4
5
|
return value !== null && typeof value === 'object' ? value : {};
|
|
@@ -12,6 +13,18 @@ export function positiveInt(name) {
|
|
|
12
13
|
return value;
|
|
13
14
|
};
|
|
14
15
|
}
|
|
16
|
+
export function overrideLevel(name) {
|
|
17
|
+
return (raw) => {
|
|
18
|
+
if (!/^[1-4]$/.test(raw)) {
|
|
19
|
+
throw new UsageError(`${name} must be 1, 2, 3 or 4, not "${raw}".`);
|
|
20
|
+
}
|
|
21
|
+
return Number(raw);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** `--override-level`, shared by every command that can book outside availability. */
|
|
25
|
+
export function overrideLevelOption() {
|
|
26
|
+
return new Option('--override-level <level>', 'book outside availability: 2 also outside working hours, 3 also over calendar entries, 4 also over other bookings (double-books the host)').argParser(overrideLevel('--override-level'));
|
|
27
|
+
}
|
|
15
28
|
export function isoDate(name) {
|
|
16
29
|
return (raw) => {
|
|
17
30
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw) || Number.isNaN(Date.parse(raw))) {
|
package/dist/errors.js
CHANGED
|
@@ -45,7 +45,8 @@ export function exitCodeFor(error) {
|
|
|
45
45
|
/**
|
|
46
46
|
* The API answers errors in two shapes: controllers and validation send `{ message, errors? }`,
|
|
47
47
|
* with validation messages listed per field, and the shared exception handler sends
|
|
48
|
-
* `{ error: { type, message } }` for not found, forbidden and similar. A
|
|
48
|
+
* `{ error: { type, message } }` for not found, forbidden and similar. A refused override carries
|
|
49
|
+
* `data.blockedReason`, the kind of entry that blocks the time (such as `calendar`). A 401 means the token was
|
|
49
50
|
* revoked or expired server-side, so it asks for a new login.
|
|
50
51
|
*/
|
|
51
52
|
export function apiErrorFrom(status, body) {
|
|
@@ -60,7 +61,9 @@ export function apiErrorFrom(status, body) {
|
|
|
60
61
|
? Object.values(record.errors).flat().filter((item) => typeof item === 'string')
|
|
61
62
|
: [];
|
|
62
63
|
const unique = details.filter((detail) => detail !== message);
|
|
63
|
-
|
|
64
|
+
const data = typeof record.data === 'object' && record.data !== null ? record.data : {};
|
|
65
|
+
const blocked = typeof data.blockedReason === 'string' && data.blockedReason !== '' ? `\nBlocked by: ${data.blockedReason}` : '';
|
|
66
|
+
return new ApiError((unique.length > 0 ? `${message}\n- ${unique.join('\n- ')}` : message) + blocked);
|
|
64
67
|
}
|
|
65
68
|
/** `fetch`, with a network failure reported as an ApiError that names the origin. */
|
|
66
69
|
export async function fetchFrom(fetchImpl, baseUrl, input, init) {
|
package/package.json
CHANGED