Skip to content

feat(cli): Parse and resolve org/project targets in their positional - #1594

Merged
BYK merged 7 commits into
mainfrom
feat/cli-parsed-target-positionals
Oct 23, 2026
Merged

BYK merged 7 commits into
mainfrom
feat/cli-parsed-target-positionals

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Refs #1564. This is the first slice: the mechanism plus five commands.

A [<org>/][<project>] positional now owns both parsing and resolution. The command picks a parser from src/lib/target-positional.ts, and that parser comes with its resolver:

parse: orgOptionalTarget,                    // resolveOrgOptionalTarget
parse: projectBoundTarget,                   // resolveProjectBoundTarget
parse: orgProjectTarget(resolveViewTargets), // command-specific resolver

func gets a LazyPositional with three members: target.parsed (the ParsedOrgProject), target.resolved (a promise of the resolver's result), and target.raw (what the user typed):

async *func(this: SentryContext, flags: ListFlags, target: OrgOptionalTarget) {
  const resolved = await target.resolved;

func never calls parseOrgProjectArg or a resolver, never picks which resolver to use, and needs no default for an omitted target. An omitted optional target arrives as auto-detect. project view moves its target switch into resolveViewTargets and reads target.parsed.type.

  • No command names to pass. Resolvers read the command path from this.commandPrefix, which Stricli's forCommand sets and the SDK invoker now sets from its command path. The COMMAND_NAME constants these commands used are gone. A context without commandPrefix makes resolution throw rather than print a hint with a blank command name; test mock contexts set it.
  • Destructive commands guard automatically. buildDeleteCommand calls markDestructive on its lazy positionals, and the target resolver then runs requireExplicitTarget. project delete uses plain projectBoundTarget. Once docs(cli): Generate and check docs that mirror CLI behavior #1626's destructive flag lands, the call can move into buildCommand so issue merge and trial start get it too.
  • Lint rule. sentry-cli/require-target-resolution fails a command function that takes OrgOptionalTarget, ProjectBoundTarget or OrgProjectTarget<R> but never reads .resolved, since skipping it skips the resolver and the destructive guard.

When things run. Both steps are lazy (LazyPositional in src/lib/positional-parse.ts, a plain class with memoized getters). The parse runs on the first parsed read and the resolver on the first resolved read; each runs once. Both happen inside func, so the order matches main:

  • The auth guard and rc URL check run first. A bad target with no login still reports "Not authenticated" (exit 10).
  • Parse errors keep their CliError semantics. sentry project view @latest exits 21 with the ValidationError, not Stricli's Failed to parse … (exit -4). A parse error also rejects resolved.
  • parseOrgProjectArg's URL side effect still runs after those checks. For a pasted Sentry URL it calls applySentryUrlContext, which can switch SENTRY_URL/SENTRY_HOST, save that host as the default, or throw HostScopeError. Running it at Stricli scan time would have let the auth guard and rc URL check run against the host taken from the URL.

Resolution stays a promise because resolvers need auth, config, DSN detection, and API calls. Shared code that handles any positional (buildListCommand's plural-alias interception, help recovery, setArgsContext) reads the raw input with rawPositional(value), an instanceof LazyPositional check. Stricli calls parsers with the command context as this; the resolver gets that context plus commandName.

Callers that skip Stricli. The typed SDK (buildInvoker) and tests that call command.loader() pass raw strings. buildCommand runs applyLazyPositionals on them, and on omitted optional positionals, so every caller keeps passing what the user typed and func always gets the same shape. When plural-alias interception drops the target, buildListCommand runs it again so the dropped target becomes auto-detect. sdk.project.view({ orgProject: "@latest" }) is rejected instead of silently auto-detecting. The SDK invoker also sets directInvoke, which turns off help positional recovery: a failing SDK call whose target is help throws its error, as on main, instead of returning help text.

Shell completion is unchanged. Completion kinds come from a tag on the parser (withCompletionTarget), shared with parseOrgTarget and parseOrgProjectTarget. The generated targets file matches main.

Migrated: trace list, replay list, feedback list, project view, project delete.

Not in this PR:

  • LIST_TARGET_POSITIONAL / buildOrgListCommand. feat(cli): Name filter for project, team, repo, monitor and integration lists #1575 (splitListTargetAndFilter) reads the raw target and splits it before parsing (for example acme/web-*). These should move after it lands, using target.raw.
  • Issue and dashboard positionals. feat(cli): Complete issue short IDs and dashboards in shell completion #1572 adds parseIssueTarget / parseDashboardTarget in the same files. When one of the two PRs rebases, its parsers should switch from the PARSER_TARGETS map to withCompletionTarget.
  • [<org>/<project>/]<id> positionals (trace view, span list, event view, log view, …). Stricli parses each argument on its own, but what each one means depends on how many were given. Those keep parsing in func.
  • Release org/version positionals. They already share resolveReleaseTarget.

via BYK.

--

View Junior Session [Sentry]

Add parseOrgProjectTargetArg, a positional parser that hands func a
TargetArg wrapping the raw input and its parseOrgProjectArg result. The
parse runs lazily on first access inside func, so bad input still throws
the parser's CliError with its exit code instead of Stricli's argument
parse error. Plural-alias interception, help recovery and telemetry read
the raw input.

The typed SDK invoke path now runs positional parsers too, since it calls
func without Stricli's scanner.

Completion kinds now come from a tag on the parser
(withCompletionTarget), so parsing and marker-only parsers complete the
same way.

Migrate trace list, replay list, feedback list, project view and project
delete.

Refs #1564

Co-Authored-By: Burak Yigit Kaya <burak.kaya@sentry.io>
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Oct 23, 2026 4:45pm UTC

Request Review

@sentry-junior sentry-junior Bot added the CLI Sentry CLI and CLI documentation label Oct 22, 2026

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a fan of the two-stage parsing. I do understand the benefits but I was hoping to find a way to avoid the in-func resolve or parse call.

Also I'd love to make tests use the applyPositionalParsers or something like that rather than repeating the resolver/parser calls over and over.

Review on #1594 asked to drop the two-stage TargetArg parse inside
func. parseOrgProjectPositional now returns the ParsedOrgProject
directly and commands take it as their argument, defaulting an omitted
optional target to AUTO_DETECT_TARGET.

buildCommand wraps every positional parser (positional-parse.ts). A
CliError from a parser comes back as a DeferredPositionalError value,
and the command wrapper throws it before the auth guard. Bad input keeps
its CliError exit code and output instead of Stricli's "Failed to
parse" (exit -4). The wrapper also records the raw input of parsed
values, which plural-alias interception, help recovery and telemetry
read through rawPositional.

Tests call commands through loadCommandFunc, a test helper that runs
the command's own positional parsers via applyPositionalParsers, so
they pass raw strings again.

Refs #1564
…t-positionals

# Conflicts:
#	packages/cli/src/lib/sdk-invoke.ts
#	packages/cli/test/helpers.ts

Co-Authored-By: BYK <burak.kaya@sentry.io>

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the URL side effect you keep mentioning with the new approach and what are the dangers/gotchas of it?

Comment thread packages/cli/src/commands/feedback/list.ts Outdated
Comment thread packages/cli/src/lib/arg-parsing.ts Outdated
Comment thread packages/cli/test/commands/feedback/list.test.ts Outdated
Comment thread packages/cli/test/commands/project/view.func.test.ts Outdated
Review on #1594 asked for lazy parsing that stays transparent to
commands, tests, and the SDK. parseOrgProjectPositional now hands func
a stand-in typed as ParsedOrgProject that runs parseOrgProjectArg on
first property access. Commands read it like the parsed value, but the
parse (and its URL-context side effect) runs inside func after the auth
guard and rc URL check, as on main. Bad input throws its CliError from
func, so there is nothing to defer out of Stricli's scanner.

buildCommand runs these lazy parsers on positionals that arrive as
plain strings, so tests calling command.loader() and the SDK invoke
path pass raw strings again. That drops DeferredPositionalError,
loadCommandFunc, and the SDK's applyPositionalParsers.

Refs #1564

Co-Authored-By: BYK <burak.kaya@sentry.io>
Target positionals now carry their resolver. A command declares
`orgOptionalTarget(COMMAND_NAME)`, `projectBoundTarget(COMMAND_NAME)`,
or `orgProjectTarget(resolve)` as the parser and gets back a value that
reads like a `ParsedOrgProject` plus a `resolved` promise. `func` awaits
`target.resolved` instead of choosing and calling a resolver.

Parsing still runs on the first property read and resolution on the
first `resolved` read, both inside `func`, so the auth guard, rc URL
check, and pasted-URL host handling keep main's order. Omitted optional
targets get a stand-in too, so there is no default value to remember.

Refs #1564

Co-Authored-By: Burak Yigit Kaya <burak.kaya@sentry.io>
@sentry-junior sentry-junior Bot changed the title feat(cli): Parse org/project targets in their positional parser feat(cli): Parse and resolve org/project targets in their positional Oct 23, 2026

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay looking much better!

Comment thread packages/cli/src/commands/feedback/list.ts Outdated
Comment thread packages/cli/src/commands/project/delete.ts Outdated
Comment thread packages/cli/src/commands/project/delete.ts
Comment thread packages/cli/src/commands/project/view.ts
Comment thread packages/cli/src/lib/positional-parse.ts Outdated
Target resolvers now read the command name from `commandPrefix`, which
Stricli's `forCommand` sets and the SDK invoker now sets too. So
`orgOptionalTarget` and `projectBoundTarget` are plain parsers and the
`COMMAND_NAME` constants they needed are gone.

`buildDeleteCommand` marks its lazy positionals destructive, and the
target resolver then refuses an auto-detected target with
`requireExplicitTarget`. `project delete` no longer needs its own
resolver for that.

A new `sentry-cli/require-target-resolution` lint rule fails a command
function that takes a bound target type but never reads `.resolved`,
since skipping it would also skip the resolver and the destructive
guard.

Refs #1564

Co-Authored-By: Burak Yigit Kaya <burak.kaya@sentry.io>
`LazyPositional` now exposes `parsed`, `resolved`, and `raw` as plain
class members instead of a Proxy that mimics the parsed value. Commands
read `target.parsed.type`. The thenable guard, own-key and descriptor
traps, and the raw-input WeakMap are gone; `rawPositional` is an
`instanceof` check.

The typed SDK invoker also sets `directInvoke` on its context, which
turns off `help` positional recovery. A failing SDK call whose target
is `help` throws its error again, as on main, instead of returning
help text.

Refs #1564

Co-Authored-By: Burak Yigit Kaya <burak.kaya@sentry.io>

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is HOT HOT HOT 🔥

@BYK
BYK marked this pull request as ready for review October 23, 2026 19:41
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 23, 2026
@BYK
BYK merged commit b6ea691 into main Oct 23, 2026
37 checks passed
@BYK
BYK deleted the feat/cli-parsed-target-positionals branch October 23, 2026 20:12
sentry-junior Bot added a commit that referenced this pull request Oct 23, 2026
Follow-up to #1594: `buildCommand` now calls `markDestructive` when
`destructive: true`, instead of `buildDeleteCommand` doing it, so the
`destructive` flag is the single source for the Safety Rules list and the
auto-detect guard. `issue merge` and `trial start` get the guard if they
move to lazy target positionals.

check:doc-examples now recognizes target positionals by their
`org/project` completion kind, which covers the new lazy target parsers
as well as `parseOrgProjectTarget`.
sentry-junior Bot added a commit that referenced this pull request Oct 23, 2026
Move attribute list to the target positional API from #1594: the target
uses orgOptionalTarget, func awaits target.resolved, and the
COMMAND_NAME constant goes away. The help's target section now matches
the other migrated list commands.

Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK added a commit that referenced this pull request Oct 24, 2026
Refs #1564. This continues #1594 with the one remaining
`[<org>/][<project>]` target that nothing blocks.

`explore` declared its target with `String` and called
`resolveOrgOptionalFromArg` inside `func`. It now uses
`orgOptionalTarget` and awaits `target.resolved`, the same way `feedback
list` and `replay list` do. Resolution is unchanged: it still goes
through `resolveOrgOptionalTarget` with the same command name.

User-facing change: the parser carries the org/project completion tag,
so `sentry explore <TAB>` now completes `org/`, `org/project` and
project aliases. Before this, `explore` had no target completion.

Other users of the `*FromArg` helpers are left alone on purpose:
- `agent-conversation list` uses `LIST_TARGET_POSITIONAL`, which waits
on #1575.
- `log list`, `replay view` and `replay download` take variadic
positionals. What each argument means depends on how many were given, so
they keep parsing in `func`.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3Aa3773d7739de1b873375a6f1 -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3Aa3773d7739de1b873375a6f1/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>

This branch was previously deployed

1 inactive deployment
Preview — 94d324db Deployed Oct 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLI Sentry CLI and CLI documentation risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant