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
6 changes: 3 additions & 3 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -826,11 +826,11 @@ All command docs and skill files are generated via `pnpm run generate:docs`. Thi

- **Command docs** (`apps/cli-docs/src/content/docs/commands/*.md`) and the configuration page (`apps/cli-docs/src/content/docs/configuration.md`) are **gitignored** and generated from CLI metadata + hand-written fragments in `apps/cli-docs/src/fragments/` (paths live in `script/paths.ts`).
- **Skill files** (`plugins/sentry-cli/skills/sentry-cli/`) are **committed** (consumed by external plugin systems) and auto-committed by CI when stale.
- **Hints name the running program**: the same code runs as `sentry`, `npx sentry`, and `sentry-mcp`, so write a hint's command with the `` sentry`…` `` tag from `src/lib/app-identity.ts`, without the program name (`` sentry`issue list ${org}/` ``), inside the function that builds the hint. The tag inserts interpolated values as they are and nothing rewrites a hint later, so errors such as `ContextError` print usage lines as given. Keep a command in a constant or table as a `LazyCommand`, `` const usageHint = () => sentry`issue view <id>` ``, and call it where the hint is shown: the entry point registers the program only after the command modules load (`test/lib/app-identity-import.test.ts` checks that no hint is built at import). Command help (`docs`, `fullDescription`, `brief`, `examples`) stays `sentry …`. Lint (`sentry-cli/no-hardcoded-sentry-command`) flags a bare `sentry <route>` (routes come from `src/command-manifest.ts`), and `pnpm run check:doc-examples` checks each tag's command and flags against the route tree.
- **Examples belong to the command**: add them to `docs.examples` (`{ description, command }[]`, see `src/commands/agent-conversation/list.ts`). They render in `--help`, the generated docs, and the skill references. Don't hand-write examples in `fullDescription` (an `Examples:` block, or indented `sentry …` lines under any label) or an Examples heading in a fragment; the `sentry-cli/no-examples-in-full-description` lint rule and `pnpm run check:fragments` reject both.
- **Hints name the running program**: the same code runs as `sentry`, `npx sentry`, and `sentry-mcp`, so write a hint's command with the `` sentry`…` `` tag from `src/lib/app-identity.ts`, without the program name (`` sentry`issue list ${org}/` ``), inside the function that builds the hint. The tag inserts interpolated values as they are and nothing rewrites a hint later, so errors such as `ContextError` print usage lines as given. Keep a command in a constant or table as a `LazyCommand`, `` const usageHint = () => sentry`issue view <id>` ``, and call it where the hint is shown: the entry point registers the program only after the command modules load (`test/lib/app-identity-import.test.ts` checks that no hint is built at import). Command help prose (`docs`, `fullDescription`, `brief`) stays `sentry …`; `docs.examples` steps leave the program out. Lint (`sentry-cli/no-hardcoded-sentry-command`) flags a bare `sentry <route>` (routes come from `src/command-manifest.ts`), and `pnpm run check:doc-examples` checks each tag's command and flags against the route tree.
- **Examples belong to the command**: add them to `docs.examples` (`{ description, run }[]`, see `src/commands/agent-conversation/list.ts`). Write each step without the program name, as `run: "issue list my-org/"`; `--help` adds the running program and the generated docs and skill references add `sentry`. A step inside other shell text keeps that text around it, as `{ prefix: "sudo ", args: "cli fix" }`. They render in `--help`, the generated docs, and the skill references. Don't hand-write examples in `fullDescription` (an `Examples:` block, or indented `sentry …` lines under any label) or an Examples heading in a fragment; the `sentry-cli/no-examples-in-full-description` lint rule and `pnpm run check:fragments` reject both.
- **Target syntax belongs to the positional**: describe accepted target forms in the positional `brief` (use `ORG_ALL_TARGET_BRIEF` or `PROJECT_TARGET_BRIEF` from `src/lib/list-command.ts` for `[<org>/][<project>]` targets), not in a `Target patterns:` block. The brief renders in `--help`, the generated docs, and the skill references.
- Use fragments in `apps/cli-docs/src/fragments/commands/` only for prose the command metadata can't carry: guides, permissions, caveats.
- `pnpm run check:fragments` also rejects any shell code block in a fragment that runs a `sentry` command, whatever its heading. Multi-step workflows belong in `docs.examples` too: pass `command` an array of steps (see `release create` and `local run`), and every step is checked and shown in `--help`, the docs, and the skill. Reserve `<!-- allow-commands: <reason> -->` (on the line directly above the block) for sample output and guides that interleave non-`sentry` commands.
- `pnpm run check:fragments` also rejects any shell code block in a fragment that runs a `sentry` command, whatever its heading. Multi-step workflows belong in `docs.examples` too: pass `run` an array of steps (see `release create` and `local run`), and every step is checked and shown in `--help`, the docs, and the skill. Reserve `<!-- allow-commands: <reason> -->` (on the line directly above the block) for sample output and guides that interleave non-`sentry` commands.
- `pnpm run check:fragments` validates fragment ↔ route consistency.
- Positional `placeholder` values must be descriptive: `"org/project/trace-id"` not `"args"`.
- Don't restate a value flag's `default` in its `brief`; help and docs already show it (`test/lib/flag-briefs.test.ts`).
Expand Down
29 changes: 28 additions & 1 deletion packages/cli/lint-rules/cli-oxlint-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,24 @@ const HELP_TEXT_KEYS = new Set([
"examples",
]);

/** Key of a property node, as written. */
function propertyKey(node) {
return node.key?.name ?? node.key?.value;
}

/**
* Whether `node` is a step of a `docs.examples` entry: the `run` value, an
* element of a `run` array, or the `args` of a `{ prefix, args }` step.
*/
function isExampleStep(node) {
let current = node.parent;
if (current?.type === "Property" && propertyKey(current) === "args") {
current = current.parent?.parent;
}
if (current?.type === "ArrayExpression") current = current.parent;
return current?.type === "Property" && propertyKey(current) === "run";
}

/** Whether `node` sits inside a function body. */
function hasFunctionAncestor(node) {
for (let current = node.parent; current; current = current.parent) {
Expand Down Expand Up @@ -919,7 +937,16 @@ const rules = {
);
},
Literal(node) {
if (typeof node.value === "string") check(node, node.value);
if (typeof node.value !== "string") return;
if (isExampleStep(node)) {
if (node.value.startsWith("sentry "))
report(
node,
"Write example steps without the program name, as `issue list`: help, docs and skills each add it.",
);
return;
}
check(node, node.value);
},
TemplateLiteral(node) {
if (!isSentryTag(node.parent)) check(node, templateText(node));
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/script/check-doc-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ function commandExamples(
}
const source = `\`${["sentry", ...path].join(" ")}\` help`;
return (target.__examples ?? []).flatMap((e) =>
exampleSteps(e).map((invocation) => ({ source, invocation })),
exampleSteps(e, "sentry").map((invocation) => ({ source, invocation })),
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/script/generate-docs-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ function commandAt(path: string[]): Command {
*/
function sdkRunStatement(path: string[], variable: string): string {
for (const example of commandAt(path).__examples ?? []) {
const steps = exampleSteps(example);
const steps = exampleSteps(example, "sentry");
const commands = steps.length === 1 ? shellCommands(steps[0] ?? "") : [];
const words = commands.length === 1 ? (commands[0] ?? []) : [];
if (words[0] === "sentry" && words.some((w) => w.startsWith("-"))) {
Expand Down
27 changes: 12 additions & 15 deletions packages/cli/src/commands/agent-conversation/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,54 +138,51 @@ export const listCommand = buildListCommand("agent-conversation", {
examples: [
{
description: "List recent agent conversations",
command: "sentry agent-conversation list",
run: "agent-conversation list",
},
{
description: "Explicit organization (all projects)",
command: "sentry agent-conversation list my-org/",
run: "agent-conversation list my-org/",
},
{
description: "One project",
command: "sentry agent-conversation list my-org/my-project",
run: "agent-conversation list my-org/my-project",
},
{
description: "Find a project across organizations",
command: "sentry agent-conversation list my-project",
run: "agent-conversation list my-project",
},
{
description: "Show more, last 24 hours",
command: "sentry agent-conversation list --limit 50 --period 24h",
run: "agent-conversation list --limit 50 --period 24h",
},
{
description: "Sort by highest error count",
command: "sentry agent-conversation list --sort -conversation.errors",
run: "agent-conversation list --sort -conversation.errors",
},
{
description: "Sort by lowest total cost",
command: "sentry agent-conversation list --sort conversation.totalCost",
run: "agent-conversation list --sort conversation.totalCost",
},
{
description: "Find conversations with errors",
command: 'sentry agent-conversation list -q "conversation.errors:>0"',
run: 'agent-conversation list -q "conversation.errors:>0"',
},
{
description: "Find conversations for an agent",
command:
'sentry agent-conversation list -q "gen_ai.agent.name:my-agent"',
run: 'agent-conversation list -q "gen_ai.agent.name:my-agent"',
},
{
description: "Find conversations with more than two tool calls",
command:
'sentry agent-conversation list -q "conversation.toolCalls:>2"',
run: 'agent-conversation list -q "conversation.toolCalls:>2"',
},
{
description: "Find conversations that used a tool",
command:
'sentry agent-conversation list -q "gen_ai.tool.name:search_issues"',
run: 'agent-conversation list -q "gen_ai.tool.name:search_issues"',
},
{
description: "Paginate through project results",
command: "sentry agent-conversation list my-org/my-project -c next",
run: "agent-conversation list my-org/my-project -c next",
},
],
},
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/agent-conversation/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,15 @@ export const viewCommand = buildCommand({
examples: [
{
description: "View full transcript (organization auto-detected)",
command: "sentry agent-conversation view conv-123",
run: "agent-conversation view conv-123",
},
{
description: "Explicit organization",
command: "sentry agent-conversation view my-org/conv-123",
run: "agent-conversation view my-org/conv-123",
},
{
description: "JSON output",
command: "sentry agent-conversation view my-org/conv-123 --json",
run: "agent-conversation view my-org/conv-123 --json",
},
],
},
Expand Down
12 changes: 4 additions & 8 deletions packages/cli/src/commands/alert/issues/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,25 +126,21 @@ export const createCommand = buildCommand({
examples: [
{
description: "Alert on new issues and email a team",
command:
'sentry alert issues create my-org/my-project --name \'New Issues\' --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\'',
run: 'alert issues create my-org/my-project --name \'New Issues\' --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\'',
},
{
description:
"Preview a high-priority alert that emails a user at most every 30 minutes",
command:
'sentry alert issues create my-org/my-project --name \'High Priority\' --condition \'{"type":"new_high_priority_issue","comparison":true,"conditionResult":true}\' --action \'{"type":"email","data":{},"config":{"targetType":"user","targetIdentifier":"56789"}}\' --frequency 30 --dry-run',
run: 'alert issues create my-org/my-project --name \'High Priority\' --condition \'{"type":"new_high_priority_issue","comparison":true,"conditionResult":true}\' --action \'{"type":"email","data":{},"config":{"targetType":"user","targetIdentifier":"56789"}}\' --frequency 30 --dry-run',
},
{
description: "Alert on new issues and regressions, using trigger names",
command:
'sentry alert issues create my-org/my-project --name \'New or Regressed\' --condition first-seen,regression --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\'',
run: 'alert issues create my-org/my-project --name \'New or Regressed\' --condition first-seen,regression --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\'',
},
{
description:
"Alert when an issue sees more than 50 events in a day (filters only, checked on every event)",
command:
'sentry alert issues create my-org/my-project --name \'Busy Issues\' --filter \'{"type":"event_frequency_count","comparison":{"value":50,"interval":"1d"},"conditionResult":true}\' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\' --frequency 1440',
run: 'alert issues create my-org/my-project --name \'Busy Issues\' --filter \'{"type":"event_frequency_count","comparison":{"value":50,"interval":"1d"},"conditionResult":true}\' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\' --frequency 1440',
},
],
},
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/alert/issues/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ export const deleteCommand = buildDeleteCommand({
examples: [
{
description: "Delete an issue alert rule (prompts for confirmation)",
command: "sentry alert issues delete my-org/my-project/12345",
run: "alert issues delete my-org/my-project/12345",
},
{
description: "Delete a rule selected by name without prompting",
command: "sentry alert issues delete my-org/my-project/'My Rule' --yes",
run: "alert issues delete my-org/my-project/'My Rule' --yes",
},
{
description: "Preview the deletion",
command: "sentry alert issues delete my-org/my-project/12345 --dry-run",
run: "alert issues delete my-org/my-project/12345 --dry-run",
},
],
},
Expand Down
12 changes: 4 additions & 8 deletions packages/cli/src/commands/alert/issues/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,24 +202,20 @@ export const editCommand = buildCommand({
examples: [
{
description: "Rename an issue alert rule",
command:
"sentry alert issues edit my-org/my-project/12345 --name 'Prod errors'",
run: "alert issues edit my-org/my-project/12345 --name 'Prod errors'",
},
{
description: "Disable a rule selected by name",
command:
"sentry alert issues edit my-org/my-project/'Old name' --status disabled",
run: "alert issues edit my-org/my-project/'Old name' --status disabled",
},
{
description:
"Rename and enable a rule (organization and project auto-detected)",
command:
"sentry alert issues edit 12345 --name 'Renamed' --status active",
run: "alert issues edit 12345 --name 'Renamed' --status active",
},
{
description: "Replace the rule's conditions",
command:
'sentry alert issues edit my-org/my-project/12345 --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\'',
run: 'alert issues edit my-org/my-project/12345 --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\'',
},
],
},
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/commands/alert/issues/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,24 +506,24 @@ export const listCommand = buildListCommand("alert issues", {
examples: [
{
description: "List issue alert rules (project auto-detected)",
command: "sentry alert issues list",
run: "alert issues list",
},
{
description: "List issue alert rules for a project",
command: "sentry alert issues list my-org/my-project",
run: "alert issues list my-org/my-project",
},
{
description: "Filter rules by name",
command: 'sentry alert issues list my-org/my-project --query "spike"',
run: 'alert issues list my-org/my-project --query "spike"',
},
{
description:
"List issue alert rules for all projects in an organization",
command: "sentry alert issues list my-org/",
run: "alert issues list my-org/",
},
{
description: "Find a project across organizations",
command: "sentry alert issues list frontend",
run: "alert issues list frontend",
},
],
},
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/alert/issues/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ export const viewCommand = buildCommand({
{
description:
"View an issue alert rule by ID (organization and project auto-detected)",
command: "sentry alert issues view 12345",
run: "alert issues view 12345",
},
{
description: "View by ID in an explicit project",
command: "sentry alert issues view my-org/my-project/12345",
run: "alert issues view my-org/my-project/12345",
},
{
description: "View by name",
command: "sentry alert issues view my-org/my-project/'Error Spike'",
run: "alert issues view my-org/my-project/'Error Spike'",
},
],
},
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/commands/alert/metrics/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,12 @@ export const createCommand = buildCommand({
examples: [
{
description: "Alert when P95 span duration goes above 500ms",
command:
"sentry alert metrics create my-org/ --name 'P95 latency' --project my-project --query 'environment:prod' --aggregate 'p95(span.duration)' --dataset spans --time-window 5 --trigger '{\"alertThreshold\":500}'",
run: "alert metrics create my-org/ --name 'P95 latency' --project my-project --query 'environment:prod' --aggregate 'p95(span.duration)' --dataset spans --time-window 5 --trigger '{\"alertThreshold\":500}'",
},
{
description:
"Preview an alert on any matching log line that notifies through an existing alert",
command:
"sentry alert metrics create my-org/ --name 'AI chat failures' --project my-project --query 'action:ai.chat result:error' --aggregate 'count()' --dataset logs --time-window 5 --trigger '{\"alertThreshold\":0,\"resolveThreshold\":0}' --workflow 12345 --dry-run",
run: "alert metrics create my-org/ --name 'AI chat failures' --project my-project --query 'action:ai.chat result:error' --aggregate 'count()' --dataset logs --time-window 5 --trigger '{\"alertThreshold\":0,\"resolveThreshold\":0}' --workflow 12345 --dry-run",
},
],
},
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/alert/metrics/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export const deleteCommand = buildDeleteCommand({
examples: [
{
description: "Delete a metric alert rule (prompts for confirmation)",
command: "sentry alert metrics delete my-org/12345",
run: "alert metrics delete my-org/12345",
},
{
description: "Delete a rule selected by name without prompting",
command: "sentry alert metrics delete my-org/'P95 alert' --yes",
run: "alert metrics delete my-org/'P95 alert' --yes",
},
],
},
Expand Down
Loading
Loading