diff --git a/apps/cli-docs/src/fragments/configuration.md b/apps/cli-docs/src/fragments/configuration.md index f28662cce..76ffa8904 100644 --- a/apps/cli-docs/src/fragments/configuration.md +++ b/apps/cli-docs/src/fragments/configuration.md @@ -101,6 +101,22 @@ sentry issue list --verbose The `sentry api` command also uses `--verbose` to show full HTTP request/response details. When used with `sentry api`, it serves both purposes (debug logging + HTTP output). ::: +### `--no-tips` + +Suppress the `Tip: ...` footer hints that some commands print below their output (for example `sentry issue view`). Useful for scripting or when you find the extra guidance noisy. + +```bash +sentry issue view CLI-K9 --no-tips +``` + +You can also disable tips for every command by setting the `SENTRY_DISABLE_TIPS` environment variable: + +```bash +export SENTRY_DISABLE_TIPS=1 +``` + +The cache-age footer (`cached · 3m ago · use -f to refresh`) is a staleness indicator, not a tip, and is not affected. + ## Credential Storage We store credentials and caches in a SQLite database (`cli.db`) inside the config directory (`~/.sentry/` by default, overridable via `SENTRY_CONFIG_DIR`). The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches: diff --git a/packages/cli/script/generate-command-docs.ts b/packages/cli/script/generate-command-docs.ts index cdc8865be..4a3b0c5d3 100644 --- a/packages/cli/script/generate-command-docs.ts +++ b/packages/cli/script/generate-command-docs.ts @@ -82,6 +82,7 @@ const GLOBAL_FLAG_NAMES = new Set([ "help", "helpAll", "log-level", + "tips", ]); /** Routes that don't need their own documentation page */ diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 16f6e8cc6..b68e2c097 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -66,6 +66,7 @@ import { setLogLevel, } from "./logger.js"; import { setArgsContext, setFlagContext, withTracing } from "./telemetry.js"; +import { tipsSuppressed } from "./tips.js"; /** * Parse a string input as a number. @@ -249,6 +250,21 @@ export const FIELDS_FLAG = { optional: true as const, } as const; +/** + * Hidden `--tips` flag injected into every command by {@link buildCommand}. + * + * Defaults to `true`; passing `--no-tips` sets it to `false`, which suppresses + * the "Tip: ..." footer hints. Hidden so it doesn't clutter individual command + * `--help` output — it's documented at the CLI level. Also controllable via the + * `SENTRY_DISABLE_TIPS` environment variable (see `tips.ts`). + */ +export const TIPS_FLAG = { + kind: "boolean" as const, + brief: "Show tip hints in command output (use --no-tips to disable)", + default: true, + hidden: true as const, +} as const; + // --------------------------------------------------------------------------- // Hidden org/project compat flags (LLM error recovery) // --------------------------------------------------------------------------- @@ -435,6 +451,7 @@ const GLOBAL_FLAG_DEFAULTS: Record = { verbose: VERBOSE_FLAG, org: ORG_FLAG, project: PROJECT_FLAG, + tips: TIPS_FLAG, }; /** Flags that are always stripped (command never sees them). */ @@ -794,7 +811,12 @@ export function buildCommand< // "cached · 3m ago · use -f to refresh" when data was cached. // Skip bare `return;` paths (e.g. `--web` which opens a browser // without yielding) — no rendered output should mean no footer. - const finalHint = returned ? appendCacheHint(returned.hint) : undefined; + // Drop the command's "Tip: ..." hint when tips are suppressed via + // --no-tips or SENTRY_DISABLE_TIPS. The cache-age footer is a staleness + // indicator, not a tip, so it is still appended. + const suppressTips = tipsSuppressed(flags.tips as boolean | undefined); + const commandHint = suppressTips ? undefined : returned?.hint; + const finalHint = returned ? appendCacheHint(commandHint) : undefined; await withTracing("render", "cli.command.render", () => { writeFinalization(stdout, finalHint, cleanFlags.json, renderer); }); diff --git a/packages/cli/src/lib/global-flags.ts b/packages/cli/src/lib/global-flags.ts index a01bf6064..afd57ee69 100644 --- a/packages/cli/src/lib/global-flags.ts +++ b/packages/cli/src/lib/global-flags.ts @@ -46,6 +46,9 @@ export const GLOBAL_FLAGS: readonly GlobalFlagDef[] = [ { name: "log-level", short: null, kind: "value" }, { name: "json", short: null, kind: "boolean" }, { name: "fields", short: null, kind: "value" }, + // `--no-tips` suppresses the "Tip: ..." footer hints. Registered as a + // boolean so Stricli recognizes the `--no-tips` negation token. + { name: "tips", short: null, kind: "boolean" }, // Hidden compat shims: LLMs trained on the older sentry-cli generate // `--org` and `--project` flags. We silently accept them and map to // SENTRY_ORG / SENTRY_PROJECT env vars so the resolution chain handles them. diff --git a/packages/cli/src/lib/tips.ts b/packages/cli/src/lib/tips.ts new file mode 100644 index 000000000..3195196ad --- /dev/null +++ b/packages/cli/src/lib/tips.ts @@ -0,0 +1,39 @@ +/** + * Tip-hint suppression. + * + * Commands return a footer `{ hint: "Tip: ..." }` that `buildCommand` renders + * below the output. Users who find these tips noisy can turn them off with the + * global `--no-tips` flag or the `SENTRY_DISABLE_TIPS` environment variable. + * + * The cache-age footer ("cached · 3m ago · use -f to refresh") is a staleness + * indicator rather than a tip, so it is left untouched. + * + * getsentry/cli#1412 + * + * @module + */ + +import { getEnv } from "./env.js"; +import { isTruthyEnv } from "./formatters/plain-detect.js"; + +/** + * Decide whether command tip hints should be suppressed. + * + * - Explicit `--no-tips` (flag value `false`) always wins. + * - Otherwise, honor `SENTRY_DISABLE_TIPS` using the shared truthy-env + * semantics (`"0"` / `"false"` / `""` are falsy). + * - Default: tips are shown. + * + * @param tipsFlag - Value of the injected global `tips` flag (defaults to + * `true`; `false` when the user passed `--no-tips`). + */ +export function tipsSuppressed(tipsFlag: boolean | undefined): boolean { + if (tipsFlag === false) { + return true; + } + const envVal = getEnv().SENTRY_DISABLE_TIPS; + if (envVal !== undefined) { + return isTruthyEnv(envVal); + } + return false; +} diff --git a/packages/cli/test/lib/global-flags.test.ts b/packages/cli/test/lib/global-flags.test.ts index 43cb2aae7..ec6feeb07 100644 --- a/packages/cli/test/lib/global-flags.test.ts +++ b/packages/cli/test/lib/global-flags.test.ts @@ -60,7 +60,15 @@ describe("buildTopLevelFlags", () => { test("matches the current GLOBAL_FLAGS definition", () => { const { booleanFlags, valueFlags } = buildTopLevelFlags(); expect([...booleanFlags].sort()).toEqual( - ["--verbose", "-v", "--no-verbose", "--json", "--no-json"].sort() + [ + "--verbose", + "-v", + "--no-verbose", + "--json", + "--no-json", + "--tips", + "--no-tips", + ].sort() ); expect([...valueFlags].sort()).toEqual( ["--log-level", "--fields", "--org", "--project"].sort() diff --git a/packages/cli/test/lib/tips.test.ts b/packages/cli/test/lib/tips.test.ts new file mode 100644 index 000000000..e7085b8a7 --- /dev/null +++ b/packages/cli/test/lib/tips.test.ts @@ -0,0 +1,47 @@ +/** + * Tip-hint suppression tests (getsentry/cli#1412). + * + * Covers the `--no-tips` flag and `SENTRY_DISABLE_TIPS` env var precedence. + */ + +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { setEnv } from "../../src/lib/env.js"; +import { tipsSuppressed } from "../../src/lib/tips.js"; + +describe("tipsSuppressed", () => { + beforeEach(() => { + setEnv({}); + }); + + afterEach(() => { + setEnv(process.env); + }); + + test("shows tips by default", () => { + expect(tipsSuppressed(true)).toBe(false); + expect(tipsSuppressed(undefined)).toBe(false); + }); + + test("suppresses tips when --no-tips passed (flag false)", () => { + expect(tipsSuppressed(false)).toBe(true); + }); + + test("--no-tips wins even if env would enable tips", () => { + setEnv({ SENTRY_DISABLE_TIPS: "0" }); + expect(tipsSuppressed(false)).toBe(true); + }); + + test("suppresses tips when SENTRY_DISABLE_TIPS is truthy", () => { + for (const val of ["1", "true", "yes"]) { + setEnv({ SENTRY_DISABLE_TIPS: val }); + expect(tipsSuppressed(true)).toBe(true); + } + }); + + test("does not suppress when SENTRY_DISABLE_TIPS is falsy", () => { + for (const val of ["0", "false", ""]) { + setEnv({ SENTRY_DISABLE_TIPS: val }); + expect(tipsSuppressed(true)).toBe(false); + } + }); +});