Skip to content

Commit 8801c8e

Browse files
betegonclaude
andcommitted
merge: resolve conflict in api-client.ts keeping both branch additions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 55b9cde + 63d397e commit 8801c8e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+6804
-2062
lines changed

AGENTS.md

Lines changed: 82 additions & 62 deletions
Large diffs are not rendered by default.

bun.lock

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,9 @@
11
{
22
"name": "sentry",
33
"version": "0.14.0-dev.0",
4-
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
5-
"type": "module",
6-
"bin": {
7-
"sentry": "./dist/bin.cjs"
8-
},
9-
"files": [
10-
"dist/bin.cjs"
11-
],
12-
"scripts": {
13-
"dev": "bun run src/bin.ts",
14-
"build": "bun run script/build.ts --single",
15-
"build:all": "bun run script/build.ts",
16-
"bundle": "bun run script/bundle.ts",
17-
"typecheck": "tsc --noEmit",
18-
"lint": "bunx ultracite check",
19-
"lint:fix": "bunx ultracite fix",
20-
"test": "bun run test:unit && bun run test:isolated",
21-
"test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov",
22-
"test:isolated": "bun test test/isolated",
23-
"test:e2e": "bun test test/e2e",
24-
"generate:skill": "bun run script/generate-skill.ts",
25-
"check:skill": "bun run script/check-skill.ts",
26-
"check:deps": "bun run script/check-no-deps.ts"
4+
"repository": {
5+
"type": "git",
6+
"url": "git+https://github.com/getsentry/cli.git"
277
},
288
"devDependencies": {
299
"@biomejs/biome": "2.3.8",
@@ -39,29 +19,53 @@
3919
"@types/semver": "^7.7.1",
4020
"binpunch": "^1.0.0",
4121
"chalk": "^5.6.2",
22+
"cli-highlight": "^2.1.11",
4223
"esbuild": "^0.25.0",
4324
"fast-check": "^4.5.3",
4425
"ignore": "^7.0.5",
26+
"marked": "^15",
4527
"p-limit": "^7.2.0",
4628
"pretty-ms": "^9.3.0",
4729
"qrcode-terminal": "^0.12.0",
4830
"semver": "^7.7.3",
31+
"string-width": "^8.2.0",
4932
"tinyglobby": "^0.2.15",
5033
"typescript": "^5",
5134
"ultracite": "6.3.10",
5235
"uuidv7": "^1.1.0",
36+
"wrap-ansi": "^10.0.0",
5337
"zod": "^3.24.0"
5438
},
55-
"repository": {
56-
"type": "git",
57-
"url": "https://github.com/getsentry/cli.git"
39+
"bin": {
40+
"sentry": "./dist/bin.cjs"
5841
},
59-
"license": "FSL-1.1-Apache-2.0",
42+
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
6043
"engines": {
6144
"node": ">=22"
6245
},
46+
"files": [
47+
"dist/bin.cjs"
48+
],
49+
"license": "FSL-1.1-Apache-2.0",
6350
"packageManager": "bun@1.3.9",
6451
"patchedDependencies": {
6552
"@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch"
66-
}
53+
},
54+
"scripts": {
55+
"dev": "bun run src/bin.ts",
56+
"build": "bun run script/build.ts --single",
57+
"build:all": "bun run script/build.ts",
58+
"bundle": "bun run script/bundle.ts",
59+
"typecheck": "tsc --noEmit",
60+
"lint": "bunx ultracite check",
61+
"lint:fix": "bunx ultracite fix",
62+
"test": "bun run test:unit && bun run test:isolated",
63+
"test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov",
64+
"test:isolated": "bun test test/isolated",
65+
"test:e2e": "bun test test/e2e",
66+
"generate:skill": "bun run script/generate-skill.ts",
67+
"check:skill": "bun run script/check-skill.ts",
68+
"check:deps": "bun run script/check-no-deps.ts"
69+
},
70+
"type": "module"
6771
}

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ List issues in a project
215215
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
216216
- `-t, --period <value> - Time period for issue activity (e.g. 24h, 14d, 90d) - (default: "90d")`
217217
- `--json - Output JSON`
218-
- `-c, --cursor <value> - Pagination cursor — only for <org>/ mode (use "last" to continue)`
218+
- `-c, --cursor <value> - Pagination cursor for <org>/ or multi-target modes (use "last" to continue)`
219219

220220
**Examples:**
221221

@@ -608,7 +608,7 @@ List issues in a project
608608
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
609609
- `-t, --period <value> - Time period for issue activity (e.g. 24h, 14d, 90d) - (default: "90d")`
610610
- `--json - Output JSON`
611-
- `-c, --cursor <value> - Pagination cursor — only for <org>/ mode (use "last" to continue)`
611+
- `-c, --cursor <value> - Pagination cursor for <org>/ or multi-target modes (use "last" to continue)`
612612

613613
### Orgs
614614

src/commands/api.ts

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type { SentryContext } from "../context.js";
99
import { rawApiRequest } from "../lib/api-client.js";
1010
import { buildCommand } from "../lib/command.js";
11+
import { ValidationError } from "../lib/errors.js";
1112
import type { Writer } from "../types/index.js";
1213

1314
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
@@ -295,12 +296,67 @@ export function setNestedValue(
295296
}
296297
}
297298

299+
/**
300+
* Auto-correct fields that use ':' instead of '=' as the separator, and warn
301+
* the user on stderr.
302+
*
303+
* This recovers from a common mistake where users write Sentry search-query
304+
* style syntax (`-F status:resolved`) instead of the required key=value form
305+
* (`-F status=resolved`). The correction is safe to apply unconditionally
306+
* because this function is only called for fields that have already been
307+
* confirmed to contain no '=' — at that point the request would fail anyway.
308+
*
309+
* Splitting on the *first* ':' is intentional so that values that themselves
310+
* contain colons (e.g. ISO timestamps, URLs) are preserved intact:
311+
* `since:2026-02-25T11:20:00` → key=`since`, value=`2026-02-25T11:20:00`
312+
*
313+
* Fields with no ':' (truly uncorrectable) are returned unchanged so that the
314+
* downstream parser can throw its normal error.
315+
*
316+
* @param fields - Raw field strings from --field or --raw-field flags
317+
* @param stderr - Writer to emit warnings on (command's stderr)
318+
* @returns New array with corrected field strings (or the original array if no
319+
* corrections were needed)
320+
* @internal Exported for testing
321+
*/
322+
export function normalizeFields(
323+
fields: string[] | undefined,
324+
stderr: Writer
325+
): string[] | undefined {
326+
if (!fields || fields.length === 0) {
327+
return fields;
328+
}
329+
330+
return fields.map((field) => {
331+
// Already valid: has '=' or is the empty-array syntax "key[]"
332+
if (field.includes("=") || field.endsWith("[]")) {
333+
return field;
334+
}
335+
336+
const colonIndex = field.indexOf(":");
337+
// ':' must exist and not be the very first character (that would make an
338+
// empty key, which the parser rejects regardless)
339+
if (colonIndex > 0) {
340+
const key = field.substring(0, colonIndex);
341+
const value = field.substring(colonIndex + 1);
342+
const corrected = `${key}=${value}`;
343+
stderr.write(
344+
`warning: field '${field}' looks like it uses ':' instead of '=' — interpreting as '${corrected}'\n`
345+
);
346+
return corrected;
347+
}
348+
349+
// No correction possible; let the downstream parser throw.
350+
return field;
351+
});
352+
}
353+
298354
/**
299355
* Process a single field string and set its value in the result object.
300356
* @param result - Target object to modify
301357
* @param field - Field string in "key=value" or "key[]" format
302358
* @param raw - If true, keep value as string (no JSON parsing)
303-
* @throws {Error} When field format is invalid
359+
* @throws {ValidationError} When field format is invalid
304360
*/
305361
function processField(
306362
result: Record<string, unknown>,
@@ -315,7 +371,10 @@ function processField(
315371
setNestedValue(result, field, undefined);
316372
return;
317373
}
318-
throw new Error(`Invalid field format: ${field}. Expected key=value`);
374+
throw new ValidationError(
375+
`Invalid field format: ${field}. Expected key=value`,
376+
"field"
377+
);
319378
}
320379

321380
const key = field.substring(0, eqIndex);
@@ -377,14 +436,17 @@ export function buildQueryParams(
377436
for (const field of fields) {
378437
const eqIndex = field.indexOf("=");
379438
if (eqIndex === -1) {
380-
throw new Error(`Invalid field format: ${field}. Expected key=value`);
439+
throw new ValidationError(
440+
`Invalid field format: ${field}. Expected key=value`,
441+
"field"
442+
);
381443
}
382444

383445
const key = field.substring(0, eqIndex);
384446

385447
// Validate key format (same validation as parseFieldKey for consistency)
386448
if (!FIELD_KEY_REGEX.test(key)) {
387-
throw new Error(`Invalid field key format: ${key}`);
449+
throw new ValidationError(`Invalid field key format: ${key}`, "field");
388450
}
389451

390452
const rawValue = field.substring(eqIndex + 1);
@@ -409,7 +471,7 @@ export function buildQueryParams(
409471
*
410472
* @param fields - Array of "key=value" strings
411473
* @returns Record suitable for URLSearchParams
412-
* @throws {Error} When field doesn't contain "=" or key is empty
474+
* @throws {ValidationError} When field doesn't contain "=" or key is empty
413475
* @internal Exported for testing
414476
*/
415477
export function buildRawQueryParams(
@@ -420,12 +482,18 @@ export function buildRawQueryParams(
420482
for (const field of fields) {
421483
const eqIndex = field.indexOf("=");
422484
if (eqIndex === -1) {
423-
throw new Error(`Invalid field format: ${field}. Expected key=value`);
485+
throw new ValidationError(
486+
`Invalid field format: ${field}. Expected key=value`,
487+
"field"
488+
);
424489
}
425490

426491
const key = field.substring(0, eqIndex);
427492
if (key === "") {
428-
throw new Error("Invalid field key format: key cannot be empty");
493+
throw new ValidationError(
494+
"Invalid field key format: key cannot be empty",
495+
"field"
496+
);
429497
}
430498

431499
const value = field.substring(eqIndex + 1);
@@ -796,7 +864,7 @@ export const apiCommand = buildCommand({
796864
flags: ApiFlags,
797865
endpoint: string
798866
): Promise<void> {
799-
const { stdout, stdin } = this;
867+
const { stdout, stderr, stdin } = this;
800868

801869
// Normalize endpoint to ensure trailing slash (Sentry API requirement)
802870
const normalizedEndpoint = normalizeEndpoint(endpoint);
@@ -810,12 +878,13 @@ export const apiCommand = buildCommand({
810878
// --input takes precedence for body content
811879
body = await buildBodyFromInput(flags.input, stdin);
812880
} else {
881+
// Auto-correct ':'-separated fields (e.g. -F status:resolved → -F status=resolved)
882+
// before routing to body or params so the correction applies everywhere.
883+
const field = normalizeFields(flags.field, stderr);
884+
const rawField = normalizeFields(flags["raw-field"], stderr);
885+
813886
// Route fields to body or params based on HTTP method
814-
const options = prepareRequestOptions(
815-
flags.method,
816-
flags.field,
817-
flags["raw-field"]
818-
);
887+
const options = prepareRequestOptions(flags.method, field, rawField);
819888
body = options.body;
820889
params = options.params;
821890
}

src/commands/event/view.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,7 @@ type HumanOutputOptions = {
5454
function writeHumanOutput(stdout: Writer, options: HumanOutputOptions): void {
5555
const { event, detectedFrom, spanTreeLines } = options;
5656

57-
const lines = formatEventDetails(event, `Event ${event.eventID}`);
58-
59-
// Skip leading empty line for standalone display
60-
const output = lines.slice(1);
61-
stdout.write(`${output.join("\n")}\n`);
57+
stdout.write(`${formatEventDetails(event, `Event ${event.eventID}`)}\n`);
6258

6359
if (spanTreeLines && spanTreeLines.length > 0) {
6460
stdout.write(`${spanTreeLines.join("\n")}\n`);

src/commands/issue/explain.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ export const explainCommand = buildCommand({
107107
}
108108

109109
// Human-readable output
110-
const lines = formatRootCauseList(causes);
111-
stdout.write(`${lines.join("\n")}\n`);
110+
stdout.write(`${formatRootCauseList(causes)}\n`);
112111
writeFooter(
113112
stdout,
114113
`To create a plan, run: sentry issue plan ${issueArg}`

0 commit comments

Comments
 (0)