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
67 changes: 67 additions & 0 deletions src/lib/arg-parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,58 @@ 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. 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;
}

// 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(),
};
}
Comment thread
BYK marked this conversation as resolved.

/**
* Parse issue arg with dash but no slash (project-suffix).
*/
Expand Down Expand Up @@ -931,6 +983,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();
Expand Down Expand Up @@ -985,6 +1038,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
}
Comment thread
cursor[bot] marked this conversation as resolved.

// 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("/")) {
Expand Down
78 changes: 78 additions & 0 deletions test/lib/arg-parsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,84 @@ 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",
});
});

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", () => {
test("@latest returns selector type", () => {
expect(parseIssueArg("@latest")).toEqual({
Expand Down
Loading