@ncukondo/gcal-cli 0.2.2 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +339 -57
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,25 +6,43 @@ var __getProtoOf = Object.getPrototypeOf;
6
6
  var __defProp = Object.defineProperty;
7
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ function __accessProp(key) {
10
+ return this[key];
11
+ }
12
+ var __toESMCache_node;
13
+ var __toESMCache_esm;
9
14
  var __toESM = (mod, isNodeMode, target) => {
15
+ var canCache = mod != null && typeof mod === "object";
16
+ if (canCache) {
17
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
+ var cached = cache.get(mod);
19
+ if (cached)
20
+ return cached;
21
+ }
10
22
  target = mod != null ? __create(__getProtoOf(mod)) : {};
11
23
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
24
  for (let key of __getOwnPropNames(mod))
13
25
  if (!__hasOwnProp.call(to, key))
14
26
  __defProp(to, key, {
15
- get: () => mod[key],
27
+ get: __accessProp.bind(mod, key),
16
28
  enumerable: true
17
29
  });
30
+ if (canCache)
31
+ cache.set(mod, to);
18
32
  return to;
19
33
  };
20
34
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
35
+ var __returnValue = (v) => v;
36
+ function __exportSetter(name, newValue) {
37
+ this[name] = __returnValue.bind(null, newValue);
38
+ }
21
39
  var __export = (target, all) => {
22
40
  for (var name in all)
23
41
  __defProp(target, name, {
24
42
  get: all[name],
25
43
  enumerable: true,
26
44
  configurable: true,
27
- set: (newValue) => all[name] = () => newValue
45
+ set: __exportSetter.bind(all, name)
28
46
  });
29
47
  };
30
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -587718,7 +587736,7 @@ function errorCodeToExitCode(code) {
587718
587736
  // package.json
587719
587737
  var package_default = {
587720
587738
  name: "@ncukondo/gcal-cli",
587721
- version: "0.2.2",
587739
+ version: "0.3.0",
587722
587740
  type: "module",
587723
587741
  exports: {
587724
587742
  ".": "./dist/index.js"
@@ -588406,7 +588424,6 @@ function applyFilters(events, options) {
588406
588424
  const afterTransparency = filterByTransparency(events, options.transparency);
588407
588425
  return filterByStatus(afterTransparency, options);
588408
588426
  }
588409
-
588410
588427
  // node_modules/date-fns/locale/en-US/_lib/formatDistance.mjs
588411
588428
  var formatDistanceLocale = {
588412
588429
  lessThanXSeconds: {
@@ -590579,7 +590596,7 @@ function parseDateTimeInZone(dateStr, timezone) {
590579
590596
  // src/commands/search.ts
590580
590597
  var DEFAULT_SEARCH_DAYS = 30;
590581
590598
  async function handleSearch(opts) {
590582
- const { api: api2, query, format: format3, calendars, timezone, write } = opts;
590599
+ const { api: api2, query, format: format4, calendars, timezone, write } = opts;
590583
590600
  const writeErr = opts.writeErr ?? (() => {});
590584
590601
  const now = new Date;
590585
590602
  const days = opts.days ?? DEFAULT_SEARCH_DAYS;
@@ -590625,7 +590642,7 @@ async function handleSearch(opts) {
590625
590642
  if (opts.includeTentative !== undefined)
590626
590643
  filterOpts.includeTentative = opts.includeTentative;
590627
590644
  const filtered = applyFilters(allEvents, filterOpts);
590628
- if (format3 === "json") {
590645
+ if (format4 === "json") {
590629
590646
  write(formatJsonSuccess({
590630
590647
  query,
590631
590648
  events: filtered,
@@ -590656,10 +590673,10 @@ function createSearchCommand() {
590656
590673
 
590657
590674
  // src/commands/show.ts
590658
590675
  async function handleShow(opts) {
590659
- const { api: api2, eventId, calendarId, calendarName, format: format3, timezone, write } = opts;
590676
+ const { api: api2, eventId, calendarId, calendarName, format: format4, timezone, write } = opts;
590660
590677
  try {
590661
590678
  const event = await getEvent(api2, calendarId, calendarName, eventId, timezone);
590662
- if (format3 === "json") {
590679
+ if (format4 === "json") {
590663
590680
  write(formatJsonSuccess({ event }));
590664
590681
  } else {
590665
590682
  write(formatEventDetailText(event));
@@ -590667,7 +590684,7 @@ async function handleShow(opts) {
590667
590684
  return { exitCode: ExitCode.SUCCESS };
590668
590685
  } catch (error) {
590669
590686
  if (error instanceof ApiError) {
590670
- if (format3 === "json") {
590687
+ if (format4 === "json") {
590671
590688
  write(formatJsonError(error.code, error.message));
590672
590689
  } else {
590673
590690
  write(`Error: ${error.message}`);
@@ -591733,19 +591750,19 @@ function resolveDateRange(input, timezone, now = () => new Date) {
591733
591750
  }
591734
591751
  if (input.from) {
591735
591752
  const fromDate = parseDateTimeInZone(input.from, timezone);
591736
- const toDate3 = input.to ? addDays(parseDateTimeInZone(input.to, timezone), 1) : addDays(fromDate, 7);
591753
+ const toDate4 = input.to ? addDays(parseDateTimeInZone(input.to, timezone), 1) : addDays(fromDate, 7);
591737
591754
  return {
591738
591755
  timeMin: formatDateTimeInZone(fromDate, timezone),
591739
- timeMax: formatDateTimeInZone(toDate3, timezone)
591756
+ timeMax: formatDateTimeInZone(toDate4, timezone)
591740
591757
  };
591741
591758
  }
591742
591759
  if (input.to) {
591743
591760
  const todayStr2 = todayInZone(now(), timezone);
591744
591761
  const fromDate = parseDateTimeInZone(todayStr2, timezone);
591745
- const toDate3 = addDays(parseDateTimeInZone(input.to, timezone), 1);
591762
+ const toDate4 = addDays(parseDateTimeInZone(input.to, timezone), 1);
591746
591763
  return {
591747
591764
  timeMin: formatDateTimeInZone(fromDate, timezone),
591748
- timeMax: formatDateTimeInZone(toDate3, timezone),
591765
+ timeMax: formatDateTimeInZone(toDate4, timezone),
591749
591766
  warning: "--from not specified, defaulting to today"
591750
591767
  };
591751
591768
  }
@@ -591856,10 +591873,194 @@ function createListCommand() {
591856
591873
  return cmd;
591857
591874
  }
591858
591875
 
591876
+ // src/lib/date-utils.ts
591877
+ var DATE_ONLY_RE2 = /^\d{4}-\d{2}-\d{2}$/;
591878
+ function isDateOnly(input) {
591879
+ if (!DATE_ONLY_RE2.test(input))
591880
+ return false;
591881
+ const [y, m, d] = input.split("-").map(Number);
591882
+ const date3 = new Date(Date.UTC(y, m - 1, d));
591883
+ return date3.getUTCFullYear() === y && date3.getUTCMonth() === m - 1 && date3.getUTCDate() === d;
591884
+ }
591885
+ function addDaysToDateString(dateStr, days) {
591886
+ const [y, m, d] = dateStr.split("-").map(Number);
591887
+ const date3 = new Date(Date.UTC(y, m - 1, d + days));
591888
+ return date3.toISOString().slice(0, 10);
591889
+ }
591890
+
591891
+ // src/lib/duration.ts
591892
+ var DURATION_RE = /^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/;
591893
+ function parseDuration(input) {
591894
+ const match2 = DURATION_RE.exec(input);
591895
+ if (!match2 || input === "") {
591896
+ throw new Error(`Invalid duration: "${input}". Use formats like 30m, 1h, 2d, 1h30m.`);
591897
+ }
591898
+ const days = Number(match2[1] || 0);
591899
+ const hours = Number(match2[2] || 0);
591900
+ const minutes = Number(match2[3] || 0);
591901
+ const ms = ((days * 24 + hours) * 60 + minutes) * 60 * 1000;
591902
+ if (ms === 0) {
591903
+ throw new Error("Duration must be greater than zero.");
591904
+ }
591905
+ return ms;
591906
+ }
591907
+
591859
591908
  // src/commands/update.ts
591909
+ var MS_PER_DAY = 24 * 60 * 60 * 1000;
591910
+ function resolveTimedEvent(startStr, endStr, timezone) {
591911
+ const parsedStart = parseDateTimeInZone(startStr, timezone);
591912
+ const parsedEnd = parseDateTimeInZone(endStr, timezone);
591913
+ return {
591914
+ start: formatDateTimeInZone(parsedStart, timezone),
591915
+ end: formatDateTimeInZone(parsedEnd, timezone),
591916
+ allDay: false
591917
+ };
591918
+ }
591919
+ function resolveAllDayEvent(startStr, endStr) {
591920
+ return {
591921
+ start: startStr,
591922
+ end: addDaysToDateString(endStr, 1),
591923
+ allDay: true
591924
+ };
591925
+ }
591926
+ function resolveStartAndEnd(startStr, endStr, allDay, timezone) {
591927
+ if (allDay) {
591928
+ return resolveAllDayEvent(startStr, endStr);
591929
+ }
591930
+ return resolveTimedEvent(startStr, endStr, timezone);
591931
+ }
591932
+ function resolveStartAndDuration(startStr, durationMs, allDay, timezone) {
591933
+ if (allDay) {
591934
+ const days = durationMs / MS_PER_DAY;
591935
+ return {
591936
+ start: startStr,
591937
+ end: addDaysToDateString(startStr, days),
591938
+ allDay: true
591939
+ };
591940
+ }
591941
+ const parsedStart = parseDateTimeInZone(startStr, timezone);
591942
+ const endDate = new Date(parsedStart.getTime() + durationMs);
591943
+ return {
591944
+ start: formatDateTimeInZone(parsedStart, timezone),
591945
+ end: formatDateTimeInZone(endDate, timezone),
591946
+ allDay: false
591947
+ };
591948
+ }
591949
+ function resolveStartOnly(startStr, existing, allDay, timezone) {
591950
+ if (allDay) {
591951
+ const existingStartMs2 = new Date(existing.start).getTime();
591952
+ const existingEndMs2 = new Date(existing.end).getTime();
591953
+ const durationDays = Math.round((existingEndMs2 - existingStartMs2) / MS_PER_DAY);
591954
+ return {
591955
+ start: startStr,
591956
+ end: addDaysToDateString(startStr, durationDays),
591957
+ allDay: true,
591958
+ existingEvent: existing
591959
+ };
591960
+ }
591961
+ const existingStartMs = new Date(existing.start).getTime();
591962
+ const existingEndMs = new Date(existing.end).getTime();
591963
+ const durationMs = existingEndMs - existingStartMs;
591964
+ const parsedStart = parseDateTimeInZone(startStr, timezone);
591965
+ const endDate = new Date(parsedStart.getTime() + durationMs);
591966
+ return {
591967
+ start: formatDateTimeInZone(parsedStart, timezone),
591968
+ end: formatDateTimeInZone(endDate, timezone),
591969
+ allDay: false,
591970
+ existingEvent: existing
591971
+ };
591972
+ }
591973
+ function resolveEndOnly(endStr, existing, allDay, timezone) {
591974
+ if (allDay) {
591975
+ return {
591976
+ start: existing.start,
591977
+ end: addDaysToDateString(endStr, 1),
591978
+ allDay: true,
591979
+ existingEvent: existing
591980
+ };
591981
+ }
591982
+ const parsedEnd = parseDateTimeInZone(endStr, timezone);
591983
+ return {
591984
+ start: existing.start,
591985
+ end: formatDateTimeInZone(parsedEnd, timezone),
591986
+ allDay: false,
591987
+ existingEvent: existing
591988
+ };
591989
+ }
591990
+ function resolveDurationOnly(durationMs, existing, allDay, timezone) {
591991
+ if (allDay) {
591992
+ const days = durationMs / MS_PER_DAY;
591993
+ return {
591994
+ start: existing.start,
591995
+ end: addDaysToDateString(existing.start, days),
591996
+ allDay: true,
591997
+ existingEvent: existing
591998
+ };
591999
+ }
592000
+ const existingStartMs = new Date(existing.start).getTime();
592001
+ const endDate = new Date(existingStartMs + durationMs);
592002
+ return {
592003
+ start: existing.start,
592004
+ end: formatDateTimeInZone(endDate, timezone),
592005
+ allDay: false,
592006
+ existingEvent: existing
592007
+ };
592008
+ }
592009
+ async function resolveTimeUpdate(opts) {
592010
+ const { timezone, calendarId, calendarName, eventId, getEvent: getEvent2 } = opts;
592011
+ const hasStart = opts.start !== undefined;
592012
+ const hasEnd = opts.end !== undefined;
592013
+ const hasDuration = opts.duration !== undefined;
592014
+ if (!hasStart && !hasEnd && !hasDuration)
592015
+ return null;
592016
+ const durationMs = hasDuration ? parseDuration(opts.duration) : undefined;
592017
+ const needExisting = hasStart && !hasEnd && !hasDuration || hasEnd && !hasStart || hasDuration && !hasStart;
592018
+ let existing;
592019
+ if (needExisting) {
592020
+ existing = await getEvent2(calendarId, calendarName, eventId, timezone);
592021
+ }
592022
+ const allDay = hasStart ? isDateOnly(opts.start) : existing.all_day;
592023
+ if (hasStart && hasEnd) {
592024
+ const startIsDateOnly = isDateOnly(opts.start);
592025
+ const endIsDateOnly = isDateOnly(opts.end);
592026
+ if (startIsDateOnly !== endIsDateOnly) {
592027
+ throw new ApiError("INVALID_ARGS", "--start and --end must be the same type (both date-only or both datetime)");
592028
+ }
592029
+ }
592030
+ if (hasEnd && !hasStart && existing) {
592031
+ const endIsDateOnly = isDateOnly(opts.end);
592032
+ if (existing.all_day && !endIsDateOnly) {
592033
+ throw new ApiError("INVALID_ARGS", "--end format (datetime) does not match existing event type (all-day). Use date-only format (YYYY-MM-DD) or provide --start to change event type.");
592034
+ }
592035
+ if (!existing.all_day && endIsDateOnly) {
592036
+ throw new ApiError("INVALID_ARGS", "--end format (date-only) does not match existing event type (timed). Use datetime format (YYYY-MM-DDTHH:MM) or provide --start to change event type.");
592037
+ }
592038
+ }
592039
+ if (durationMs !== undefined && allDay) {
592040
+ if (durationMs % MS_PER_DAY !== 0) {
592041
+ throw new ApiError("INVALID_ARGS", "All-day events require day-unit duration (e.g. 1d, 2d). Sub-day durations like hours or minutes are not allowed.");
592042
+ }
592043
+ }
592044
+ if (hasStart && hasEnd) {
592045
+ return resolveStartAndEnd(opts.start, opts.end, allDay, timezone);
592046
+ }
592047
+ if (hasStart && durationMs !== undefined) {
592048
+ return resolveStartAndDuration(opts.start, durationMs, allDay, timezone);
592049
+ }
592050
+ if (hasStart) {
592051
+ return resolveStartOnly(opts.start, existing, allDay, timezone);
592052
+ }
592053
+ if (hasEnd) {
592054
+ return resolveEndOnly(opts.end, existing, allDay, timezone);
592055
+ }
592056
+ if (durationMs !== undefined) {
592057
+ return resolveDurationOnly(durationMs, existing, allDay, timezone);
592058
+ }
592059
+ return null;
592060
+ }
591860
592061
  async function handleUpdate(opts) {
591861
- const { api: api2, eventId, calendarId, calendarName, format: format3, timezone, write } = opts;
591862
- const hasUpdate = opts.title !== undefined || opts.start !== undefined || opts.end !== undefined || opts.description !== undefined || opts.busy !== undefined || opts.free !== undefined;
592062
+ const { api: api2, eventId, calendarId, calendarName, format: format4, timezone, write } = opts;
592063
+ const hasUpdate = opts.title !== undefined || opts.start !== undefined || opts.end !== undefined || opts.duration !== undefined || opts.description !== undefined || opts.busy !== undefined || opts.free !== undefined;
591863
592064
  if (!hasUpdate) {
591864
592065
  throw new ApiError("INVALID_ARGS", "at least one update option must be provided");
591865
592066
  }
@@ -591875,19 +592076,21 @@ async function handleUpdate(opts) {
591875
592076
  } else if (opts.free) {
591876
592077
  input.transparency = "transparent";
591877
592078
  }
591878
- if (opts.start !== undefined || opts.end !== undefined) {
591879
- if (opts.start === undefined || opts.end === undefined) {
591880
- throw new ApiError("INVALID_ARGS", "start, end, and allDay must all be provided together");
591881
- }
591882
- const startStr = opts.start;
591883
- const endStr = opts.end;
591884
- const parsedStart = parseDateTimeInZone(startStr, timezone);
591885
- const parsedEnd = parseDateTimeInZone(endStr, timezone);
592079
+ const timeResult = await resolveTimeUpdate(opts);
592080
+ if (timeResult) {
591886
592081
  const withTime = input;
591887
- withTime.start = formatDateTimeInZone(parsedStart, timezone);
591888
- withTime.end = formatDateTimeInZone(parsedEnd, timezone);
591889
- withTime.allDay = false;
592082
+ withTime.start = timeResult.start;
592083
+ withTime.end = timeResult.end;
592084
+ withTime.allDay = timeResult.allDay;
591890
592085
  input.timeZone = timezone;
592086
+ if (timeResult.existingEvent) {
592087
+ const existing = timeResult.existingEvent;
592088
+ if (existing.all_day && !timeResult.allDay) {
592089
+ opts.writeStderr("⚠ Event type changed from all-day to timed");
592090
+ } else if (!existing.all_day && timeResult.allDay) {
592091
+ opts.writeStderr("⚠ Event type changed from timed to all-day");
592092
+ }
592093
+ }
591891
592094
  }
591892
592095
  if (opts.dryRun) {
591893
592096
  const changes = {};
@@ -591902,7 +592105,9 @@ async function handleUpdate(opts) {
591902
592105
  changes.start = withTime.start;
591903
592106
  if (withTime.end !== undefined)
591904
592107
  changes.end = withTime.end;
591905
- if (format3 === "json") {
592108
+ if (withTime.allDay !== undefined)
592109
+ changes.allDay = withTime.allDay;
592110
+ if (format4 === "json") {
591906
592111
  write(formatJsonSuccess({
591907
592112
  dry_run: true,
591908
592113
  action: "update",
@@ -591927,7 +592132,7 @@ async function handleUpdate(opts) {
591927
592132
  return { exitCode: ExitCode.SUCCESS };
591928
592133
  }
591929
592134
  const updated = await updateEvent(api2, calendarId, calendarName, eventId, input);
591930
- if (format3 === "json") {
592135
+ if (format4 === "json") {
591931
592136
  write(formatJsonSuccess({ event: updated }));
591932
592137
  } else {
591933
592138
  write(formatEventDetailText(updated));
@@ -591938,16 +592143,34 @@ function createUpdateCommand() {
591938
592143
  const cmd = new Command("update").description("Update an existing event").argument("<event-id>", "Event ID to update");
591939
592144
  cmd.option("-c, --calendar <id>", "Calendar ID");
591940
592145
  cmd.option("-t, --title <title>", "New title");
591941
- cmd.option("-s, --start <datetime>", "New start datetime");
591942
- cmd.option("-e, --end <datetime>", "New end datetime");
592146
+ cmd.option("-s, --start <datetime>", "Start date or datetime. Date-only (YYYY-MM-DD) → all-day. Datetime (YYYY-MM-DDTHH:MM) → timed. Can be specified alone (preserves existing duration).");
592147
+ cmd.option("-e, --end <datetime>", "End date or datetime. Can be specified alone (preserves existing start). All-day end is inclusive.");
592148
+ cmd.option("--duration <duration>", "Duration instead of --end (e.g. 30m, 1h, 2d, 1h30m). Mutually exclusive with --end. Can be specified alone (preserves existing start).");
591943
592149
  cmd.option("-d, --description <text>", "New description");
591944
592150
  cmd.option("--busy", "Mark as busy");
591945
592151
  cmd.option("--free", "Mark as free");
591946
592152
  cmd.option("--dry-run", "Preview without executing");
592153
+ const endOpt = cmd.options.find((o) => o.long === "--end");
592154
+ const durationOpt = cmd.options.find((o) => o.long === "--duration");
592155
+ endOpt.conflicts(["duration"]);
592156
+ durationOpt.conflicts(["end"]);
591947
592157
  const busyOpt = cmd.options.find((o) => o.long === "--busy");
591948
592158
  const freeOpt = cmd.options.find((o) => o.long === "--free");
591949
592159
  busyOpt.conflicts(["free"]);
591950
592160
  freeOpt.conflicts(["busy"]);
592161
+ cmd.addHelpText("after", `
592162
+ Examples:
592163
+ gcal update abc123 -t "Updated Meeting" # Title only
592164
+ gcal update abc123 -s "2026-01-24T11:00" # Start only, keep duration
592165
+ gcal update abc123 -e "2026-01-24T12:00" # End only, keep start
592166
+ gcal update abc123 --duration 2h # Duration only, keep start
592167
+ gcal update abc123 -s "2026-01-24T11:00" -e "2026-01-24T12:30" # Start + end
592168
+ gcal update abc123 -s "2026-01-24T10:00" --duration 30m # Start + duration
592169
+ gcal update abc123 -s "2026-03-01" -e "2026-03-03" # All-day, 3 days (inclusive)
592170
+ gcal update abc123 -s "2026-03-01" --duration 2d # All-day, 2 days
592171
+ gcal update abc123 --free # Transparency only
592172
+ gcal update abc123 --dry-run -t "Preview" # Dry run
592173
+ `);
591951
592174
  return cmd;
591952
592175
  }
591953
592176
 
@@ -591961,10 +592184,18 @@ async function handleAdd(options, deps) {
591961
592184
  deps.write(formatJsonError("INVALID_ARGS", "--start is required"));
591962
592185
  return { exitCode: ExitCode.ARGUMENT };
591963
592186
  }
591964
- if (!options.end) {
591965
- deps.write(formatJsonError("INVALID_ARGS", "--end is required"));
592187
+ if (options.end && options.duration) {
592188
+ deps.write(formatJsonError("INVALID_ARGS", "--end and --duration cannot be used together"));
591966
592189
  return { exitCode: ExitCode.ARGUMENT };
591967
592190
  }
592191
+ const allDay = isDateOnly(options.start);
592192
+ if (options.end) {
592193
+ const endIsDateOnly = isDateOnly(options.end);
592194
+ if (allDay !== endIsDateOnly) {
592195
+ deps.write(formatJsonError("INVALID_ARGS", "--start and --end must be the same type (both date-only or both datetime)"));
592196
+ return { exitCode: ExitCode.ARGUMENT };
592197
+ }
592198
+ }
591968
592199
  const config2 = deps.loadConfig();
591969
592200
  const timezone = resolveTimezone(options.timezone, config2.timezone);
591970
592201
  const calendars = selectCalendars(options.calendar ? [options.calendar] : undefined, config2);
@@ -591976,20 +592207,54 @@ async function handleAdd(options, deps) {
591976
592207
  transparency = "transparent";
591977
592208
  let start;
591978
592209
  let end;
591979
- if (options.allDay) {
591980
- start = options.start.slice(0, 10);
591981
- end = options.end.slice(0, 10);
592210
+ if (allDay) {
592211
+ start = options.start;
592212
+ if (options.end) {
592213
+ end = addDaysToDateString(options.end, 1);
592214
+ } else if (options.duration) {
592215
+ let durationMs;
592216
+ try {
592217
+ durationMs = parseDuration(options.duration);
592218
+ } catch {
592219
+ deps.write(formatJsonError("INVALID_ARGS", `Invalid duration: "${options.duration}". Use formats like 30m, 1h, 2d, 1h30m.`));
592220
+ return { exitCode: ExitCode.ARGUMENT };
592221
+ }
592222
+ const MS_PER_DAY2 = 24 * 60 * 60 * 1000;
592223
+ if (durationMs % MS_PER_DAY2 !== 0) {
592224
+ deps.write(formatJsonError("INVALID_ARGS", "All-day events require day-unit duration (e.g. 1d, 2d). Sub-day durations like hours or minutes are not allowed."));
592225
+ return { exitCode: ExitCode.ARGUMENT };
592226
+ }
592227
+ const days = durationMs / MS_PER_DAY2;
592228
+ end = addDaysToDateString(options.start, days);
592229
+ } else {
592230
+ end = addDaysToDateString(options.start, 1);
592231
+ }
591982
592232
  } else {
591983
592233
  const startDate = parseDateTimeInZone(options.start, timezone);
591984
- const endDate = parseDateTimeInZone(options.end, timezone);
591985
592234
  start = formatDateTimeInZone(startDate, timezone);
591986
- end = formatDateTimeInZone(endDate, timezone);
592235
+ if (options.end) {
592236
+ const endDate = parseDateTimeInZone(options.end, timezone);
592237
+ end = formatDateTimeInZone(endDate, timezone);
592238
+ } else if (options.duration) {
592239
+ let durationMs;
592240
+ try {
592241
+ durationMs = parseDuration(options.duration);
592242
+ } catch {
592243
+ deps.write(formatJsonError("INVALID_ARGS", `Invalid duration: "${options.duration}". Use formats like 30m, 1h, 2d, 1h30m.`));
592244
+ return { exitCode: ExitCode.ARGUMENT };
592245
+ }
592246
+ const endDate = new Date(startDate.getTime() + durationMs);
592247
+ end = formatDateTimeInZone(endDate, timezone);
592248
+ } else {
592249
+ const endDate = new Date(startDate.getTime() + 60 * 60 * 1000);
592250
+ end = formatDateTimeInZone(endDate, timezone);
592251
+ }
591987
592252
  }
591988
592253
  const input = {
591989
592254
  title: options.title,
591990
592255
  start,
591991
592256
  end,
591992
- allDay: options.allDay ?? false,
592257
+ allDay,
591993
592258
  timeZone: timezone,
591994
592259
  transparency
591995
592260
  };
@@ -592009,29 +592274,43 @@ ${detail}`);
592009
592274
  }
592010
592275
  function createAddCommand() {
592011
592276
  const cmd = new Command("add").description("Create a new event");
592012
- cmd.option("-t, --title <title>", "Event title");
592013
- cmd.option("-s, --start <datetime>", "Start datetime (ISO 8601)");
592014
- cmd.option("-e, --end <datetime>", "End datetime (ISO 8601)");
592015
- cmd.option("--all-day", "Create all-day event (use date only)");
592277
+ cmd.requiredOption("-t, --title <title>", "Event title");
592278
+ cmd.requiredOption("-s, --start <datetime>", "Start date or datetime. Date-only (YYYY-MM-DD) creates all-day event. Datetime (YYYY-MM-DDTHH:MM) creates timed event.");
592279
+ cmd.option("-e, --end <datetime>", "End date or datetime. Optional. Default: same day (all-day) or +1h (timed). All-day end is inclusive.");
592280
+ cmd.option("--duration <duration>", "Duration instead of --end (e.g. 30m, 1h, 2d, 1h30m). Mutually exclusive with --end.");
592016
592281
  cmd.option("-d, --description <text>", "Event description");
592017
592282
  cmd.option("--busy", "Mark as busy (default)");
592018
592283
  cmd.option("--free", "Mark as free (transparent)");
592284
+ const endOpt = cmd.options.find((o) => o.long === "--end");
592285
+ const durationOpt = cmd.options.find((o) => o.long === "--duration");
592286
+ endOpt.conflicts(["duration"]);
592287
+ durationOpt.conflicts(["end"]);
592019
592288
  const busyOpt = cmd.options.find((o) => o.long === "--busy");
592020
592289
  const freeOpt = cmd.options.find((o) => o.long === "--free");
592021
592290
  busyOpt.conflicts(["free"]);
592022
592291
  freeOpt.conflicts(["busy"]);
592292
+ cmd.addHelpText("after", `
592293
+ Examples:
592294
+ gcal add -t "Holiday" -s "2026-01-24" # All-day, 1 day
592295
+ gcal add -t "Vacation" -s "2026-01-24" -e "2026-01-26" # All-day, 3 days (inclusive)
592296
+ gcal add -t "Camp" -s "2026-01-24" --duration 2d # All-day, 2 days
592297
+ gcal add -t "Meeting" -s "2026-01-24T10:00" # Timed, 1h default
592298
+ gcal add -t "Meeting" -s "2026-01-24T10:00" -e "2026-01-24T11:30" # Timed, explicit end
592299
+ gcal add -t "Standup" -s "2026-01-24T10:00" --duration 30m # Timed, 30 min
592300
+ gcal add -t "Focus" -s "2026-01-24T09:00" --duration 2h --free # Timed, free
592301
+ `);
592023
592302
  return cmd;
592024
592303
  }
592025
592304
 
592026
592305
  // src/commands/delete.ts
592027
592306
  async function handleDelete(opts) {
592028
- const { api: api2, eventId, calendarId, format: format3, quiet, dryRun = false, write } = opts;
592307
+ const { api: api2, eventId, calendarId, format: format4, quiet, dryRun = false, write } = opts;
592029
592308
  if (!eventId) {
592030
592309
  write(formatJsonError("INVALID_ARGS", "event-id is required"));
592031
592310
  return { exitCode: ExitCode.ARGUMENT };
592032
592311
  }
592033
592312
  if (dryRun) {
592034
- if (format3 === "json") {
592313
+ if (format4 === "json") {
592035
592314
  write(formatJsonSuccess({
592036
592315
  dry_run: true,
592037
592316
  action: "delete",
@@ -592046,7 +592325,7 @@ async function handleDelete(opts) {
592046
592325
  try {
592047
592326
  await deleteEvent(api2, calendarId, eventId);
592048
592327
  if (!quiet) {
592049
- if (format3 === "json") {
592328
+ if (format4 === "json") {
592050
592329
  write(formatJsonSuccess({ deleted_id: eventId, message: "Event deleted" }));
592051
592330
  } else {
592052
592331
  write("Event deleted");
@@ -592055,7 +592334,7 @@ async function handleDelete(opts) {
592055
592334
  return { exitCode: ExitCode.SUCCESS };
592056
592335
  } catch (error) {
592057
592336
  if (error instanceof ApiError) {
592058
- if (format3 === "json") {
592337
+ if (format4 === "json") {
592059
592338
  write(formatJsonError(error.code, error.message));
592060
592339
  } else {
592061
592340
  write(`Error: ${error.message}`);
@@ -592081,13 +592360,13 @@ function mergeCalendarsWithConfig(apiCalendars, configCalendars) {
592081
592360
  });
592082
592361
  }
592083
592362
  async function handleCalendars(opts) {
592084
- const { api: api2, format: format3, quiet, write, configCalendars } = opts;
592363
+ const { api: api2, format: format4, quiet, write, configCalendars } = opts;
592085
592364
  let apiCalendars;
592086
592365
  try {
592087
592366
  apiCalendars = await listCalendars(api2);
592088
592367
  } catch (error) {
592089
592368
  if (error instanceof ApiError) {
592090
- if (format3 === "json") {
592369
+ if (format4 === "json") {
592091
592370
  write(formatJsonError(error.code, error.message));
592092
592371
  } else {
592093
592372
  write(error.message);
@@ -592102,7 +592381,7 @@ async function handleCalendars(opts) {
592102
592381
  `));
592103
592382
  return { exitCode: ExitCode.SUCCESS };
592104
592383
  }
592105
- if (format3 === "json") {
592384
+ if (format4 === "json") {
592106
592385
  write(formatJsonSuccess({ calendars }));
592107
592386
  } else {
592108
592387
  write(formatCalendarListText(calendars));
@@ -592119,12 +592398,12 @@ function resolveTimezone2(cliTimezone) {
592119
592398
  return cliTimezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
592120
592399
  }
592121
592400
  async function handleInit(opts) {
592122
- const { fs, format: format3, quiet, write, force, all, local, requestAuth } = opts;
592401
+ const { fs, format: format4, quiet, write, force, all, local, requestAuth } = opts;
592123
592402
  const configPath = local ? `${process.cwd()}/gcal-cli.toml` : getDefaultConfigPath();
592124
592403
  if (!force && fs.existsSync(configPath)) {
592125
592404
  const msg = `Config file already exists: ${configPath}
592126
592405
  Use --force to overwrite.`;
592127
- if (format3 === "json") {
592406
+ if (format4 === "json") {
592128
592407
  write(formatJsonError("CONFIG_ERROR", msg));
592129
592408
  } else {
592130
592409
  write(msg);
@@ -592140,7 +592419,7 @@ Use --force to overwrite.`;
592140
592419
  calendars = await opts.listCalendars();
592141
592420
  } else if (isAuthRequiredError(error)) {
592142
592421
  const msg = "Not authenticated. Run `gcal auth` to authenticate.";
592143
- if (format3 === "json") {
592422
+ if (format4 === "json") {
592144
592423
  write(formatJsonError("AUTH_REQUIRED", msg));
592145
592424
  } else {
592146
592425
  write(msg);
@@ -592152,7 +592431,7 @@ Use --force to overwrite.`;
592152
592431
  }
592153
592432
  if (calendars.length === 0) {
592154
592433
  const msg = "No calendars found in Google Calendar.";
592155
- if (format3 === "json") {
592434
+ if (format4 === "json") {
592156
592435
  write(formatJsonError("API_ERROR", msg));
592157
592436
  } else {
592158
592437
  write(msg);
@@ -592174,7 +592453,7 @@ Use --force to overwrite.`;
592174
592453
  write(configPath);
592175
592454
  return { exitCode: ExitCode.SUCCESS };
592176
592455
  }
592177
- if (format3 === "json") {
592456
+ if (format4 === "json") {
592178
592457
  write(formatJsonSuccess({
592179
592458
  path: configPath,
592180
592459
  timezone,
@@ -592293,10 +592572,10 @@ function getErrorCode2(error) {
592293
592572
  }
592294
592573
  return "API_ERROR";
592295
592574
  }
592296
- function handleError2(error, format3) {
592575
+ function handleError2(error, format4) {
592297
592576
  const errorCode = getErrorCode2(error);
592298
592577
  const message2 = error instanceof Error ? error.message : String(error);
592299
- if (format3 === "json") {
592578
+ if (format4 === "json") {
592300
592579
  process.stderr.write(formatJsonError(errorCode, message2));
592301
592580
  } else {
592302
592581
  process.stderr.write(`Error: ${message2}
@@ -592551,7 +592830,7 @@ ${url}`);
592551
592830
  title: addOpts.title,
592552
592831
  start: addOpts.start,
592553
592832
  end: addOpts.end,
592554
- allDay: addOpts.allDay,
592833
+ duration: addOpts.duration,
592555
592834
  description: addOpts.description,
592556
592835
  busy: addOpts.busy,
592557
592836
  free: addOpts.free,
@@ -592648,6 +592927,9 @@ ${authUrl}`);
592648
592927
  timezone,
592649
592928
  write: (msg) => process.stdout.write(msg + `
592650
592929
  `),
592930
+ writeStderr: (msg) => process.stderr.write(msg + `
592931
+ `),
592932
+ getEvent: (calId, calName, evtId, tz) => getEvent(api2, calId, calName, evtId, tz),
592651
592933
  ...updateOpts
592652
592934
  });
592653
592935
  process.exit(result.exitCode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncukondo/gcal-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"