@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.
- package/dist/index.js +227 -50
- 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.
|
|
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
|
-
|
|
591896
|
-
|
|
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 =
|
|
591905
|
-
withTime.end =
|
|
591906
|
-
withTime.allDay =
|
|
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>", "
|
|
591959
|
-
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).");
|
|
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
|
|
592049
|
-
if (durationMs %
|
|
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 /
|
|
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);
|