diff --git a/packages/cli/src/commands/log/view.ts b/packages/cli/src/commands/log/view.ts index 8cb59dbc2..be88e5386 100644 --- a/packages/cli/src/commands/log/view.ts +++ b/packages/cli/src/commands/log/view.ts @@ -92,6 +92,69 @@ function parseSingleSlashLogArg( return null; } +/** + * Split a fully-qualified `//` arg into its target and + * log ID, or return `null` when the arg isn't in that shape (fewer than two + * slashes, or the trailing segment isn't a 32-char hex ID). + * + * Agents often pass several fully-qualified paths to one `log view` call + * (CLI-1XC). Without this, the first path was used verbatim as the target + * (yielding project `"/"`) and the rest failed hex validation. + */ +function splitFullLogPath( + arg: string, +): { target: string; logId: string } | null { + const trimmed = arg.trim(); + const firstSlash = trimmed.indexOf("/"); + const lastSlash = trimmed.lastIndexOf("/"); + if (firstSlash === -1 || firstSlash === lastSlash) { + return null; + } + const logId = normalizeHexId(trimmed.slice(lastSlash + 1)); + if (!HEX_ID_RE.test(logId)) { + return null; + } + return { target: trimmed.slice(0, lastSlash), logId }; +} + +/** + * Parse multiple args where the first is a fully-qualified log path. + * Every later arg may be a fully-qualified path or a bare log ID; all paths + * must share the same org/project target. + * + * @throws {ValidationError} If the paths reference different targets + */ +function parseMultipleFullPaths( + args: string[], + target: string, +): { rawLogIds: string[]; targetArg: string } { + const rawLogIds: string[] = []; + for (const arg of args) { + const full = splitFullLogPath(arg); + if (full) { + if (full.target !== target) { + throw new ValidationError( + `Log IDs reference different projects ('${target}' and '${full.target}'). ` + + `View logs from one project per call: ${USAGE_HINT}`, + ); + } + rawLogIds.push(full.logId); + } else { + rawLogIds.push(...splitNewlineArg(arg)); + } + } + return { rawLogIds, targetArg: target }; +} + +/** + * Strip a redundant `/` prefix from a log ID arg, so + * `sentry log view org/proj org/proj/` resolves `` (CLI-1XC). + */ +function stripTargetPrefix(rawId: string, target: string): string { + const prefix = `${target.trim()}/`; + return rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId; +} + /** * Parse positional arguments for log view. * Handles: @@ -196,7 +259,16 @@ export function parsePositionalArgs(args: string[]): { } } - const rawLogIds = args.slice(1).flatMap(splitNewlineArg); + // Every arg is (or starts with) a fully-qualified `org/project/log-id`. + const firstFull = splitFullLogPath(first); + if (firstFull) { + return parseMultipleFullPaths(args, firstFull.target); + } + + const rawLogIds = args + .slice(1) + .flatMap(splitNewlineArg) + .map((id) => stripTargetPrefix(id, first)); if (rawLogIds.length === 0) { throw new ContextError("Log ID", USAGE_HINT, []); } diff --git a/packages/cli/test/commands/log/view.test.ts b/packages/cli/test/commands/log/view.test.ts index 46db6e8ae..be524cbc5 100644 --- a/packages/cli/test/commands/log/view.test.ts +++ b/packages/cli/test/commands/log/view.test.ts @@ -299,6 +299,42 @@ describe("parsePositionalArgs", () => { }); }); + describe("multiple fully-qualified org/project/logId args (CLI-1XC)", () => { + test("splits each full path into a shared target + log IDs", () => { + const result = parsePositionalArgs([ + `my-org/frontend/${ID1}`, + `my-org/frontend/${ID2}`, + ]); + expect(result.targetArg).toBe("my-org/frontend"); + expect(result.rawLogIds).toEqual([ID1, ID2]); + }); + + test("accepts a full path followed by bare log IDs", () => { + const result = parsePositionalArgs([`my-org/frontend/${ID1}`, ID2, ID3]); + expect(result.targetArg).toBe("my-org/frontend"); + expect(result.rawLogIds).toEqual([ID1, ID2, ID3]); + }); + + test("throws ValidationError when full paths reference different projects", () => { + expect(() => + parsePositionalArgs([ + `my-org/frontend/${ID1}`, + `my-org/backend/${ID2}`, + ]), + ).toThrow(ValidationError); + }); + + test("strips a redundant target prefix from log ID args", () => { + const result = parsePositionalArgs([ + "my-org/frontend", + `my-org/frontend/${ID1}`, + ID2, + ]); + expect(result.targetArg).toBe("my-org/frontend"); + expect(result.rawLogIds).toEqual([ID1, ID2]); + }); + }); + describe("the exact CLI-BC scenario", () => { test("newline-delimited log IDs as a single arg with target", () => { const ids = [