@zeegme/cli 0.4.1 → 0.5.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.
@@ -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
- .option('--require-availability', 'only hand over to a host who is free then')
146
- .option('--fallback-to-reschedule', 'offer the invitee a reschedule when nobody is free')
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,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 401 means the token was
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
- return new ApiError(unique.length > 0 ? `${message}\n- ${unique.join('\n- ')}` : message);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeegme/cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "The Zeeg command line: bookings, availability, notes, CRM, routing forms and AI phone calls from your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",