From 993de4403acc6ac7e36f649092a266065b32dfc9 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:37:37 +0000 Subject: [PATCH 1/2] fix(help): simplify human command overview Co-Authored-By: Miguel Betegon --- packages/cli/src/lib/help.ts | 12 ++++++--- packages/cli/test/lib/help.test.ts | 39 +++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/lib/help.ts b/packages/cli/src/lib/help.ts index 5be883026..fd24abb18 100644 --- a/packages/cli/src/lib/help.ts +++ b/packages/cli/src/lib/help.ts @@ -9,6 +9,7 @@ import { routes } from "../app.js"; import { formatBanner } from "./banner.js"; import { isAuthenticated } from "./db/auth.js"; +import { detectAgent } from "./detect-agent.js"; import { TOP_LEVEL_ENV_VARS } from "./env-registry.js"; import { cyan, magenta, muted } from "./formatters/colors.js"; import { @@ -91,7 +92,7 @@ const COMMON_FLAGS: readonly CommonFlagEntry[] = [ * Generate the commands list dynamically from Stricli's route structure. * This ensures help text stays in sync with actual registered commands. */ -function generateCommands(): HelpCommand[] { +function generateCommands(includeSubcommands: boolean): HelpCommand[] { // Cast to our introspection types — Stricli's generic types are compatible const routeMap = routes as unknown as RouteMap; const entries = routeMap.getAllEntries(); @@ -111,7 +112,9 @@ function generateCommands(): HelpCommand[] { .map((sub: RouteMapEntry) => sub.name.original) .join(" | "); return { - usage: `sentry ${routeName} ${subNames}`, + usage: includeSubcommands + ? `sentry ${routeName} ${subNames}` + : `sentry ${routeName}`, description: brief, }; } @@ -227,8 +230,9 @@ export function printCustomHelp(): string { lines.push(` ${TAGLINE}`); lines.push(""); - // Commands (auto-generated from Stricli routes) - lines.push(formatCommands(generateCommands())); + // Keep the human overview scannable. Agent-driven runs retain inline + // subcommands so they can discover the full CLI with fewer help calls. + lines.push(formatCommands(generateCommands(Boolean(detectAgent())))); lines.push(""); // Common flags diff --git a/packages/cli/test/lib/help.test.ts b/packages/cli/test/lib/help.test.ts index 37955e7ac..481c689c5 100644 --- a/packages/cli/test/lib/help.test.ts +++ b/packages/cli/test/lib/help.test.ts @@ -5,8 +5,10 @@ * command generation from routes, and contextual examples. */ -import { describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test } from "vitest"; import { bannerLinesForWidth, formatBanner } from "../../src/lib/banner.js"; +import { ENV_VAR_AGENTS } from "../../src/lib/detect-agent.js"; +import { setEnv } from "../../src/lib/env.js"; import { introspectAllCommands, printCustomHelp } from "../../src/lib/help.js"; import { useTestConfigDir } from "../helpers.js"; @@ -17,6 +19,20 @@ function stripAnsi(str: string): string { return str.replace(ANSI_RE, ""); } +function withoutAgentEnv(): NodeJS.ProcessEnv { + const agentKeys = new Set([ + "AI_AGENT", + "AGENT", + "CLAUDECODE", + "CLAUDE_CODE", + "CURSOR_EXTENSION_HOST_ROLE", + ...ENV_VAR_AGENTS.keys(), + ]); + return Object.fromEntries( + Object.entries(process.env).filter(([key]) => !agentKeys.has(key)) + ); +} + /** Widest line (in code points) in a rendered banner, ignoring ANSI codes. */ function maxLineWidth(banner: string): number { return Math.max( @@ -67,6 +83,10 @@ describe("formatBanner", () => { describe("printCustomHelp", () => { useTestConfigDir("help-test-"); + afterEach(() => { + setEnv(process.env); + }); + test("returns non-empty string", async () => { const output = printCustomHelp(); expect(output.length).toBeGreaterThan(0); @@ -93,6 +113,23 @@ describe("printCustomHelp", () => { expect(output).toContain("cli.sentry.dev"); }); + test("hides route subcommands for human users", () => { + setEnv(withoutAgentEnv()); + + const output = stripAnsi(printCustomHelp()); + + expect(output).toMatch(/\$ sentry auth\s+Authenticate with Sentry/); + expect(output).not.toContain("sentry auth login | logout"); + }); + + test("shows route subcommands for agent-driven runs", () => { + setEnv({ ...withoutAgentEnv(), AI_AGENT: "test-agent" }); + + const output = stripAnsi(printCustomHelp()); + + expect(output).toContain("sentry auth login | logout"); + }); + test("includes the banner only when stdout is a TTY", () => { const savedTty = process.stdout.isTTY; try { From 2c5adf6753c30d017abbf4b813b8495af261230d Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:51:39 +0000 Subject: [PATCH 2/2] fix(help): feature relevant human commands --- packages/cli/src/lib/help.ts | 98 +++++++++++++++++++----------- packages/cli/test/lib/help.test.ts | 27 +++++--- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/packages/cli/src/lib/help.ts b/packages/cli/src/lib/help.ts index fd24abb18..0041eebcd 100644 --- a/packages/cli/src/lib/help.ts +++ b/packages/cli/src/lib/help.ts @@ -88,54 +88,80 @@ const COMMON_FLAGS: readonly CommonFlagEntry[] = [ }, ]; +/** Human-facing command order, capped to keep the root help scannable. */ +const HUMAN_COMMAND_ORDER = ["auth", "issue", "event", "trace", "log"] as const; + +/** Human-facing subcommand order for the featured route groups. */ +const HUMAN_SUBCOMMAND_ORDER: Readonly> = { + auth: ["login", "status", "whoami"], + issue: ["list", "view", "explain", "plan", "resolve"], + event: ["list", "view"], + trace: ["list", "view", "logs"], + log: ["list", "view"], +}; + /** * Generate the commands list dynamically from Stricli's route structure. * This ensures help text stays in sync with actual registered commands. */ -function generateCommands(includeSubcommands: boolean): HelpCommand[] { +function generateCommands(includeAllCommands: boolean): HelpCommand[] { // Cast to our introspection types — Stricli's generic types are compatible const routeMap = routes as unknown as RouteMap; const entries = routeMap.getAllEntries(); - return entries - .filter((entry: RouteMapEntry) => !entry.hidden) - .map((entry: RouteMapEntry) => { - const routeName = entry.name.original; - const brief = entry.target.brief; - - if (isRouteMap(entry.target)) { - // Get visible subcommand names and join with pipes - const subEntries = entry.target - .getAllEntries() - .filter((sub: RouteMapEntry) => !sub.hidden); - const subNames = subEntries - .map((sub: RouteMapEntry) => sub.name.original) - .join(" | "); - return { - usage: includeSubcommands - ? `sentry ${routeName} ${subNames}` - : `sentry ${routeName}`, - description: brief, - }; - } - - // Direct command - use any public syntax override before raw parameters - if (isCommand(entry.target)) { - const placeholder = - entry.target.__primaryUsage ?? - getPositionalString(entry.target.parameters.positional); - const usageSuffix = placeholder ? ` ${placeholder}` : ""; - return { - usage: `sentry ${routeName}${usageSuffix}`, - description: brief, - }; - } + const visibleEntries = entries.filter( + (entry: RouteMapEntry) => !entry.hidden + ); + const displayedEntries = includeAllCommands + ? visibleEntries + : HUMAN_COMMAND_ORDER.flatMap((name) => { + const entry = visibleEntries.find( + (candidate) => candidate.name.original === name + ); + return entry ? [entry] : []; + }); + return displayedEntries.map((entry: RouteMapEntry) => { + const routeName = entry.name.original; + const brief = entry.target.brief; + + if (isRouteMap(entry.target)) { + // Get visible subcommand names and join with pipes + const subEntries = entry.target + .getAllEntries() + .filter((sub: RouteMapEntry) => !sub.hidden); + const visibleSubNames = subEntries.map( + (sub: RouteMapEntry) => sub.name.original + ); + const preferredSubcommands = HUMAN_SUBCOMMAND_ORDER[routeName]; + const subNames = includeAllCommands + ? visibleSubNames + : (preferredSubcommands?.filter((name) => + visibleSubNames.includes(name) + ) ?? visibleSubNames.slice(0, 5)); return { - usage: `sentry ${routeName}`, + usage: `sentry ${routeName} ${subNames.join(" | ")}`, description: brief, }; - }); + } + + // Direct command - use any public syntax override before raw parameters + if (isCommand(entry.target)) { + const placeholder = + entry.target.__primaryUsage ?? + getPositionalString(entry.target.parameters.positional); + const usageSuffix = placeholder ? ` ${placeholder}` : ""; + return { + usage: `sentry ${routeName}${usageSuffix}`, + description: brief, + }; + } + + return { + usage: `sentry ${routeName}`, + description: brief, + }; + }); } const EXAMPLE_LOGGED_OUT = "sentry auth"; diff --git a/packages/cli/test/lib/help.test.ts b/packages/cli/test/lib/help.test.ts index 481c689c5..8ad444930 100644 --- a/packages/cli/test/lib/help.test.ts +++ b/packages/cli/test/lib/help.test.ts @@ -104,8 +104,7 @@ describe("printCustomHelp", () => { expect(output).toContain("sentry"); // Route map command (exercises isRouteMap branch) expect(output).toContain("auth"); - // Direct command with tuple positional (exercises isCommand + getPositionalPlaceholder) - expect(output).toContain("init"); + expect(output).toContain("event"); }); test("output contains docs URL", async () => { @@ -113,21 +112,35 @@ describe("printCustomHelp", () => { expect(output).toContain("cli.sentry.dev"); }); - test("hides route subcommands for human users", () => { + test("shows prioritized commands and subcommands for human users", () => { setEnv(withoutAgentEnv()); const output = stripAnsi(printCustomHelp()); - - expect(output).toMatch(/\$ sentry auth\s+Authenticate with Sentry/); - expect(output).not.toContain("sentry auth login | logout"); + const authIndex = output.indexOf("sentry auth login | status | whoami"); + const issueIndex = output.indexOf( + "sentry issue list | view | explain | plan | resolve" + ); + const eventIndex = output.indexOf("sentry event list | view"); + const traceIndex = output.indexOf("sentry trace list | view | logs"); + const logIndex = output.indexOf("sentry log list | view"); + + expect(authIndex).toBeGreaterThan(-1); + expect(issueIndex).toBeGreaterThan(authIndex); + expect(eventIndex).toBeGreaterThan(issueIndex); + expect(traceIndex).toBeGreaterThan(eventIndex); + expect(logIndex).toBeGreaterThan(traceIndex); + expect(output).not.toContain("sentry dashboard"); + expect(output).not.toContain("sentry project"); }); - test("shows route subcommands for agent-driven runs", () => { + test("shows all commands and subcommands for agent-driven runs", () => { setEnv({ ...withoutAgentEnv(), AI_AGENT: "test-agent" }); const output = stripAnsi(printCustomHelp()); expect(output).toContain("sentry auth login | logout"); + expect(output).toContain("sentry dashboard"); + expect(output).toContain("sentry project"); }); test("includes the banner only when stdout is a TTY", () => {