@@ -13,7 +13,6 @@ import {
1313 listProjects ,
1414} from "../../lib/api-client.js" ;
1515import { parseOrgProjectArg } from "../../lib/arg-parsing.js" ;
16- import { buildCommand , numberParser } from "../../lib/command.js" ;
1716import {
1817 clearProjectAliases ,
1918 setProjectAliases ,
@@ -28,6 +27,10 @@ import {
2827 muted ,
2928 writeJson ,
3029} from "../../lib/formatters/index.js" ;
30+ import {
31+ listCommand as buildListCommand ,
32+ type ListResult ,
33+ } from "../../lib/list-helpers.js" ;
3134import {
3235 type ResolvedTarget ,
3336 resolveAllTargets ,
@@ -38,13 +41,6 @@ import type {
3841 Writer ,
3942} from "../../types/index.js" ;
4043
41- type ListFlags = {
42- readonly query ?: string ;
43- readonly limit : number ;
44- readonly sort : "date" | "new" | "freq" | "user" ;
45- readonly json : boolean ;
46- } ;
47-
4844type SortValue = "date" | "new" | "freq" | "user" ;
4945
5046const VALID_SORT_VALUES : SortValue [ ] = [ "date" , "new" , "freq" , "user" ] ;
@@ -55,32 +51,6 @@ const USAGE_HINT = "sentry issue list <org>/<project>";
5551/** Error type classification for fetch failures */
5652type FetchErrorType = "permission" | "network" | "unknown" ;
5753
58- function parseSort ( value : string ) : SortValue {
59- if ( ! VALID_SORT_VALUES . includes ( value as SortValue ) ) {
60- throw new Error (
61- `Invalid sort value. Must be one of: ${ VALID_SORT_VALUES . join ( ", " ) } `
62- ) ;
63- }
64- return value as SortValue ;
65- }
66-
67- /**
68- * Write the issue list header with column titles.
69- *
70- * @param stdout - Output writer
71- * @param title - Section title
72- * @param isMultiProject - Whether to show ALIAS column for multi-project mode
73- */
74- function writeListHeader (
75- stdout : Writer ,
76- title : string ,
77- isMultiProject = false
78- ) : void {
79- stdout . write ( `${ title } :\n\n` ) ;
80- stdout . write ( muted ( `${ formatIssueListHeader ( isMultiProject ) } \n` ) ) ;
81- stdout . write ( `${ divider ( isMultiProject ? 96 : 80 ) } \n` ) ;
82- }
83-
8454/** Issue with formatting options attached */
8555type IssueWithOptions = {
8656 issue : SentryIssue ;
@@ -100,34 +70,6 @@ function writeIssueRows(
10070 }
10171}
10272
103- /**
104- * Write footer with usage tip.
105- *
106- * @param stdout - Output writer
107- * @param mode - Display mode: 'single' (one project), 'multi' (multiple projects), or 'none'
108- */
109- function writeListFooter (
110- stdout : Writer ,
111- mode : "single" | "multi" | "none"
112- ) : void {
113- switch ( mode ) {
114- case "single" :
115- stdout . write (
116- "\nTip: Use 'sentry issue view <ID>' to view details (bold part works as shorthand).\n"
117- ) ;
118- break ;
119- case "multi" :
120- stdout . write (
121- "\nTip: Use 'sentry issue view <ALIAS>' to view details (see ALIAS column).\n"
122- ) ;
123- break ;
124- default :
125- stdout . write (
126- "\nTip: Use 'sentry issue view <SHORT_ID>' to view issue details.\n"
127- ) ;
128- }
129- }
130-
13173/** Issue list with target context */
13274type IssueListResult = {
13375 target : ResolvedTarget ;
@@ -380,7 +322,23 @@ async function fetchIssuesForTarget(
380322 }
381323}
382324
383- export const listCommand = buildCommand ( {
325+ /**
326+ * Pick the footer tip text based on display mode.
327+ */
328+ function pickFooterTip (
329+ isMultiProject : boolean ,
330+ hasSingleProject : boolean
331+ ) : string {
332+ if ( isMultiProject ) {
333+ return "Tip: Use 'sentry issue view <ALIAS>' to view details (see ALIAS column)." ;
334+ }
335+ if ( hasSingleProject ) {
336+ return "Tip: Use 'sentry issue view <ID>' to view details (bold part works as shorthand)." ;
337+ }
338+ return "Tip: Use 'sentry issue view <SHORT_ID>' to view issue details." ;
339+ }
340+
341+ export const listCommand = buildListCommand < IssueWithOptions > ( {
384342 docs : {
385343 brief : "List issues in a project" ,
386344 fullDescription :
@@ -392,53 +350,19 @@ export const listCommand = buildCommand({
392350 " sentry issue list <project> # find project across all orgs\n\n" +
393351 "In monorepos with multiple Sentry projects, shows issues from all detected projects." ,
394352 } ,
395- parameters : {
396- positional : {
397- kind : "tuple" ,
398- parameters : [
399- {
400- placeholder : "target" ,
401- brief : "Target: <org>/<project>, <org>/, or <project>" ,
402- parse : String ,
403- optional : true ,
404- } ,
405- ] ,
406- } ,
407- flags : {
408- query : {
409- kind : "parsed" ,
410- parse : String ,
411- brief : "Search query (Sentry search syntax)" ,
412- optional : true ,
413- } ,
414- limit : {
415- kind : "parsed" ,
416- parse : numberParser ,
417- brief : "Maximum number of issues to return" ,
418- // Stricli requires string defaults (raw CLI input); numberParser converts to number
419- default : "10" ,
420- } ,
421- sort : {
422- kind : "parsed" ,
423- parse : parseSort ,
424- brief : "Sort by: date, new, freq, user" ,
425- default : "date" as const ,
426- } ,
427- json : {
428- kind : "boolean" ,
429- brief : "Output as JSON" ,
430- default : false ,
431- } ,
432- } ,
433- aliases : { q : "query" , s : "sort" , n : "limit" } ,
353+ limit : 10 ,
354+ features : {
355+ query : true ,
356+ sort : VALID_SORT_VALUES ,
357+ } ,
358+ positional : {
359+ placeholder : "target" ,
360+ brief : "Target: <org>/<project>, <org>/, or <project>" ,
361+ optional : true ,
434362 } ,
435363 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: command entry point with inherent complexity
436- async func (
437- this : SentryContext ,
438- flags : ListFlags ,
439- target ?: string
440- ) : Promise < void > {
441- const { stdout, cwd, setContext } = this ;
364+ async fetch ( this : SentryContext , flags , target ) {
365+ const { cwd, setContext } = this ;
442366
443367 // Parse positional argument to determine resolution strategy
444368 const parsed = parseOrgProjectArg ( target ) ;
@@ -465,12 +389,12 @@ export const listCommand = buildCommand({
465389 }
466390
467391 // Fetch issues from all targets in parallel
468- const results = await Promise . all (
392+ const fetchResults = await Promise . all (
469393 targets . map ( ( t ) =>
470394 fetchIssuesForTarget ( t , {
471395 query : flags . query ,
472396 limit : flags . limit ,
473- sort : flags . sort ,
397+ sort : ( flags . sort as SortValue | undefined ) ?? "date" ,
474398 } )
475399 )
476400 ) ;
@@ -479,7 +403,7 @@ export const listCommand = buildCommand({
479403 const validResults : IssueListResult [ ] = [ ] ;
480404 const errorTypes = new Set < FetchErrorType > ( ) ;
481405
482- for ( const result of results ) {
406+ for ( const result of fetchResults ) {
483407 if ( result . success ) {
484408 validResults . push ( result . data ) ;
485409 } else {
@@ -535,47 +459,45 @@ export const listCommand = buildCommand({
535459 ) ;
536460
537461 // Sort by user preference
462+ const sortValue = ( flags . sort as SortValue | undefined ) ?? "date" ;
538463 issuesWithOptions . sort ( ( a , b ) =>
539- getComparator ( flags . sort ) ( a . issue , b . issue )
464+ getComparator ( sortValue ) ( a . issue , b . issue )
540465 ) ;
541466
542- // JSON output
543- if ( flags . json ) {
544- const allIssues = issuesWithOptions . map ( ( i ) => i . issue ) ;
545- writeJson ( stdout , allIssues ) ;
546- return ;
547- }
548-
549- if ( issuesWithOptions . length === 0 ) {
550- stdout . write ( "No issues found.\n" ) ;
551- if ( footer ) {
552- stdout . write ( `\n${ footer } \n` ) ;
553- }
554- return ;
555- }
556-
557- // Header depends on single vs multiple projects
558- const title =
467+ // Build title for the header line (written by render)
468+ // Colon suffix matches original output: "Issues in org/project:"
469+ const header =
559470 isSingleProject && firstTarget
560- ? `Issues in ${ firstTarget . orgDisplay } /${ firstTarget . projectDisplay } `
561- : `Issues from ${ validResults . length } projects` ;
562-
563- writeListHeader ( stdout , title , isMultiProject ) ;
564-
471+ ? `Issues in ${ firstTarget . orgDisplay } /${ firstTarget . projectDisplay } :`
472+ : `Issues from ${ validResults . length } projects:` ;
473+
474+ return {
475+ items : issuesWithOptions ,
476+ footer,
477+ skippedSelfHosted,
478+ header,
479+ } satisfies ListResult < IssueWithOptions > ;
480+ } ,
481+ render ( items , stdout , _flags ) {
482+ const isMultiProject = items [ 0 ] ?. formatOptions . isMultiProject ?? false ;
483+ // The factory already wrote the header title line; write only column headers + divider
484+ stdout . write ( "\n" ) ;
485+ stdout . write ( muted ( `${ formatIssueListHeader ( isMultiProject ) } \n` ) ) ;
486+ stdout . write ( `${ divider ( isMultiProject ? 96 : 80 ) } \n` ) ;
565487 const termWidth = process . stdout . columns || 80 ;
566- writeIssueRows ( stdout , issuesWithOptions , termWidth ) ;
567-
568- // Footer mode
569- let footerMode : "single" | "multi" | "none" = "none" ;
570- if ( isMultiProject ) {
571- footerMode = "multi" ;
572- } else if ( isSingleProject ) {
573- footerMode = "single" ;
574- }
575- writeListFooter ( stdout , footerMode ) ;
576-
577- if ( footer ) {
578- stdout . write ( `\n${ footer } \n` ) ;
579- }
488+ writeIssueRows ( stdout , items , termWidth ) ;
489+ } ,
490+ formatJson ( result , stdout ) {
491+ writeJson (
492+ stdout ,
493+ result . items . map ( ( i ) => i . issue )
494+ ) ;
495+ } ,
496+ footerTip ( result ) {
497+ const isMultiProject =
498+ result . items [ 0 ] ?. formatOptions . isMultiProject ?? false ;
499+ const isSingleProject = result . items . length > 0 && ! isMultiProject ;
500+ return pickFooterTip ( isMultiProject , isSingleProject ) ;
580501 } ,
502+ emptyMessage : "No issues found." ,
581503} ) ;
0 commit comments