@bli-cockpit/cli 0.2.142 → 0.2.144

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.
@@ -0,0 +1,29 @@
1
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
2
+ import { writeLine } from "./cli-io.js";
3
+ const TAG = "[cal cli]";
4
+ const DELETE_DEADLINE_MS = 60_000;
5
+ export async function runCalDelete(command, door) {
6
+ const answer = await askAgentDoor(door, {
7
+ path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}`,
8
+ method: "DELETE",
9
+ label: "cal delete",
10
+ timeoutMs: DELETE_DEADLINE_MS,
11
+ });
12
+ if (!answer.ok)
13
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
14
+ const deleted = answer.body.deleted;
15
+ if (door.json)
16
+ return emitAgentDoor(door, { ok: true, deleted });
17
+ if (!deleted) {
18
+ writeLine(door.io.stdout, "Tower answered without a result. Nothing to report.");
19
+ return 0;
20
+ }
21
+ const alreadyGone = deleted.google === "already_gone_at_google";
22
+ const stored = deleted.local_store === "removed"
23
+ ? "Tower's copy is removed."
24
+ : "Tower's copy is still there for now — the next sync will pick up that it is gone.";
25
+ writeLine(door.io.stdout, alreadyGone
26
+ ? `Google already had that event gone. ${stored}`
27
+ : `Deleted at Google${deleted.notified === "all" ? " — attendees were notified." : "."} ${stored}`);
28
+ return 0;
29
+ }
@@ -21,6 +21,7 @@
21
21
  * computed in UTC and "today" would end at 5pm in Vancouver.
22
22
  */
23
23
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
24
+ import { runCalDelete } from "./cal-delete.js";
24
25
  import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
25
26
  const TAG = "[cal cli]";
26
27
  const READ_DEADLINE_MS = 30_000;
@@ -49,6 +50,8 @@ export async function runCal(command, io) {
49
50
  return detach(command, door);
50
51
  case "sync":
51
52
  return sync(command, door);
53
+ case "delete":
54
+ return runCalDelete(command, door);
52
55
  }
53
56
  }
54
57
  /**
@@ -305,6 +308,12 @@ async function create(command, door) {
305
308
  return emitAgentDoor(door, { ok: true, event });
306
309
  const zone = command.timeZone ?? localTimeZone();
307
310
  writeLine(door.io.stdout, `Created "${event?.title ?? command.title}" on ${event ? `${dayLabel(event.starts_at, zone)} ${clock(event.starts_at, zone)}` : "that calendar"}.`);
311
+ if (event?.event_id) {
312
+ writeLine(door.io.stdout, `Tower event: ${event.event_id}`);
313
+ }
314
+ else if (event?.local_store === "failed" || event?.local_store === "unreadable") {
315
+ writeLine(door.io.stdout, "Google has the event; Tower has not stored it yet (the next `cockpit cal sync` will), so there is no Tower id yet.");
316
+ }
308
317
  if (event?.html_link)
309
318
  writeLine(door.io.stdout, event.html_link);
310
319
  if (command.meet) {
@@ -30,9 +30,10 @@ const CAL_ACTIONS = new Set([
30
30
  "share",
31
31
  "detach",
32
32
  "sync",
33
+ "delete",
33
34
  ]);
34
- /** Every verb that names ONE calendar first. */
35
- const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["sync", "detach", "share"]);
35
+ /** Every verb that names ONE thing first — a calendar id, or (delete) an event id. */
36
+ const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["sync", "detach", "share", "delete"]);
36
37
  export function parseCalArgs(args) {
37
38
  const values = parseNamedArgs(args, {
38
39
  allowedFlags: [
@@ -57,6 +58,7 @@ export function parseCalArgs(args) {
57
58
  "--org-visible",
58
59
  "--private",
59
60
  "--full",
61
+ "--yes",
60
62
  "--json",
61
63
  ],
62
64
  valueFlags: [
@@ -81,6 +83,7 @@ export function parseCalArgs(args) {
81
83
  const { subject, query } = readCalSubjectOrQuery(action, rest);
82
84
  const { title, startsAt, endsAt } = readCalCreateDetails(action, values);
83
85
  validateCalSharing(action, values);
86
+ validateCalDelete(action, values);
84
87
  // Read in the order the flags have always been checked, so a caller who got
85
88
  // two of them wrong is told about --limit first, as before.
86
89
  const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
@@ -91,7 +94,7 @@ function readCalAction(values) {
91
94
  const first = values.positionals[0];
92
95
  const action = (first === undefined ? "today" : first);
93
96
  if (!CAL_ACTIONS.has(action)) {
94
- throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, or sync.`);
97
+ throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, sync, or delete.`);
95
98
  }
96
99
  return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
97
100
  }
@@ -100,8 +103,11 @@ function readCalSubjectOrQuery(action, rest) {
100
103
  let query;
101
104
  if (CAL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
102
105
  subject = optionalNonEmpty(rest[0]);
103
- if (!subject)
104
- throw new Error(`cal ${action} needs a calendar id — take one from \`cockpit cal calendars\`.`);
106
+ if (!subject) {
107
+ throw new Error(action === "delete"
108
+ ? "cal delete needs an event id — take one from a window verb's output (today/week/next/find)."
109
+ : `cal ${action} needs a calendar id — take one from \`cockpit cal calendars\`.`);
110
+ }
105
111
  if (rest.length > 1)
106
112
  throw new Error(`cal ${action} takes one id, not ${rest.length}.`);
107
113
  }
@@ -139,6 +145,12 @@ function validateCalSharing(action, values) {
139
145
  throw new Error("cal share needs --org-visible (every member may read it) or --private (only you). Sharing is a decision, not a default.");
140
146
  }
141
147
  }
148
+ /** `delete` notifies attendees, exactly like `create`'s invitations — never without saying so first. */
149
+ function validateCalDelete(action, values) {
150
+ if (action === "delete" && !values.booleans.has("--yes")) {
151
+ throw new Error("cal delete needs --yes: it removes the event at Google and can email everybody on it. Add --yes once you mean it.");
152
+ }
153
+ }
142
154
  function readCalOffset(values) {
143
155
  const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
144
156
  const offset = offsetRaw === undefined ? undefined : Number.parseInt(offsetRaw, 10);
@@ -178,6 +190,7 @@ function buildCalCommand(values, action, subject, query, title, startsAt, endsAt
178
190
  orgVisible: values.booleans.has("--org-visible"),
179
191
  makePrivate: values.booleans.has("--private"),
180
192
  full: values.booleans.has("--full"),
193
+ yes: values.booleans.has("--yes"),
181
194
  json: values.booleans.has("--json"),
182
195
  };
183
196
  }
@@ -131,7 +131,7 @@ export const TOWER_COMMAND_HELP = [
131
131
  [
132
132
  "cal",
133
133
  [
134
- 'Usage: cockpit cal [today|week|next|find "<words>"|calendars|add-ical|create|share <id>|sync <id>|detach <id>] [flags]',
134
+ 'Usage: cockpit cal [today|week|next|find "<words>"|calendars|add-ical|create|share <id>|sync <id>|detach <id>|delete <event id> --yes] [flags]',
135
135
  "",
136
136
  "Every calendar you attached, in one place — plus any calendar a colleague marked shared.",
137
137
  "today [--offset 1] [--tz <zone>] — what is on today, in YOUR zone (this machine's, unless --tz says otherwise). --offset 1 is tomorrow, -1 yesterday.",
@@ -149,8 +149,9 @@ export const TOWER_COMMAND_HELP = [
149
149
  "share <id> --org-visible|--private — makes one of YOUR calendars readable by everyone at BLI, or private again. Sharing never lets anybody else change it.",
150
150
  "sync <id> [--full] — reads that calendar now instead of waiting for the cron. --full ignores the sync token / ETag and re-reads everything.",
151
151
  "detach <id> — removes the calendar and its stored events from Tower. The calendar itself is untouched at Google.",
152
+ "delete <event id> --yes — removes ONE event, AT GOOGLE first, then here. Refuses without --yes: a delete can email everybody on it, exactly like create's invitations do. Google already having it gone (404/410) is answered as success, not a refusal.",
152
153
  "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
153
- "A refusal keeps Tower's own reason label — needs_rls_client, calendar_not_found_or_unreadable, calendar_not_yours, ical_url_invalid, google_oauth_not_configured, write_not_supported_for_provider, and so on.",
154
+ "A refusal keeps Tower's own reason label — needs_rls_client, calendar_not_found_or_unreadable, calendar_not_yours, event_not_found_or_unreadable, ical_url_invalid, google_oauth_not_configured, write_not_supported_for_provider, and so on.",
154
155
  "The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
155
156
  ],
156
157
  ],
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.142");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.144");
25
25
  return 0;
26
26
  }
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.142",
3
+ "version": "0.2.144",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@bli-cockpit/memory-mcp": "0.1.36",
31
- "@bli-cockpit/mcp": "0.1.67",
31
+ "@bli-cockpit/mcp": "0.1.69",
32
32
  "@bli-cockpit/telemetry-core": "0.1.48"
33
33
  }
34
34
  }