@@ -7,12 +7,24 @@ import {
77import { invalidParameterError } from "../common/errors.js" ;
88import { handleCommand , outputSuccess , parseLimit } from "../common/output.js" ;
99import { type DomainMeta , formatDomainUsage } from "../common/usage.js" ;
10+ import type {
11+ IssueLabelCreateInput ,
12+ IssueLabelUpdateInput ,
13+ } from "../gql/graphql.js" ;
14+ import {
15+ type LabelResolverScope ,
16+ resolveLabelId ,
17+ } from "../resolvers/label-resolver.js" ;
1018import { resolveTeamId } from "../resolvers/team-resolver.js" ;
1119import {
20+ createLabel ,
21+ deleteLabel ,
22+ getLabel ,
1223 type LabelScope ,
1324 type LabelType ,
1425 listLabels ,
1526 listProjectLabels ,
27+ updateLabel ,
1628} from "../services/label-service.js" ;
1729
1830interface ListLabelsOptions extends CommandOptions {
@@ -23,6 +35,23 @@ interface ListLabelsOptions extends CommandOptions {
2335 after ?: string ;
2436}
2537
38+ interface LabelLookupOptions extends CommandOptions {
39+ team ?: string ;
40+ scope ?: string ;
41+ }
42+
43+ interface CreateLabelOptions extends CommandOptions {
44+ team ?: string ;
45+ color ?: string ;
46+ description ?: string ;
47+ }
48+
49+ interface UpdateLabelOptions extends LabelLookupOptions {
50+ name ?: string ;
51+ color ?: string ;
52+ description ?: string ;
53+ }
54+
2655function parseLabelType ( value ?: string ) : LabelType {
2756 if ( value === undefined || value === "issue" || value === "project" ) {
2857 return value ?? "issue" ;
@@ -42,16 +71,89 @@ function parseLabelScope(value?: string): LabelScope | undefined {
4271 ) ;
4372}
4473
74+ function parseLabelColor ( value ?: string ) : string | undefined {
75+ if ( value === undefined ) {
76+ return undefined ;
77+ }
78+
79+ if ( ! / ^ # [ 0 - 9 a - f A - F ] { 6 } $ / . test ( value ) ) {
80+ throw invalidParameterError ( "--color" , "must be a hex color like #B45309" ) ;
81+ }
82+
83+ return value ;
84+ }
85+
86+ async function resolveIssueLabelLookup (
87+ command : Command ,
88+ label : string ,
89+ options : LabelLookupOptions ,
90+ ) : Promise < { ctx : ReturnType < typeof createContext > ; labelId : string } > {
91+ const ctx = createContext ( getRootOpts ( command ) ) ;
92+ const scope = parseLabelScope ( options . scope ) ;
93+
94+ if ( scope === "team" && ! options . team ) {
95+ throw invalidParameterError ( "--scope" , "team scope requires --team" ) ;
96+ }
97+
98+ if ( scope === "workspace" && options . team ) {
99+ throw invalidParameterError (
100+ "--team" ,
101+ "cannot be used with --scope workspace" ,
102+ ) ;
103+ }
104+
105+ const teamId = options . team
106+ ? await resolveTeamId ( ctx . sdk , options . team )
107+ : undefined ;
108+ const labelId = await resolveLabelId ( ctx . sdk , label , {
109+ teamId,
110+ scope : scope as LabelResolverScope | undefined ,
111+ } ) ;
112+
113+ return { ctx, labelId } ;
114+ }
115+
116+ function buildUpdateInput ( options : UpdateLabelOptions ) : IssueLabelUpdateInput {
117+ const input : IssueLabelUpdateInput = { } ;
118+ const color = parseLabelColor ( options . color ) ;
119+
120+ if ( options . name ) {
121+ input . name = options . name ;
122+ }
123+
124+ if ( color ) {
125+ input . color = color ;
126+ }
127+
128+ if ( options . description ) {
129+ input . description = options . description ;
130+ }
131+
132+ if ( Object . keys ( input ) . length === 0 ) {
133+ throw invalidParameterError (
134+ "label update" ,
135+ "at least one option must be provided" ,
136+ ) ;
137+ }
138+
139+ return input ;
140+ }
141+
45142export const LABELS_META : DomainMeta = {
46143 name : "labels" ,
47144 summary : "categorization tags for issues and projects" ,
48145 context : [
49146 "issue labels can exist at workspace level or be scoped to a specific" ,
50- "team. project labels are workspace-level only. use with issues" ,
51- "create/update --labels and projects create/update --labels." ,
147+ "team. project labels are workspace-level only. use labels list to" ,
148+ "inspect existing labels, labels create/read/update/delete for issue" ,
149+ "labels, and issues/projects create/update --labels to apply them." ,
52150 ] . join ( "\n" ) ,
53- arguments : { } ,
151+ arguments : { name : "label name or UUID" } ,
54152 seeAlso : [
153+ "labels create <name>" ,
154+ "labels read <label>" ,
155+ "labels update <label>" ,
156+ "labels delete <label>" ,
55157 "issues create --labels" ,
56158 "issues update --labels" ,
57159 "projects create --labels" ,
@@ -122,6 +224,119 @@ export function setupLabelsCommands(program: Command): void {
122224 } ) ,
123225 ) ;
124226
227+ labels
228+ . command ( "create <name>" )
229+ . description ( "create an issue label" )
230+ . option ( "--team <team>" , "create a team-scoped label (key, name, or UUID)" )
231+ . option ( "--color <hex>" , "label color as a hex code (for example #B45309)" )
232+ . option ( "--description <text>" , "label description" )
233+ . action (
234+ handleCommand ( async ( ...args : unknown [ ] ) => {
235+ const [ name , options , command ] = args as [
236+ string ,
237+ CreateLabelOptions ,
238+ Command ,
239+ ] ;
240+ const ctx = createContext ( getRootOpts ( command ) ) ;
241+
242+ const input : IssueLabelCreateInput = { name } ;
243+ const color = parseLabelColor ( options . color ) ;
244+
245+ if ( options . team ) {
246+ input . teamId = await resolveTeamId ( ctx . sdk , options . team ) ;
247+ }
248+
249+ if ( color ) {
250+ input . color = color ;
251+ }
252+
253+ if ( options . description ) {
254+ input . description = options . description ;
255+ }
256+
257+ outputSuccess ( await createLabel ( ctx . gql , input ) ) ;
258+ } ) ,
259+ ) ;
260+
261+ labels
262+ . command ( "read <label>" )
263+ . description ( "read an issue label" )
264+ . option (
265+ "--team <team>" ,
266+ "resolve a team-scoped label by team (key, name, or UUID)" ,
267+ )
268+ . option ( "--scope <scope>" , "resolve within workspace or team scope" )
269+ . action (
270+ handleCommand ( async ( ...args : unknown [ ] ) => {
271+ const [ label , options , command ] = args as [
272+ string ,
273+ LabelLookupOptions ,
274+ Command ,
275+ ] ;
276+ const { ctx, labelId } = await resolveIssueLabelLookup (
277+ command ,
278+ label ,
279+ options ,
280+ ) ;
281+
282+ outputSuccess ( await getLabel ( ctx . gql , labelId ) ) ;
283+ } ) ,
284+ ) ;
285+
286+ labels
287+ . command ( "update <label>" )
288+ . description ( "update an issue label" )
289+ . option (
290+ "--team <team>" ,
291+ "resolve a team-scoped label by team (key, name, or UUID)" ,
292+ )
293+ . option ( "--scope <scope>" , "resolve within workspace or team scope" )
294+ . option ( "--name <name>" , "new label name" )
295+ . option ( "--color <hex>" , "new label color as a hex code" )
296+ . option ( "--description <text>" , "new label description" )
297+ . action (
298+ handleCommand ( async ( ...args : unknown [ ] ) => {
299+ const [ label , options , command ] = args as [
300+ string ,
301+ UpdateLabelOptions ,
302+ Command ,
303+ ] ;
304+ const input = buildUpdateInput ( options ) ;
305+ const { ctx, labelId } = await resolveIssueLabelLookup (
306+ command ,
307+ label ,
308+ options ,
309+ ) ;
310+
311+ outputSuccess ( await updateLabel ( ctx . gql , labelId , input ) ) ;
312+ } ) ,
313+ ) ;
314+
315+ labels
316+ . command ( "delete <label>" )
317+ . description ( "delete an issue label" )
318+ . option (
319+ "--team <team>" ,
320+ "resolve a team-scoped label by team (key, name, or UUID)" ,
321+ )
322+ . option ( "--scope <scope>" , "resolve within workspace or team scope" )
323+ . action (
324+ handleCommand ( async ( ...args : unknown [ ] ) => {
325+ const [ label , options , command ] = args as [
326+ string ,
327+ LabelLookupOptions ,
328+ Command ,
329+ ] ;
330+ const { ctx, labelId } = await resolveIssueLabelLookup (
331+ command ,
332+ label ,
333+ options ,
334+ ) ;
335+
336+ outputSuccess ( await deleteLabel ( ctx . gql , labelId ) ) ;
337+ } ) ,
338+ ) ;
339+
125340 labels
126341 . command ( "usage" )
127342 . description ( "show detailed usage for labels" )
0 commit comments