From 6e2061d614f2dd493011536670c87a5e0522adc5 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Wed, 18 Feb 2026 11:23:22 +0000 Subject: [PATCH 1/2] fix(commands): support org/project/id as single positional arg (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse slash-separated single arguments (e.g., `sentry event view sentry/cli/abc123`) as org/project/id instead of treating the entire string as the ID. Event IDs, trace IDs, and log IDs are hex strings that never contain slashes, so any single arg with slashes is unambiguously a structured format. - 0 slashes: plain ID (existing behavior) - 1 slash: org/project missing ID → ContextError - 2+ slashes: split on last / → target + ID --- src/commands/event/view.ts | 27 ++++++++++++-- src/commands/log/view.ts | 27 ++++++++++++-- src/commands/trace/view.ts | 27 ++++++++++++-- test/commands/event/view.test.ts | 40 +++++++++++++++++++++ test/commands/log/view.property.test.ts | 43 ++++++++++++++++++++--- test/commands/log/view.test.ts | 36 +++++++++++++++++++ test/commands/trace/view.property.test.ts | 40 +++++++++++++++++++-- test/commands/trace/view.test.ts | 36 +++++++++++++++++++ 8 files changed, 263 insertions(+), 13 deletions(-) diff --git a/src/commands/event/view.ts b/src/commands/event/view.ts index faa72a4349..7dae500d5e 100644 --- a/src/commands/event/view.ts +++ b/src/commands/event/view.ts @@ -113,8 +113,31 @@ export function parsePositionalArgs(args: string[]): { } if (args.length === 1) { - // Single arg - must be event ID - return { eventId: first, targetArg: undefined }; + const slashIdx = first.indexOf("/"); + + if (slashIdx === -1) { + // No slashes — plain event ID + return { eventId: first, targetArg: undefined }; + } + + // Event IDs are hex and never contain "/" — this must be a structured + // "org/project/eventId" or "org/project" (missing event ID) + const lastSlashIdx = first.lastIndexOf("/"); + + if (slashIdx === lastSlashIdx) { + // Exactly one slash: "org/project" without event ID + throw new ContextError("Event ID", USAGE_HINT); + } + + // Two+ slashes: split on last "/" → target + eventId + const targetArg = first.slice(0, lastSlashIdx); + const eventId = first.slice(lastSlashIdx + 1); + + if (!eventId) { + throw new ContextError("Event ID", USAGE_HINT); + } + + return { eventId, targetArg }; } const second = args[1]; diff --git a/src/commands/log/view.ts b/src/commands/log/view.ts index 36430ccda9..1981f2562d 100644 --- a/src/commands/log/view.ts +++ b/src/commands/log/view.ts @@ -48,8 +48,31 @@ export function parsePositionalArgs(args: string[]): { } if (args.length === 1) { - // Single arg - must be log ID - return { logId: first, targetArg: undefined }; + const slashIdx = first.indexOf("/"); + + if (slashIdx === -1) { + // No slashes — plain log ID + return { logId: first, targetArg: undefined }; + } + + // Log IDs are hex and never contain "/" — this must be a structured + // "org/project/logId" or "org/project" (missing log ID) + const lastSlashIdx = first.lastIndexOf("/"); + + if (slashIdx === lastSlashIdx) { + // Exactly one slash: "org/project" without log ID + throw new ContextError("Log ID", USAGE_HINT); + } + + // Two+ slashes: split on last "/" → target + logId + const targetArg = first.slice(0, lastSlashIdx); + const logId = first.slice(lastSlashIdx + 1); + + if (!logId) { + throw new ContextError("Log ID", USAGE_HINT); + } + + return { logId, targetArg }; } const second = args[1]; diff --git a/src/commands/trace/view.ts b/src/commands/trace/view.ts index f9ca92b22c..3e6a20d74a 100644 --- a/src/commands/trace/view.ts +++ b/src/commands/trace/view.ts @@ -55,8 +55,31 @@ export function parsePositionalArgs(args: string[]): { } if (args.length === 1) { - // Single arg - must be trace ID - return { traceId: first, targetArg: undefined }; + const slashIdx = first.indexOf("/"); + + if (slashIdx === -1) { + // No slashes — plain trace ID + return { traceId: first, targetArg: undefined }; + } + + // Trace IDs are hex and never contain "/" — this must be a structured + // "org/project/traceId" or "org/project" (missing trace ID) + const lastSlashIdx = first.lastIndexOf("/"); + + if (slashIdx === lastSlashIdx) { + // Exactly one slash: "org/project" without trace ID + throw new ContextError("Trace ID", USAGE_HINT); + } + + // Two+ slashes: split on last "/" → target + traceId + const targetArg = first.slice(0, lastSlashIdx); + const traceId = first.slice(lastSlashIdx + 1); + + if (!traceId) { + throw new ContextError("Trace ID", USAGE_HINT); + } + + return { traceId, targetArg }; } const second = args[1]; diff --git a/test/commands/event/view.test.ts b/test/commands/event/view.test.ts index 19eb9562fe..aff99fee68 100644 --- a/test/commands/event/view.test.ts +++ b/test/commands/event/view.test.ts @@ -72,6 +72,46 @@ describe("parsePositionalArgs", () => { }); }); + describe("slash-separated org/project/eventId (single arg)", () => { + test("parses org/project/eventId as target + event ID", () => { + const result = parsePositionalArgs(["sentry/cli/abc123def"]); + expect(result.targetArg).toBe("sentry/cli"); + expect(result.eventId).toBe("abc123def"); + }); + + test("parses with long hex event ID", () => { + const result = parsePositionalArgs([ + "my-org/frontend/a1b2c3d4e5f67890abcdef1234567890", + ]); + expect(result.targetArg).toBe("my-org/frontend"); + expect(result.eventId).toBe("a1b2c3d4e5f67890abcdef1234567890"); + }); + + test("handles hyphenated org and project slugs", () => { + const result = parsePositionalArgs(["my-org/my-project/deadbeef"]); + expect(result.targetArg).toBe("my-org/my-project"); + expect(result.eventId).toBe("deadbeef"); + }); + + test("one slash (org/project, missing event ID) throws ContextError", () => { + expect(() => parsePositionalArgs(["sentry/cli"])).toThrow(ContextError); + }); + + test("trailing slash (org/project/) throws ContextError", () => { + expect(() => parsePositionalArgs(["sentry/cli/"])).toThrow(ContextError); + }); + + test("one-slash ContextError mentions Event ID", () => { + try { + parsePositionalArgs(["sentry/cli"]); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ContextError); + expect((error as ContextError).message).toContain("Event ID"); + } + }); + }); + describe("edge cases", () => { test("handles more than two args (ignores extras)", () => { const result = parsePositionalArgs([ diff --git a/test/commands/log/view.property.test.ts b/test/commands/log/view.property.test.ts index c0582d424e..50bfdde1bf 100644 --- a/test/commands/log/view.property.test.ts +++ b/test/commands/log/view.property.test.ts @@ -9,6 +9,7 @@ import { describe, expect, test } from "bun:test"; import { array, assert as fcAssert, + pre, property, string, stringMatching, @@ -27,10 +28,13 @@ const slugArb = stringMatching(/^[a-z][a-z0-9-]{1,20}[a-z0-9]$/); /** Non-empty strings for general args */ const nonEmptyStringArb = string({ minLength: 1, maxLength: 50 }); +/** Non-empty strings without slashes (valid plain IDs) */ +const plainIdArb = nonEmptyStringArb.filter((s) => !s.includes("/")); + describe("parsePositionalArgs properties", () => { - test("single arg: always returns it as logId with undefined targetArg", async () => { + test("single arg without slashes: returns it as logId with undefined targetArg", async () => { await fcAssert( - property(nonEmptyStringArb, (input) => { + property(plainIdArb, (input) => { const result = parsePositionalArgs([input]); expect(result.logId).toBe(input); expect(result.targetArg).toBeUndefined(); @@ -39,6 +43,29 @@ describe("parsePositionalArgs properties", () => { ); }); + test("single arg org/project/logId: splits into target and logId", async () => { + await fcAssert( + property(tuple(slugArb, slugArb, logIdArb), ([org, project, logId]) => { + const combined = `${org}/${project}/${logId}`; + const result = parsePositionalArgs([combined]); + expect(result.targetArg).toBe(`${org}/${project}`); + expect(result.logId).toBe(logId); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("single arg with one slash: throws ContextError (missing log ID)", async () => { + await fcAssert( + property(tuple(slugArb, slugArb), ([org, project]) => { + expect(() => parsePositionalArgs([`${org}/${project}`])).toThrow( + ContextError + ); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + test("two args: first is always targetArg, second is always logId", async () => { await fcAssert( property( @@ -91,6 +118,9 @@ describe("parsePositionalArgs properties", () => { property( array(nonEmptyStringArb, { minLength: 1, maxLength: 3 }), (args) => { + // Skip single-arg with slashes — those throw ContextError (tested separately) + pre(args.length > 1 || !args[0]?.includes("/")); + const result1 = parsePositionalArgs(args); const result2 = parsePositionalArgs(args); expect(result1).toEqual(result2); @@ -109,6 +139,9 @@ describe("parsePositionalArgs properties", () => { property( array(nonEmptyStringArb, { minLength: 1, maxLength: 3 }), (args) => { + // Skip single-arg with slashes — those throw ContextError (tested separately) + pre(args.length > 1 || !args[0]?.includes("/")); + const result = parsePositionalArgs(args); expect(result.logId).toBeDefined(); expect(typeof result.logId).toBe("string"); @@ -118,10 +151,10 @@ describe("parsePositionalArgs properties", () => { ); }); - test("result targetArg is undefined for single arg, defined for multiple", async () => { - // Single arg case + test("result targetArg is undefined for single slash-free arg, defined for multiple", async () => { + // Single arg case (without slashes) await fcAssert( - property(nonEmptyStringArb, (input) => { + property(plainIdArb, (input) => { const result = parsePositionalArgs([input]); expect(result.targetArg).toBeUndefined(); }), diff --git a/test/commands/log/view.test.ts b/test/commands/log/view.test.ts index db3b8c2f7b..5cdf098947 100644 --- a/test/commands/log/view.test.ts +++ b/test/commands/log/view.test.ts @@ -70,6 +70,42 @@ describe("parsePositionalArgs", () => { }); }); + describe("slash-separated org/project/logId (single arg)", () => { + test("parses org/project/logId as target + log ID", () => { + const result = parsePositionalArgs([ + "sentry/cli/968c763c740cfda8b6728f27fb9e9b01", + ]); + expect(result.targetArg).toBe("sentry/cli"); + expect(result.logId).toBe("968c763c740cfda8b6728f27fb9e9b01"); + }); + + test("handles hyphenated org and project slugs", () => { + const result = parsePositionalArgs([ + "my-org/my-project/deadbeef12345678", + ]); + expect(result.targetArg).toBe("my-org/my-project"); + expect(result.logId).toBe("deadbeef12345678"); + }); + + test("one slash (org/project, missing log ID) throws ContextError", () => { + expect(() => parsePositionalArgs(["sentry/cli"])).toThrow(ContextError); + }); + + test("trailing slash (org/project/) throws ContextError", () => { + expect(() => parsePositionalArgs(["sentry/cli/"])).toThrow(ContextError); + }); + + test("one-slash ContextError mentions Log ID", () => { + try { + parsePositionalArgs(["sentry/cli"]); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ContextError); + expect((error as ContextError).message).toContain("Log ID"); + } + }); + }); + describe("edge cases", () => { test("handles more than two args (ignores extras)", () => { const result = parsePositionalArgs([ diff --git a/test/commands/trace/view.property.test.ts b/test/commands/trace/view.property.test.ts index 0ceaca5e36..479960795b 100644 --- a/test/commands/trace/view.property.test.ts +++ b/test/commands/trace/view.property.test.ts @@ -9,6 +9,7 @@ import { describe, expect, test } from "bun:test"; import { array, assert as fcAssert, + pre, property, string, stringMatching, @@ -27,10 +28,13 @@ const slugArb = stringMatching(/^[a-z][a-z0-9-]{1,20}[a-z0-9]$/); /** Non-empty strings for general args */ const nonEmptyStringArb = string({ minLength: 1, maxLength: 50 }); +/** Non-empty strings without slashes (valid plain IDs) */ +const plainIdArb = nonEmptyStringArb.filter((s) => !s.includes("/")); + describe("parsePositionalArgs properties", () => { - test("single arg: always returns it as traceId with undefined targetArg", async () => { + test("single arg without slashes: returns it as traceId with undefined targetArg", async () => { await fcAssert( - property(nonEmptyStringArb, (input) => { + property(plainIdArb, (input) => { const result = parsePositionalArgs([input]); expect(result.traceId).toBe(input); expect(result.targetArg).toBeUndefined(); @@ -39,6 +43,32 @@ describe("parsePositionalArgs properties", () => { ); }); + test("single arg org/project/traceId: splits into target and traceId", async () => { + await fcAssert( + property( + tuple(slugArb, slugArb, traceIdArb), + ([org, project, traceId]) => { + const combined = `${org}/${project}/${traceId}`; + const result = parsePositionalArgs([combined]); + expect(result.targetArg).toBe(`${org}/${project}`); + expect(result.traceId).toBe(traceId); + } + ), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("single arg with one slash: throws ContextError (missing trace ID)", async () => { + await fcAssert( + property(tuple(slugArb, slugArb), ([org, project]) => { + expect(() => parsePositionalArgs([`${org}/${project}`])).toThrow( + ContextError + ); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + test("two args: first is always targetArg, second is always traceId", async () => { await fcAssert( property( @@ -94,6 +124,9 @@ describe("parsePositionalArgs properties", () => { property( array(nonEmptyStringArb, { minLength: 1, maxLength: 3 }), (args) => { + // Skip single-arg with slashes — those throw ContextError (tested separately) + pre(args.length > 1 || !args[0]?.includes("/")); + const result1 = parsePositionalArgs(args); const result2 = parsePositionalArgs(args); expect(result1).toEqual(result2); @@ -112,6 +145,9 @@ describe("parsePositionalArgs properties", () => { property( array(nonEmptyStringArb, { minLength: 1, maxLength: 3 }), (args) => { + // Skip single-arg with slashes — those throw ContextError (tested separately) + pre(args.length > 1 || !args[0]?.includes("/")); + const result = parsePositionalArgs(args); expect(result.traceId).toBeDefined(); expect(typeof result.traceId).toBe("string"); diff --git a/test/commands/trace/view.test.ts b/test/commands/trace/view.test.ts index 071b701c5a..f3d3df7327 100644 --- a/test/commands/trace/view.test.ts +++ b/test/commands/trace/view.test.ts @@ -58,6 +58,42 @@ describe("parsePositionalArgs", () => { }); }); + describe("slash-separated org/project/traceId (single arg)", () => { + test("parses org/project/traceId as target + trace ID", () => { + const result = parsePositionalArgs([ + "sentry/cli/aaaa1111bbbb2222cccc3333dddd4444", + ]); + expect(result.targetArg).toBe("sentry/cli"); + expect(result.traceId).toBe("aaaa1111bbbb2222cccc3333dddd4444"); + }); + + test("handles hyphenated org and project slugs", () => { + const result = parsePositionalArgs([ + "my-org/my-project/deadbeef12345678", + ]); + expect(result.targetArg).toBe("my-org/my-project"); + expect(result.traceId).toBe("deadbeef12345678"); + }); + + test("one slash (org/project, missing trace ID) throws ContextError", () => { + expect(() => parsePositionalArgs(["sentry/cli"])).toThrow(ContextError); + }); + + test("trailing slash (org/project/) throws ContextError", () => { + expect(() => parsePositionalArgs(["sentry/cli/"])).toThrow(ContextError); + }); + + test("one-slash ContextError mentions Trace ID", () => { + try { + parsePositionalArgs(["sentry/cli"]); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ContextError); + expect((error as ContextError).message).toContain("Trace ID"); + } + }); + }); + describe("edge cases", () => { test("handles more than two args (ignores extras)", () => { const result = parsePositionalArgs([ From f948e3f7409deefe2550b2eac68aaa1be896d7b6 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Wed, 18 Feb 2026 11:41:08 +0000 Subject: [PATCH 2/2] refactor(arg-parsing): extract shared parseSlashSeparatedArg helper The slash-splitting logic was duplicated across event/view.ts, trace/view.ts, and log/view.ts. Centralise it in src/lib/arg-parsing.ts so future bug fixes only need to be made in one place. --- src/commands/event/view.ts | 30 +++++----------------- src/commands/log/view.ts | 34 +++++++------------------ src/commands/trace/view.ts | 35 ++++++++------------------ src/lib/arg-parsing.ts | 51 +++++++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/commands/event/view.ts b/src/commands/event/view.ts index 7dae500d5e..a7244cf02c 100644 --- a/src/commands/event/view.ts +++ b/src/commands/event/view.ts @@ -9,6 +9,7 @@ import { getEvent } from "../../lib/api-client.js"; import { ProjectSpecificationType, parseOrgProjectArg, + parseSlashSeparatedArg, spansFlag, } from "../../lib/arg-parsing.js"; import { openInBrowser } from "../../lib/browser.js"; @@ -113,30 +114,11 @@ export function parsePositionalArgs(args: string[]): { } if (args.length === 1) { - const slashIdx = first.indexOf("/"); - - if (slashIdx === -1) { - // No slashes — plain event ID - return { eventId: first, targetArg: undefined }; - } - - // Event IDs are hex and never contain "/" — this must be a structured - // "org/project/eventId" or "org/project" (missing event ID) - const lastSlashIdx = first.lastIndexOf("/"); - - if (slashIdx === lastSlashIdx) { - // Exactly one slash: "org/project" without event ID - throw new ContextError("Event ID", USAGE_HINT); - } - - // Two+ slashes: split on last "/" → target + eventId - const targetArg = first.slice(0, lastSlashIdx); - const eventId = first.slice(lastSlashIdx + 1); - - if (!eventId) { - throw new ContextError("Event ID", USAGE_HINT); - } - + const { id: eventId, targetArg } = parseSlashSeparatedArg( + first, + "Event ID", + USAGE_HINT + ); return { eventId, targetArg }; } diff --git a/src/commands/log/view.ts b/src/commands/log/view.ts index 1981f2562d..71bafe360c 100644 --- a/src/commands/log/view.ts +++ b/src/commands/log/view.ts @@ -7,7 +7,10 @@ import { buildCommand } from "@stricli/core"; import type { SentryContext } from "../../context.js"; import { getLog } from "../../lib/api-client.js"; -import { parseOrgProjectArg } from "../../lib/arg-parsing.js"; +import { + parseOrgProjectArg, + parseSlashSeparatedArg, +} from "../../lib/arg-parsing.js"; import { openInBrowser } from "../../lib/browser.js"; import { ContextError, ValidationError } from "../../lib/errors.js"; import { formatLogDetails, writeJson } from "../../lib/formatters/index.js"; @@ -48,30 +51,11 @@ export function parsePositionalArgs(args: string[]): { } if (args.length === 1) { - const slashIdx = first.indexOf("/"); - - if (slashIdx === -1) { - // No slashes — plain log ID - return { logId: first, targetArg: undefined }; - } - - // Log IDs are hex and never contain "/" — this must be a structured - // "org/project/logId" or "org/project" (missing log ID) - const lastSlashIdx = first.lastIndexOf("/"); - - if (slashIdx === lastSlashIdx) { - // Exactly one slash: "org/project" without log ID - throw new ContextError("Log ID", USAGE_HINT); - } - - // Two+ slashes: split on last "/" → target + logId - const targetArg = first.slice(0, lastSlashIdx); - const logId = first.slice(lastSlashIdx + 1); - - if (!logId) { - throw new ContextError("Log ID", USAGE_HINT); - } - + const { id: logId, targetArg } = parseSlashSeparatedArg( + first, + "Log ID", + USAGE_HINT + ); return { logId, targetArg }; } diff --git a/src/commands/trace/view.ts b/src/commands/trace/view.ts index 3e6a20d74a..c882e3981d 100644 --- a/src/commands/trace/view.ts +++ b/src/commands/trace/view.ts @@ -7,7 +7,11 @@ import { buildCommand } from "@stricli/core"; import type { SentryContext } from "../../context.js"; import { getDetailedTrace } from "../../lib/api-client.js"; -import { parseOrgProjectArg, spansFlag } from "../../lib/arg-parsing.js"; +import { + parseOrgProjectArg, + parseSlashSeparatedArg, + spansFlag, +} from "../../lib/arg-parsing.js"; import { openInBrowser } from "../../lib/browser.js"; import { ContextError, ValidationError } from "../../lib/errors.js"; import { @@ -55,30 +59,11 @@ export function parsePositionalArgs(args: string[]): { } if (args.length === 1) { - const slashIdx = first.indexOf("/"); - - if (slashIdx === -1) { - // No slashes — plain trace ID - return { traceId: first, targetArg: undefined }; - } - - // Trace IDs are hex and never contain "/" — this must be a structured - // "org/project/traceId" or "org/project" (missing trace ID) - const lastSlashIdx = first.lastIndexOf("/"); - - if (slashIdx === lastSlashIdx) { - // Exactly one slash: "org/project" without trace ID - throw new ContextError("Trace ID", USAGE_HINT); - } - - // Two+ slashes: split on last "/" → target + traceId - const targetArg = first.slice(0, lastSlashIdx); - const traceId = first.slice(lastSlashIdx + 1); - - if (!traceId) { - throw new ContextError("Trace ID", USAGE_HINT); - } - + const { id: traceId, targetArg } = parseSlashSeparatedArg( + first, + "Trace ID", + USAGE_HINT + ); return { traceId, targetArg }; } diff --git a/src/lib/arg-parsing.ts b/src/lib/arg-parsing.ts index b2bbd3a362..157b52b337 100644 --- a/src/lib/arg-parsing.ts +++ b/src/lib/arg-parsing.ts @@ -6,7 +6,7 @@ * project list) and single-item commands (issue view, explain, plan). */ -import { ValidationError } from "./errors.js"; +import { ContextError, ValidationError } from "./errors.js"; import type { ParsedSentryUrl } from "./sentry-url-parser.js"; import { applySentryUrlContext, parseSentryUrl } from "./sentry-url-parser.js"; import { isAllDigits } from "./utils.js"; @@ -345,6 +345,55 @@ function parseWithDash(arg: string): ParsedIssueArg { return { type: "project-search", projectSlug, suffix }; } +/** + * Parse a single positional arg that may be a plain hex ID or a slash-separated + * `org/project/id` pattern. + * + * Used by commands whose IDs are hex strings that never contain `/` + * (event, trace, log), making the pattern unambiguous: + * - No slashes → plain ID, no target + * - Exactly one slash → `org/project` without ID → throws {@link ContextError} + * - Two or more slashes → splits on last `/` → `targetArg` + `id` + * + * @param arg - The raw single positional argument + * @param idLabel - Human-readable ID label for error messages (e.g. `"Event ID"`) + * @param usageHint - Usage example shown in error messages + * @returns Parsed `{ id, targetArg }` — `targetArg` is `undefined` for plain IDs + * @throws {ContextError} When the arg contains exactly one slash (missing ID) + * or ends with a trailing slash (empty ID segment) + */ +export function parseSlashSeparatedArg( + arg: string, + idLabel: string, + usageHint: string +): { id: string; targetArg: string | undefined } { + const slashIdx = arg.indexOf("/"); + + if (slashIdx === -1) { + // No slashes — plain ID + return { id: arg, targetArg: undefined }; + } + + // IDs are hex and never contain "/" — this must be a structured + // "org/project/id" or "org/project" (missing ID) + const lastSlashIdx = arg.lastIndexOf("/"); + + if (slashIdx === lastSlashIdx) { + // Exactly one slash: "org/project" without ID + throw new ContextError(idLabel, usageHint); + } + + // Two+ slashes: split on last "/" → target + id + const targetArg = arg.slice(0, lastSlashIdx); + const id = arg.slice(lastSlashIdx + 1); + + if (!id) { + throw new ContextError(idLabel, usageHint); + } + + return { id, targetArg }; +} + export function parseIssueArg(arg: string): ParsedIssueArg { // 0. URL detection — extract issue ID from Sentry web URLs const urlParsed = parseSentryUrl(arg);