@ncukondo/gcal-cli 0.2.3 → 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 +227 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -587736,7 +587736,7 @@ function errorCodeToExitCode(code) {
587736
587736
  // package.json
587737
587737
  var package_default = {
587738
587738
  name: "@ncukondo/gcal-cli",
587739
- version: "0.2.3",
587739
+ version: "0.3.0",
587740
587740
  type: "module",
587741
587741
  exports: {
587742
587742
  ".": "./dist/index.js"
@@ -591873,10 +591873,194 @@ function createListCommand() {
591873
591873
  return cmd;
591874
591874
  }
591875
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
+
591876
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
+ }
591877
592061
  async function handleUpdate(opts) {
591878
592062
  const { api: api2, eventId, calendarId, calendarName, format: format4, timezone, write } = opts;
591879
- const hasUpdate = opts.title !== undefined || opts.start !== undefined || opts.end !== undefined || opts.description !== undefined || opts.busy !== undefined || opts.free !== undefined;
592063
+ const hasUpdate = opts.title !== undefined || opts.start !== undefined || opts.end !== undefined || opts.duration !== undefined || opts.description !== undefined || opts.busy !== undefined || opts.free !== undefined;
591880
592064
  if (!hasUpdate) {
591881
592065
  throw new ApiError("INVALID_ARGS", "at least one update option must be provided");
591882
592066
  }
@@ -591892,19 +592076,21 @@ async function handleUpdate(opts) {
591892
592076
  } else if (opts.free) {
591893
592077
  input.transparency = "transparent";
591894
592078
  }
591895
- if (opts.start !== undefined || opts.end !== undefined) {
591896
- if (opts.start === undefined || opts.end === undefined) {
591897
- throw new ApiError("INVALID_ARGS", "start, end, and allDay must all be provided together");
591898
- }
591899
- const startStr = opts.start;
591900
- const endStr = opts.end;
591901
- const parsedStart = parseDateTimeInZone(startStr, timezone);
591902
- const parsedEnd = parseDateTimeInZone(endStr, timezone);
592079
+ const timeResult = await resolveTimeUpdate(opts);
592080
+ if (timeResult) {
591903
592081
  const withTime = input;
591904
- withTime.start = formatDateTimeInZone(parsedStart, timezone);
591905
- withTime.end = formatDateTimeInZone(parsedEnd, timezone);
591906
- withTime.allDay = false;
592082
+ withTime.start = timeResult.start;
592083
+ withTime.end = timeResult.end;
592084
+ withTime.allDay = timeResult.allDay;
591907
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
+ }
591908
592094
  }
591909
592095
  if (opts.dryRun) {
591910
592096
  const changes = {};
@@ -591919,6 +592105,8 @@ async function handleUpdate(opts) {
591919
592105
  changes.start = withTime.start;
591920
592106
  if (withTime.end !== undefined)
591921
592107
  changes.end = withTime.end;
592108
+ if (withTime.allDay !== undefined)
592109
+ changes.allDay = withTime.allDay;
591922
592110
  if (format4 === "json") {
591923
592111
  write(formatJsonSuccess({
591924
592112
  dry_run: true,
@@ -591955,51 +592143,37 @@ function createUpdateCommand() {
591955
592143
  const cmd = new Command("update").description("Update an existing event").argument("<event-id>", "Event ID to update");
591956
592144
  cmd.option("-c, --calendar <id>", "Calendar ID");
591957
592145
  cmd.option("-t, --title <title>", "New title");
591958
- cmd.option("-s, --start <datetime>", "New start datetime");
591959
- 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).");
591960
592149
  cmd.option("-d, --description <text>", "New description");
591961
592150
  cmd.option("--busy", "Mark as busy");
591962
592151
  cmd.option("--free", "Mark as free");
591963
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"]);
591964
592157
  const busyOpt = cmd.options.find((o) => o.long === "--busy");
591965
592158
  const freeOpt = cmd.options.find((o) => o.long === "--free");
591966
592159
  busyOpt.conflicts(["free"]);
591967
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
+ `);
591968
592174
  return cmd;
591969
592175
  }
591970
592176
 
591971
- // src/lib/date-utils.ts
591972
- var DATE_ONLY_RE2 = /^\d{4}-\d{2}-\d{2}$/;
591973
- function isDateOnly(input) {
591974
- if (!DATE_ONLY_RE2.test(input))
591975
- return false;
591976
- const [y, m, d] = input.split("-").map(Number);
591977
- const date3 = new Date(Date.UTC(y, m - 1, d));
591978
- return date3.getUTCFullYear() === y && date3.getUTCMonth() === m - 1 && date3.getUTCDate() === d;
591979
- }
591980
- function addDaysToDateString(dateStr, days) {
591981
- const [y, m, d] = dateStr.split("-").map(Number);
591982
- const date3 = new Date(Date.UTC(y, m - 1, d + days));
591983
- return date3.toISOString().slice(0, 10);
591984
- }
591985
-
591986
- // src/lib/duration.ts
591987
- var DURATION_RE = /^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/;
591988
- function parseDuration(input) {
591989
- const match2 = DURATION_RE.exec(input);
591990
- if (!match2 || input === "") {
591991
- throw new Error(`Invalid duration: "${input}". Use formats like 30m, 1h, 2d, 1h30m.`);
591992
- }
591993
- const days = Number(match2[1] || 0);
591994
- const hours = Number(match2[2] || 0);
591995
- const minutes = Number(match2[3] || 0);
591996
- const ms = ((days * 24 + hours) * 60 + minutes) * 60 * 1000;
591997
- if (ms === 0) {
591998
- throw new Error("Duration must be greater than zero.");
591999
- }
592000
- return ms;
592001
- }
592002
-
592003
592177
  // src/commands/add.ts
592004
592178
  async function handleAdd(options, deps) {
592005
592179
  if (!options.title) {
@@ -592045,12 +592219,12 @@ async function handleAdd(options, deps) {
592045
592219
  deps.write(formatJsonError("INVALID_ARGS", `Invalid duration: "${options.duration}". Use formats like 30m, 1h, 2d, 1h30m.`));
592046
592220
  return { exitCode: ExitCode.ARGUMENT };
592047
592221
  }
592048
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
592049
- if (durationMs % MS_PER_DAY !== 0) {
592222
+ const MS_PER_DAY2 = 24 * 60 * 60 * 1000;
592223
+ if (durationMs % MS_PER_DAY2 !== 0) {
592050
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."));
592051
592225
  return { exitCode: ExitCode.ARGUMENT };
592052
592226
  }
592053
- const days = durationMs / MS_PER_DAY;
592227
+ const days = durationMs / MS_PER_DAY2;
592054
592228
  end = addDaysToDateString(options.start, days);
592055
592229
  } else {
592056
592230
  end = addDaysToDateString(options.start, 1);
@@ -592753,6 +592927,9 @@ ${authUrl}`);
592753
592927
  timezone,
592754
592928
  write: (msg) => process.stdout.write(msg + `
592755
592929
  `),
592930
+ writeStderr: (msg) => process.stderr.write(msg + `
592931
+ `),
592932
+ getEvent: (calId, calName, evtId, tz) => getEvent(api2, calId, calName, evtId, tz),
592756
592933
  ...updateOpts
592757
592934
  });
592758
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.3",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"