@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.
- package/dist/index.js +339 -57
- 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: (
|
|
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: (
|
|
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.
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
591762
|
+
const toDate4 = addDays(parseDateTimeInZone(input.to, timezone), 1);
|
|
591746
591763
|
return {
|
|
591747
591764
|
timeMin: formatDateTimeInZone(fromDate, timezone),
|
|
591748
|
-
timeMax: formatDateTimeInZone(
|
|
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:
|
|
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
|
-
|
|
591879
|
-
|
|
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 =
|
|
591888
|
-
withTime.end =
|
|
591889
|
-
withTime.allDay =
|
|
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 (
|
|
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 (
|
|
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>", "
|
|
591942
|
-
cmd.option("-e, --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 (
|
|
591965
|
-
deps.write(formatJsonError("INVALID_ARGS", "--end
|
|
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 (
|
|
591980
|
-
start = options.start
|
|
591981
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
592013
|
-
cmd.
|
|
592014
|
-
cmd.option("-e, --end <datetime>", "End datetime (
|
|
592015
|
-
cmd.option("--
|
|
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:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
592575
|
+
function handleError2(error, format4) {
|
|
592297
592576
|
const errorCode = getErrorCode2(error);
|
|
592298
592577
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
592299
|
-
if (
|
|
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
|
-
|
|
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);
|