linear-cli-agents 0.2.1 → 0.4.1
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.
- package/README.md +166 -3
- package/bin/dev.js +0 -0
- package/dist/commands/comments/add.d.ts +13 -0
- package/dist/commands/comments/add.js +89 -0
- package/dist/commands/comments/delete.d.ts +12 -0
- package/dist/commands/comments/delete.js +50 -0
- package/dist/commands/comments/list.d.ts +14 -0
- package/dist/commands/comments/list.js +103 -0
- package/dist/commands/comments/update.d.ts +13 -0
- package/dist/commands/comments/update.js +81 -0
- package/dist/commands/issues/add-labels.d.ts +13 -0
- package/dist/commands/issues/add-labels.js +90 -0
- package/dist/commands/issues/archive.d.ts +13 -0
- package/dist/commands/issues/archive.js +83 -0
- package/dist/commands/issues/remove-labels.d.ts +13 -0
- package/dist/commands/issues/remove-labels.js +90 -0
- package/dist/commands/labels/create.d.ts +14 -0
- package/dist/commands/labels/create.js +102 -0
- package/dist/commands/labels/delete.d.ts +12 -0
- package/dist/commands/labels/delete.js +50 -0
- package/dist/commands/labels/list.d.ts +12 -0
- package/dist/commands/labels/list.js +117 -0
- package/dist/commands/labels/update.d.ts +16 -0
- package/dist/commands/labels/update.js +109 -0
- package/dist/commands/me.js +1 -5
- package/dist/commands/milestones/create.d.ts +15 -0
- package/dist/commands/milestones/create.js +90 -0
- package/dist/commands/milestones/get.d.ts +12 -0
- package/dist/commands/milestones/get.js +74 -0
- package/dist/commands/milestones/list.d.ts +14 -0
- package/dist/commands/milestones/list.js +97 -0
- package/dist/commands/milestones/update.d.ts +15 -0
- package/dist/commands/milestones/update.js +94 -0
- package/dist/commands/project-updates/create.d.ts +14 -0
- package/dist/commands/project-updates/create.js +96 -0
- package/dist/commands/project-updates/get.d.ts +12 -0
- package/dist/commands/project-updates/get.js +80 -0
- package/dist/commands/project-updates/list.d.ts +14 -0
- package/dist/commands/project-updates/list.js +120 -0
- package/dist/commands/project-updates/update.d.ts +14 -0
- package/dist/commands/project-updates/update.js +96 -0
- package/dist/commands/projects/archive.d.ts +13 -0
- package/dist/commands/projects/archive.js +79 -0
- package/dist/commands/projects/create.d.ts +16 -0
- package/dist/commands/projects/create.js +115 -0
- package/dist/commands/projects/delete.d.ts +12 -0
- package/dist/commands/projects/delete.js +50 -0
- package/dist/commands/projects/get.d.ts +12 -0
- package/dist/commands/projects/get.js +102 -0
- package/dist/commands/projects/list.d.ts +13 -0
- package/dist/commands/projects/list.js +141 -0
- package/dist/commands/projects/update.d.ts +18 -0
- package/dist/commands/projects/update.js +125 -0
- package/dist/commands/relations/create.d.ts +14 -0
- package/dist/commands/relations/create.js +98 -0
- package/dist/commands/relations/delete.d.ts +12 -0
- package/dist/commands/relations/delete.js +47 -0
- package/dist/commands/relations/list.d.ts +12 -0
- package/dist/commands/relations/list.js +128 -0
- package/dist/commands/search.d.ts +15 -0
- package/dist/commands/search.js +102 -0
- package/dist/commands/states/list.d.ts +12 -0
- package/dist/commands/states/list.js +151 -0
- package/dist/commands/teams/list.js +1 -1
- package/dist/commands/templates/create.d.ts +14 -0
- package/dist/commands/templates/create.js +102 -0
- package/dist/commands/templates/get.d.ts +12 -0
- package/dist/commands/templates/get.js +84 -0
- package/dist/commands/templates/list.d.ts +12 -0
- package/dist/commands/templates/list.js +110 -0
- package/dist/commands/templates/update.d.ts +15 -0
- package/dist/commands/templates/update.js +101 -0
- package/dist/commands/users/get.d.ts +12 -0
- package/dist/commands/users/get.js +91 -0
- package/dist/commands/users/list.d.ts +12 -0
- package/dist/commands/users/list.js +99 -0
- package/dist/lib/config.js +1 -1
- package/oclif.manifest.json +2402 -189
- package/package.json +47 -17
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# linear-cli-agents
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/linear-cli-agents)
|
|
4
|
-
[](https://github.com/nchgn/linear-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/nchgn/linear-cli-agents/actions/workflows/ci.yml)
|
|
5
5
|
[](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
|
|
15
|
-
- **
|
|
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
|
+
}
|