linear-cli-agents 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +166 -3
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/comments/add.d.ts +13 -0
  4. package/dist/commands/comments/add.js +89 -0
  5. package/dist/commands/comments/delete.d.ts +12 -0
  6. package/dist/commands/comments/delete.js +50 -0
  7. package/dist/commands/comments/list.d.ts +14 -0
  8. package/dist/commands/comments/list.js +103 -0
  9. package/dist/commands/comments/update.d.ts +13 -0
  10. package/dist/commands/comments/update.js +81 -0
  11. package/dist/commands/issues/add-labels.d.ts +13 -0
  12. package/dist/commands/issues/add-labels.js +90 -0
  13. package/dist/commands/issues/archive.d.ts +13 -0
  14. package/dist/commands/issues/archive.js +83 -0
  15. package/dist/commands/issues/remove-labels.d.ts +13 -0
  16. package/dist/commands/issues/remove-labels.js +90 -0
  17. package/dist/commands/labels/create.d.ts +14 -0
  18. package/dist/commands/labels/create.js +102 -0
  19. package/dist/commands/labels/delete.d.ts +12 -0
  20. package/dist/commands/labels/delete.js +50 -0
  21. package/dist/commands/labels/list.d.ts +12 -0
  22. package/dist/commands/labels/list.js +117 -0
  23. package/dist/commands/labels/update.d.ts +16 -0
  24. package/dist/commands/labels/update.js +109 -0
  25. package/dist/commands/me.js +1 -5
  26. package/dist/commands/milestones/create.d.ts +15 -0
  27. package/dist/commands/milestones/create.js +90 -0
  28. package/dist/commands/milestones/get.d.ts +12 -0
  29. package/dist/commands/milestones/get.js +74 -0
  30. package/dist/commands/milestones/list.d.ts +14 -0
  31. package/dist/commands/milestones/list.js +97 -0
  32. package/dist/commands/milestones/update.d.ts +15 -0
  33. package/dist/commands/milestones/update.js +94 -0
  34. package/dist/commands/project-updates/create.d.ts +14 -0
  35. package/dist/commands/project-updates/create.js +96 -0
  36. package/dist/commands/project-updates/get.d.ts +12 -0
  37. package/dist/commands/project-updates/get.js +80 -0
  38. package/dist/commands/project-updates/list.d.ts +14 -0
  39. package/dist/commands/project-updates/list.js +120 -0
  40. package/dist/commands/project-updates/update.d.ts +14 -0
  41. package/dist/commands/project-updates/update.js +96 -0
  42. package/dist/commands/projects/archive.d.ts +13 -0
  43. package/dist/commands/projects/archive.js +79 -0
  44. package/dist/commands/projects/create.d.ts +16 -0
  45. package/dist/commands/projects/create.js +115 -0
  46. package/dist/commands/projects/delete.d.ts +12 -0
  47. package/dist/commands/projects/delete.js +50 -0
  48. package/dist/commands/projects/get.d.ts +12 -0
  49. package/dist/commands/projects/get.js +102 -0
  50. package/dist/commands/projects/list.d.ts +13 -0
  51. package/dist/commands/projects/list.js +141 -0
  52. package/dist/commands/projects/update.d.ts +18 -0
  53. package/dist/commands/projects/update.js +125 -0
  54. package/dist/commands/relations/create.d.ts +14 -0
  55. package/dist/commands/relations/create.js +98 -0
  56. package/dist/commands/relations/delete.d.ts +12 -0
  57. package/dist/commands/relations/delete.js +47 -0
  58. package/dist/commands/relations/list.d.ts +12 -0
  59. package/dist/commands/relations/list.js +128 -0
  60. package/dist/commands/search.d.ts +15 -0
  61. package/dist/commands/search.js +102 -0
  62. package/dist/commands/states/list.d.ts +12 -0
  63. package/dist/commands/states/list.js +151 -0
  64. package/dist/commands/templates/create.d.ts +14 -0
  65. package/dist/commands/templates/create.js +102 -0
  66. package/dist/commands/templates/get.d.ts +12 -0
  67. package/dist/commands/templates/get.js +84 -0
  68. package/dist/commands/templates/list.d.ts +12 -0
  69. package/dist/commands/templates/list.js +110 -0
  70. package/dist/commands/templates/update.d.ts +15 -0
  71. package/dist/commands/templates/update.js +101 -0
  72. package/dist/commands/users/get.d.ts +12 -0
  73. package/dist/commands/users/get.js +91 -0
  74. package/dist/commands/users/list.d.ts +12 -0
  75. package/dist/commands/users/list.js +99 -0
  76. package/dist/lib/config.js +1 -1
  77. package/oclif.manifest.json +2397 -184
  78. package/package.json +47 -17
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # linear-cli-agents
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/linear-cli-agents.svg)](https://www.npmjs.com/package/linear-cli-agents)
4
- [![CI](https://github.com/nchgn/linear-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/nchgn/linear-cli/actions/workflows/ci.yml)
4
+ [![CI](https://github.com/nchgn/linear-cli-agents/actions/workflows/ci.yml/badge.svg)](https://github.com/nchgn/linear-cli-agents/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  A CLI for interacting with [Linear](https://linear.app), designed for LLMs and agents.
@@ -11,9 +11,12 @@ A CLI for interacting with [Linear](https://linear.app), designed for LLMs and a
11
11
  - **JSON output**: All commands return structured JSON, perfect for parsing by LLMs
12
12
  - **Multiple formats**: JSON (default), table (colored), or plain text output
13
13
  - **Schema introspection**: Discover available operations programmatically
14
- - **Full CRUD for issues**: List, create, update, and delete issues
15
- - **Team management**: List and browse teams
14
+ - **Full CRUD**: Issues, projects, labels, comments, templates, milestones
15
+ - **Issue relations**: Manage blocks, duplicates, and related issues
16
+ - **Project management**: Projects, milestones, and status updates
17
+ - **Team management**: List and browse teams, states, users
16
18
  - **Browser integration**: Open issues, teams, inbox directly in Linear
19
+ - **Search**: Find issues across workspace
17
20
  - **Raw GraphQL queries**: Execute any GraphQL query directly
18
21
 
19
22
  ## Installation
@@ -78,6 +81,166 @@ linear issues update ENG-123 --state-id <state-id> --assignee-id <user-id>
78
81
 
79
82
  # Delete an issue (moves to trash)
80
83
  linear issues delete ENG-123
84
+
85
+ # Archive/unarchive an issue
86
+ linear issues archive ENG-123
87
+ linear issues archive ENG-123 --unarchive
88
+
89
+ # Manage labels on issues
90
+ linear issues add-labels ENG-123 --label-ids LABEL_ID1,LABEL_ID2
91
+ linear issues remove-labels ENG-123 --label-ids LABEL_ID1
92
+ ```
93
+
94
+ ### Projects
95
+
96
+ ```bash
97
+ # List projects
98
+ linear projects list
99
+ linear projects list --team ENG
100
+ linear projects list --state started
101
+
102
+ # Get project details
103
+ linear projects get PROJECT_ID
104
+
105
+ # Create a project
106
+ linear projects create --name "Q1 Goals" --team-ids TEAM_ID
107
+ linear projects create --name "Feature X" --team-ids TEAM_ID --target-date 2024-06-30
108
+
109
+ # Update a project
110
+ linear projects update PROJECT_ID --name "Updated Name"
111
+ linear projects update PROJECT_ID --state completed
112
+
113
+ # Delete a project
114
+ linear projects delete PROJECT_ID
115
+
116
+ # Archive/unarchive a project
117
+ linear projects archive PROJECT_ID
118
+ linear projects archive PROJECT_ID --unarchive
119
+ ```
120
+
121
+ ### Project Milestones
122
+
123
+ ```bash
124
+ # List milestones for a project
125
+ linear milestones list PROJECT_ID
126
+
127
+ # Get milestone details
128
+ linear milestones get MILESTONE_ID
129
+
130
+ # Create a milestone
131
+ linear milestones create PROJECT_ID --name "Alpha Release" --target-date 2024-03-01
132
+
133
+ # Update a milestone
134
+ linear milestones update MILESTONE_ID --name "Beta Release"
135
+ ```
136
+
137
+ ### Project Updates
138
+
139
+ ```bash
140
+ # List status updates for a project
141
+ linear project-updates list PROJECT_ID
142
+
143
+ # Get update details
144
+ linear project-updates get UPDATE_ID
145
+
146
+ # Create a status update
147
+ linear project-updates create PROJECT_ID --body "Sprint completed" --health onTrack
148
+
149
+ # Update a status update
150
+ linear project-updates update UPDATE_ID --body "Updated status"
151
+ ```
152
+
153
+ ### Issue Relations
154
+
155
+ ```bash
156
+ # List relations for an issue
157
+ linear relations list ENG-123
158
+
159
+ # Create a relation
160
+ linear relations create ENG-123 ENG-456 --type blocks
161
+ linear relations create ENG-123 ENG-456 --type duplicate
162
+ linear relations create ENG-123 ENG-456 --type related
163
+
164
+ # Delete a relation
165
+ linear relations delete RELATION_ID
166
+ ```
167
+
168
+ ### Labels
169
+
170
+ ```bash
171
+ # List labels
172
+ linear labels list
173
+ linear labels list --team ENG
174
+
175
+ # Create a label
176
+ linear labels create --name "Bug" --color "#FF0000"
177
+ linear labels create --name "Feature" --color "#00FF00" --team-id TEAM_ID
178
+
179
+ # Update a label
180
+ linear labels update LABEL_ID --name "Critical Bug" --color "#FF0000"
181
+
182
+ # Delete a label
183
+ linear labels delete LABEL_ID
184
+ ```
185
+
186
+ ### Templates
187
+
188
+ ```bash
189
+ # List templates
190
+ linear templates list
191
+ linear templates list --team ENG
192
+
193
+ # Get template details
194
+ linear templates get TEMPLATE_ID
195
+
196
+ # Create a template
197
+ linear templates create --name "Bug Report" --type issue --team-id TEAM_ID \
198
+ --template-data '{"title":"Bug: ","priority":2}'
199
+
200
+ # Update a template
201
+ linear templates update TEMPLATE_ID --name "Updated Template"
202
+ ```
203
+
204
+ ### Comments
205
+
206
+ ```bash
207
+ # List comments on an issue
208
+ linear comments list ENG-123
209
+
210
+ # Add a comment
211
+ linear comments add ENG-123 --body "This looks good!"
212
+
213
+ # Update a comment
214
+ linear comments update COMMENT_ID --body "Updated comment"
215
+
216
+ # Delete a comment
217
+ linear comments delete COMMENT_ID
218
+ ```
219
+
220
+ ### States
221
+
222
+ ```bash
223
+ # List workflow states
224
+ linear states list
225
+ linear states list --team ENG
226
+ ```
227
+
228
+ ### Users
229
+
230
+ ```bash
231
+ # List users
232
+ linear users list
233
+
234
+ # Get user details
235
+ linear users get USER_ID
236
+ ```
237
+
238
+ ### Search
239
+
240
+ ```bash
241
+ # Search issues
242
+ linear search "login bug"
243
+ linear search "SSO" --team ENG
81
244
  ```
82
245
 
83
246
  ### Teams
package/bin/dev.js CHANGED
File without changes
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CommentsAdd extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ issue: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ body: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,89 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print, printItem } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ import { resolveIssueId } from '../../lib/issue-utils.js';
6
+ export default class CommentsAdd extends Command {
7
+ static description = 'Add a comment to an issue';
8
+ static examples = [
9
+ '<%= config.bin %> comments add ENG-123 --body "This is a comment"',
10
+ '<%= config.bin %> comments add ENG-123 --body "Looks good!" --format table',
11
+ ];
12
+ static args = {
13
+ issue: Args.string({
14
+ description: 'Issue ID or identifier (e.g., ENG-123)',
15
+ required: true,
16
+ }),
17
+ };
18
+ static flags = {
19
+ format: Flags.string({
20
+ char: 'F',
21
+ description: 'Output format',
22
+ options: ['json', 'table', 'plain'],
23
+ default: 'json',
24
+ }),
25
+ body: Flags.string({
26
+ char: 'b',
27
+ description: 'Comment body (supports markdown)',
28
+ required: true,
29
+ }),
30
+ };
31
+ async run() {
32
+ try {
33
+ const { args, flags } = await this.parse(CommentsAdd);
34
+ const format = flags.format;
35
+ const client = getClient();
36
+ const issueId = await resolveIssueId(client, args.issue);
37
+ const issue = await client.issue(issueId);
38
+ if (!issue) {
39
+ throw new CliError(ErrorCodes.NOT_FOUND, `Issue ${args.issue} not found`);
40
+ }
41
+ const payload = await client.createComment({
42
+ issueId,
43
+ body: flags.body,
44
+ });
45
+ if (!payload.success || !payload.comment) {
46
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to create comment');
47
+ }
48
+ const comment = await payload.comment;
49
+ const user = await comment.user;
50
+ const data = {
51
+ id: comment.id,
52
+ body: comment.body,
53
+ createdAt: comment.createdAt,
54
+ user: user
55
+ ? {
56
+ id: user.id,
57
+ name: user.name,
58
+ email: user.email,
59
+ }
60
+ : null,
61
+ issue: {
62
+ id: issue.id,
63
+ identifier: issue.identifier,
64
+ title: issue.title,
65
+ },
66
+ };
67
+ if (format === 'json') {
68
+ print(success(data));
69
+ }
70
+ else if (format === 'table') {
71
+ printItem({
72
+ id: data.id,
73
+ issue: data.issue.identifier,
74
+ user: data.user?.name ?? 'Unknown',
75
+ body: data.body,
76
+ createdAt: data.createdAt,
77
+ }, format);
78
+ }
79
+ else {
80
+ // plain: just the comment ID
81
+ console.log(data.id);
82
+ }
83
+ }
84
+ catch (err) {
85
+ handleError(err);
86
+ this.exit(1);
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CommentsDelete extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,50 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ export default class CommentsDelete extends Command {
6
+ static description = 'Delete a comment';
7
+ static examples = ['<%= config.bin %> comments delete COMMENT_ID'];
8
+ static args = {
9
+ id: Args.string({
10
+ description: 'Comment ID',
11
+ required: true,
12
+ }),
13
+ };
14
+ static flags = {
15
+ format: Flags.string({
16
+ char: 'F',
17
+ description: 'Output format',
18
+ options: ['json', 'table', 'plain'],
19
+ default: 'json',
20
+ }),
21
+ };
22
+ async run() {
23
+ try {
24
+ const { args, flags } = await this.parse(CommentsDelete);
25
+ const format = flags.format;
26
+ const client = getClient();
27
+ const payload = await client.deleteComment(args.id);
28
+ if (!payload.success) {
29
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to delete comment');
30
+ }
31
+ const data = {
32
+ id: args.id,
33
+ deleted: true,
34
+ };
35
+ if (format === 'json') {
36
+ print(success(data));
37
+ }
38
+ else if (format === 'table') {
39
+ console.log(`Comment ${args.id} deleted successfully`);
40
+ }
41
+ else {
42
+ console.log(args.id);
43
+ }
44
+ }
45
+ catch (err) {
46
+ handleError(err);
47
+ this.exit(1);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CommentsList extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ issue: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ first: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ after: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,103 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { successList, print, printList } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ import { resolveIssueId } from '../../lib/issue-utils.js';
6
+ import { colors, truncate } from '../../lib/formatter.js';
7
+ const COLUMNS = [
8
+ {
9
+ key: 'userName',
10
+ header: 'USER',
11
+ format: (value) => colors.cyan(String(value)),
12
+ },
13
+ {
14
+ key: 'body',
15
+ header: 'COMMENT',
16
+ format: (value) => truncate(String(value).replace(/\n/g, ' '), 60),
17
+ },
18
+ {
19
+ key: 'createdAt',
20
+ header: 'DATE',
21
+ format: (value) => {
22
+ const date = new Date(value);
23
+ return colors.dim(date.toLocaleDateString());
24
+ },
25
+ },
26
+ ];
27
+ export default class CommentsList extends Command {
28
+ static description = 'List comments on an issue';
29
+ static examples = [
30
+ '<%= config.bin %> comments list ENG-123',
31
+ '<%= config.bin %> comments list ENG-123 --format table',
32
+ ];
33
+ static args = {
34
+ issue: Args.string({
35
+ description: 'Issue ID or identifier (e.g., ENG-123)',
36
+ required: true,
37
+ }),
38
+ };
39
+ static flags = {
40
+ format: Flags.string({
41
+ char: 'F',
42
+ description: 'Output format',
43
+ options: ['json', 'table', 'plain'],
44
+ default: 'json',
45
+ }),
46
+ first: Flags.integer({
47
+ description: 'Number of comments to fetch (default: 50)',
48
+ default: 50,
49
+ }),
50
+ after: Flags.string({
51
+ description: 'Cursor for pagination',
52
+ }),
53
+ };
54
+ async run() {
55
+ try {
56
+ const { args, flags } = await this.parse(CommentsList);
57
+ const format = flags.format;
58
+ const client = getClient();
59
+ const issueId = await resolveIssueId(client, args.issue);
60
+ const issue = await client.issue(issueId);
61
+ if (!issue) {
62
+ throw new CliError(ErrorCodes.NOT_FOUND, `Issue ${args.issue} not found`);
63
+ }
64
+ const comments = await issue.comments({
65
+ first: flags.first,
66
+ after: flags.after,
67
+ });
68
+ const data = await Promise.all(comments.nodes.map(async (comment) => {
69
+ const user = await comment.user;
70
+ return {
71
+ id: comment.id,
72
+ body: comment.body,
73
+ createdAt: comment.createdAt,
74
+ updatedAt: comment.updatedAt,
75
+ userId: user?.id ?? '',
76
+ userName: user?.name ?? 'Unknown',
77
+ userEmail: user?.email ?? '',
78
+ };
79
+ }));
80
+ const pageInfo = {
81
+ hasNextPage: comments.pageInfo.hasNextPage,
82
+ hasPreviousPage: comments.pageInfo.hasPreviousPage,
83
+ startCursor: comments.pageInfo.startCursor,
84
+ endCursor: comments.pageInfo.endCursor,
85
+ };
86
+ if (format === 'json') {
87
+ print(successList(data, pageInfo));
88
+ }
89
+ else {
90
+ printList(data, format, {
91
+ columns: COLUMNS,
92
+ primaryKey: 'userName',
93
+ secondaryKey: 'body',
94
+ pageInfo,
95
+ });
96
+ }
97
+ }
98
+ catch (err) {
99
+ handleError(err);
100
+ this.exit(1);
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CommentsUpdate extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ body: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,81 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print, printItem } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ export default class CommentsUpdate extends Command {
6
+ static description = 'Update a comment';
7
+ static examples = ['<%= config.bin %> comments update COMMENT_ID --body "Updated comment text"'];
8
+ static args = {
9
+ id: Args.string({
10
+ description: 'Comment ID',
11
+ required: true,
12
+ }),
13
+ };
14
+ static flags = {
15
+ format: Flags.string({
16
+ char: 'F',
17
+ description: 'Output format',
18
+ options: ['json', 'table', 'plain'],
19
+ default: 'json',
20
+ }),
21
+ body: Flags.string({
22
+ char: 'b',
23
+ description: 'Comment body (supports markdown)',
24
+ required: true,
25
+ }),
26
+ };
27
+ async run() {
28
+ try {
29
+ const { args, flags } = await this.parse(CommentsUpdate);
30
+ const format = flags.format;
31
+ const client = getClient();
32
+ const payload = await client.updateComment(args.id, {
33
+ body: flags.body,
34
+ });
35
+ if (!payload.success || !payload.comment) {
36
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to update comment');
37
+ }
38
+ const comment = await payload.comment;
39
+ const [user, issue] = await Promise.all([comment.user, comment.issue]);
40
+ const data = {
41
+ id: comment.id,
42
+ body: comment.body,
43
+ createdAt: comment.createdAt,
44
+ updatedAt: comment.updatedAt,
45
+ user: user
46
+ ? {
47
+ id: user.id,
48
+ name: user.name,
49
+ email: user.email,
50
+ }
51
+ : null,
52
+ issue: issue
53
+ ? {
54
+ id: issue.id,
55
+ identifier: issue.identifier,
56
+ title: issue.title,
57
+ }
58
+ : null,
59
+ };
60
+ if (format === 'json') {
61
+ print(success(data));
62
+ }
63
+ else if (format === 'table') {
64
+ printItem({
65
+ id: data.id,
66
+ issue: data.issue?.identifier ?? 'N/A',
67
+ user: data.user?.name ?? 'Unknown',
68
+ body: data.body,
69
+ updatedAt: data.updatedAt,
70
+ }, format);
71
+ }
72
+ else {
73
+ console.log(data.id);
74
+ }
75
+ }
76
+ catch (err) {
77
+ handleError(err);
78
+ this.exit(1);
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class IssuesAddLabels extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'label-ids': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,90 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print, printItem } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ import { resolveIssueId } from '../../lib/issue-utils.js';
6
+ export default class IssuesAddLabels extends Command {
7
+ static description = 'Add labels to an issue';
8
+ static examples = ['<%= config.bin %> issues add-labels ENG-123 --label-ids LABEL_ID1,LABEL_ID2'];
9
+ static args = {
10
+ id: Args.string({
11
+ description: 'Issue ID or identifier (e.g., ENG-123)',
12
+ required: true,
13
+ }),
14
+ };
15
+ static flags = {
16
+ format: Flags.string({
17
+ char: 'F',
18
+ description: 'Output format',
19
+ options: ['json', 'table', 'plain'],
20
+ default: 'json',
21
+ }),
22
+ 'label-ids': Flags.string({
23
+ description: 'Comma-separated label IDs to add',
24
+ required: true,
25
+ }),
26
+ };
27
+ async run() {
28
+ try {
29
+ const { args, flags } = await this.parse(IssuesAddLabels);
30
+ const format = flags.format;
31
+ const client = getClient();
32
+ const issueId = await resolveIssueId(client, args.id);
33
+ const issue = await client.issue(issueId);
34
+ if (!issue) {
35
+ throw new CliError(ErrorCodes.NOT_FOUND, `Issue ${args.id} not found`);
36
+ }
37
+ // Get existing labels
38
+ const existingLabels = await issue.labels();
39
+ const existingLabelIds = existingLabels.nodes.map((l) => l.id);
40
+ // Parse new label IDs
41
+ const newLabelIds = flags['label-ids'].split(',').map((id) => id.trim());
42
+ // Combine existing and new labels (avoiding duplicates)
43
+ const combinedLabelIds = [...new Set([...existingLabelIds, ...newLabelIds])];
44
+ // Update the issue with combined labels
45
+ const payload = await client.updateIssue(issueId, {
46
+ labelIds: combinedLabelIds,
47
+ });
48
+ if (!payload.success) {
49
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to add labels to issue');
50
+ }
51
+ const updatedIssue = await payload.issue;
52
+ if (!updatedIssue) {
53
+ throw new CliError(ErrorCodes.API_ERROR, 'Issue not found in response');
54
+ }
55
+ const labels = await updatedIssue.labels();
56
+ const data = {
57
+ id: updatedIssue.id,
58
+ identifier: updatedIssue.identifier,
59
+ title: updatedIssue.title,
60
+ labels: labels.nodes.map((l) => ({
61
+ id: l.id,
62
+ name: l.name,
63
+ color: l.color,
64
+ })),
65
+ labelsAdded: newLabelIds.filter((id) => !existingLabelIds.includes(id)),
66
+ url: updatedIssue.url,
67
+ };
68
+ if (format === 'json') {
69
+ print(success(data));
70
+ }
71
+ else if (format === 'table') {
72
+ printItem({
73
+ id: data.id,
74
+ identifier: data.identifier,
75
+ title: data.title,
76
+ labels: data.labels.map((l) => l.name).join(', '),
77
+ labelsAdded: data.labelsAdded.length,
78
+ url: data.url,
79
+ }, format);
80
+ }
81
+ else {
82
+ console.log(data.identifier);
83
+ }
84
+ }
85
+ catch (err) {
86
+ handleError(err);
87
+ this.exit(1);
88
+ }
89
+ }
90
+ }