From 69a9cd65dc4593d4647fb910829e54ecd7fb6115 Mon Sep 17 00:00:00 2001 From: Jeffrey Holm Date: Wed, 25 Mar 2026 11:36:08 -0400 Subject: [PATCH 1/6] feat: add --created-after and --updated-after filters to issue list Add date filter options to `issue list` that leverage the Linear API's DateComparator on createdAt and updatedAt fields. Invalid date strings are rejected early with a ValidationError and a helpful suggestion. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/issue/issue-list.ts | 12 +++++ src/utils/linear.ts | 24 +++++++++ .../__snapshots__/issue-list.test.ts.snap | 2 + test/commands/issue/issue-list.test.ts | 49 +++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/src/commands/issue/issue-list.ts b/src/commands/issue/issue-list.ts index 841bfcc5..d7fe329c 100644 --- a/src/commands/issue/issue-list.ts +++ b/src/commands/issue/issue-list.ts @@ -97,6 +97,14 @@ export const listCommand = new Command() default: 50, }, ) + .option( + "--created-after ", + "Filter issues created after this date (ISO 8601 or YYYY-MM-DD)", + ) + .option( + "--updated-after ", + "Filter issues updated after this date (ISO 8601 or YYYY-MM-DD)", + ) .option("-w, --web", "Open in web browser") .option("-a, --app", "Open in Linear.app") .option("--no-pager", "Disable automatic paging for long output") @@ -117,6 +125,8 @@ export const listCommand = new Command() milestone, limit, pager, + createdAfter, + updatedAfter, }, ) => { const usePager = pager !== false @@ -221,6 +231,8 @@ export const listCommand = new Command() sort, cycleId, milestoneId, + createdAfter, + updatedAfter, ) spinner?.stop() const issues = result.issues?.nodes || [] diff --git a/src/utils/linear.ts b/src/utils/linear.ts index cd4d2c0c..b29c2dc6 100644 --- a/src/utils/linear.ts +++ b/src/utils/linear.ts @@ -422,6 +422,8 @@ export async function fetchIssuesForState( sortParam?: "manual" | "priority", cycleId?: string, milestoneId?: string, + createdAfter?: string, + updatedAfter?: string, ) { const sort = sortParam ?? getOption("issue_sort") as "manual" | "priority" | undefined @@ -469,6 +471,28 @@ export async function fetchIssuesForState( filter.projectMilestone = { id: { eq: milestoneId } } } + if (createdAfter) { + const parsed = new Date(createdAfter) + if (isNaN(parsed.getTime())) { + throw new ValidationError( + `Invalid date for --created-after: "${createdAfter}"`, + { suggestion: "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string." }, + ) + } + filter.createdAt = { gte: parsed.toISOString() } + } + + if (updatedAfter) { + const parsed = new Date(updatedAfter) + if (isNaN(parsed.getTime())) { + throw new ValidationError( + `Invalid date for --updated-after: "${updatedAfter}"`, + { suggestion: "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string." }, + ) + } + filter.updatedAt = { gte: parsed.toISOString() } + } + const query = gql(/* GraphQL */ ` query GetIssuesForState($sort: [IssueSortInput!], $filter: IssueFilter!, $first: Int, $after: String) { issues(filter: $filter, sort: $sort, first: $first, after: $after) { diff --git a/test/commands/issue/__snapshots__/issue-list.test.ts.snap b/test/commands/issue/__snapshots__/issue-list.test.ts.snap index 68b208a7..de24447b 100644 --- a/test/commands/issue/__snapshots__/issue-list.test.ts.snap +++ b/test/commands/issue/__snapshots__/issue-list.test.ts.snap @@ -24,6 +24,8 @@ Options: --cycle - Filter by cycle name, number, or 'active' --milestone - Filter by project milestone name (requires --project) --limit - Maximum number of issues to fetch (default: 50, use 0 for unlimited) (Default: \\x1b[33m50\\x1b[39m) + --created-after - Filter issues created after this date (ISO 8601 or YYYY-MM-DD) + --updated-after - Filter issues updated after this date (ISO 8601 or YYYY-MM-DD) -w, --web - Open in web browser -a, --app - Open in Linear.app --no-pager - Disable automatic paging for long output diff --git a/test/commands/issue/issue-list.test.ts b/test/commands/issue/issue-list.test.ts index 277bae42..1f8cf9f0 100644 --- a/test/commands/issue/issue-list.test.ts +++ b/test/commands/issue/issue-list.test.ts @@ -1,5 +1,8 @@ import { snapshotTest } from "@cliffy/testing" +import { assertRejects } from "@std/assert" import { listCommand } from "../../../src/commands/issue/issue-list.ts" +import { fetchIssuesForState } from "../../../src/utils/linear.ts" +import { ValidationError } from "../../../src/utils/errors.ts" import { commonDenoArgs } from "../../utils/test-helpers.ts" // Test help output @@ -13,3 +16,49 @@ await snapshotTest({ await listCommand.parse() }, }) + +// Test invalid --created-after date +Deno.test("fetchIssuesForState - rejects invalid createdAfter date", async () => { + await assertRejects( + () => + fetchIssuesForState( + "TEST", + ["unstarted"], + undefined, + false, + false, + 50, + undefined, + "manual", + undefined, + undefined, + "not-a-date", + undefined, + ), + ValidationError, + 'Invalid date for --created-after: "not-a-date"', + ) +}) + +// Test invalid --updated-after date +Deno.test("fetchIssuesForState - rejects invalid updatedAfter date", async () => { + await assertRejects( + () => + fetchIssuesForState( + "TEST", + ["unstarted"], + undefined, + false, + false, + 50, + undefined, + "manual", + undefined, + undefined, + undefined, + "xyz-bad-date", + ), + ValidationError, + 'Invalid date for --updated-after: "xyz-bad-date"', + ) +}) From 0fd4cc0134d2dc54f811ad9ee0fa20f655921762 Mon Sep 17 00:00:00 2001 From: Jeffrey Holm Date: Wed, 25 Mar 2026 18:57:34 -0400 Subject: [PATCH 2/6] fix: apply deno fmt and document --created-after/--updated-after in docs Run deno fmt to fix formatting in src/utils/linear.ts (CI failure). Add --created-after and --updated-after examples to README.md and docs/usage.md to match the new flags added in this PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 ++ docs/usage.md | 10 ++++++++++ src/utils/linear.ts | 10 ++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbda35c1..05e5de39 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ linear issue url # prints the Linear.app URL for the issue linear issue pr # creates a GitHub PR with issue details via `gh pr create` linear issue list # list your issues in a table view (supports -s/--state and --sort) linear issue list --project "My Project" --milestone "Phase 1" # filter by milestone +linear issue list --created-after 2024-01-01 # issues created after a date +linear issue list --updated-after 2024-01-01 # issues updated after a date linear issue list -w # open issue list in web browser linear issue list -a # open issue list in Linear.app linear issue start # create/switch to issue branch and mark as started diff --git a/docs/usage.md b/docs/usage.md index d98fefc6..1fe1f734 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -53,6 +53,16 @@ linear issue list --unassigned linear issue list --all-assignees ``` +filter by date: + +```bash +# List issues created after a specific date +linear issue list --created-after 2024-01-01 + +# List issues updated after a specific date +linear issue list --updated-after 2024-01-01 +``` + other options: ```bash diff --git a/src/utils/linear.ts b/src/utils/linear.ts index b29c2dc6..2d50a65d 100644 --- a/src/utils/linear.ts +++ b/src/utils/linear.ts @@ -476,7 +476,10 @@ export async function fetchIssuesForState( if (isNaN(parsed.getTime())) { throw new ValidationError( `Invalid date for --created-after: "${createdAfter}"`, - { suggestion: "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string." }, + { + suggestion: + "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string.", + }, ) } filter.createdAt = { gte: parsed.toISOString() } @@ -487,7 +490,10 @@ export async function fetchIssuesForState( if (isNaN(parsed.getTime())) { throw new ValidationError( `Invalid date for --updated-after: "${updatedAfter}"`, - { suggestion: "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string." }, + { + suggestion: + "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string.", + }, ) } filter.updatedAt = { gte: parsed.toISOString() } From f45cbf0e9e7f269f4b11e4e3d6a8faa9be84e290 Mon Sep 17 00:00:00 2001 From: Jeffrey Holm Date: Wed, 25 Mar 2026 19:20:21 -0400 Subject: [PATCH 3/6] fix: add strict ISO 8601 validation for date filter flags Extract parseDateFilter helper to deduplicate date validation logic for --created-after and --updated-after. Add regex validation to reject permissive date strings like "1" or "March 2024" that new Date() would accept. Only YYYY-MM-DD and full ISO 8601 formats are now allowed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/linear.ts | 52 +++++++++++++----------- test/commands/issue/issue-list.test.ts | 55 ++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/utils/linear.ts b/src/utils/linear.ts index 2d50a65d..86e0070c 100644 --- a/src/utils/linear.ts +++ b/src/utils/linear.ts @@ -13,6 +13,34 @@ import { getGraphQLClient } from "./graphql.ts" import { getCurrentIssueFromVcs } from "./vcs.ts" import { NotFoundError, ValidationError } from "./errors.ts" +/** + * Validate and parse a date string in ISO 8601 format (YYYY-MM-DD or full ISO 8601). + * Rejects permissive date strings that `new Date()` would accept (e.g. "1", "March 2024"). + */ +export function parseDateFilter(value: string, flagName: string): string { + const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.]+Z?([+-]\d{2}:?\d{2})?)?$/ + if (!ISO_DATE_RE.test(value)) { + throw new ValidationError( + `Invalid date format for ${flagName}: "${value}"`, + { + suggestion: + "Use YYYY-MM-DD or ISO 8601 format (e.g. 2024-01-15 or 2024-01-15T09:00:00Z).", + }, + ) + } + const parsed = new Date(value) + if (isNaN(parsed.getTime())) { + throw new ValidationError( + `Invalid date for ${flagName}: "${value}"`, + { + suggestion: + "Use YYYY-MM-DD or ISO 8601 format (e.g. 2024-01-15 or 2024-01-15T09:00:00Z).", + }, + ) + } + return parsed.toISOString() +} + function isValidLinearIdentifier(id: string): boolean { return /^[a-zA-Z0-9]+-[1-9][0-9]*$/i.test(id) } @@ -472,31 +500,11 @@ export async function fetchIssuesForState( } if (createdAfter) { - const parsed = new Date(createdAfter) - if (isNaN(parsed.getTime())) { - throw new ValidationError( - `Invalid date for --created-after: "${createdAfter}"`, - { - suggestion: - "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string.", - }, - ) - } - filter.createdAt = { gte: parsed.toISOString() } + filter.createdAt = { gte: parseDateFilter(createdAfter, "--created-after") } } if (updatedAfter) { - const parsed = new Date(updatedAfter) - if (isNaN(parsed.getTime())) { - throw new ValidationError( - `Invalid date for --updated-after: "${updatedAfter}"`, - { - suggestion: - "Use ISO 8601 format (e.g. 2024-01-15) or a valid date string.", - }, - ) - } - filter.updatedAt = { gte: parsed.toISOString() } + filter.updatedAt = { gte: parseDateFilter(updatedAfter, "--updated-after") } } const query = gql(/* GraphQL */ ` diff --git a/test/commands/issue/issue-list.test.ts b/test/commands/issue/issue-list.test.ts index 1f8cf9f0..aeee7df2 100644 --- a/test/commands/issue/issue-list.test.ts +++ b/test/commands/issue/issue-list.test.ts @@ -1,7 +1,10 @@ import { snapshotTest } from "@cliffy/testing" -import { assertRejects } from "@std/assert" +import { assertEquals, assertRejects, assertThrows } from "@std/assert" import { listCommand } from "../../../src/commands/issue/issue-list.ts" -import { fetchIssuesForState } from "../../../src/utils/linear.ts" +import { + fetchIssuesForState, + parseDateFilter, +} from "../../../src/utils/linear.ts" import { ValidationError } from "../../../src/utils/errors.ts" import { commonDenoArgs } from "../../utils/test-helpers.ts" @@ -36,7 +39,7 @@ Deno.test("fetchIssuesForState - rejects invalid createdAfter date", async () => undefined, ), ValidationError, - 'Invalid date for --created-after: "not-a-date"', + 'Invalid date format for --created-after: "not-a-date"', ) }) @@ -59,6 +62,50 @@ Deno.test("fetchIssuesForState - rejects invalid updatedAfter date", async () => "xyz-bad-date", ), ValidationError, - 'Invalid date for --updated-after: "xyz-bad-date"', + 'Invalid date format for --updated-after: "xyz-bad-date"', + ) +}) + +// parseDateFilter unit tests + +Deno.test("parseDateFilter - accepts YYYY-MM-DD format", () => { + const result = parseDateFilter("2024-01-15", "--created-after") + assertEquals(result, new Date("2024-01-15").toISOString()) +}) + +Deno.test("parseDateFilter - accepts full ISO 8601 with time and Z", () => { + const result = parseDateFilter("2024-01-15T09:00:00Z", "--created-after") + assertEquals(result, "2024-01-15T09:00:00.000Z") +}) + +Deno.test("parseDateFilter - accepts ISO 8601 with timezone offset", () => { + const result = parseDateFilter( + "2024-01-15T09:00:00+05:30", + "--created-after", + ) + assertEquals(result, new Date("2024-01-15T09:00:00+05:30").toISOString()) +}) + +Deno.test('parseDateFilter - rejects permissive date string "1"', () => { + assertThrows( + () => parseDateFilter("1", "--created-after"), + ValidationError, + 'Invalid date format for --created-after: "1"', + ) +}) + +Deno.test('parseDateFilter - rejects permissive date string "March 2024"', () => { + assertThrows( + () => parseDateFilter("March 2024", "--updated-after"), + ValidationError, + 'Invalid date format for --updated-after: "March 2024"', + ) +}) + +Deno.test('parseDateFilter - rejects permissive date string "Jan 1"', () => { + assertThrows( + () => parseDateFilter("Jan 1", "--created-after"), + ValidationError, + 'Invalid date format for --created-after: "Jan 1"', ) }) From 207edbb6fb25913012c35d7cdc37e3a089cf200f Mon Sep 17 00:00:00 2001 From: Jeffrey Holm Date: Wed, 25 Mar 2026 19:51:39 -0400 Subject: [PATCH 4/6] style: format SVGs with deno 2.7.8 --- docs/cast-issue-create.svg | 6052 ++++++++++++++++++++++++++---------- docs/cast-issue-start.svg | 1467 ++++++--- 2 files changed, 5500 insertions(+), 2019 deletions(-) diff --git a/docs/cast-issue-create.svg b/docs/cast-issue-create.svg index 9ff98288..1ccc142e 100644 --- a/docs/cast-issue-create.svg +++ b/docs/cast-issue-create.svg @@ -4,15 +4,15 @@ width="1300" height="607.88" > - + - + + diff --git a/docs/cast-issue-start.svg b/docs/cast-issue-start.svg index 0c9d81a5..edc08372 100644 --- a/docs/cast-issue-start.svg +++ b/docs/cast-issue-start.svg @@ -4,15 +4,15 @@ width="1300" height="607.88" > - + - + + From bc14858e4d12dd4c110c6d5780e981eebbc2293f Mon Sep 17 00:00:00 2001 From: Jeffrey Holm Date: Wed, 25 Mar 2026 19:56:57 -0400 Subject: [PATCH 5/6] chore: regenerate skill docs --- skills/linear-cli/references/issue.md | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/linear-cli/references/issue.md b/skills/linear-cli/references/issue.md index 16f8bc03..26cffbf2 100644 --- a/skills/linear-cli/references/issue.md +++ b/skills/linear-cli/references/issue.md @@ -84,6 +84,7 @@ Options: --limit - Maximum number of issues to fetch (default: 50, use 0 for unlimited) (Default: 50) -w, --web - Open in web browser -a, --app - Open in Linear.app + -j, --json - Output issues as JSON --no-pager - Disable automatic paging for long output ``` From 256183777fac72ec60a011303f1709374cee2699 Mon Sep 17 00:00:00 2001 From: Jeffrey Holm Date: Wed, 25 Mar 2026 20:06:34 -0400 Subject: [PATCH 6/6] chore: regenerate skill docs from branch CLI --- skills/linear-cli/references/issue.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skills/linear-cli/references/issue.md b/skills/linear-cli/references/issue.md index 26cffbf2..113a9727 100644 --- a/skills/linear-cli/references/issue.md +++ b/skills/linear-cli/references/issue.md @@ -82,9 +82,10 @@ Options: --cycle - Filter by cycle name, number, or 'active' --milestone - Filter by project milestone name (requires --project) --limit - Maximum number of issues to fetch (default: 50, use 0 for unlimited) (Default: 50) + --created-after - Filter issues created after this date (ISO 8601 or YYYY-MM-DD) + --updated-after - Filter issues updated after this date (ISO 8601 or YYYY-MM-DD) -w, --web - Open in web browser -a, --app - Open in Linear.app - -j, --json - Output issues as JSON --no-pager - Disable automatic paging for long output ```