From 72feb64d03141e7e09dddf362b577c9714e6e557 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 28 Apr 2026 22:45:44 +0000 Subject: [PATCH 1/3] fix(arg-parsing): handle colon-separated issue identifiers (CLI-PH) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users sometimes type PROJECT:SHORTID or PROJECT:NUMERICID where the colon separates the project slug from the issue identifier. Before this fix, the colon was silently treated as part of the project slug, causing the API to fail with 'Short ID not found' errors (64 users, 108 events). Now parseIssueArg recognizes colons as project:id separators: - CHATEX:CHATEX-W9 → project=chatex, suffix=W9 - MYAH-FRONTEND:115562020 → numeric ID 115562020 - ARES-BACKEND:4P → project=ares-backend, suffix=4P --- src/lib/arg-parsing.ts | 62 +++++++++++++++++++++++++++++++++ test/lib/arg-parsing.test.ts | 67 ++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/src/lib/arg-parsing.ts b/src/lib/arg-parsing.ts index f2f626137..53b5a1c51 100644 --- a/src/lib/arg-parsing.ts +++ b/src/lib/arg-parsing.ts @@ -835,6 +835,53 @@ function parseWithSlash(arg: string): ParsedIssueArg { return parseAfterSlash(arg, org, rest); } +/** + * Parse issue arg containing a colon as a project:identifier separator. + * + * Handles formats like: + * - `PROJECT:SUFFIX` → project-search with suffix + * - `PROJECT:PROJECT-SUFFIX` → project-search, extracts suffix from last dash + * - `PROJECT:NUMERICID` → numeric lookup (project context ignored) + * + * Returns null if the colon is not a valid separator (e.g., empty parts). + */ +function parseWithColon(arg: string): ParsedIssueArg | null { + const colonIdx = arg.indexOf(":"); + const projectPart = arg.slice(0, colonIdx); + const idPart = arg.slice(colonIdx + 1); + + // Both parts must be non-empty + if (!(projectPart && idPart)) { + return null; + } + + // Numeric ID part → direct numeric lookup (project context not needed) + if (isAllDigits(idPart)) { + return { type: "numeric", id: idPart }; + } + + // ID part contains a dash → likely a full short ID like "PROJECT-SUFFIX" + // Extract just the suffix from the last dash + if (idPart.includes("-")) { + const lastDash = idPart.lastIndexOf("-"); + const suffix = idPart.slice(lastDash + 1).toUpperCase(); + if (suffix) { + return { + type: "project-search", + projectSlug: projectPart.toLowerCase(), + suffix, + }; + } + } + + // Plain suffix (no dash) → use as-is + return { + type: "project-search", + projectSlug: projectPart.toLowerCase(), + suffix: idPart.toUpperCase(), + }; +} + /** * Parse issue arg with dash but no slash (project-suffix). */ @@ -931,6 +978,7 @@ export function parseSlashSeparatedArg( return { id, targetArg }; } +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: issue ID parsing has many format branches by design export function parseIssueArg(arg: string): ParsedIssueArg { // Trim whitespace — agents may pass trailing newlines (CLI-16M) const input = arg.trim(); @@ -985,6 +1033,20 @@ export function parseIssueArg(arg: string): ParsedIssueArg { return { type: "numeric", id: input }; } + // 2b. Colon separator — treat as project:identifier notation. + // Users sometimes type "PROJECT:SHORTID" or "PROJECT:NUMERICID" where + // the colon separates the project slug from the issue identifier. + // e.g., "CHATEX:CHATEX-W9" → project=chatex, suffix=W9 + // "MYAH-FRONTEND:115562020" → numeric ID 115562020 + // "CHATEX:W9" → project=chatex, suffix=W9 + if (input.includes(":")) { + const colonResult = parseWithColon(input); + if (colonResult) { + return colonResult; + } + // Colon not parseable as project:id — fall through to normal parsing + } + // 3. Has slash → check slash FIRST (takes precedence over dashes) // This ensures "my-org/123" parses as org="my-org", not project="my" if (input.includes("/")) { diff --git a/test/lib/arg-parsing.test.ts b/test/lib/arg-parsing.test.ts index 2bfa884f0..bda9889ea 100644 --- a/test/lib/arg-parsing.test.ts +++ b/test/lib/arg-parsing.test.ts @@ -693,6 +693,73 @@ describe("parseIssueArg", () => { }); }); + // Colon-separated issue args — users type PROJECT:SHORTID or PROJECT:NUMERICID + describe("colon-separated issue args (CLI-PH)", () => { + test("PROJECT:SUFFIX returns project-search", () => { + expect(parseIssueArg("CHATEX:W9")).toEqual({ + type: "project-search", + projectSlug: "chatex", + suffix: "W9", + }); + }); + + test("PROJECT:PROJECT-SUFFIX extracts suffix from last dash", () => { + expect(parseIssueArg("CHATEX:CHATEX-W9")).toEqual({ + type: "project-search", + projectSlug: "chatex", + suffix: "W9", + }); + }); + + test("PROJECT:PROJECT-SUFFIX with multi-hyphen project", () => { + expect(parseIssueArg("CHATEX:CHATEX-12A")).toEqual({ + type: "project-search", + projectSlug: "chatex", + suffix: "12A", + }); + }); + + test("MULTI-PROJECT:NUMERICID returns numeric", () => { + expect(parseIssueArg("MYAH-FRONTEND:115562020")).toEqual({ + type: "numeric", + id: "115562020", + }); + }); + + test("PROJECT:NUMERICID returns numeric", () => { + expect(parseIssueArg("CLI:123456789")).toEqual({ + type: "numeric", + id: "123456789", + }); + }); + + test("colon with empty project falls through to normal parsing", () => { + // ":W9" has empty project part — parseWithColon returns null, + // falls through to normal parsing (no slash, no dash → suffix-only) + expect(parseIssueArg(":W9")).toEqual({ + type: "suffix-only", + suffix: ":W9", + }); + }); + + test("colon with empty suffix falls through to normal parsing", () => { + // "CLI:" has empty id part — parseWithColon returns null, + // falls through to normal parsing (no slash, no dash → suffix-only) + expect(parseIssueArg("CLI:")).toEqual({ + type: "suffix-only", + suffix: "CLI:", + }); + }); + + test("multi-hyphen project with colon-separated short ID", () => { + expect(parseIssueArg("ARES-BACKEND:4P")).toEqual({ + type: "project-search", + projectSlug: "ares-backend", + suffix: "4P", + }); + }); + }); + describe("magic @ selectors", () => { test("@latest returns selector type", () => { expect(parseIssueArg("@latest")).toEqual({ From f0d5549a82838ef31ac05e9576618374cd4b6036 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 28 Apr 2026 22:56:44 +0000 Subject: [PATCH 2/3] fix: guard parseWithColon against slash-containing inputs When input contains both a slash and colon (e.g., sentry/CLI:W9), parseWithColon now returns null so the slash parser handles it correctly. Addresses Cursor Bugbot review comment. --- src/lib/arg-parsing.ts | 5 +++-- test/lib/arg-parsing.test.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/lib/arg-parsing.ts b/src/lib/arg-parsing.ts index 53b5a1c51..4f6e4bf06 100644 --- a/src/lib/arg-parsing.ts +++ b/src/lib/arg-parsing.ts @@ -850,8 +850,9 @@ function parseWithColon(arg: string): ParsedIssueArg | null { const projectPart = arg.slice(0, colonIdx); const idPart = arg.slice(colonIdx + 1); - // Both parts must be non-empty - if (!(projectPart && idPart)) { + // Both parts must be non-empty, and project part must not contain a slash + // (slashes indicate org/project notation which should be handled by parseWithSlash) + if (!(projectPart && idPart) || projectPart.includes("/")) { return null; } diff --git a/test/lib/arg-parsing.test.ts b/test/lib/arg-parsing.test.ts index bda9889ea..44512375a 100644 --- a/test/lib/arg-parsing.test.ts +++ b/test/lib/arg-parsing.test.ts @@ -758,6 +758,17 @@ describe("parseIssueArg", () => { suffix: "4P", }); }); + + test("org/project:suffix falls through to slash parsing", () => { + // When input has both slash and colon, slash parsing takes precedence + // because parseWithColon returns null for slash-containing project parts. + // "CLI:W9" has no dash, so parseAfterSlash returns explicit-org-suffix. + expect(parseIssueArg("sentry/CLI:W9")).toEqual({ + type: "explicit-org-suffix", + org: "sentry", + suffix: "CLI:W9", + }); + }); }); describe("magic @ selectors", () => { From 6ebebe91e45d75eae08e09b39a6311fd730b9b22 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 28 Apr 2026 23:05:45 +0000 Subject: [PATCH 3/3] fix: also reject slashes in idPart of parseWithColon Prevents suffix corruption when input like CLI:foo/bar reaches parseWithColon. Now returns null so the slash parser handles it. Addresses Cursor Bugbot low-severity finding. --- src/lib/arg-parsing.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/arg-parsing.ts b/src/lib/arg-parsing.ts index 4f6e4bf06..df8d076f7 100644 --- a/src/lib/arg-parsing.ts +++ b/src/lib/arg-parsing.ts @@ -850,9 +850,13 @@ function parseWithColon(arg: string): ParsedIssueArg | null { const projectPart = arg.slice(0, colonIdx); const idPart = arg.slice(colonIdx + 1); - // Both parts must be non-empty, and project part must not contain a slash - // (slashes indicate org/project notation which should be handled by parseWithSlash) - if (!(projectPart && idPart) || projectPart.includes("/")) { + // Both parts must be non-empty. Neither part should contain a slash — + // slashes indicate org/project notation which should be handled by parseWithSlash. + if ( + !(projectPart && idPart) || + projectPart.includes("/") || + idPart.includes("/") + ) { return null; }