Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack",
"version": "0.15.25-shared.5",
"version": "0.15.25-shared.6",
"description": "Shared pstack engineering workflows for Claude Code and Codex, with one configuration and host-specific runtime adapters.",
"author": {
"name": "Local adaptation; upstream Lauren Tan"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack-codex",
"version": "0.15.25-shared.5",
"version": "0.15.25-shared.6",
"description": "Shared pstack engineering workflows for Codex and Claude Code, with one configuration and native host adapters.",
"author": {
"name": "Lauren Tan; local Codex adaptation"
Expand Down
33 changes: 22 additions & 11 deletions bin/transcripts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,30 @@ function listTranscripts({ host, cwds, since, until }) {
};
}

// The proleptic Gregorian calendar Date.parse uses, independent of the local time zone.
function isCalendarDate(year, month, date) {
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
return month >= 1 && month <= 12 && date >= 1 && date <= [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
}

// setFullYear, unlike the Date constructor, does not map years 0 to 99 onto 1900 to 1999.
function localMidnight(year, monthIndex, date) {
const day = new Date(0);
day.setFullYear(year, monthIndex, date);
return day.setHours(0, 0, 0, 0);
}

// A bare date is a local calendar day: --since starts at its midnight and --until includes the whole day.
// Date.parse rolls a date-time's day past the month's end into the next month, so both forms check the date first.
function parseTime(value, flag, { endOfDay }) {
const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (day) {
const [year, month, date] = day.slice(1).map(Number);
const start = new Date(year, month - 1, date);
if (start.getFullYear() !== year || start.getMonth() !== month - 1 || start.getDate() !== date) {
throw new Error(`${flag} ${value} is not a calendar date`);
}
return endOfDay ? new Date(year, month - 1, date + 1).getTime() - 1 : start.getTime();
}
const time = /^\d{4}-\d{2}-\d{2}T/.test(value) ? Date.parse(value) : Number.NaN;
if (Number.isNaN(time)) throw new Error(`${flag} must be YYYY-MM-DD or an ISO 8601 date-time`);
const malformed = `${flag} must be YYYY-MM-DD or an ISO 8601 date-time`;
const day = /^(\d{4})-(\d{2})-(\d{2})(T.*)?$/.exec(value);
if (!day) throw new Error(malformed);
const [year, month, date] = day.slice(1, 4).map(Number);
if (!isCalendarDate(year, month, date)) throw new Error(`${flag} ${value} is not a calendar date`);
if (day[4] === undefined) return endOfDay ? localMidnight(year, month - 1, date + 1) - 1 : localMidnight(year, month - 1, date);
const time = Date.parse(value);
if (Number.isNaN(time)) throw new Error(malformed);
return time;
}

Expand Down
13 changes: 13 additions & 0 deletions scripts/transcripts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,26 @@ test("Given relocated host homes and several checkouts, When listed, Then CLAUDE
assert.deepEqual(paths(list(home, ["--host", "codex", "--cwd", "/work/repo"], env)), [rollout]);
});

test("Given bounds in years 0 to 99 or on a day the local calendar skipped, When the CLI parses them, Then it keeps the dates as written", (t) => {
const home = tempHome(t);
const ancient = list(home, ["--host", "claude", "--since", "0000-01-01T00:00:00Z", "--until", "0000-01-02T00:00:00Z"], { TZ: "UTC" });
assert.equal(ancient.since, "0000-01-01T00:00:00.000Z");
const bare = list(home, ["--host", "claude", "--since", "0050-01-01", "--until", "0050-01-01"], { TZ: "UTC" });
assert.deepEqual([bare.since, bare.until], ["0050-01-01T00:00:00.000Z", "0050-01-01T23:59:59.999Z"]);
// Samoa skipped 2011-12-30 locally, but that UTC date still exists.
const skipped = list(home, ["--host", "claude", "--since", "2011-12-30T12:00:00Z", "--until", "2011-12-31T00:00:00Z"], { TZ: "Pacific/Apia" });
assert.equal(skipped.since, "2011-12-30T12:00:00.000Z");
});

test("Given invalid arguments, When the CLI runs, Then it exits 1 with the reason", (t) => {
const home = tempHome(t);
const cases = [
[[], /--host is required/],
[["--host", "cursor"], /host must be one of claude or codex/],
[["--host", "claude", "--since", "last week"], /--since must be YYYY-MM-DD or an ISO 8601 date-time/],
[["--host", "claude", "--until", "2026-02-30"], /--until 2026-02-30 is not a calendar date/],
[["--host", "claude", "--until", "2026-02-30T23:00:00Z"], /--until 2026-02-30T23:00:00Z is not a calendar date/],
[["--host", "claude", "--since", "2026-04-31T00:00"], /--since 2026-04-31T00:00 is not a calendar date/],
[["--host", "claude", "--since", "2026-10-10", "--until", "2026-10-01"], /since must not be later than until/],
[["--host", "claude", "--host", "codex"], /may only be provided once/],
[["--host", "codex", "--cwd"], /--cwd requires a value/],
Expand Down
Loading