From 404b3143d32fcb2931a6d0b8b442aae97697b11b Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sun, 26 Jul 2026 10:36:21 -0700 Subject: [PATCH] feat(cli): add events date filters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f892a21-0f5d-41e7-aebb-afbda057615d --- docs/cli-reference.md | 4 +-- packages/cli/src/command-registry.ts | 9 ++++-- packages/cli/src/commands/events.ts | 28 +++++++++++++++++- packages/cli/tests/events.test.ts | 43 ++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 4a8bcf3..4109ab7 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -570,14 +570,14 @@ npx thermoworks metrics [--host HOST] [--port N] [--device SN] [--interval N] None. -### `thermoworks events [--device SERIAL] [--type TYPE] [--limit N] [--json]` +### `thermoworks events [--device SERIAL] [--type TYPE] [--limit N] [--since ISO] [--until ISO] [--json]` Show device event history **Usage** ```bash -npx thermoworks events [--device SERIAL] [--type TYPE] [--limit N] [--json] +npx thermoworks events [--device SERIAL] [--type TYPE] [--limit N] [--since ISO] [--until ISO] [--json] ``` **Options** diff --git a/packages/cli/src/command-registry.ts b/packages/cli/src/command-registry.ts index 9c6eace..788b6ee 100644 --- a/packages/cli/src/command-registry.ts +++ b/packages/cli/src/command-registry.ts @@ -532,8 +532,13 @@ export const commandDefinitions: readonly CommandDefinition[] = [ { name: "events", summary: "Show device event history", - usage: "events [--device SERIAL] [--type TYPE] [--limit N] [--json]", - usageLines: ["events Show device event history (alarms, status changes)"], + usage: + "events [--device SERIAL] [--type TYPE] [--limit N] [--since ISO] [--until ISO] [--json]", + usageLines: [ + "events Show device event history (alarms, status changes)", + " --since ISO Only show events at or after this time", + " --until ISO Only show events at or before this time", + ], supportsJson: true, handler: ({ args, options }: CommandContext) => events(parseEventsArgs(args.slice(1)), options), }, diff --git a/packages/cli/src/commands/events.ts b/packages/cli/src/commands/events.ts index 9356abb..03afe78 100644 --- a/packages/cli/src/commands/events.ts +++ b/packages/cli/src/commands/events.ts @@ -13,6 +13,24 @@ export interface EventsCommandOptions { device?: string; type?: string; limit?: number; + since?: Date; + until?: Date; +} + +/** Parse an ISO date flag value, exiting with a clear error when invalid. */ +function parseDateFlag(flag: string, value: string | undefined): Date { + if (!value) { + console.error(`${flag} requires an ISO date value`); + process.exit(1); + } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + console.error( + `Invalid ${flag} value: ${value}. Use an ISO date, for example 2026-06-07T12:00:00Z.`, + ); + process.exit(1); + } + return date; } /** Map a numeric severity to a labeled color badge. */ @@ -28,7 +46,7 @@ export function formatSeverityBadge(severity: number): string { /** * Parse events-specific flags from remaining CLI args. - * Handles: --device SERIAL, --type TYPE, --limit N + * Handles: --device SERIAL, --type TYPE, --limit N, --since ISO, --until ISO */ export function parseEventsArgs(args: string[]): EventsCommandOptions { const options: EventsCommandOptions = {}; @@ -49,6 +67,12 @@ export function parseEventsArgs(args: string[]): EventsCommandOptions { options.limit = parsed; } i++; + } else if (arg === "--since") { + options.since = parseDateFlag("--since", next); + i++; + } else if (arg === "--until") { + options.until = parseDateFlag("--until", next); + i++; } } @@ -71,6 +95,8 @@ export async function events( const eventList = await client.getEvents({ deviceId: commandOptions.device, eventType: commandOptions.type, + startTime: commandOptions.since, + endTime: commandOptions.until, limit: commandOptions.limit, }); diff --git a/packages/cli/tests/events.test.ts b/packages/cli/tests/events.test.ts index 339551e..6d386fd 100644 --- a/packages/cli/tests/events.test.ts +++ b/packages/cli/tests/events.test.ts @@ -81,6 +81,26 @@ describe("parseEventsArgs", () => { expect(result.limit).toBe(20); }); + it("parses --since and --until flags as dates", () => { + const result = parseEventsArgs([ + "--since", + "2026-06-07T12:00:00Z", + "--until", + "2026-06-07T13:00:00Z", + ]); + expect(result.since?.toISOString()).toBe("2026-06-07T12:00:00.000Z"); + expect(result.until?.toISOString()).toBe("2026-06-07T13:00:00.000Z"); + }); + + it("exits when a date filter is invalid", () => { + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + expect(() => parseEventsArgs(["--since", "not-a-date"])).toThrow("process.exit"); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid --since value")); + exitSpy.mockRestore(); + }); + it("ignores invalid --limit values", () => { const result = parseEventsArgs(["--limit", "abc"]); expect(result.limit).toBeUndefined(); @@ -229,6 +249,8 @@ describe("events", () => { expect(mockGetEvents).toHaveBeenCalledWith({ deviceId: "NODE5", eventType: undefined, + startTime: undefined, + endTime: undefined, limit: undefined, }); }); @@ -242,6 +264,8 @@ describe("events", () => { expect(mockGetEvents).toHaveBeenCalledWith({ deviceId: undefined, eventType: "alarm", + startTime: undefined, + endTime: undefined, limit: undefined, }); }); @@ -255,10 +279,29 @@ describe("events", () => { expect(mockGetEvents).toHaveBeenCalledWith({ deviceId: undefined, eventType: undefined, + startTime: undefined, + endTime: undefined, limit: 10, }); }); + it("passes date filters to SDK", async () => { + mockGetCredentials.mockResolvedValue({ email: "a@b.com", password: "pw" }); + mockGetEvents.mockResolvedValue([]); + const since = new Date("2026-06-07T12:00:00Z"); + const until = new Date("2026-06-07T13:00:00Z"); + + await events({ since, until }, { json: false }); + + expect(mockGetEvents).toHaveBeenCalledWith({ + deviceId: undefined, + eventType: undefined, + startTime: since, + endTime: until, + limit: undefined, + }); + }); + it("shows plural 'events' for multiple results", async () => { mockGetCredentials.mockResolvedValue({ email: "a@b.com", password: "pw" }); vi.spyOn(Date, "now").mockReturnValue(new Date("2026-06-07T12:05:00Z").getTime());