Skip to content

Commit 23cb92c

Browse files
betegonclaude
andauthored
fix(project): fallback to org listing when bare slug matches an organization (#475)
## Summary `sentry project list acme-corp` throws a `ResolutionError` when `acme-corp` is an organization slug, not a project. Users naturally type the org name as the argument expecting to see their projects listed. The shared `handleProjectSearch` in `org-list.ts` already handles this — it checks if the bare slug matches an org and falls back gracefully. But the project list command has its own custom `handleProjectSearch` that skipped this check. > **Note:** `sentry project list` (no args) already works fine for single-org users via auto-detect. This fix covers the case where users explicitly pass their org name as the argument. ## Before / After ### Before ``` $ sentry project list acme-corp ✘ Project 'acme-corp' not found. Try: sentry project list <org>/acme-corp Or: - No project with this slug found in any accessible organization ``` ### After ``` $ sentry project list acme-corp ⚠ 'acme-corp' is an organization, not a project. Listing all projects in 'acme-corp'. Slug Platform ──────────── ────────────── frontend javascript backend python mobile-ios apple-ios ``` ## Resolution flow ``` sentry project list <arg> │ ▼ parseOrgProjectArg(arg) │ ├─ no arg ──────────► auto-detect (DSN/config/all-orgs) ✅ already works ├─ "acme-corp/" ────► org-all: list projects in org ✅ already works ├─ "acme-corp/web" ─► explicit: fetch specific project ✅ already works └─ "acme-corp" ────► project-search mode │ ▼ findProjectsBySlug("acme-corp") │ ├─ project found ──► show it ✅ already works └─ no project found │ ▼ slug matches an org? ◄── this is the fix │ ├─ yes ──► warn + fallback to handleOrgAll ✅ NEW └─ no ───► ResolutionError (not found) (unchanged) ``` ## Test plan - [x] `bun run typecheck` passes - [x] `bun run lint` passes - [x] All 59 project list tests pass (`bun test test/commands/project/list.test.ts`) Fixes https://sentry.sentry.io/issues/7346957149/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b7b240e commit 23cb92c

File tree

1 file changed

+31
-3
lines changed

1 file changed

+31
-3
lines changed

src/commands/project/list.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ import {
5858
import { getApiBaseUrl } from "../../lib/sentry-client.js";
5959
import type { SentryProject } from "../../types/index.js";
6060

61+
/** Extended result type with optional title shown above the table. */
62+
type ProjectListResult = ListResult<ProjectWithOrg> & { title?: string };
63+
6164
/** Command key for pagination cursor storage */
6265
export const PAGINATION_KEY = "project-list";
6366

@@ -507,7 +510,7 @@ export async function handleProjectSearch(
507510
projectSlug: string,
508511
flags: ListFlags
509512
): Promise<ListResult<ProjectWithOrg>> {
510-
const { projects } = await withProgress(
513+
const { projects, orgs } = await withProgress(
511514
{
512515
message: `Fetching projects (up to ${flags.limit})...`,
513516
json: flags.json,
@@ -523,6 +526,26 @@ export async function handleProjectSearch(
523526
hint: `No project '${projectSlug}' found matching platform '${flags.platform}'.`,
524527
};
525528
}
529+
530+
// Check if slug matches an org — user likely meant "project list <org>/"
531+
const matchingOrg = orgs.find((o) => o.slug === projectSlug);
532+
if (matchingOrg) {
533+
const contextKey = buildContextKey(
534+
{ type: "org-all", org: projectSlug },
535+
flags,
536+
getApiBaseUrl()
537+
);
538+
const result = await handleOrgAll({
539+
org: projectSlug,
540+
flags,
541+
contextKey,
542+
cursor: undefined,
543+
});
544+
const r = result as ProjectListResult;
545+
r.title = `'${projectSlug}' is an organization, not a project. Showing all projects in '${projectSlug}'`;
546+
return r;
547+
}
548+
526549
// JSON mode returns empty array; human mode throws a helpful error
527550
if (flags.json) {
528551
return { items: [] };
@@ -581,11 +604,16 @@ export const listCommand = buildListCommand("project", {
581604
"Alias: `sentry projects` → `sentry project list`",
582605
},
583606
output: {
584-
human: (result: ListResult<ProjectWithOrg>) => {
607+
human: (data: ListResult<ProjectWithOrg>) => {
608+
const result = data as ProjectListResult;
585609
if (result.items.length === 0) {
586610
return result.hint ?? "No projects found.";
587611
}
588-
const parts: string[] = [displayProjectTable(result.items)];
612+
const parts: string[] = [];
613+
if (result.title) {
614+
parts.push(`\n${result.title}\n\n`);
615+
}
616+
parts.push(displayProjectTable(result.items));
589617
if (result.header) {
590618
parts.push(`\n${result.header}`);
591619
}

0 commit comments

Comments
 (0)