-
-
Notifications
You must be signed in to change notification settings - Fork 10
feat(help): support JSON output for --help flags #1337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
48b5383
a619440
0d4f5d6
2caa8fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,6 +221,154 @@ export function isVersionRequest(argv: readonly string[]): boolean { | |
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Accumulator for {@link rewriteHelpJsonRequest} while scanning argv. | ||
| */ | ||
| type HelpJsonScan = { | ||
| hasHelp: boolean; | ||
| hasJson: boolean; | ||
| commandPath: string[]; | ||
| fields: string | undefined; | ||
| }; | ||
|
|
||
| /** | ||
| * True when `token` is a boolean-style flag that does NOT consume a following | ||
| * value token — a known boolean global flag (`--verbose`, `--json`, its `-v` | ||
| * alias, or a `--no-<flag>` negation). Everything else that starts with `-` is | ||
| * assumed to be value-taking, so its next token is a flag value rather than a | ||
| * command-path segment. | ||
| * | ||
| * Used by {@link scanHelpJsonToken} to avoid swallowing a real path segment | ||
| * after a boolean flag (`issue --verbose list`) while still discarding the | ||
| * values of value flags (`--org acme`, `--limit 5`). | ||
| */ | ||
| function isBooleanFlagToken(token: string): boolean { | ||
| if (token.length === 2 && token[0] === "-" && token[1] !== "-") { | ||
| const flag = FLAG_BY_SHORT.get(token[1] ?? ""); | ||
| return flag ? !flag.takesValue : false; | ||
| } | ||
| if (!token.startsWith("--")) { | ||
| return false; | ||
| } | ||
| const name = token.slice(2); | ||
| if (name.startsWith("no-") && NEGATABLE_NAMES.has(name.slice(3))) { | ||
| return true; | ||
| } | ||
| const flag = FLAG_BY_NAME.get(name); | ||
| return flag ? !flag.takesValue : false; | ||
| } | ||
|
|
||
| /** | ||
| * Fold a single argv token into the {@link HelpJsonScan} accumulator. | ||
| * | ||
| * Recognizes `--help` (and its `-h` alias), `--json`, and `--fields` (both | ||
| * spaced and `=` forms), collects non-flag tokens as the command path, and | ||
| * drops all other flags — including the spaced value of any value-taking flag | ||
| * (`--org acme`, `--limit 5`) so those values never leak into the resolved | ||
| * command path. | ||
| * | ||
| * @returns The number of tokens consumed (1, or 2 when a value flag's spaced | ||
| * value is dropped alongside it). | ||
| */ | ||
| function scanHelpJsonToken( | ||
| argv: readonly string[], | ||
| index: number, | ||
| scan: HelpJsonScan | ||
| ): number { | ||
| const token = argv[index] ?? ""; | ||
| // `-h` is Stricli's built-in short alias for `--help`, so it must trigger the | ||
| // JSON rewrite too — otherwise `sentry -h --json` falls through to text usage. | ||
| if (token === "--help" || token === "-h") { | ||
| scan.hasHelp = true; | ||
| return 1; | ||
| } | ||
| if (token === "--json") { | ||
| scan.hasJson = true; | ||
| return 1; | ||
| } | ||
| if (token.startsWith("--fields=")) { | ||
| scan.fields = token.slice("--fields=".length); | ||
| return 1; | ||
| } | ||
| const next = argv[index + 1]; | ||
| // A spaced value is only present when the next token isn't itself a flag — | ||
| // `--fields --json` leaves --fields valueless rather than eating --json. | ||
| const hasSpacedValue = next !== undefined && !next.startsWith("-"); | ||
| if (token === "--fields") { | ||
| if (hasSpacedValue) { | ||
| scan.fields = next; | ||
| return 2; | ||
| } | ||
| return 1; | ||
| } | ||
| if (!token.startsWith("-")) { | ||
| scan.commandPath.push(token); | ||
| return 1; | ||
| } | ||
| // Any other flag is irrelevant to the help command's structured output and | ||
| // is dropped. A value flag (`--org acme`, `--limit 5`) also drops its spaced | ||
| // value so it isn't mistaken for a command-path segment; a boolean flag | ||
| // (`--verbose`) leaves the following token for the path. An `=`-form flag | ||
| // (`--org=acme`) already carries its value inline, so it never consumes the | ||
| // following token — dropping it would swallow a real command-path segment. | ||
| if (hasSpacedValue && !token.includes("=") && !isBooleanFlagToken(token)) { | ||
| return 2; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| return 1; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** | ||
| * Rewrite a flag-based `--help --json` request into a `help` command invocation. | ||
| * | ||
| * Stricli handles `--help` internally by printing its own text usage and | ||
| * ignores `--json` entirely, so `sentry --help --json` and | ||
| * `sentry <command> --help --json` never produce structured output. Agents and | ||
| * tooling reach for `--help` first, so we rewrite these forms to the dedicated | ||
| * `help` command — which already emits JSON via {@link introspectAllCommands} | ||
| * and {@link introspectCommand} — giving both help UX paths identical JSON. | ||
| * | ||
| * The rewrite only fires when **both** `--help` and `--json` appear before any | ||
| * `--` escape separator. A bare `--help` (no `--json`) is left untouched so | ||
| * Stricli's existing human usage output is preserved unchanged. | ||
| * | ||
| * The command path is the sequence of non-flag tokens (e.g. `issue list`), and | ||
| * a `--fields <value>` (or `--fields=<value>`) flag is carried through so field | ||
| * selection keeps working. The result is `["help", "--json", ...path]` with | ||
| * `--fields` appended when present. | ||
| * | ||
| * @param argv - Raw CLI arguments (e.g., `process.argv.slice(2)`) | ||
| * @returns The rewritten `help`-command argv, or `null` if the request is not a | ||
| * `--help --json` combination and should be processed normally. | ||
| */ | ||
| export function rewriteHelpJsonRequest( | ||
| argv: readonly string[] | ||
| ): string[] | null { | ||
| const scan: HelpJsonScan = { | ||
| hasHelp: false, | ||
| hasJson: false, | ||
| commandPath: [], | ||
| fields: undefined, | ||
| }; | ||
|
|
||
| for (let i = 0; i < argv.length; ) { | ||
| // Tokens after -- are positional/pass-through — a --help there is not ours. | ||
| if (argv[i] === "--") { | ||
| return null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape aborts valid help rewriteLow Severity On encountering Reviewed by Cursor Bugbot for commit 0d4f5d6. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intentional. Returning null on any pre- |
||
| } | ||
| i += scanHelpJsonToken(argv, i, scan); | ||
| } | ||
|
|
||
| if (!(scan.hasHelp && scan.hasJson)) { | ||
| return null; | ||
| } | ||
|
|
||
| const rewritten = ["help", "--json", ...scan.commandPath]; | ||
| if (scan.fields !== undefined) { | ||
| rewritten.push("--fields", scan.fields); | ||
| } | ||
| return rewritten; | ||
| } | ||
|
|
||
| /** | ||
| * Move global flags from any position in argv to the end. | ||
| * | ||
|
|
@@ -309,10 +457,14 @@ export function rewriteDashedFlagValues(argv: readonly string[]): string[] { | |
| * Preprocess raw CLI argv before Stricli dispatch. | ||
| * | ||
| * Composes the argv transforms applied on every invocation: | ||
| * 1. A top-level `--version` (see {@link isVersionRequest}) is normalized to a | ||
| * 1. A flag-based `--help --json` request (see {@link rewriteHelpJsonRequest}) | ||
| * is rewritten to the dedicated `help` command so JSON help works for the | ||
| * `--help` forms agents reach for (`sentry --help --json`, | ||
| * `sentry issue --help --json`), matching `sentry help --json`. | ||
| * 2. A top-level `--version` (see {@link isVersionRequest}) is normalized to a | ||
| * plain `["--version"]` so the application-level version handler prints it | ||
| * regardless of how deep in the route tree it appeared. | ||
| * 2. Otherwise, dashed flag values are rewritten (see | ||
| * 3. Otherwise, dashed flag values are rewritten (see | ||
| * {@link rewriteDashedFlagValues}), then global flags are hoisted to the | ||
| * tail (see {@link hoistGlobalFlags}). | ||
| * | ||
|
|
@@ -323,6 +475,10 @@ export function rewriteDashedFlagValues(argv: readonly string[]): string[] { | |
| * @returns The argv to hand to Stricli's `run` | ||
| */ | ||
| export function preprocessArgv(argv: readonly string[]): string[] { | ||
| const helpJson = rewriteHelpJsonRequest(argv); | ||
| if (helpJson) { | ||
| return helpJson; | ||
| } | ||
| if (isVersionRequest(argv)) { | ||
| return ["--version"]; | ||
| } | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Positionals leak into help path
Medium Severity
rewriteHelpJsonRequesttreats every non-flag token as a command path segment, so positionals like an issue id are forwarded intohelp --json ….introspectCommandthen rejects the extra segment and returns a not-found JSON error instead of help for the leaf command. Stricli’s bare--helpignores those positionals, so adding--jsonregresses those invocations from usable text help to an error.Additional Locations (1)
packages/cli/src/lib/argv-hoist.ts#L361-L362Reviewed by Cursor Bugbot for commit 0d4f5d6. Configure here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving this as-is by design. The rewrite is scoped to command paths (
sentry [<group> <command>] --help --json) — the forms agents actually use to discover a command's shape. When a positional like an issue id is present, forwarding it produces a structured JSONCommand not founderror (exit 60), which is the documented, machine-readable behavior for the JSON/agent use case this feature targets; it doesn't crash or hang. Making<command> <positional> --help --jsonfall back to the leaf command's help would require running route-tree resolution inside the argv preprocessor (argv-hoist.tsis currently a dependency-light string transform on the hotpreprocessArgvpath). Given this PR is alreadyrisk: high, I'd rather not couple the preprocessor to the command graph here — happy to do it as a follow-up if maintainers want the fallback.