Skip to content

Commit 4bd38df

Browse files
committed
include hasMore metadata in auto-detect JSON output
Auto-detect mode now wraps JSON output in a {data, hasMore, hint} envelope so JSON consumers can detect truncation. When results are incomplete, the hint field tells consumers which paginated command to use for full results. Also extracted fetchAutoDetectProjects helper to reduce function complexity.
1 parent 6c5b301 commit 4bd38df

File tree

2 files changed

+77
-34
lines changed

2 files changed

+77
-34
lines changed

src/commands/project/list.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,37 @@ async function fetchPaginatedSafe(
308308
}
309309

310310
/**
311-
* Handle auto-detect mode: resolve orgs from config/DSN, fetch all projects,
312-
* apply client-side filtering and limiting.
311+
* Fetch projects for auto-detect mode.
313312
*
314313
* Optimization: when targeting a single org without platform filter, uses
315314
* single-page pagination (`perPage=limit`) to avoid fetching all projects.
316315
* Multi-org or filtered queries still require full fetch + client-side slicing.
317316
*/
317+
async function fetchAutoDetectProjects(
318+
orgs: string[],
319+
flags: ListFlags
320+
): Promise<{ projects: ProjectWithOrg[]; nextCursor?: string }> {
321+
if (orgs.length === 1 && !flags.platform) {
322+
return fetchPaginatedSafe(orgs[0] as string, flags.limit);
323+
}
324+
if (orgs.length > 0) {
325+
const results = await Promise.all(orgs.map(fetchOrgProjectsSafe));
326+
return { projects: results.flat() };
327+
}
328+
return { projects: await fetchAllOrgProjects() };
329+
}
330+
331+
/** Build a pagination hint for auto-detect JSON output. */
332+
function autoDetectPaginationHint(orgs: string[]): string {
333+
return orgs.length === 1
334+
? `sentry project list ${orgs[0]}/ --json`
335+
: "sentry project list <org>/ --json";
336+
}
337+
338+
/**
339+
* Handle auto-detect mode: resolve orgs from config/DSN, fetch all projects,
340+
* apply client-side filtering and limiting.
341+
*/
318342
export async function handleAutoDetect(
319343
stdout: Writer,
320344
cwd: string,
@@ -326,31 +350,24 @@ export async function handleAutoDetect(
326350
skippedSelfHosted,
327351
} = await resolveOrgsForAutoDetect(cwd);
328352

329-
let allProjects: ProjectWithOrg[];
330-
let nextCursor: string | undefined;
331-
332-
// Fast path: single org, no platform filter — fetch only one page
333-
if (orgsToFetch.length === 1 && !flags.platform) {
334-
const result = await fetchPaginatedSafe(
335-
orgsToFetch[0] as string,
336-
flags.limit
337-
);
338-
allProjects = result.projects;
339-
nextCursor = result.nextCursor;
340-
} else if (orgsToFetch.length > 0) {
341-
const results = await Promise.all(orgsToFetch.map(fetchOrgProjectsSafe));
342-
allProjects = results.flat();
343-
} else {
344-
allProjects = await fetchAllOrgProjects();
345-
}
353+
const { projects: allProjects, nextCursor } = await fetchAutoDetectProjects(
354+
orgsToFetch,
355+
flags
356+
);
346357

347358
const filtered = filterByPlatform(allProjects, flags.platform);
348359
const limitCount =
349360
orgsToFetch.length > 1 ? flags.limit * orgsToFetch.length : flags.limit;
350361
const limited = filtered.slice(0, limitCount);
351362

363+
const hasMore = filtered.length > limited.length || !!nextCursor;
364+
352365
if (flags.json) {
353-
writeJson(stdout, limited);
366+
const output: Record<string, unknown> = { data: limited, hasMore };
367+
if (hasMore) {
368+
output.hint = autoDetectPaginationHint(orgsToFetch);
369+
}
370+
writeJson(stdout, output);
354371
return;
355372
}
356373

@@ -362,7 +379,7 @@ export async function handleAutoDetect(
362379

363380
displayProjectTable(stdout, limited);
364381

365-
if (filtered.length > limited.length || nextCursor) {
382+
if (hasMore) {
366383
if (nextCursor && orgsToFetch.length === 1) {
367384
const org = orgsToFetch[0] as string;
368385
stdout.write(

test/commands/project/list.test.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,7 +1218,7 @@ describe("handleAutoDetect", () => {
12181218
expect(text).toContain("backend");
12191219
});
12201220

1221-
test("--json outputs array", async () => {
1221+
test("--json outputs envelope with hasMore", async () => {
12221222
globalThis.fetch = mockProjectFetch(sampleProjects);
12231223
const { writer, output } = createCapture();
12241224

@@ -1228,8 +1228,9 @@ describe("handleAutoDetect", () => {
12281228
});
12291229

12301230
const parsed = JSON.parse(output());
1231-
expect(Array.isArray(parsed)).toBe(true);
1232-
expect(parsed).toHaveLength(2);
1231+
expect(parsed).toHaveProperty("data");
1232+
expect(parsed).toHaveProperty("hasMore", false);
1233+
expect(parsed.data).toHaveLength(2);
12331234
});
12341235

12351236
test("empty results shows no projects message", async () => {
@@ -1244,7 +1245,7 @@ describe("handleAutoDetect", () => {
12441245
expect(output()).toContain("No projects found");
12451246
});
12461247

1247-
test("respects --limit flag", async () => {
1248+
test("respects --limit flag and indicates truncation", async () => {
12481249
const manyProjects = Array.from({ length: 5 }, (_, i) =>
12491250
makeProject({ id: String(i), slug: `proj-${i}`, name: `Project ${i}` })
12501251
);
@@ -1257,7 +1258,9 @@ describe("handleAutoDetect", () => {
12571258
});
12581259

12591260
const parsed = JSON.parse(output());
1260-
expect(parsed).toHaveLength(2);
1261+
expect(parsed.data).toHaveLength(2);
1262+
expect(parsed.hasMore).toBe(true);
1263+
expect(parsed.hint).toBeString();
12611264
});
12621265

12631266
test("respects --platform flag", async () => {
@@ -1271,8 +1274,9 @@ describe("handleAutoDetect", () => {
12711274
});
12721275

12731276
const parsed = JSON.parse(output());
1274-
expect(parsed).toHaveLength(1);
1275-
expect(parsed[0].platform).toBe("python");
1277+
expect(parsed.data).toHaveLength(1);
1278+
expect(parsed.data[0].platform).toBe("python");
1279+
expect(parsed.hasMore).toBe(false);
12761280
});
12771281

12781282
test("shows limit message when more projects exist", async () => {
@@ -1304,10 +1308,10 @@ describe("handleAutoDetect", () => {
13041308
});
13051309

13061310
const parsed = JSON.parse(output());
1307-
expect(Array.isArray(parsed)).toBe(true);
1308-
expect(parsed).toHaveLength(2);
1311+
expect(parsed.data).toHaveLength(2);
13091312
// Verify orgSlug is attached
1310-
expect(parsed[0].orgSlug).toBe("test-org");
1313+
expect(parsed.data[0].orgSlug).toBe("test-org");
1314+
expect(parsed.hasMore).toBe(false);
13111315
});
13121316

13131317
test("fast path: shows truncation message when server has more results", async () => {
@@ -1329,6 +1333,26 @@ describe("handleAutoDetect", () => {
13291333
expect(text).not.toContain("--limit");
13301334
});
13311335

1336+
test("fast path: JSON includes hasMore and hint when server has more results", async () => {
1337+
await setDefaults("test-org");
1338+
globalThis.fetch = mockProjectFetch(sampleProjects, {
1339+
hasMore: true,
1340+
nextCursor: "1735689600000:0:0",
1341+
});
1342+
const { writer, output } = createCapture();
1343+
1344+
await handleAutoDetect(writer, "/tmp/test-project", {
1345+
limit: 30,
1346+
json: true,
1347+
});
1348+
1349+
const parsed = JSON.parse(output());
1350+
expect(parsed.hasMore).toBe(true);
1351+
expect(parsed.data).toHaveLength(2);
1352+
expect(parsed.hint).toContain("test-org/");
1353+
expect(parsed.hint).toContain("--json");
1354+
});
1355+
13321356
test("fast path: non-auth API errors return empty results instead of throwing", async () => {
13331357
await setDefaults("test-org");
13341358
// Mock returns 403 for projects endpoint (stale org, no access)
@@ -1350,7 +1374,8 @@ describe("handleAutoDetect", () => {
13501374
});
13511375

13521376
const parsed = JSON.parse(output());
1353-
expect(parsed).toEqual([]);
1377+
expect(parsed.data).toEqual([]);
1378+
expect(parsed.hasMore).toBe(false);
13541379
});
13551380

13561381
test("fast path: AuthError still propagates", async () => {
@@ -1380,7 +1405,8 @@ describe("handleAutoDetect", () => {
13801405
});
13811406

13821407
const parsed = JSON.parse(output());
1383-
expect(parsed).toHaveLength(1);
1384-
expect(parsed[0].platform).toBe("python");
1408+
expect(parsed.data).toHaveLength(1);
1409+
expect(parsed.data[0].platform).toBe("python");
1410+
expect(parsed.hasMore).toBe(false);
13851411
});
13861412
});

0 commit comments

Comments
 (0)