diff --git a/apps/cli-docs/src/fragments/commands/agent-conversation.md b/apps/cli-docs/src/fragments/commands/agent-conversation.md index 9ae15932f..e69de29bb 100644 --- a/apps/cli-docs/src/fragments/commands/agent-conversation.md +++ b/apps/cli-docs/src/fragments/commands/agent-conversation.md @@ -1,36 +0,0 @@ - - - -## Examples - -### List conversations - -```bash -# List recent agent conversations -sentry agent-conversation list - -# Explicit organization -sentry agent-conversation list my-org - -# Show more, last 24 hours -sentry agent-conversation list --limit 50 --period 24h - -# Filter conversations -sentry agent-conversation list -q "has:errors" - -# Paginate through results -sentry agent-conversation list my-org -c next -``` - -### View a conversation transcript - -```bash -# View full transcript (org auto-detected) -sentry agent-conversation view conv-123 - -# Explicit org (slash-separated) -sentry agent-conversation view my-org/conv-123 - -# JSON output -sentry agent-conversation view my-org/conv-123 --json -``` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 9dd7712e2..a7b5b8eff 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -389,7 +389,7 @@ Authenticate with Sentry Work with Sentry organizations - `sentry org list` — List organizations -- `sentry org view ` — View details of an organization +- `sentry org view []` — View details of an organization → Full flags and examples: `references/org.md` @@ -399,8 +399,8 @@ Work with Sentry projects - `sentry project create [/]:...` — Create one or more projects - `sentry project delete ` — Delete a project -- `sentry project list ` — List projects -- `sentry project view ` — View details of a project +- `sentry project list []` — List projects +- `sentry project view []` — View details of a project → Full flags and examples: `references/project.md` @@ -408,7 +408,7 @@ Work with Sentry projects Manage Sentry issues -- `sentry issue list ` — List issues in a project +- `sentry issue list []` — List issues in a project - `sentry issue events ` — List events for a specific issue - `sentry issue explain ` — Analyze one or more issues using Seer AI - `sentry issue plan ` — Generate a solution plan using Seer AI @@ -444,12 +444,12 @@ Make an authenticated API request Manage Sentry alert rules -- `sentry alert issues list ` — List issue alert rules +- `sentry alert issues list []` — List issue alert rules - `sentry alert issues view ` — View an issue alert rule -- `sentry alert issues create ` — Create an issue alert rule +- `sentry alert issues create []` — Create an issue alert rule - `sentry alert issues delete ` — Delete an issue alert rule - `sentry alert issues edit ` — Edit an issue alert rule -- `sentry alert metrics list ` — List metric alert rules +- `sentry alert metrics list []` — List metric alert rules - `sentry alert metrics view ` — View a metric alert rule - `sentry alert metrics create ` — Create a metric alert rule - `sentry alert metrics delete ` — Delete a metric alert rule @@ -470,14 +470,14 @@ Manage mobile build artifacts CLI-related commands -- `sentry cli completion ` — Print the shell completion script +- `sentry cli completion []` — Print the shell completion script - `sentry cli defaults ` — View and manage default settings - `sentry cli feedback ` — Send feedback about the CLI - `sentry cli fix` — Diagnose and repair CLI database issues - `sentry cli import` — Import settings from legacy .sentryclirc files - `sentry cli setup` — Configure shell integration - `sentry cli uninstall` — Uninstall Sentry CLI -- `sentry cli upgrade ` — Update the Sentry CLI to the latest version +- `sentry cli upgrade []` — Update the Sentry CLI to the latest version → Full flags and examples: `references/cli.md` @@ -493,8 +493,8 @@ Manage code mappings for stack trace linking List and view agent conversations -- `sentry agent-conversation list ` — List recent agent conversations -- `sentry agent-conversation view ` — View an agent conversation transcript +- `sentry agent-conversation list []` — List recent agent conversations +- `sentry agent-conversation view [/]` — View an agent conversation transcript → Full flags and examples: `references/agent-conversation.md` @@ -547,7 +547,7 @@ Search and query current Sentry documentation Find Sentry DSNs -- `sentry dsn list ` — List DSNs +- `sentry dsn list []` — List DSNs → Full flags and examples: `references/dsn.md` @@ -581,7 +581,7 @@ Upload React Native sourcemaps from build steps Search and inspect Session Replays -- `sentry replay list ` — List recent Session Replays +- `sentry replay list []` — List recent Session Replays - `sentry replay view ` — View a Session Replay - `sentry replay download ` — Download a Session Replay as rrweb JSON @@ -591,14 +591,14 @@ Search and inspect Session Replays Work with Sentry releases -- `sentry release list ` — List releases with adoption and health metrics +- `sentry release list []` — List releases with adoption and health metrics - `sentry release view ` — View release details with health metrics - `sentry release create ` — Create a release - `sentry release finalize ` — Finalize a release - `sentry release delete ` — Delete a release - `sentry release archive ` — Archive a release - `sentry release restore ` — Restore an archived release -- `sentry release deploy ` — Create a deploy for a release +- `sentry release deploy []` — Create a deploy for a release - `sentry release deploys ` — List deploys for a release - `sentry release set-commits ` — Set commits for a release - `sentry release propose-version` — Propose a release version @@ -609,7 +609,7 @@ Work with Sentry releases Work with Sentry repositories -- `sentry repo list ` — List repositories +- `sentry repo list []` — List repositories → Full flags and examples: `references/repo.md` @@ -617,7 +617,7 @@ Work with Sentry repositories Work with Sentry teams -- `sentry team list ` — List teams +- `sentry team list []` — List teams → Full flags and examples: `references/team.md` @@ -625,7 +625,7 @@ Work with Sentry teams Query aggregate event data (Explore) -- `sentry explore ` — Query aggregate event data (Explore) +- `sentry explore []` — Query aggregate event data (Explore) → Full flags and examples: `references/explore.md` @@ -633,7 +633,7 @@ Query aggregate event data (Explore) Manage User Feedback -- `sentry feedback list ` — List and search User Feedback +- `sentry feedback list []` — List and search User Feedback - `sentry feedback view ` — View a User Feedback item - `sentry feedback resolve ` — Mark User Feedback as resolved - `sentry feedback unresolve ` — Return User Feedback to the inbox @@ -655,7 +655,7 @@ View Sentry logs Work with Sentry cron monitors - `sentry monitor run ` — Wrap a command with cron monitor check-ins -- `sentry monitor list ` — List cron monitors +- `sentry monitor list []` — List cron monitors → Full flags and examples: `references/monitor.md` @@ -700,7 +700,7 @@ Check Sentry service status View distributed traces -- `sentry trace list ` — List recent traces in a project +- `sentry trace list []` — List recent traces in a project - `sentry trace view ` — View details of a specific trace - `sentry trace logs ` — View logs associated with a trace @@ -710,8 +710,8 @@ View distributed traces Manage product trials -- `sentry trial list ` — List product trials -- `sentry trial start ` — Start a product trial +- `sentry trial list []` — List product trials +- `sentry trial start []` — Start a product trial → Full flags and examples: `references/trial.md` @@ -719,7 +719,7 @@ Manage product trials Initialize Sentry in your project (experimental) -- `sentry init ` — Initialize Sentry in your project (experimental) +- `sentry init [] []` — Initialize Sentry in your project (experimental) → Full flags and examples: `references/init.md` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md index 0ba3bdab8..3fc4cc1eb 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md @@ -11,7 +11,7 @@ requires: List and view agent conversations -### `sentry agent-conversation list ` +### `sentry agent-conversation list []` List recent agent conversations @@ -64,7 +64,7 @@ sentry agent-conversation list -q "has:errors" sentry agent-conversation list my-org -c next ``` -### `sentry agent-conversation view ` +### `sentry agent-conversation view [/]` View an agent conversation transcript @@ -74,10 +74,10 @@ View an agent conversation transcript **Examples:** ```bash -# View full transcript (org auto-detected) +# View full transcript (organization auto-detected) sentry agent-conversation view conv-123 -# Explicit org (slash-separated) +# Explicit organization sentry agent-conversation view my-org/conv-123 # JSON output diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md index e742f6276..75108dc2d 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md @@ -11,7 +11,7 @@ requires: Manage Sentry alert rules -### `sentry alert issues list ` +### `sentry alert issues list []` List issue alert rules @@ -49,7 +49,7 @@ sentry alert issues view my-org/my-project/12345 sentry alert issues view my-org/my-project/"Error Spike" ``` -### `sentry alert issues create ` +### `sentry alert issues create []` Create an issue alert rule @@ -112,7 +112,7 @@ Edit an issue alert rule sentry alert issues edit my-org/my-project/12345 --name "Prod Error Spike" --status disabled ``` -### `sentry alert metrics list ` +### `sentry alert metrics list []` List metric alert rules diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md index 8798683ce..3025e135f 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md @@ -11,7 +11,7 @@ requires: CLI-related commands -### `sentry cli completion ` +### `sentry cli completion []` Print the shell completion script @@ -172,7 +172,7 @@ sentry cli uninstall --yes --keep-config sentry cli uninstall ``` -### `sentry cli upgrade ` +### `sentry cli upgrade []` Update the Sentry CLI to the latest version diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dsn.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dsn.md index 042f65e72..cc9caabd1 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dsn.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dsn.md @@ -11,7 +11,7 @@ requires: Find Sentry DSNs -### `sentry dsn list ` +### `sentry dsn list []` List DSNs diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md index 745ab082f..3b144b770 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -11,7 +11,7 @@ requires: Query aggregate event data (Explore) -### `sentry explore ` +### `sentry explore []` Query aggregate event data (Explore) diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md index f6822c432..7e3ba03aa 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md @@ -11,7 +11,7 @@ requires: Manage User Feedback -### `sentry feedback list ` +### `sentry feedback list []` List and search User Feedback diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md index 491f3c1c3..d04ccc67c 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md @@ -11,7 +11,7 @@ requires: Initialize Sentry in your project (experimental) -### `sentry init ` +### `sentry init [] []` Initialize Sentry in your project (experimental) diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 9ea2c76db..a674c3a48 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -11,7 +11,7 @@ requires: Manage Sentry issues -### `sentry issue list ` +### `sentry issue list []` List issues in a project diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md index 5288bd644..c7d4b573a 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md @@ -25,7 +25,7 @@ Wrap a command with cron monitor check-ins - `--failure-issue-threshold - Consecutive failures before an issue is created (requires --schedule)` - `--recovery-threshold - Consecutive successes before an issue is resolved (requires --schedule)` -### `sentry monitor list ` +### `sentry monitor list []` List cron monitors diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md index 285a19849..1eaf2afbe 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md @@ -19,7 +19,7 @@ List organizations - `-n, --limit - Maximum number of organizations to list - (default: "25")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` -### `sentry org view ` +### `sentry org view []` View details of an organization diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md index 4609b61ec..b3a2ba519 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md @@ -55,7 +55,7 @@ sentry project delete my-org/old-project sentry project delete my-org/old-project --yes ``` -### `sentry project list ` +### `sentry project list []` List projects @@ -65,7 +65,7 @@ List projects - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` -### `sentry project view ` +### `sentry project view []` View details of a project diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md index 51b33b3d3..e8b750e73 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md @@ -11,7 +11,7 @@ requires: Work with Sentry releases -### `sentry release list ` +### `sentry release list []` List releases with adoption and health metrics @@ -74,7 +74,7 @@ Restore an archived release **Flags:** - `-n, --dry-run - Show what would happen without making changes` -### `sentry release deploy ` +### `sentry release deploy []` Create a deploy for a release diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md index ebb72a753..d332a2798 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md @@ -11,7 +11,7 @@ requires: Search and inspect Session Replays -### `sentry replay list ` +### `sentry replay list []` List recent Session Replays diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md index 03ff3b504..0e1c779b9 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md @@ -11,7 +11,7 @@ requires: Work with Sentry repositories -### `sentry repo list ` +### `sentry repo list []` List repositories diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md index a074f709b..4e923f18c 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md @@ -11,7 +11,7 @@ requires: Work with Sentry teams -### `sentry team list ` +### `sentry team list []` List teams diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md index 67112896a..577995ca6 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md @@ -11,7 +11,7 @@ requires: View distributed traces -### `sentry trace list ` +### `sentry trace list []` List recent traces in a project diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md index 3b857dddc..af511f1ab 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md @@ -11,7 +11,7 @@ requires: Manage product trials -### `sentry trial list ` +### `sentry trial list []` List product trials @@ -26,7 +26,7 @@ List product trials | `isStarted` | boolean | Whether the trial has started | | `lengthDays` | number \| null | Trial duration in days | -### `sentry trial start ` +### `sentry trial start []` Start a product trial diff --git a/packages/cli/script/check-fragments.ts b/packages/cli/script/check-fragments.ts index 5137b7eae..64b3a9abe 100644 --- a/packages/cli/script/check-fragments.ts +++ b/packages/cli/script/check-fragments.ts @@ -263,6 +263,9 @@ for (const [routeName, route] of multiCommandRoutes) { const missing: string[] = []; for (const cmd of route.commands) { + if (cmd.examples.length > 0) { + continue; + } const sub = cmd.path.slice(`sentry ${routeName} `.length); const leaf = sub.split(" ").at(-1) ?? sub; const isDefault = leaf === defaultCmd; diff --git a/packages/cli/script/generate-command-docs.ts b/packages/cli/script/generate-command-docs.ts index ad17639e1..e09d28d39 100644 --- a/packages/cli/script/generate-command-docs.ts +++ b/packages/cli/script/generate-command-docs.ts @@ -25,6 +25,10 @@ import { mkdirSync, rmSync } from "node:fs"; import { access, readFile, writeFile } from "node:fs/promises"; +import { + formatCommandArguments, + formatCommandExamples, +} from "./generate-skill-markdown.js"; import { DOCS_CONTENT, DOCS_FRAGMENTS } from "./paths.js"; // Ensure src/generated/skill-content.ts exists before importing the route tree. @@ -49,7 +53,6 @@ import type { EnvVarEntry } from "../src/lib/env-registry.js"; import type { CommandInfo, FlagInfo, - PositionalInfo, RouteInfo, RouteMap, } from "../src/lib/introspect.js"; @@ -134,32 +137,6 @@ function formatFlagRow( return `| \`${syntax}\` | ${desc} |`; } -/** - * Escape angle brackets in text so they render as literal `<` / `>` - * in HTML output rather than being interpreted as HTML tags. - */ -function escapeAngleBrackets(text: string): string { - return text.replace(//g, ">"); -} - -/** Format positional arguments as a markdown table */ -function formatPositionalsTable(positionals: PositionalInfo[]): string { - if (positionals.length === 0) { - return ""; - } - - const lines: string[] = []; - lines.push("**Arguments:**"); - lines.push(""); - lines.push("| Argument | Description |"); - lines.push("|----------|-------------|"); - for (const p of positionals) { - const placeholder = `\`<${p.placeholder}>\``; - lines.push(`| ${placeholder} | ${escapeAngleBrackets(p.brief)} |`); - } - return lines.join("\n"); -} - /** Format flags as a markdown options table */ function formatFlagsTable( flags: FlagInfo[], @@ -197,7 +174,7 @@ function generateCommandSection(cmd: CommandInfo): string { // Arguments table if (cmd.positionals.length > 0) { lines.push(""); - lines.push(formatPositionalsTable(cmd.positionals)); + lines.push(formatCommandArguments(cmd.positionals)); } // Options table @@ -207,6 +184,11 @@ function generateCommandSection(cmd: CommandInfo): string { lines.push(formatFlagsTable(visibleFlags, cmd.aliases)); } + const examples = formatCommandExamples(cmd.examples); + if (examples) { + lines.push("", examples); + } + return lines.join("\n"); } diff --git a/packages/cli/script/generate-skill-markdown.ts b/packages/cli/script/generate-skill-markdown.ts index 4b9c24782..f470c079f 100644 --- a/packages/cli/script/generate-skill-markdown.ts +++ b/packages/cli/script/generate-skill-markdown.ts @@ -1,6 +1,7 @@ /** * Markdown parsing helpers shared by the skill generator and its tests. */ +import type { PositionalInfo } from "../src/lib/introspect.js"; /** Matches a generated command heading and stops before positional usage. */ const COMMAND_HEADING_RE = @@ -35,3 +36,36 @@ export function matchExampleToCommand( // one exists (login), not on a synthetic group-only path. return defaultCommandPath ?? groupFallback; } + +/** Render structured command examples in the same form for docs and skills. */ +export function formatCommandExamples(examples: readonly string[]): string { + if (examples.length === 0) { + return ""; + } + return ["**Examples:**", "", "```bash", examples.join("\n\n"), "```"].join( + "\n", + ); +} + +/** Format canonical positional syntax as a Markdown argument table. */ +export function formatCommandArguments( + positionals: readonly PositionalInfo[], +): string { + if (positionals.length === 0) { + return ""; + } + const lines = [ + "**Arguments:**", + "", + "| Argument | Description |", + "|----------|-------------|", + ]; + for (const positional of positionals) { + const value = `<${positional.placeholder}${positional.variadic ? "..." : ""}>`; + const syntax = + positional.syntax ?? (positional.optional ? `[${value}]` : value); + const brief = positional.brief.replace(//g, ">"); + lines.push(`| \`${syntax}\` | ${brief} |`); + } + return lines.join("\n"); +} diff --git a/packages/cli/script/generate-skill.ts b/packages/cli/script/generate-skill.ts index ce5993acd..2ebe69b2b 100644 --- a/packages/cli/script/generate-skill.ts +++ b/packages/cli/script/generate-skill.ts @@ -27,6 +27,7 @@ import type { Token } from "marked"; import { marked } from "marked"; import { extractCommandPathFromHeading, + formatCommandExamples, matchExampleToCommand, } from "./generate-skill-markdown.js"; import { DOCS_CONTENT, DOCS_PUBLIC } from "./paths.js"; @@ -596,13 +597,9 @@ function generateFullCommandDoc(cmd: CommandInfo): string { } } - if (cmd.examples.length > 0) { - lines.push(""); - lines.push("**Examples:**"); - lines.push(""); - lines.push("```bash"); - lines.push(cmd.examples.join("\n\n")); - lines.push("```"); + const examples = formatCommandExamples(cmd.examples); + if (examples) { + lines.push("", examples); } return lines.join("\n"); diff --git a/packages/cli/src/commands/agent-conversation/list.ts b/packages/cli/src/commands/agent-conversation/list.ts index d122fb385..a91abb263 100644 --- a/packages/cli/src/commands/agent-conversation/list.ts +++ b/packages/cli/src/commands/agent-conversation/list.ts @@ -17,6 +17,8 @@ import { ContextError } from "../../lib/errors.js"; import { formatConversationTable } from "../../lib/formatters/conversation.js"; import { filterFields } from "../../lib/formatters/json.js"; import { CommandOutput } from "../../lib/formatters/output.js"; +import { validateResourceId } from "../../lib/input-validation.js"; +import { getPositionalString } from "../../lib/introspect.js"; import { buildListCommand, LIST_DEFAULT_LIMIT, @@ -60,6 +62,18 @@ type ConversationListResult = { const COMMAND_NAME = "agent-conversation list"; const PAGINATION_KEY = "agent-conversation-list"; const DEFAULT_PERIOD = "7d"; +const POSITIONAL = { + kind: "tuple", + parameters: [ + { + placeholder: "org", + brief: "Organization slug", + parse: String, + optional: true, + }, + ], +} as const; +const USAGE_HINT = `sentry ${COMMAND_NAME} ${getPositionalString(POSITIONAL)}`; function parseLimit(value: string): number { return validateLimit(value, LIST_MIN_LIMIT, LIST_MAX_LIMIT); @@ -100,12 +114,29 @@ export const listCommand = buildListCommand("agent-conversation", { brief: "List recent agent conversations", fullDescription: "List recent agent conversations from a Sentry organization.\n\n" + - "Examples:\n" + - " sentry agent-conversation list # List recent conversations\n" + - " sentry agent-conversation list my-org # Explicit org\n" + - " sentry agent-conversation list --limit 50 # Show more\n" + - " sentry agent-conversation list --period 24h # Last 24 hours\n" + - ' sentry agent-conversation list -q "has:errors" # Filter\n', + "The organization is auto-detected when omitted.", + examples: [ + { + description: "List recent agent conversations", + command: "sentry agent-conversation list", + }, + { + description: "Explicit organization", + command: "sentry agent-conversation list my-org", + }, + { + description: "Show more, last 24 hours", + command: "sentry agent-conversation list --limit 50 --period 24h", + }, + { + description: "Filter conversations", + command: 'sentry agent-conversation list -q "has:errors"', + }, + { + description: "Paginate through results", + command: "sentry agent-conversation list my-org -c next", + }, + ], }, output: { human: formatListHuman, @@ -113,17 +144,7 @@ export const listCommand = buildListCommand("agent-conversation", { schema: ConversationListItemSchema, }, parameters: { - positional: { - kind: "tuple", - parameters: [ - { - placeholder: "org", - brief: "Organization slug", - parse: String, - optional: true, - }, - ], - }, + positional: POSITIONAL, flags: { limit: { kind: "parsed", @@ -147,10 +168,13 @@ export const listCommand = buildListCommand("agent-conversation", { }, async *func(this: SentryContext, flags: ListFlags, target?: string) { const { cwd } = this; + if (target !== undefined) { + validateResourceId(target, "organization slug"); + } const resolved = await resolveOrg({ org: target, cwd }); if (!resolved) { - throw new ContextError("Organization", `sentry ${COMMAND_NAME} `); + throw new ContextError("Organization", USAGE_HINT); } const org = resolved.org; diff --git a/packages/cli/src/commands/agent-conversation/view.ts b/packages/cli/src/commands/agent-conversation/view.ts index 29423a37f..64e7e432d 100644 --- a/packages/cli/src/commands/agent-conversation/view.ts +++ b/packages/cli/src/commands/agent-conversation/view.ts @@ -7,13 +7,14 @@ import type { SentryContext } from "../../context.js"; import { getConversationSpans } from "../../lib/api-client.js"; import { buildCommand } from "../../lib/command.js"; -import { ContextError } from "../../lib/errors.js"; +import { ContextError, validationError } from "../../lib/errors.js"; import { buildTranscriptResult, formatTranscriptResult, type TranscriptResult, } from "../../lib/formatters/conversation.js"; import { CommandOutput } from "../../lib/formatters/output.js"; +import { validateResourceId } from "../../lib/input-validation.js"; import { applyFreshFlag, FRESH_ALIASES, @@ -27,7 +28,8 @@ type ViewFlags = { readonly fresh: boolean; }; -const USAGE_HINT = "sentry agent-conversation view [/]"; +const USAGE = "[/]"; +const USAGE_HINT = `sentry agent-conversation view ${USAGE}`; /** * Split a `[/]` positional into its parts. @@ -37,7 +39,7 @@ const USAGE_HINT = "sentry agent-conversation view [/]"; * before the first `/` is the org, the remainder is the conversation ID. With * no slash the whole value is the conversation ID and the org is auto-detected. * - * @throws {ContextError} When the conversation ID segment is empty. + * @throws {ValidationError} When the target contains empty segments or multiple slashes. */ function parseConversationTarget(target: string): { org?: string; @@ -46,27 +48,52 @@ function parseConversationTarget(target: string): { const trimmed = target.trim(); const slashIdx = trimmed.indexOf("/"); if (slashIdx === -1) { + validateResourceId(trimmed, "conversation ID"); return { conversationId: trimmed }; } + if (slashIdx !== trimmed.lastIndexOf("/")) { + throw validationError( + "Conversation target must contain at most one '/'.", + [USAGE_HINT], + "conversation-id", + ); + } const org = trimmed.slice(0, slashIdx); const conversationId = trimmed.slice(slashIdx + 1); if (!(org && conversationId)) { - throw new ContextError("Conversation ID", USAGE_HINT, []); + throw validationError( + "Conversation target must include both an organization and conversation ID when using '/'.", + [USAGE_HINT], + "conversation-id", + ); } + validateResourceId(org, "organization slug"); + validateResourceId(conversationId, "conversation ID"); return { org, conversationId }; } export const viewCommand = buildCommand({ docs: { brief: "View an agent conversation transcript", + customUsage: [USAGE], fullDescription: "View the full transcript of an agent conversation.\n\n" + "The org is optional and auto-detected from your project context when\n" + - "omitted. Prefix the ID with an org slug to target a specific org.\n\n" + - "Examples:\n" + - " sentry agent-conversation view conv-123\n" + - " sentry agent-conversation view my-org/conv-123\n" + - " sentry agent-conversation view my-org/conv-123 --json\n", + "omitted. Prefix the ID with an org slug to target a specific org.", + examples: [ + { + description: "View full transcript (organization auto-detected)", + command: "sentry agent-conversation view conv-123", + }, + { + description: "Explicit organization", + command: "sentry agent-conversation view my-org/conv-123", + }, + { + description: "JSON output", + command: "sentry agent-conversation view my-org/conv-123 --json", + }, + ], }, output: { human: formatTranscriptResult, @@ -77,8 +104,7 @@ export const viewCommand = buildCommand({ parameters: [ { placeholder: "org/conversation-id", - brief: - "[/] - Org (optional) and conversation ID", + brief: "Organization slug (optional) and conversation ID", parse: String, }, ], @@ -92,9 +118,16 @@ export const viewCommand = buildCommand({ applyFreshFlag(flags); const { cwd } = this; - if (!target?.trim()) { + if (target === undefined) { throw new ContextError("Conversation ID", USAGE_HINT, []); } + if (!target.trim()) { + throw validationError( + "Conversation ID cannot be empty.", + [USAGE_HINT], + "conversation-id", + ); + } const { org: orgArg, conversationId } = parseConversationTarget(target); const resolved = await resolveOrg({ org: orgArg, cwd }); diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index bc120ea56..e5ef0d76a 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -94,12 +94,19 @@ type BaseArgs = readonly unknown[]; type StricliBuilderArgs = import("@stricli/core").CommandBuilderArguments; +/** A runnable example stored with its command for generated documentation. */ +export type CommandExample = { + readonly description: string; + readonly command: string; +}; + /** - * Native Stricli documentation. When `customUsage` is present, its first line - * must be the canonical signature suffix used by introspection and generated - * docs; later lines may document equivalent forms. + * Native Stricli documentation plus canonical examples for generated docs. */ -type CommandDocumentation = StricliBuilderArgs["docs"]; +export type CommandDocumentation = + StricliBuilderArgs["docs"] & { + readonly examples?: readonly CommandExample[]; + }; /** * Command function type for Sentry CLI commands. @@ -425,6 +432,23 @@ function enrichDocsWithSchema( }; } +/** Render command-owned examples in native help without exposing custom fields to Stricli. */ +function prepareNativeDocs( + docs: CommandDocumentation, +): StricliBuilderArgs["docs"] { + const { examples, ...nativeDocs } = docs; + if (!examples?.length) { + return nativeDocs; + } + const rendered = examples + .map(({ description, command }) => ` ${command} # ${description}`) + .join("\n"); + return { + ...nativeDocs, + fullDescription: `${nativeDocs.fullDescription ?? nativeDocs.brief}\n\nExamples:\n${rendered}`, + }; +} + /** * Global flag defaults keyed by flag name. * Used by {@link mergeGlobalFlags} to inject flags when the command @@ -512,9 +536,10 @@ export function buildCommand< const { mergedFlags, commandOwnsOrg, commandOwnsProject, stripKeys } = mergeGlobalFlags(existingFlags, outputConfig); - // Enrich fullDescription with JSON fields when schema is registered. - // This makes field info visible in Stricli's --help output. - const enrichedDocs = enrichDocsWithSchema(builderArgs.docs, outputConfig); + const enrichedDocs = enrichDocsWithSchema( + prepareNativeDocs(builderArgs.docs), + outputConfig, + ); // Inject short aliases for global flags (e.g., -v → --verbose). // Derived from the shared GLOBAL_FLAGS definition so adding a new @@ -845,6 +870,10 @@ export function buildCommand< (cmd as unknown as Record).__primaryUsage = typeof primaryUsage === "string" ? primaryUsage : primaryUsage.input; } + if (builderArgs.docs.examples?.length) { + (cmd as unknown as Record).__examples = + builderArgs.docs.examples; + } // Attach the JSON schema to the built command as a non-standard property. // introspect.ts reads this to populate CommandInfo.jsonFields for help diff --git a/packages/cli/src/lib/introspect.ts b/packages/cli/src/lib/introspect.ts index a6040ffc9..b907bf2a0 100644 --- a/packages/cli/src/lib/introspect.ts +++ b/packages/cli/src/lib/introspect.ts @@ -10,6 +10,7 @@ * for introspection and documentation generation. */ +import type { CommandExample } from "./command.js"; import { extractSchemaFields, type SchemaFieldInfo, @@ -59,6 +60,7 @@ export type Command = { * introspection because Stricli does not expose it on the built command. */ __primaryUsage?: string; + __examples?: readonly CommandExample[]; }; /** Positional parameter definitions — either fixed-length tuple or variadic array */ @@ -75,9 +77,14 @@ export type PositionalParam = { /** Extracted metadata for a single positional argument */ export type PositionalInfo = { + /** Placeholder text without angle brackets or an ellipsis. */ placeholder: string; brief: string; + /** Only fixed tuple parameters can be omitted. */ optional: boolean; + variadic: boolean; + /** Canonical public syntax for compound positional forms. */ + syntax?: string; }; /** Flag definition as stored in Stricli's command parameters */ @@ -196,7 +203,10 @@ export function getPositionalString(params?: PositionalParams): string { if (params.kind === "tuple") { return params.parameters - .map((p, i) => `<${p.placeholder ?? `arg${i}`}>`) + .map((p, i) => { + const value = `<${p.placeholder ?? `arg${i}`}>`; + return p.optional ? `[${value}]` : value; + }) .join(" "); } @@ -230,15 +240,17 @@ export function extractPositionals( placeholder: p.placeholder ?? `arg${i}`, brief: p.brief ?? "", optional: p.optional ?? false, + variadic: false, })); } if (params.kind === "array") { return [ { - placeholder: `${params.parameter.placeholder ?? "args"}...`, + placeholder: params.parameter.placeholder ?? "args", brief: params.parameter.brief ?? "", - optional: true, + optional: false, + variadic: true, }, ]; } @@ -285,6 +297,11 @@ export function buildCommandInfo( const jsonFields = cmd.__jsonSchema ? extractSchemaFields(cmd.__jsonSchema) : undefined; + const positionals = extractPositionals(cmd.parameters.positional); + const [positional] = positionals; + if (cmd.__primaryUsage && positional && positionals.length === 1) { + positionals[0] = { ...positional, syntax: cmd.__primaryUsage }; + } return { path, @@ -293,9 +310,13 @@ export function buildCommandInfo( flags: extractFlags(cmd.parameters.flags), positional: cmd.__primaryUsage ?? getPositionalString(cmd.parameters.positional), - positionals: extractPositionals(cmd.parameters.positional), + positionals, aliases: cmd.parameters.aliases ?? {}, - examples, + examples: cmd.__examples?.length + ? cmd.__examples.map( + ({ description, command }) => `# ${description}\n${command}`, + ) + : examples, jsonFields: jsonFields?.length ? jsonFields : undefined, }; } diff --git a/packages/cli/src/lib/list-command.ts b/packages/cli/src/lib/list-command.ts index 8b70e9b22..142a0b562 100644 --- a/packages/cli/src/lib/list-command.ts +++ b/packages/cli/src/lib/list-command.ts @@ -20,7 +20,11 @@ import type { SentryContext } from "../context.js"; const _require = createRequire(import.meta.url); import { parseOrgProjectArg } from "./arg-parsing.js"; -import { buildCommand, numberParser } from "./command.js"; +import { + buildCommand, + type CommandDocumentation, + numberParser, +} from "./command.js"; import { disableOrgCache } from "./db/regions.js"; import { logger } from "./logger.js"; @@ -546,10 +550,7 @@ export function buildListCommand< routeName: string, builderArgs: { readonly parameters?: Record; - readonly docs: { - readonly brief: string; - readonly fullDescription?: string; - }; + readonly docs: CommandDocumentation; readonly func: ListCommandFunction; // oxlint-disable-next-line typescript/no-explicit-any -- OutputConfig is generic but type is erased at the builder level readonly output?: OutputConfig; @@ -627,13 +628,8 @@ export function buildListCommand< // Level D: full command builder for dispatchOrgScopedList-based commands // --------------------------------------------------------------------------- -/** Documentation strings for a list command built with `buildOrgListCommand`. */ -export type OrgListCommandDocs = { - /** One-line description shown in `--help` summaries. */ - readonly brief: string; - /** Multi-line description shown in the command's own `--help` output. */ - readonly fullDescription?: string; -}; +/** Documentation for a list command built with `buildOrgListCommand`. */ +export type OrgListCommandDocs = CommandDocumentation; /** * Format a {@link ListResult} as human-readable output using the config's diff --git a/packages/cli/src/lib/mutate-command.ts b/packages/cli/src/lib/mutate-command.ts index 14332540e..f79a24ba6 100644 --- a/packages/cli/src/lib/mutate-command.ts +++ b/packages/cli/src/lib/mutate-command.ts @@ -19,7 +19,7 @@ import { isatty } from "node:tty"; import type { Command, CommandContext } from "@stricli/core"; import type { ParsedOrgProject } from "./arg-parsing.js"; -import { buildCommand } from "./command.js"; +import { buildCommand, type CommandDocumentation } from "./command.js"; import { CliError, ContextError } from "./errors.js"; import type { CommandReturn } from "./formatters/output.js"; import { logger } from "./logger.js"; @@ -332,10 +332,7 @@ export function buildDeleteCommand< >( builderArgs: { readonly parameters?: Record; - readonly docs: { - readonly brief: string; - readonly fullDescription?: string; - }; + readonly docs: CommandDocumentation; readonly func: DeleteCommandFunction; // oxlint-disable-next-line typescript/no-explicit-any -- OutputConfig is generic but type is erased at the builder level readonly output?: import("./formatters/output.js").OutputConfig; diff --git a/packages/cli/test/commands/agent-conversation/list.test.ts b/packages/cli/test/commands/agent-conversation/list.test.ts index f8d6dae71..476dd14e3 100644 --- a/packages/cli/test/commands/agent-conversation/list.test.ts +++ b/packages/cli/test/commands/agent-conversation/list.test.ts @@ -44,7 +44,7 @@ vi.mock("../../../src/lib/db/auth.js", async (importOriginal) => { // oxlint-disable-next-line sentry-cli/no-namespace-import -- needed for spyOn mocking import * as dbAuth from "../../../src/lib/db/auth.js"; -import { ContextError } from "../../../src/lib/errors.js"; +import { ContextError, ValidationError } from "../../../src/lib/errors.js"; vi.mock("../../../src/lib/polling.js", async (importOriginal) => { const actual = @@ -280,6 +280,36 @@ describe("listCommand.func", () => { ); }); + test("uses the optional-org syntax in resolution errors", async () => { + resolveOrgSpy.mockResolvedValue(null); + const { context } = createMockContext(); + const func = await listCommand.loader(); + await expect( + func.call(context, HUMAN_FLAGS, undefined), + ).rejects.toMatchObject({ + command: "sentry agent-conversation list []", + }); + }); + + test.each([ + "acme?x=1", + "acme#fragment", + "acme%20bad", + "acme bad", + "acme\tbad", + ])( + "rejects unsafe explicit organization %s before resolution", + async (target) => { + const { context } = createMockContext(); + const func = await listCommand.loader(); + await expect(func.call(context, HUMAN_FLAGS, target)).rejects.toThrow( + ValidationError, + ); + expect(resolveOrgSpy).not.toHaveBeenCalled(); + expect(listConversationsSpy).not.toHaveBeenCalled(); + }, + ); + test("yields CommandOutput with conversation data (JSON)", async () => { listConversationsSpy.mockResolvedValue({ data: sampleConversations, diff --git a/packages/cli/test/commands/agent-conversation/view.test.ts b/packages/cli/test/commands/agent-conversation/view.test.ts index 57ec32e75..7fa0f9c7e 100644 --- a/packages/cli/test/commands/agent-conversation/view.test.ts +++ b/packages/cli/test/commands/agent-conversation/view.test.ts @@ -68,7 +68,7 @@ vi.mock("../../../src/lib/resolve-target.js", async (importOriginal) => { ); }); -import { ContextError } from "../../../src/lib/errors.js"; +import { ContextError, ValidationError } from "../../../src/lib/errors.js"; // oxlint-disable-next-line sentry-cli/no-namespace-import -- needed for spyOn mocking import * as resolveTarget from "../../../src/lib/resolve-target.js"; import type { AgentConversationSpan } from "../../../src/types/conversation.js"; @@ -236,6 +236,37 @@ describe("viewCommand.func", () => { ); }); + test.each([ + `${ORG}/${CONVERSATION_ID}/extra`, + `${ORG}//${CONVERSATION_ID}`, + `/${CONVERSATION_ID}`, + `${ORG}/`, + `acme?x=1/${CONVERSATION_ID}`, + `acme#fragment/${CONVERSATION_ID}`, + `acme%20bad/${CONVERSATION_ID}`, + `acme bad/${CONVERSATION_ID}`, + `${ORG}/conv?x=1`, + `${ORG}/conv#fragment`, + `${ORG}/conv%20bad`, + `${ORG}/conv\tbad`, + "conv?x=1", + "conv#fragment", + "conv%20bad", + "conv bad", + "conv\tbad", + ])( + "rejects malformed target %s before resolution or API access", + async (target) => { + const { context } = createMockContext(); + const func = await viewCommand.loader(); + await expect(func.call(context, HUMAN_FLAGS, target)).rejects.toThrow( + ValidationError, + ); + expect(resolveOrgSpy).not.toHaveBeenCalled(); + expect(getConversationSpansSpy).not.toHaveBeenCalled(); + }, + ); + test("throws error when no args provided", async () => { const { context } = createMockContext(); const func = await viewCommand.loader(); diff --git a/packages/cli/test/lib/introspect.test.ts b/packages/cli/test/lib/introspect.test.ts index 0de1ddb83..b788294a2 100644 --- a/packages/cli/test/lib/introspect.test.ts +++ b/packages/cli/test/lib/introspect.test.ts @@ -16,6 +16,7 @@ import { buildCommandInfo, extractAllRoutes, extractFlags, + extractPositionals, extractRouteGroupCommands, getPositionalString, isCommand, @@ -114,6 +115,18 @@ describe("getPositionalString", () => { expect(result).toBe(" "); }); + test("distinguishes optional tuple arguments from required ones", () => { + expect( + getPositionalString({ + kind: "tuple", + parameters: [ + { placeholder: "target" }, + { placeholder: "org", optional: true }, + ], + }), + ).toBe(" []"); + }); + test("returns array placeholder with ellipsis", () => { const result = getPositionalString({ kind: "array", @@ -131,6 +144,29 @@ describe("getPositionalString", () => { }); }); +describe("extractPositionals", () => { + test("keeps arrays required and variadic even when the parser accepts zero", () => { + expect( + extractPositionals({ + kind: "array", + parameter: { placeholder: "issue" }, + }), + ).toEqual([ + { placeholder: "issue", brief: "", optional: false, variadic: true }, + ]); + }); + test("preserves optional tuple arguments", () => { + expect( + extractPositionals({ + kind: "tuple", + parameters: [{ placeholder: "org", optional: true }], + }), + ).toEqual([ + { placeholder: "org", brief: "", optional: true, variadic: false }, + ]); + }); +}); + // --------------------------------------------------------------------------- // extractFlags // --------------------------------------------------------------------------- @@ -247,6 +283,12 @@ describe("buildCommandInfo", () => { const info = buildCommandInfo(cmd, "sentry project create"); expect(info.positional).toBe(":..."); + expect(info.positionals[0]).toMatchObject({ + placeholder: "name:kind", + optional: false, + variadic: true, + syntax: ":...", + }); }); }); diff --git a/packages/cli/test/lib/scanner-flags.integration.test.ts b/packages/cli/test/lib/scanner-flags.integration.test.ts index 9e1b04ba9..8571d7fb0 100644 --- a/packages/cli/test/lib/scanner-flags.integration.test.ts +++ b/packages/cli/test/lib/scanner-flags.integration.test.ts @@ -47,21 +47,22 @@ async function runApp( return true; }, }; - const context: SentryContext = { - process: { - ...process, - // Route the underlying process streams into our buffers too — Stricli's - // argument-scanner errors write to context.process.stderr, not context.stderr. - stdout: { - write(data: string | Uint8Array) { - stdout += - typeof data === "string" ? data : new TextDecoder().decode(data); - return true; - }, + const mockProcess = { + ...process, + // Route the underlying process streams into our buffers too — Stricli's + // argument-scanner errors write to context.process.stderr, not context.stderr. + stdout: { + write(data: string | Uint8Array) { + stdout += + typeof data === "string" ? data : new TextDecoder().decode(data); + return true; }, - stderr: captureStderr, - exitCode: undefined, - } as unknown as typeof process, + }, + stderr: captureStderr, + exitCode: undefined, + } as unknown as typeof process; + const context: SentryContext = { + process: mockProcess, env: { ...process.env }, cwd: emptyCwd, homeDir: "/tmp", @@ -77,8 +78,8 @@ async function runApp( stdin: process.stdin, }; - const exitCode = await run(app, args, context); - return { stdout, stderr, exitCode: exitCode ?? 0 }; + await run(app, args, context); + return { stdout, stderr, exitCode: mockProcess.exitCode ?? 0 }; } /** @@ -87,6 +88,15 @@ async function runApp( */ const NO_COMMAND_REGISTERED = "No command registered"; +describe("agent-conversation view required target", () => { + test("rejects missing target at the parser before authentication", async () => { + const { stderr, exitCode } = await runApp(["agent-conversation", "view"]); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Expected argument"); + expect(stderr).not.toMatch(/not authenticated|log in|authorization/i); + }); +}); + describe("top-level flags on a leaf command (bash-hook, no auth)", () => { // bash-hook runs without auth and emits its script to stdout, so a successful // route + execution is observable regardless of where the global flag sits. diff --git a/packages/cli/test/script/generate-skill-markdown.test.ts b/packages/cli/test/script/generate-skill-markdown.test.ts index ee7e6ea18..78265a027 100644 --- a/packages/cli/test/script/generate-skill-markdown.test.ts +++ b/packages/cli/test/script/generate-skill-markdown.test.ts @@ -4,8 +4,16 @@ import { lstat, readFile, realpath } from "node:fs/promises"; import { describe, expect, test } from "vitest"; import { extractCommandPathFromHeading, + formatCommandArguments, + formatCommandExamples, matchExampleToCommand, } from "../../script/generate-skill-markdown.js"; +import { listCommand } from "../../src/commands/agent-conversation/list.js"; +import { viewCommand } from "../../src/commands/agent-conversation/view.js"; +import { sendCommand } from "../../src/commands/event/send.js"; +import { mergeCommand } from "../../src/commands/issue/merge.js"; +import { createCommand } from "../../src/commands/project/create.js"; +import { buildCommandInfo } from "../../src/lib/introspect.js"; describe("extractCommandPathFromHeading", () => { test.each([ @@ -26,6 +34,84 @@ describe("extractCommandPathFromHeading", () => { }); describe("matchExampleToCommand", () => { + test("derives conversation signatures and examples from command metadata", () => { + const list = buildCommandInfo( + listCommand as never, + "sentry agent-conversation list", + ); + const view = buildCommandInfo( + viewCommand as never, + "sentry agent-conversation view", + ); + expect(list.positional).toBe("[]"); + expect(list.examples).toContain( + "# Explicit organization\nsentry agent-conversation list my-org", + ); + expect(view.positional).toBe("[/]"); + expect(view.examples).toContain( + "# Explicit organization\nsentry agent-conversation view my-org/conv-123", + ); + expect(formatCommandExamples(view.examples)).toContain( + "sentry agent-conversation view conv-123", + ); + }); + + test("preserves tuple, variadic, and compound argument syntax", () => { + const info = (cmd: unknown, path: string) => + buildCommandInfo(cmd as never, path).positionals; + expect( + formatCommandArguments( + info(listCommand, "sentry agent-conversation list"), + ), + ).toContain("| `[]` | Organization slug |"); + expect( + formatCommandArguments( + info(viewCommand, "sentry agent-conversation view"), + ), + ).toContain( + "| `[/]` | Organization slug (optional) and conversation ID |", + ); + expect( + formatCommandArguments(info(sendCommand, "sentry event send")), + ).toContain( + "| `` | Optional DSN/project target followed by JSON event file path(s) |", + ); + expect( + formatCommandArguments(info(mergeCommand, "sentry issue merge")), + ).toContain("| `` | Issue IDs to merge (2 or more required) |"); + expect( + formatCommandArguments(info(createCommand, "sentry project create")), + ).toContain("| `[/]:...` |"); + }); + + test("generates matching command docs and skill references", async () => { + const [docs, skill] = await Promise.all([ + readFile( + "../../apps/cli-docs/src/content/docs/commands/agent-conversation.md", + "utf8", + ), + readFile( + "plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md", + "utf8", + ), + ]); + for (const content of [docs, skill]) { + expect(content).toContain( + "sentry agent-conversation view [/]", + ); + expect(content).toContain("sentry agent-conversation list []"); + expect(content).toContain( + "sentry agent-conversation view my-org/conv-123", + ); + expect(content).not.toContain( + "sentry agent-conversation view my-org conv-123", + ); + } + expect(docs).toContain( + "| `[/]` | Organization slug (optional) and conversation ID |", + ); + }); + test("associates a project create block with its command", () => { const code = [ "# Create projects",