Skip to content

Commit d76ea3c

Browse files
committed
refactor: convert org-list handlers to return ListResult instead of writing stdout
All org-scoped list handlers now return ListResult<T> containing items + metadata (hasMore, nextCursor, hint, errors, jsonExtra) instead of writing directly to stdout. This enables callers (buildOrgListCommand) to handle rendering via OutputConfig. Changes: - org-list.ts: All default handlers return ListResult, removed stdout params from handleOrgAll, handleAutoDetect, handleExplicitOrg, handleExplicitProject, handleProjectSearch - list-command.ts: buildOrgListCommand uses OutputConfig with formatListHuman and jsonTransformList for automatic rendering - project/list.ts: All 4 override handlers return ListResult<ProjectWithOrg>, command func returns CommandOutput<ListResult> with full OutputConfig - issue/list.ts: handleOrgAllIssues and handleResolvedTargets return ListResult<SentryIssue> (still write to stdout internally due to complex rendering, but satisfy the ModeHandler return type contract) - team/list.ts, repo/list.ts: displayTable returns string via formatTable - formatters/table.ts: Added formatTable() returning string (writeTable delegates to it) Test updates: - org-list.test.ts: Assert on returned ListResult instead of stdout writes - list-command.test.ts: dispatchSpy returns ListResult, buildOrgListCommand takes 3 args (config, docs, routeName), displayTable returns string - project/list.test.ts: Handler tests use new signatures without stdout param
1 parent 4304ab8 commit d76ea3c

File tree

10 files changed

+933
-932
lines changed

10 files changed

+933
-932
lines changed

src/commands/issue/list.ts

Lines changed: 207 additions & 110 deletions
Large diffs are not rendered by default.

src/commands/project/list.ts

Lines changed: 153 additions & 159 deletions
Large diffs are not rendered by default.

src/commands/repo/list.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import {
1515
listRepositoriesPaginated,
1616
} from "../../lib/api-client.js";
1717
import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
18-
import { type Column, writeTable } from "../../lib/formatters/table.js";
18+
import { type Column, formatTable } from "../../lib/formatters/table.js";
1919
import {
2020
buildOrgListCommand,
2121
type OrgListCommandDocs,
2222
} from "../../lib/list-command.js";
2323
import type { OrgListConfig } from "../../lib/org-list.js";
24-
import type { SentryRepository, Writer } from "../../types/index.js";
24+
import type { SentryRepository } from "../../types/index.js";
2525

2626
/** Command key for pagination cursor storage */
2727
export const PAGINATION_KEY = "repo-list";
@@ -47,8 +47,8 @@ const repoListConfig: OrgListConfig<SentryRepository, RepositoryWithOrg> = {
4747
listForOrg: (org) => listRepositories(org),
4848
listPaginated: (org, opts) => listRepositoriesPaginated(org, opts),
4949
withOrg: (repo, orgSlug) => ({ ...repo, orgSlug }),
50-
displayTable: (stdout: Writer, repos: RepositoryWithOrg[]) =>
51-
writeTable(stdout, repos, REPO_COLUMNS),
50+
displayTable: (repos: RepositoryWithOrg[]) =>
51+
formatTable(repos, REPO_COLUMNS),
5252
};
5353

5454
const docs: OrgListCommandDocs = {

src/commands/team/list.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import {
1616
listTeamsPaginated,
1717
} from "../../lib/api-client.js";
1818
import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
19-
import { type Column, writeTable } from "../../lib/formatters/table.js";
19+
import { type Column, formatTable } from "../../lib/formatters/table.js";
2020
import {
2121
buildOrgListCommand,
2222
type OrgListCommandDocs,
2323
} from "../../lib/list-command.js";
2424
import type { OrgListConfig } from "../../lib/org-list.js";
25-
import type { SentryTeam, Writer } from "../../types/index.js";
25+
import type { SentryTeam } from "../../types/index.js";
2626

2727
/** Command key for pagination cursor storage */
2828
export const PAGINATION_KEY = "team-list";
@@ -51,8 +51,7 @@ const teamListConfig: OrgListConfig<SentryTeam, TeamWithOrg> = {
5151
listForOrg: (org) => listTeams(org),
5252
listPaginated: (org, opts) => listTeamsPaginated(org, opts),
5353
withOrg: (team, orgSlug) => ({ ...team, orgSlug }),
54-
displayTable: (stdout: Writer, teams: TeamWithOrg[]) =>
55-
writeTable(stdout, teams, TEAM_COLUMNS),
54+
displayTable: (teams: TeamWithOrg[]) => formatTable(teams, TEAM_COLUMNS),
5655
listForProject: (org, project) => listProjectTeams(org, project),
5756
};
5857

src/lib/formatters/table.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* Generic column-based table renderer.
33
*
4-
* Provides `writeTable()` for rendering structured data as Unicode-bordered
5-
* tables directly via the text-table renderer, and `buildMarkdownTable()`
6-
* for producing raw CommonMark table syntax (used in plain/non-TTY mode).
4+
* - {@link formatTable} returns a table string (for return-based commands)
5+
* - {@link writeTable} writes the table directly to a stream (legacy path)
6+
* - {@link buildMarkdownTable} returns raw CommonMark syntax
77
*
88
* ANSI escape codes in cell values are preserved — `string-width` correctly
99
* treats them as zero-width for column sizing.
@@ -89,15 +89,20 @@ export type WriteTableOptions = {
8989
rowSeparator?: boolean | string;
9090
};
9191

92-
export function writeTable<T>(
93-
stdout: Writer,
92+
/**
93+
* Format items as a table string.
94+
*
95+
* Returns the rendered table instead of writing to a stream.
96+
* In plain/non-TTY mode emits CommonMark; in TTY mode emits a
97+
* Unicode-bordered table with ANSI styling.
98+
*/
99+
export function formatTable<T>(
94100
items: T[],
95101
columns: Column<T>[],
96102
options?: WriteTableOptions
97-
): void {
103+
): string {
98104
if (isPlainOutput()) {
99-
stdout.write(`${buildMarkdownTable(items, columns)}\n`);
100-
return;
105+
return `${buildMarkdownTable(items, columns)}\n`;
101106
}
102107

103108
const headers = columns.map((c) => c.header);
@@ -109,13 +114,26 @@ export function writeTable<T>(
109114
const minWidths = columns.map((c) => c.minWidth ?? 0);
110115
const shrinkable = columns.map((c) => c.shrinkable ?? true);
111116

112-
stdout.write(
113-
renderTextTable(headers, rows, {
114-
alignments,
115-
minWidths,
116-
shrinkable,
117-
truncate: options?.truncate,
118-
rowSeparator: options?.rowSeparator,
119-
})
120-
);
117+
return renderTextTable(headers, rows, {
118+
alignments,
119+
minWidths,
120+
shrinkable,
121+
truncate: options?.truncate,
122+
rowSeparator: options?.rowSeparator,
123+
});
124+
}
125+
126+
/**
127+
* Render items as a formatted table, writing directly to a stream.
128+
*
129+
* Delegates to {@link formatTable} and writes the result. Prefer
130+
* `formatTable` in return-based command output pipelines.
131+
*/
132+
export function writeTable<T>(
133+
stdout: Writer,
134+
items: T[],
135+
columns: Column<T>[],
136+
options?: WriteTableOptions
137+
): void {
138+
stdout.write(formatTable(items, columns, options));
121139
}

src/lib/list-command.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ import type { SentryContext } from "../context.js";
1818
import { parseOrgProjectArg } from "./arg-parsing.js";
1919
import { buildCommand, numberParser } from "./command.js";
2020
import { warning } from "./formatters/colors.js";
21-
import type { OutputConfig } from "./formatters/output.js";
22-
import { dispatchOrgScopedList, type OrgListConfig } from "./org-list.js";
21+
import type { CommandOutput, OutputConfig } from "./formatters/output.js";
22+
import {
23+
dispatchOrgScopedList,
24+
jsonTransformListResult,
25+
type ListResult,
26+
type OrgListConfig,
27+
} from "./org-list.js";
2328
import { disableResponseCache } from "./response-cache.js";
2429

2530
// ---------------------------------------------------------------------------
@@ -403,20 +408,58 @@ export type OrgListCommandDocs = {
403408
readonly fullDescription?: string;
404409
};
405410

411+
/**
412+
* Format a {@link ListResult} as human-readable output using the config's
413+
* `displayTable` function. Handles empty results, headers, table body, and hints.
414+
*
415+
* @param result - The list result from a dispatch handler
416+
* @param config - The OrgListConfig providing the `displayTable` renderer
417+
* @returns Formatted string for terminal output
418+
*/
419+
function formatListHuman<TEntity, TWithOrg>(
420+
result: ListResult<TWithOrg>,
421+
config: OrgListConfig<TEntity, TWithOrg>
422+
): string {
423+
const parts: string[] = [];
424+
425+
if (result.items.length === 0) {
426+
// Empty result — show the hint (which contains the "No X found" message)
427+
if (result.hint) {
428+
parts.push(result.hint);
429+
}
430+
return parts.join("\n");
431+
}
432+
433+
// Table body
434+
parts.push(config.displayTable(result.items));
435+
436+
// Header contains count info like "Showing N items (more available)"
437+
if (result.header) {
438+
parts.push(`\n${result.header}`);
439+
}
440+
441+
return parts.join("");
442+
}
443+
444+
// JSON transform is shared via jsonTransformListResult in org-list.ts
445+
406446
/**
407447
* Build a complete Stricli command whose entire `func` body delegates to
408448
* `dispatchOrgScopedList`.
409449
*
410450
* This covers the team and repo list commands, where all runtime behaviour is
411-
* encapsulated in the shared org-list framework. The resulting command has:
451+
* encapsulated in the shared org-list framework. The resulting command has:
412452
* - An optional positional `target` argument
413453
* - `--limit` / `-n`, `--json`, `--fields`, `--cursor` / `-c` flags
414454
* - A `func` that calls `parseOrgProjectArg` then `dispatchOrgScopedList`
415455
*
416-
* `--json` and `--fields` are injected via `output: "json"` on `buildCommand`.
456+
* Rendering is handled automatically via `OutputConfig`:
457+
* - JSON mode produces paginated envelopes or flat arrays
458+
* - Human mode uses the config's `displayTable` function
417459
*
418460
* @param config - The `OrgListConfig` that drives fetching and display
419461
* @param docs - Brief and optional full description for `--help`
462+
* @param routeName - Singular route name for subcommand interception
420463
*/
421464
export function buildOrgListCommand<TEntity, TWithOrg>(
422465
config: OrgListConfig<TEntity, TWithOrg>,
@@ -425,7 +468,12 @@ export function buildOrgListCommand<TEntity, TWithOrg>(
425468
): Command<SentryContext> {
426469
return buildListCommand(routeName, {
427470
docs,
428-
output: "json",
471+
output: {
472+
json: true,
473+
human: (result: ListResult<TWithOrg>) => formatListHuman(result, config),
474+
jsonTransform: (result: ListResult<TWithOrg>, fields?: string[]) =>
475+
jsonTransformListResult(result, fields),
476+
} satisfies OutputConfig<ListResult<TWithOrg>>,
429477
parameters: {
430478
positional: LIST_TARGET_POSITIONAL,
431479
flags: {
@@ -445,11 +493,21 @@ export function buildOrgListCommand<TEntity, TWithOrg>(
445493
readonly fields?: string[];
446494
},
447495
target?: string
448-
): Promise<void> {
496+
): Promise<CommandOutput<ListResult<TWithOrg>>> {
449497
applyFreshFlag(flags);
450498
const { stdout, cwd } = this;
451499
const parsed = parseOrgProjectArg(target);
452-
await dispatchOrgScopedList({ config, stdout, cwd, flags, parsed });
500+
const result = await dispatchOrgScopedList({
501+
config,
502+
stdout,
503+
cwd,
504+
flags,
505+
parsed,
506+
});
507+
// Only forward hint to the footer when items exist — empty results
508+
// already render hint text inside the human formatter.
509+
const hint = result.items.length > 0 ? result.hint : undefined;
510+
return { data: result, hint };
453511
},
454512
});
455513
}

0 commit comments

Comments
 (0)