@bdsqqq/lnr-cli 1.1.1 → 1.2.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.
- package/package.json +7 -3
- package/src/cli.ts +22 -24
- package/src/lib/output.test.ts +181 -0
- package/src/lib/output.ts +317 -1
- package/src/{commands → router}/auth.ts +22 -15
- package/src/router/config.ts +71 -0
- package/src/router/cycles.ts +131 -0
- package/src/router/docs.ts +153 -0
- package/src/router/index.ts +28 -0
- package/src/router/issues.ts +558 -0
- package/src/router/labels.ts +192 -0
- package/src/{commands → router}/me.ts +47 -29
- package/src/router/projects.ts +220 -0
- package/src/{commands → router}/search.ts +20 -19
- package/src/{commands → router}/teams.ts +33 -46
- package/src/router/trpc.ts +7 -0
- package/src/commands/config.ts +0 -64
- package/src/commands/cycles.ts +0 -134
- package/src/commands/issues.ts +0 -390
- package/src/commands/projects.ts +0 -214
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import {
|
|
4
|
+
getClient,
|
|
5
|
+
listIssues,
|
|
6
|
+
getIssue,
|
|
7
|
+
createIssue,
|
|
8
|
+
updateIssue,
|
|
9
|
+
addComment,
|
|
10
|
+
priorityFromString,
|
|
11
|
+
getTeamStates,
|
|
12
|
+
getTeamLabels,
|
|
13
|
+
findTeamByKeyOrName,
|
|
14
|
+
getAvailableTeamKeys,
|
|
15
|
+
getIssueComments,
|
|
16
|
+
updateComment,
|
|
17
|
+
replyToComment,
|
|
18
|
+
deleteComment,
|
|
19
|
+
archiveIssue,
|
|
20
|
+
getSubIssues,
|
|
21
|
+
createReaction,
|
|
22
|
+
deleteReaction,
|
|
23
|
+
createIssueRelation,
|
|
24
|
+
type Issue,
|
|
25
|
+
type ListIssuesFilter,
|
|
26
|
+
type Comment,
|
|
27
|
+
} from "@bdsqqq/lnr-core";
|
|
28
|
+
import { router, procedure } from "./trpc";
|
|
29
|
+
import { handleApiError, exitWithError, EXIT_CODES } from "../lib/error";
|
|
30
|
+
import {
|
|
31
|
+
outputJson,
|
|
32
|
+
outputQuiet,
|
|
33
|
+
outputTable,
|
|
34
|
+
getOutputFormat,
|
|
35
|
+
formatDate,
|
|
36
|
+
formatPriority,
|
|
37
|
+
truncate,
|
|
38
|
+
outputCommentThreads,
|
|
39
|
+
type TableColumn,
|
|
40
|
+
} from "../lib/output";
|
|
41
|
+
|
|
42
|
+
const listIssuesInput = z.object({
|
|
43
|
+
team: z.string().optional().describe("filter by team key"),
|
|
44
|
+
state: z.string().optional().describe("filter by workflow state name"),
|
|
45
|
+
assignee: z.string().optional().describe("filter by assignee email or @me"),
|
|
46
|
+
label: z.string().optional().describe("filter by label name"),
|
|
47
|
+
project: z.string().optional().describe("filter by project name"),
|
|
48
|
+
json: z.boolean().optional().describe("output as json"),
|
|
49
|
+
quiet: z.boolean().optional().describe("output ids only"),
|
|
50
|
+
verbose: z.boolean().optional().describe("show all columns"),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const issueInput = z.object({
|
|
54
|
+
idOrNew: z.string().meta({ positional: true }).describe("issue identifier (e.g. ENG-123) or 'new'"),
|
|
55
|
+
json: z.boolean().optional().describe("output as json"),
|
|
56
|
+
open: z.boolean().optional().describe("open issue in browser"),
|
|
57
|
+
state: z.string().optional().describe("set workflow state"),
|
|
58
|
+
assignee: z.string().optional().describe("set assignee by email or @me"),
|
|
59
|
+
priority: z.string().optional().describe("set priority (urgent, high, medium, low, none)"),
|
|
60
|
+
label: z.string().optional().describe("set label (+name to add, -name to remove)"),
|
|
61
|
+
comment: z.string().optional().describe("add comment to issue"),
|
|
62
|
+
blocks: z.string().optional().describe("add blocks relation to issue"),
|
|
63
|
+
blockedBy: z.string().optional().describe("add blocked-by relation to issue"),
|
|
64
|
+
relatesTo: z.string().optional().describe("add relates-to relation to issue"),
|
|
65
|
+
team: z.string().optional().describe("team key (required for new)"),
|
|
66
|
+
title: z.string().optional().describe("issue title (required for new)"),
|
|
67
|
+
description: z.string().optional().describe("issue description"),
|
|
68
|
+
comments: z.boolean().optional().describe("list comments on issue"),
|
|
69
|
+
editComment: z.string().optional().describe("comment id to edit (requires --text)"),
|
|
70
|
+
text: z.string().optional().describe("text for --edit-comment or --reply-to"),
|
|
71
|
+
replyTo: z.string().optional().describe("comment id to reply to (requires --text)"),
|
|
72
|
+
deleteComment: z.string().optional().describe("comment id to delete"),
|
|
73
|
+
archive: z.boolean().optional().describe("archive the issue"),
|
|
74
|
+
react: z.string().optional().describe("comment id to add reaction (requires --emoji)"),
|
|
75
|
+
emoji: z.string().optional().describe("emoji for --react"),
|
|
76
|
+
unreact: z.string().optional().describe("reaction id to remove"),
|
|
77
|
+
parent: z.string().optional().describe("set parent issue identifier"),
|
|
78
|
+
subIssues: z.boolean().optional().describe("list sub-issues"),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
type IssueInput = z.infer<typeof issueInput>;
|
|
82
|
+
|
|
83
|
+
const issueColumns: TableColumn<Issue>[] = [
|
|
84
|
+
{ header: "ID", value: (i) => i.identifier, width: 10 },
|
|
85
|
+
{ header: "STATE", value: (i) => i.state ?? "-", width: 15 },
|
|
86
|
+
{ header: "TITLE", value: (i) => truncate(i.title, 50), width: 50 },
|
|
87
|
+
{ header: "ASSIGNEE", value: (i) => i.assignee ?? "-", width: 15 },
|
|
88
|
+
{ header: "PRIORITY", value: (i) => formatPriority(i.priority), width: 8 },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const commentColumns: TableColumn<Comment>[] = [
|
|
92
|
+
{ header: "ID", value: (c) => c.id.slice(0, 8), width: 10 },
|
|
93
|
+
{ header: "USER", value: (c) => c.user ?? "-", width: 15 },
|
|
94
|
+
{ header: "BODY", value: (c) => truncate(c.body, 50), width: 50 },
|
|
95
|
+
{ header: "CREATED", value: (c) => formatDate(c.createdAt), width: 12 },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
async function handleListIssues(input: z.infer<typeof listIssuesInput>): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
const client = getClient();
|
|
101
|
+
const filter: ListIssuesFilter = {
|
|
102
|
+
team: input.team,
|
|
103
|
+
state: input.state,
|
|
104
|
+
assignee: input.assignee,
|
|
105
|
+
label: input.label,
|
|
106
|
+
project: input.project,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const issues = await listIssues(client, filter);
|
|
110
|
+
const format = input.json ? "json" : input.quiet ? "quiet" : getOutputFormat({});
|
|
111
|
+
|
|
112
|
+
if (format === "json") {
|
|
113
|
+
outputJson(issues);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (format === "quiet") {
|
|
118
|
+
outputQuiet(issues.map((i) => i.identifier));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
outputTable(issues, issueColumns, { verbose: input.verbose });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
handleApiError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function handleShowIssue(
|
|
129
|
+
identifier: string,
|
|
130
|
+
input: IssueInput
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const client = getClient();
|
|
134
|
+
const issue = await getIssue(client, identifier);
|
|
135
|
+
|
|
136
|
+
if (!issue) {
|
|
137
|
+
exitWithError(`issue ${identifier} not found`, undefined, EXIT_CODES.NOT_FOUND);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (input.open) {
|
|
141
|
+
const { spawn } = await import("child_process");
|
|
142
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
143
|
+
spawn(cmd, [issue.url], { detached: true, stdio: "ignore" }).unref();
|
|
144
|
+
console.log(`opened ${issue.url}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (input.comments) {
|
|
149
|
+
const { comments, error } = await getIssueComments(client, issue.id);
|
|
150
|
+
if (error) {
|
|
151
|
+
console.error(`failed to fetch comments: ${error}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const format = input.json ? "json" : getOutputFormat({});
|
|
155
|
+
if (format === "json") {
|
|
156
|
+
outputJson(comments);
|
|
157
|
+
} else {
|
|
158
|
+
outputTable(comments, commentColumns);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (input.subIssues) {
|
|
164
|
+
const subIssues = await getSubIssues(client, issue.id);
|
|
165
|
+
const format = input.json ? "json" : getOutputFormat({});
|
|
166
|
+
if (format === "json") {
|
|
167
|
+
outputJson(subIssues);
|
|
168
|
+
} else {
|
|
169
|
+
outputTable(subIssues, issueColumns);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const format = input.json ? "json" : getOutputFormat({});
|
|
175
|
+
const { comments, error: commentsError } = await getIssueComments(client, issue.id);
|
|
176
|
+
|
|
177
|
+
if (format === "json") {
|
|
178
|
+
outputJson({
|
|
179
|
+
...issue,
|
|
180
|
+
priority: formatPriority(issue.priority),
|
|
181
|
+
createdAt: formatDate(issue.createdAt),
|
|
182
|
+
updatedAt: formatDate(issue.updatedAt),
|
|
183
|
+
comments,
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`${issue.identifier}: ${issue.title}`);
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(`state: ${issue.state ?? "-"}`);
|
|
191
|
+
console.log(`assignee: ${issue.assignee ?? "-"}`);
|
|
192
|
+
console.log(`priority: ${formatPriority(issue.priority)}`);
|
|
193
|
+
if (issue.parentId) {
|
|
194
|
+
console.log(`parent: ${issue.parentId}`);
|
|
195
|
+
}
|
|
196
|
+
console.log(`created: ${formatDate(issue.createdAt)}`);
|
|
197
|
+
console.log(`updated: ${formatDate(issue.updatedAt)}`);
|
|
198
|
+
console.log(`url: ${issue.url}`);
|
|
199
|
+
|
|
200
|
+
if (issue.description) {
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(issue.description);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (commentsError) {
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(chalk.dim(`comments: failed to load (${commentsError})`));
|
|
208
|
+
} else if (comments.length > 0) {
|
|
209
|
+
console.log();
|
|
210
|
+
console.log("─".repeat(40));
|
|
211
|
+
console.log();
|
|
212
|
+
outputCommentThreads(comments);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
handleApiError(error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function handleUpdateIssue(
|
|
220
|
+
identifier: string,
|
|
221
|
+
input: IssueInput
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
try {
|
|
224
|
+
const client = getClient();
|
|
225
|
+
const issue = await getIssue(client, identifier);
|
|
226
|
+
|
|
227
|
+
if (!issue) {
|
|
228
|
+
exitWithError(`issue ${identifier} not found`, undefined, EXIT_CODES.NOT_FOUND);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (input.comment) {
|
|
232
|
+
await addComment(client, issue.id, input.comment);
|
|
233
|
+
console.log(`commented on ${identifier}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (input.editComment) {
|
|
238
|
+
if (!input.text) {
|
|
239
|
+
exitWithError("--text is required with --edit-comment");
|
|
240
|
+
}
|
|
241
|
+
await updateComment(client, input.editComment, input.text);
|
|
242
|
+
console.log(`updated comment ${input.editComment.slice(0, 8)}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (input.replyTo) {
|
|
247
|
+
if (!input.text) {
|
|
248
|
+
exitWithError("--text is required with --reply-to");
|
|
249
|
+
}
|
|
250
|
+
await replyToComment(client, issue.id, input.replyTo, input.text);
|
|
251
|
+
console.log(`replied to comment ${input.replyTo.slice(0, 8)}`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (input.deleteComment) {
|
|
256
|
+
await deleteComment(client, input.deleteComment);
|
|
257
|
+
console.log(`deleted comment ${input.deleteComment.slice(0, 8)}`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (input.archive) {
|
|
262
|
+
await archiveIssue(client, issue.id);
|
|
263
|
+
console.log(`archived ${identifier}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (input.react) {
|
|
268
|
+
if (!input.emoji) {
|
|
269
|
+
exitWithError("--emoji is required with --react");
|
|
270
|
+
}
|
|
271
|
+
const success = await createReaction(client, input.react, input.emoji);
|
|
272
|
+
if (!success) {
|
|
273
|
+
exitWithError(`failed to add reaction to comment ${input.react.slice(0, 8)}`);
|
|
274
|
+
}
|
|
275
|
+
console.log(`added reaction ${input.emoji} to comment ${input.react.slice(0, 8)}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (input.unreact) {
|
|
280
|
+
const success = await deleteReaction(client, input.unreact);
|
|
281
|
+
if (!success) {
|
|
282
|
+
exitWithError(`reaction ${input.unreact.slice(0, 8)} not found`, undefined, EXIT_CODES.NOT_FOUND);
|
|
283
|
+
}
|
|
284
|
+
console.log(`removed reaction ${input.unreact.slice(0, 8)}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const updatePayload: Record<string, unknown> = {};
|
|
289
|
+
|
|
290
|
+
if (input.state) {
|
|
291
|
+
const rawIssue = await (
|
|
292
|
+
client as unknown as {
|
|
293
|
+
issue: (id: string) => Promise<{ team?: { id: string } | null } | null>;
|
|
294
|
+
}
|
|
295
|
+
).issue(identifier);
|
|
296
|
+
const teamRef =
|
|
297
|
+
rawIssue && "team" in rawIssue
|
|
298
|
+
? await (rawIssue as unknown as { team: Promise<{ id: string } | null> }).team
|
|
299
|
+
: null;
|
|
300
|
+
if (!teamRef) {
|
|
301
|
+
exitWithError("could not determine team for issue");
|
|
302
|
+
}
|
|
303
|
+
const states = await getTeamStates(client, teamRef.id);
|
|
304
|
+
const targetState = states.find(
|
|
305
|
+
(s) => s.name.toLowerCase() === input.state!.toLowerCase()
|
|
306
|
+
);
|
|
307
|
+
if (!targetState) {
|
|
308
|
+
const available = states.map((s) => s.name).join(", ");
|
|
309
|
+
exitWithError(`state "${input.state}" not found`, `available states: ${available}`);
|
|
310
|
+
}
|
|
311
|
+
updatePayload.stateId = targetState.id;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (input.assignee) {
|
|
315
|
+
if (input.assignee === "@me") {
|
|
316
|
+
const viewer = await client.viewer;
|
|
317
|
+
updatePayload.assigneeId = viewer.id;
|
|
318
|
+
} else {
|
|
319
|
+
const users = await client.users({ filter: { email: { eq: input.assignee } } });
|
|
320
|
+
const user = users.nodes[0];
|
|
321
|
+
if (!user) {
|
|
322
|
+
exitWithError(`user "${input.assignee}" not found`);
|
|
323
|
+
}
|
|
324
|
+
updatePayload.assigneeId = user.id;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (input.priority) {
|
|
329
|
+
updatePayload.priority = priorityFromString(input.priority);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (input.label) {
|
|
333
|
+
const isAdd = input.label.startsWith("+");
|
|
334
|
+
const isRemove = input.label.startsWith("-");
|
|
335
|
+
const labelName = isAdd || isRemove ? input.label.slice(1) : input.label;
|
|
336
|
+
|
|
337
|
+
const rawIssue = await (
|
|
338
|
+
client as unknown as {
|
|
339
|
+
issue: (
|
|
340
|
+
id: string
|
|
341
|
+
) => Promise<{
|
|
342
|
+
team?: { id: string } | null;
|
|
343
|
+
labels: () => Promise<{ nodes: { id: string }[] }>;
|
|
344
|
+
} | null>;
|
|
345
|
+
}
|
|
346
|
+
).issue(identifier);
|
|
347
|
+
const teamRef =
|
|
348
|
+
rawIssue && "team" in rawIssue
|
|
349
|
+
? await (rawIssue as unknown as { team: Promise<{ id: string } | null> }).team
|
|
350
|
+
: null;
|
|
351
|
+
if (!teamRef) {
|
|
352
|
+
exitWithError("could not determine team for issue");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const labels = await getTeamLabels(client, teamRef.id);
|
|
356
|
+
const targetLabel = labels.find(
|
|
357
|
+
(l) => l.name.toLowerCase() === labelName.toLowerCase()
|
|
358
|
+
);
|
|
359
|
+
if (!targetLabel) {
|
|
360
|
+
const available = labels.map((l) => l.name).join(", ");
|
|
361
|
+
exitWithError(`label "${labelName}" not found`, `available labels: ${available}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const currentLabelsData = rawIssue ? await rawIssue.labels() : { nodes: [] };
|
|
365
|
+
const currentLabelIds = currentLabelsData.nodes.map((l) => l.id);
|
|
366
|
+
|
|
367
|
+
if (isRemove) {
|
|
368
|
+
updatePayload.labelIds = currentLabelIds.filter((id) => id !== targetLabel.id);
|
|
369
|
+
} else {
|
|
370
|
+
if (!currentLabelIds.includes(targetLabel.id)) {
|
|
371
|
+
updatePayload.labelIds = [...currentLabelIds, targetLabel.id];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (input.parent) {
|
|
377
|
+
const parentIssue = await getIssue(client, input.parent);
|
|
378
|
+
if (!parentIssue) {
|
|
379
|
+
exitWithError(`parent issue "${input.parent}" not found`);
|
|
380
|
+
}
|
|
381
|
+
updatePayload.parentId = parentIssue.id;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
385
|
+
await updateIssue(client, issue.id, updatePayload);
|
|
386
|
+
console.log(`updated ${identifier}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (input.blocks) {
|
|
390
|
+
const blockedIssue = await getIssue(client, input.blocks);
|
|
391
|
+
if (!blockedIssue) {
|
|
392
|
+
exitWithError(`issue "${input.blocks}" not found`);
|
|
393
|
+
}
|
|
394
|
+
const success = await createIssueRelation(client, issue.id, blockedIssue.id, "blocks");
|
|
395
|
+
if (!success) {
|
|
396
|
+
exitWithError(`failed to create blocks relation`);
|
|
397
|
+
}
|
|
398
|
+
console.log(`${identifier} now blocks ${input.blocks}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (input.blockedBy) {
|
|
402
|
+
const blockerIssue = await getIssue(client, input.blockedBy);
|
|
403
|
+
if (!blockerIssue) {
|
|
404
|
+
exitWithError(`issue "${input.blockedBy}" not found`);
|
|
405
|
+
}
|
|
406
|
+
const success = await createIssueRelation(client, blockerIssue.id, issue.id, "blocks");
|
|
407
|
+
if (!success) {
|
|
408
|
+
exitWithError(`failed to create blocked-by relation`);
|
|
409
|
+
}
|
|
410
|
+
console.log(`${identifier} is now blocked by ${input.blockedBy}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (input.relatesTo) {
|
|
414
|
+
const relatedIssue = await getIssue(client, input.relatesTo);
|
|
415
|
+
if (!relatedIssue) {
|
|
416
|
+
exitWithError(`issue "${input.relatesTo}" not found`);
|
|
417
|
+
}
|
|
418
|
+
const success = await createIssueRelation(client, issue.id, relatedIssue.id, "related");
|
|
419
|
+
if (!success) {
|
|
420
|
+
exitWithError(`failed to create relates-to relation`);
|
|
421
|
+
}
|
|
422
|
+
console.log(`${identifier} now relates to ${input.relatesTo}`);
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
handleApiError(error);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function handleCreateIssue(input: IssueInput): Promise<void> {
|
|
430
|
+
if (!input.team) {
|
|
431
|
+
exitWithError("--team is required", 'usage: lnr issue new --team ENG --title "..."');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!input.title) {
|
|
435
|
+
exitWithError("--title is required", 'usage: lnr issue new --team ENG --title "..."');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const client = getClient();
|
|
440
|
+
const team = await findTeamByKeyOrName(client, input.team);
|
|
441
|
+
|
|
442
|
+
if (!team) {
|
|
443
|
+
const available = (await getAvailableTeamKeys(client)).join(", ");
|
|
444
|
+
exitWithError(`team "${input.team}" not found`, `available teams: ${available}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const createPayload: {
|
|
448
|
+
teamId: string;
|
|
449
|
+
title: string;
|
|
450
|
+
description?: string;
|
|
451
|
+
assigneeId?: string;
|
|
452
|
+
priority?: number;
|
|
453
|
+
labelIds?: string[];
|
|
454
|
+
parentId?: string;
|
|
455
|
+
} = {
|
|
456
|
+
teamId: team.id,
|
|
457
|
+
title: input.title,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (input.description) {
|
|
461
|
+
createPayload.description = input.description;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (input.assignee) {
|
|
465
|
+
if (input.assignee === "@me") {
|
|
466
|
+
const viewer = await client.viewer;
|
|
467
|
+
createPayload.assigneeId = viewer.id;
|
|
468
|
+
} else {
|
|
469
|
+
const users = await client.users({ filter: { email: { eq: input.assignee } } });
|
|
470
|
+
const user = users.nodes[0];
|
|
471
|
+
if (!user) {
|
|
472
|
+
exitWithError(`user "${input.assignee}" not found`);
|
|
473
|
+
}
|
|
474
|
+
createPayload.assigneeId = user.id;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (input.priority) {
|
|
479
|
+
createPayload.priority = priorityFromString(input.priority);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (input.label) {
|
|
483
|
+
const labels = await getTeamLabels(client, team.id);
|
|
484
|
+
const targetLabel = labels.find(
|
|
485
|
+
(l) => l.name.toLowerCase() === input.label!.toLowerCase()
|
|
486
|
+
);
|
|
487
|
+
if (!targetLabel) {
|
|
488
|
+
const available = labels.map((l) => l.name).join(", ");
|
|
489
|
+
exitWithError(`label "${input.label}" not found`, `available labels: ${available}`);
|
|
490
|
+
}
|
|
491
|
+
createPayload.labelIds = [targetLabel.id];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (input.parent) {
|
|
495
|
+
const parentIssue = await getIssue(client, input.parent);
|
|
496
|
+
if (!parentIssue) {
|
|
497
|
+
exitWithError(`parent issue "${input.parent}" not found`);
|
|
498
|
+
}
|
|
499
|
+
createPayload.parentId = parentIssue.id;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const issue = await createIssue(client, createPayload);
|
|
503
|
+
if (issue) {
|
|
504
|
+
console.log(`created ${issue.identifier}: ${issue.title}`);
|
|
505
|
+
} else {
|
|
506
|
+
console.log("created issue");
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
handleApiError(error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const issuesRouter = router({
|
|
514
|
+
issues: procedure
|
|
515
|
+
.meta({
|
|
516
|
+
description: "list issues",
|
|
517
|
+
aliases: { command: ["i"] },
|
|
518
|
+
})
|
|
519
|
+
.input(listIssuesInput)
|
|
520
|
+
.query(async ({ input }) => {
|
|
521
|
+
await handleListIssues(input);
|
|
522
|
+
}),
|
|
523
|
+
|
|
524
|
+
issue: procedure
|
|
525
|
+
.meta({
|
|
526
|
+
description: "show or update an issue, or create with 'new'",
|
|
527
|
+
})
|
|
528
|
+
.input(issueInput)
|
|
529
|
+
.mutation(async ({ input }) => {
|
|
530
|
+
if (input.idOrNew === "new") {
|
|
531
|
+
await handleCreateIssue(input);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const hasUpdate =
|
|
536
|
+
input.state ||
|
|
537
|
+
input.assignee ||
|
|
538
|
+
input.priority ||
|
|
539
|
+
input.label ||
|
|
540
|
+
input.comment ||
|
|
541
|
+
input.editComment ||
|
|
542
|
+
input.replyTo ||
|
|
543
|
+
input.deleteComment ||
|
|
544
|
+
input.archive ||
|
|
545
|
+
input.react ||
|
|
546
|
+
input.unreact ||
|
|
547
|
+
input.parent ||
|
|
548
|
+
input.blocks ||
|
|
549
|
+
input.blockedBy ||
|
|
550
|
+
input.relatesTo;
|
|
551
|
+
|
|
552
|
+
if (hasUpdate) {
|
|
553
|
+
await handleUpdateIssue(input.idOrNew, input);
|
|
554
|
+
} else {
|
|
555
|
+
await handleShowIssue(input.idOrNew, input);
|
|
556
|
+
}
|
|
557
|
+
}),
|
|
558
|
+
});
|