@@ -8,51 +8,63 @@ import type { SentryContext } from "../../context.js";
88import { listDashboards } from "../../lib/api-client.js" ;
99import { parseOrgProjectArg } from "../../lib/arg-parsing.js" ;
1010import { openInBrowser } from "../../lib/browser.js" ;
11- import { ContextError } from "../../lib/errors.js" ;
12- import { writeFooter , writeJson } from "../../lib/formatters/index.js" ;
11+ import { buildCommand } from "../../lib/command.js" ;
1312import { escapeMarkdownCell } from "../../lib/formatters/markdown.js" ;
1413import { type Column , writeTable } from "../../lib/formatters/table.js" ;
1514import {
16- buildListCommand ,
17- LIST_TARGET_POSITIONAL ,
15+ applyFreshFlag ,
16+ FRESH_ALIASES ,
17+ FRESH_FLAG ,
1818} from "../../lib/list-command.js" ;
19- import { resolveOrg } from "../../lib/resolve-target.js" ;
2019import { buildDashboardsListUrl } from "../../lib/sentry-urls.js" ;
2120import type { DashboardListItem } from "../../types/dashboard.js" ;
21+ import type { Writer } from "../../types/index.js" ;
22+ import { resolveOrgFromTarget } from "./resolve.js" ;
2223
2324type ListFlags = {
2425 readonly web : boolean ;
26+ readonly fresh : boolean ;
2527 readonly json : boolean ;
2628 readonly fields ?: string [ ] ;
2729} ;
2830
29- /** Resolve org slug from parsed target argument */
30- async function resolveOrgFromTarget (
31- parsed : ReturnType < typeof parseOrgProjectArg > ,
32- cwd : string
33- ) : Promise < string > {
34- switch ( parsed . type ) {
35- case "explicit" :
36- case "org-all" :
37- return parsed . org ;
38- case "project-search" :
39- case "auto-detect" : {
40- const resolved = await resolveOrg ( { cwd } ) ;
41- if ( ! resolved ) {
42- throw new ContextError ( "Organization" , "sentry dashboard list <org>/" ) ;
43- }
44- return resolved . org ;
45- }
46- default : {
47- const _exhaustive : never = parsed ;
48- throw new Error (
49- `Unexpected parsed type: ${ ( _exhaustive as { type : string } ) . type } `
50- ) ;
51- }
31+ /**
32+ * Format dashboard list for human-readable terminal output.
33+ *
34+ * Renders a table with ID, title, and widget count columns.
35+ * Returns "No dashboards found." for empty results.
36+ */
37+ function formatDashboardListHuman ( dashboards : DashboardListItem [ ] ) : string {
38+ if ( dashboards . length === 0 ) {
39+ return "No dashboards found." ;
5240 }
41+
42+ type DashboardRow = {
43+ id : string ;
44+ title : string ;
45+ widgets : string ;
46+ } ;
47+
48+ const rows : DashboardRow [ ] = dashboards . map ( ( d ) => ( {
49+ id : d . id ,
50+ title : escapeMarkdownCell ( d . title ) ,
51+ widgets : String ( d . widgetDisplay ?. length ?? 0 ) ,
52+ } ) ) ;
53+
54+ const columns : Column < DashboardRow > [ ] = [
55+ { header : "ID" , value : ( r ) => r . id } ,
56+ { header : "TITLE" , value : ( r ) => r . title } ,
57+ { header : "WIDGETS" , value : ( r ) => r . widgets } ,
58+ ] ;
59+
60+ const parts : string [ ] = [ ] ;
61+ const buffer : Writer = { write : ( s ) => parts . push ( s ) } ;
62+ writeTable ( buffer , rows , columns ) ;
63+
64+ return parts . join ( "" ) . trimEnd ( ) ;
5365}
5466
55- export const listCommand = buildListCommand ( "dashboard" , {
67+ export const listCommand = buildCommand ( {
5668 docs : {
5769 brief : "List dashboards" ,
5870 fullDescription :
@@ -63,66 +75,52 @@ export const listCommand = buildListCommand("dashboard", {
6375 " sentry dashboard list --json\n" +
6476 " sentry dashboard list --web" ,
6577 } ,
66- output : " json" ,
78+ output : { json : true , human : formatDashboardListHuman } ,
6779 parameters : {
68- positional : LIST_TARGET_POSITIONAL ,
80+ positional : {
81+ kind : "tuple" ,
82+ parameters : [
83+ {
84+ placeholder : "org/project" ,
85+ brief :
86+ "<org>/ (all projects), <org>/<project>, or <project> (search)" ,
87+ parse : String ,
88+ optional : true ,
89+ } ,
90+ ] ,
91+ } ,
6992 flags : {
7093 web : {
7194 kind : "boolean" ,
7295 brief : "Open in browser" ,
7396 default : false ,
7497 } ,
98+ fresh : FRESH_FLAG ,
7599 } ,
76- aliases : { w : "web" } ,
100+ aliases : { ... FRESH_ALIASES , w : "web" } ,
77101 } ,
78- async func (
79- this : SentryContext ,
80- flags : ListFlags ,
81- target ?: string
82- ) : Promise < void > {
83- const { stdout, cwd } = this ;
102+ async func ( this : SentryContext , flags : ListFlags , target ?: string ) {
103+ applyFreshFlag ( flags ) ;
104+ const { cwd } = this ;
84105
85106 const parsed = parseOrgProjectArg ( target ) ;
86- const orgSlug = await resolveOrgFromTarget ( parsed , cwd ) ;
107+ const orgSlug = await resolveOrgFromTarget (
108+ parsed ,
109+ cwd ,
110+ "sentry dashboard list <org>/"
111+ ) ;
87112
88113 if ( flags . web ) {
89114 await openInBrowser ( buildDashboardsListUrl ( orgSlug ) , "dashboards" ) ;
90115 return ;
91116 }
92117
93118 const dashboards = await listDashboards ( orgSlug ) ;
119+ const url = buildDashboardsListUrl ( orgSlug ) ;
94120
95- if ( flags . json ) {
96- writeJson ( stdout , dashboards , flags . fields ) ;
97- return ;
98- }
99-
100- if ( dashboards . length === 0 ) {
101- stdout . write ( "No dashboards found.\n" ) ;
102- return ;
103- }
104-
105- type DashboardRow = {
106- id : string ;
107- title : string ;
108- widgets : string ;
121+ return {
122+ data : dashboards ,
123+ hint : dashboards . length > 0 ? `Dashboards: ${ url } ` : undefined ,
109124 } ;
110-
111- const rows : DashboardRow [ ] = dashboards . map ( ( d : DashboardListItem ) => ( {
112- id : d . id ,
113- title : escapeMarkdownCell ( d . title ) ,
114- widgets : String ( d . widgetDisplay ?. length ?? 0 ) ,
115- } ) ) ;
116-
117- const columns : Column < DashboardRow > [ ] = [
118- { header : "ID" , value : ( r ) => r . id } ,
119- { header : "TITLE" , value : ( r ) => r . title } ,
120- { header : "WIDGETS" , value : ( r ) => r . widgets } ,
121- ] ;
122-
123- writeTable ( stdout , rows , columns ) ;
124-
125- const url = buildDashboardsListUrl ( orgSlug ) ;
126- writeFooter ( stdout , `Dashboards: ${ url } ` ) ;
127125 } ,
128126} ) ;
0 commit comments