@ysicing/plane-cli 0.1.0 → 1.0.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/README.md +139 -9
- package/package.json +1 -1
- package/src/api/issue-client.js +73 -0
- package/src/api/project-client.js +16 -0
- package/src/cli.js +67 -8
- package/src/commands/issue.js +757 -6
- package/src/commands/project.js +269 -1
- package/src/core/output.js +48 -2
package/src/commands/issue.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { IssueClient } from "../api/issue-client.js";
|
|
2
|
+
import { ProjectClient } from "../api/project-client.js";
|
|
2
3
|
import { resolveRuntimeConfig } from "../core/config.js";
|
|
3
4
|
import { CliError } from "../core/errors.js";
|
|
4
5
|
import { PlaneClient } from "../core/http.js";
|
|
5
6
|
import { ensureValue, parseCommandArgs, pickDefined, splitCsv } from "../core/options.js";
|
|
6
7
|
import { printData, printTable } from "../core/output.js";
|
|
8
|
+
import { basename, extname } from "node:path";
|
|
9
|
+
import { readFile, stat } from "node:fs/promises";
|
|
7
10
|
|
|
8
11
|
function renderIssueList(data) {
|
|
9
12
|
const rows = Array.isArray(data) ? data : data.results || [];
|
|
@@ -16,6 +19,164 @@ function renderIssueList(data) {
|
|
|
16
19
|
]);
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
function renderIssueSearch(data) {
|
|
23
|
+
const rows = Array.isArray(data?.issues) ? data.issues : Array.isArray(data) ? data : [];
|
|
24
|
+
printTable(rows, [
|
|
25
|
+
{ label: "ID", get: (row) => row.id },
|
|
26
|
+
{ label: "Key", get: (row) => `${row.project__identifier || row.project_identifier || ""}-${row.sequence_id || ""}` },
|
|
27
|
+
{ label: "Project", get: (row) => row.project__identifier || row.project_identifier || "" },
|
|
28
|
+
{ label: "Name", get: (row) => row.name || "" },
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderIssueLabels(data) {
|
|
33
|
+
const rows = Array.isArray(data) ? data : data.results || [];
|
|
34
|
+
printTable(rows, [
|
|
35
|
+
{ label: "ID", get: (row) => row.id || "" },
|
|
36
|
+
{ label: "Name", get: (row) => row.name || "" },
|
|
37
|
+
{ label: "Color", get: (row) => row.color || "" },
|
|
38
|
+
{ label: "Description", get: (row) => row.description || "" },
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderIssueComments(data) {
|
|
43
|
+
const rows = Array.isArray(data) ? data : data.results || [];
|
|
44
|
+
printTable(rows, [
|
|
45
|
+
{ label: "ID", get: (row) => row.id || "" },
|
|
46
|
+
{ label: "Actor", get: (row) => row.actor || "" },
|
|
47
|
+
{ label: "Created", get: (row) => row.created_at || "" },
|
|
48
|
+
{ label: "Comment", get: (row) => String(row.comment_html || "").replace(/\s+/g, " ").trim() },
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderIssueActivities(data) {
|
|
53
|
+
const rows = Array.isArray(data) ? data : data.results || [];
|
|
54
|
+
printTable(rows, [
|
|
55
|
+
{ label: "ID", get: (row) => row.id || "" },
|
|
56
|
+
{ label: "Field", get: (row) => row.field || "" },
|
|
57
|
+
{ label: "Actor", get: (row) => row.actor || "" },
|
|
58
|
+
{ label: "Created", get: (row) => row.created_at || "" },
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderIssueLinks(data) {
|
|
63
|
+
const rows = Array.isArray(data) ? data : data.results || [];
|
|
64
|
+
printTable(rows, [
|
|
65
|
+
{ label: "ID", get: (row) => row.id || "" },
|
|
66
|
+
{ label: "Title", get: (row) => row.title || "" },
|
|
67
|
+
{ label: "URL", get: (row) => row.url || "" },
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderIssueRelations(data) {
|
|
72
|
+
const rows = Object.entries(data || {}).flatMap(([relationType, ids]) =>
|
|
73
|
+
Array.isArray(ids) ? ids.map((id) => ({ relationType, id })) : []
|
|
74
|
+
);
|
|
75
|
+
printTable(rows, [
|
|
76
|
+
{ label: "Relation", get: (row) => row.relationType },
|
|
77
|
+
{ label: "Issue ID", get: (row) => row.id },
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderIssueAttachments(data) {
|
|
82
|
+
const rows = Array.isArray(data) ? data : [];
|
|
83
|
+
printTable(rows, [
|
|
84
|
+
{ label: "ID", get: (row) => row.id || "" },
|
|
85
|
+
{ label: "Name", get: (row) => row.attributes?.name || "" },
|
|
86
|
+
{ label: "Type", get: (row) => row.attributes?.type || "" },
|
|
87
|
+
{ label: "Size", get: (row) => row.attributes?.size || row.size || "" },
|
|
88
|
+
{ label: "Uploaded", get: (row) => (row.is_uploaded ? "yes" : "no") },
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const MIME_BY_EXTENSION = {
|
|
93
|
+
".jpg": "image/jpeg",
|
|
94
|
+
".jpeg": "image/jpeg",
|
|
95
|
+
".png": "image/png",
|
|
96
|
+
".gif": "image/gif",
|
|
97
|
+
".svg": "image/svg+xml",
|
|
98
|
+
".webp": "image/webp",
|
|
99
|
+
".tif": "image/tiff",
|
|
100
|
+
".tiff": "image/tiff",
|
|
101
|
+
".bmp": "image/bmp",
|
|
102
|
+
".pdf": "application/pdf",
|
|
103
|
+
".doc": "application/msword",
|
|
104
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
105
|
+
".xls": "application/vnd.ms-excel",
|
|
106
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
107
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
108
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
109
|
+
".txt": "text/plain",
|
|
110
|
+
".md": "text/markdown",
|
|
111
|
+
".rtf": "application/rtf",
|
|
112
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
113
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
114
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
115
|
+
".odg": "application/vnd.oasis.opendocument.graphics",
|
|
116
|
+
".vsd": "application/vnd.visio",
|
|
117
|
+
".mp3": "audio/mpeg",
|
|
118
|
+
".wav": "audio/wav",
|
|
119
|
+
".ogg": "audio/ogg",
|
|
120
|
+
".mid": "audio/midi",
|
|
121
|
+
".midi": "audio/midi",
|
|
122
|
+
".aac": "audio/aac",
|
|
123
|
+
".flac": "audio/flac",
|
|
124
|
+
".m4a": "audio/x-m4a",
|
|
125
|
+
".mp4": "video/mp4",
|
|
126
|
+
".mpeg": "video/mpeg",
|
|
127
|
+
".mpg": "video/mpeg",
|
|
128
|
+
".webm": "video/webm",
|
|
129
|
+
".mov": "video/quicktime",
|
|
130
|
+
".avi": "video/x-msvideo",
|
|
131
|
+
".wmv": "video/x-ms-wmv",
|
|
132
|
+
".zip": "application/zip",
|
|
133
|
+
".rar": "application/x-rar-compressed",
|
|
134
|
+
".tar": "application/x-tar",
|
|
135
|
+
".gz": "application/gzip",
|
|
136
|
+
".7z": "application/x-7z-compressed",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export function parseIssueKey(value) {
|
|
140
|
+
const trimmed = String(value || "").trim();
|
|
141
|
+
const match = /^([A-Za-z0-9_]+)-(\d+)$/.exec(trimmed);
|
|
142
|
+
|
|
143
|
+
if (!match) {
|
|
144
|
+
throw new CliError("Issue key must look like `PROJECT-123`.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
projectIdentifier: match[1],
|
|
149
|
+
issueIdentifier: match[2],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isIssueKey(value) {
|
|
154
|
+
return /^([A-Za-z0-9_]+)-(\d+)$/.test(String(value || "").trim());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function resolveIssueTarget(issueClient, projectId, issueRef) {
|
|
158
|
+
ensureValue(issueRef, "Issue ID is required.");
|
|
159
|
+
|
|
160
|
+
if (projectId) {
|
|
161
|
+
return {
|
|
162
|
+
projectId,
|
|
163
|
+
issueId: issueRef,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!isIssueKey(issueRef)) {
|
|
168
|
+
throw new CliError("Project ID is required unless the issue is provided as a key like `GAEA-25`.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { projectIdentifier, issueIdentifier } = parseIssueKey(issueRef);
|
|
172
|
+
const issue = await issueClient.getByKey(projectIdentifier, issueIdentifier);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
projectId: issue.project,
|
|
176
|
+
issueId: issue.id,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
19
180
|
export function buildIssuePayload(values) {
|
|
20
181
|
return pickDefined({
|
|
21
182
|
name: values.name,
|
|
@@ -31,15 +192,510 @@ export function buildIssuePayload(values) {
|
|
|
31
192
|
});
|
|
32
193
|
}
|
|
33
194
|
|
|
195
|
+
export function buildIssueLabelPayload(values) {
|
|
196
|
+
return pickDefined({
|
|
197
|
+
name: values.name,
|
|
198
|
+
color: values.color,
|
|
199
|
+
description: values.description,
|
|
200
|
+
parent: values.parent,
|
|
201
|
+
sort_order: values["sort-order"],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function buildIssueCommentPayload(values) {
|
|
206
|
+
return pickDefined({
|
|
207
|
+
comment_html: values.html,
|
|
208
|
+
access: values.access,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function buildIssueLinkPayload(values, issueId) {
|
|
213
|
+
return pickDefined({
|
|
214
|
+
title: values.title,
|
|
215
|
+
url: values.url,
|
|
216
|
+
issue_id: issueId,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function buildIssueRelationPayload(values) {
|
|
221
|
+
return pickDefined({
|
|
222
|
+
relation_type: values["relation-type"],
|
|
223
|
+
issues: splitCsv(values.issues),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function inferAttachmentMimeType(filePath) {
|
|
228
|
+
return MIME_BY_EXTENSION[extname(filePath).toLowerCase()] || null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function resolveAssigneeRefs(refs, members) {
|
|
232
|
+
const resolved = [];
|
|
233
|
+
|
|
234
|
+
for (const ref of refs) {
|
|
235
|
+
const value = String(ref).trim();
|
|
236
|
+
const lowered = value.toLowerCase();
|
|
237
|
+
const match = members.find((member) => {
|
|
238
|
+
const memberId = String(member.id || "");
|
|
239
|
+
const email = String(member.email || "").toLowerCase();
|
|
240
|
+
const displayName = `${member.first_name || ""} ${member.last_name || ""}`.trim().toLowerCase();
|
|
241
|
+
return memberId === value || email === lowered || (displayName && displayName === lowered);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!match) {
|
|
245
|
+
throw new CliError(`Assignee not found in workspace members: ${value}`, {
|
|
246
|
+
details: {
|
|
247
|
+
hint: "Use `plane project members workspace` to inspect available members.",
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
resolved.push(String(match.id));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return resolved;
|
|
256
|
+
}
|
|
257
|
+
|
|
34
258
|
function printHelp() {
|
|
35
259
|
console.log(`Usage:
|
|
36
260
|
plane issue ls --project <project-id> [--limit <n>] [--cursor <cursor>] [--order-by <field>] [--state <state-id>] [--priority <value>] [--assignees <id1,id2>] [--expand <field1,field2>]
|
|
37
261
|
plane issue get --project <project-id> <issue-id>
|
|
38
|
-
plane issue
|
|
39
|
-
plane issue
|
|
262
|
+
plane issue get GAEA-25
|
|
263
|
+
plane issue key <PROJECT-123> [--expand <field1,field2>] [--fields <field1,field2>]
|
|
264
|
+
plane issue search --query <text> [--project <project-id>] [--limit <n>] [--workspace-search]
|
|
265
|
+
plane issue labels ls --project <project-id>
|
|
266
|
+
plane issue labels create --project <project-id> --name <name> [--color <hex>] [--description <text>] [--parent <label-id>] [--sort-order <n>]
|
|
267
|
+
plane issue comments ls --project <project-id> <issue-id>
|
|
268
|
+
plane issue comments ls GAEA-25
|
|
269
|
+
plane issue comments add --project <project-id> <issue-id> --html '<p>comment</p>' [--access <value>]
|
|
270
|
+
plane issue comments add GAEA-25 --html '<p>comment</p>' [--access <value>]
|
|
271
|
+
plane issue comments update --project <project-id> <issue-id> <comment-id> --html '<p>comment</p>' [--access <value>]
|
|
272
|
+
plane issue comments update GAEA-25 <comment-id> --html '<p>comment</p>' [--access <value>]
|
|
273
|
+
plane issue activities ls --project <project-id> <issue-id>
|
|
274
|
+
plane issue activities ls GAEA-25
|
|
275
|
+
plane issue links ls --project <project-id> <issue-id>
|
|
276
|
+
plane issue links ls GAEA-25
|
|
277
|
+
plane issue links add --project <project-id> <issue-id> --url <url> [--title <text>]
|
|
278
|
+
plane issue links add GAEA-25 --url <url> [--title <text>]
|
|
279
|
+
plane issue links update --project <project-id> <issue-id> <link-id> --url <url> [--title <text>]
|
|
280
|
+
plane issue links update GAEA-25 <link-id> --url <url> [--title <text>]
|
|
281
|
+
plane issue relations ls --project <project-id> <issue-id>
|
|
282
|
+
plane issue relations ls GAEA-25
|
|
283
|
+
plane issue relations add --project <project-id> <issue-id> --relation-type <blocking|blocked_by|duplicate|relates_to|start_before|start_after|finish_before|finish_after> --issues <id1,id2>
|
|
284
|
+
plane issue relations add GAEA-25 --relation-type <blocking|blocked_by|duplicate|relates_to|start_before|start_after|finish_before|finish_after> --issues <id1,id2>
|
|
285
|
+
plane issue attachments ls --project <project-id> <issue-id>
|
|
286
|
+
plane issue attachments ls GAEA-25
|
|
287
|
+
plane issue attachments upload --project <project-id> <issue-id> --file <path> [--name <filename>] [--type <mime>]
|
|
288
|
+
plane issue attachments upload GAEA-25 --file <path> [--name <filename>] [--type <mime>]
|
|
289
|
+
plane issue create --project <project-id> --name <name> [--description-html <html>] [--state <state-id>] [--priority <value>] [--assignees <id-or-email1,id-or-email2>] [--labels <id1,id2>]
|
|
290
|
+
plane issue update --project <project-id> <issue-id> [--name <name>] [--description-html <html>] [--state <state-id>] [--priority <value>] [--assignees <id-or-email1,id-or-email2>] [--labels <id1,id2>]
|
|
40
291
|
`);
|
|
41
292
|
}
|
|
42
293
|
|
|
294
|
+
async function runIssueLabelsCommand(issueClient, args, context) {
|
|
295
|
+
const [subcommand, ...rest] = args;
|
|
296
|
+
|
|
297
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
298
|
+
printHelp();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (subcommand === "ls") {
|
|
303
|
+
const parsed = parseCommandArgs(
|
|
304
|
+
rest,
|
|
305
|
+
{
|
|
306
|
+
project: { type: "string" },
|
|
307
|
+
limit: { type: "string" },
|
|
308
|
+
cursor: { type: "string" },
|
|
309
|
+
},
|
|
310
|
+
false
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
314
|
+
const result = await issueClient.listLabels(
|
|
315
|
+
parsed.values.project,
|
|
316
|
+
pickDefined({
|
|
317
|
+
per_page: parsed.values.limit,
|
|
318
|
+
cursor: parsed.values.cursor,
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
printData(result, {
|
|
323
|
+
...context.output,
|
|
324
|
+
render: renderIssueLabels,
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (subcommand === "create") {
|
|
330
|
+
const parsed = parseCommandArgs(
|
|
331
|
+
rest,
|
|
332
|
+
{
|
|
333
|
+
project: { type: "string" },
|
|
334
|
+
name: { type: "string" },
|
|
335
|
+
color: { type: "string" },
|
|
336
|
+
description: { type: "string" },
|
|
337
|
+
parent: { type: "string" },
|
|
338
|
+
"sort-order": { type: "string" },
|
|
339
|
+
},
|
|
340
|
+
false
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
344
|
+
ensureValue(parsed.values.name, "Label name is required.");
|
|
345
|
+
const result = await issueClient.createLabel(parsed.values.project, buildIssueLabelPayload(parsed.values));
|
|
346
|
+
printData(result, context.output);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
throw new CliError(`Unknown issue labels subcommand: ${subcommand}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runIssueCommentsCommand(issueClient, args, context) {
|
|
354
|
+
const [subcommand, ...rest] = args;
|
|
355
|
+
|
|
356
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
357
|
+
printHelp();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (subcommand === "ls") {
|
|
362
|
+
const parsed = parseCommandArgs(
|
|
363
|
+
rest,
|
|
364
|
+
{
|
|
365
|
+
project: { type: "string" },
|
|
366
|
+
limit: { type: "string" },
|
|
367
|
+
cursor: { type: "string" },
|
|
368
|
+
"order-by": { type: "string" },
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
373
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
374
|
+
const result = await issueClient.listComments(
|
|
375
|
+
issueRef.projectId,
|
|
376
|
+
issueRef.issueId,
|
|
377
|
+
pickDefined({
|
|
378
|
+
per_page: parsed.values.limit,
|
|
379
|
+
cursor: parsed.values.cursor,
|
|
380
|
+
order_by: parsed.values["order-by"],
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
printData(result, {
|
|
384
|
+
...context.output,
|
|
385
|
+
render: renderIssueComments,
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (subcommand === "add") {
|
|
391
|
+
const parsed = parseCommandArgs(
|
|
392
|
+
rest,
|
|
393
|
+
{
|
|
394
|
+
project: { type: "string" },
|
|
395
|
+
html: { type: "string" },
|
|
396
|
+
access: { type: "string" },
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
401
|
+
ensureValue(parsed.values.html, "Comment HTML is required.");
|
|
402
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
403
|
+
const result = await issueClient.createComment(
|
|
404
|
+
issueRef.projectId,
|
|
405
|
+
issueRef.issueId,
|
|
406
|
+
buildIssueCommentPayload(parsed.values)
|
|
407
|
+
);
|
|
408
|
+
printData(result, context.output);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (subcommand === "update") {
|
|
413
|
+
const parsed = parseCommandArgs(
|
|
414
|
+
rest,
|
|
415
|
+
{
|
|
416
|
+
project: { type: "string" },
|
|
417
|
+
html: { type: "string" },
|
|
418
|
+
access: { type: "string" },
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
423
|
+
ensureValue(parsed.positionals[1], "Comment ID is required.");
|
|
424
|
+
ensureValue(parsed.values.html, "Comment HTML is required.");
|
|
425
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
426
|
+
const result = await issueClient.updateComment(
|
|
427
|
+
issueRef.projectId,
|
|
428
|
+
issueRef.issueId,
|
|
429
|
+
parsed.positionals[1],
|
|
430
|
+
buildIssueCommentPayload(parsed.values)
|
|
431
|
+
);
|
|
432
|
+
printData(result, context.output);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
throw new CliError(`Unknown issue comments subcommand: ${subcommand}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function runIssueActivitiesCommand(issueClient, args, context) {
|
|
440
|
+
const [subcommand, ...rest] = args;
|
|
441
|
+
|
|
442
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
443
|
+
printHelp();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (subcommand !== "ls") {
|
|
448
|
+
throw new CliError(`Unknown issue activities subcommand: ${subcommand}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const parsed = parseCommandArgs(
|
|
452
|
+
rest,
|
|
453
|
+
{
|
|
454
|
+
project: { type: "string" },
|
|
455
|
+
limit: { type: "string" },
|
|
456
|
+
cursor: { type: "string" },
|
|
457
|
+
"order-by": { type: "string" },
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
462
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
463
|
+
const result = await issueClient.listActivities(
|
|
464
|
+
issueRef.projectId,
|
|
465
|
+
issueRef.issueId,
|
|
466
|
+
pickDefined({
|
|
467
|
+
per_page: parsed.values.limit,
|
|
468
|
+
cursor: parsed.values.cursor,
|
|
469
|
+
order_by: parsed.values["order-by"],
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
printData(result, {
|
|
473
|
+
...context.output,
|
|
474
|
+
render: renderIssueActivities,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function runIssueLinksCommand(issueClient, args, context) {
|
|
479
|
+
const [subcommand, ...rest] = args;
|
|
480
|
+
|
|
481
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
482
|
+
printHelp();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (subcommand === "ls") {
|
|
487
|
+
const parsed = parseCommandArgs(
|
|
488
|
+
rest,
|
|
489
|
+
{
|
|
490
|
+
project: { type: "string" },
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
495
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
496
|
+
const result = await issueClient.listLinks(issueRef.projectId, issueRef.issueId);
|
|
497
|
+
printData(result, {
|
|
498
|
+
...context.output,
|
|
499
|
+
render: renderIssueLinks,
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (subcommand === "add") {
|
|
505
|
+
const parsed = parseCommandArgs(
|
|
506
|
+
rest,
|
|
507
|
+
{
|
|
508
|
+
project: { type: "string" },
|
|
509
|
+
url: { type: "string" },
|
|
510
|
+
title: { type: "string" },
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
515
|
+
ensureValue(parsed.values.url, "Link URL is required.");
|
|
516
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
517
|
+
const result = await issueClient.createLink(
|
|
518
|
+
issueRef.projectId,
|
|
519
|
+
issueRef.issueId,
|
|
520
|
+
buildIssueLinkPayload(parsed.values, issueRef.issueId)
|
|
521
|
+
);
|
|
522
|
+
printData(result, context.output);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (subcommand === "update") {
|
|
527
|
+
const parsed = parseCommandArgs(
|
|
528
|
+
rest,
|
|
529
|
+
{
|
|
530
|
+
project: { type: "string" },
|
|
531
|
+
url: { type: "string" },
|
|
532
|
+
title: { type: "string" },
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
537
|
+
ensureValue(parsed.positionals[1], "Link ID is required.");
|
|
538
|
+
ensureValue(parsed.values.url, "Link URL is required.");
|
|
539
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
540
|
+
const result = await issueClient.updateLink(
|
|
541
|
+
issueRef.projectId,
|
|
542
|
+
issueRef.issueId,
|
|
543
|
+
parsed.positionals[1],
|
|
544
|
+
buildIssueLinkPayload(parsed.values, issueRef.issueId)
|
|
545
|
+
);
|
|
546
|
+
printData(result, context.output);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
throw new CliError(`Unknown issue links subcommand: ${subcommand}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function runIssueRelationsCommand(issueClient, args, context) {
|
|
554
|
+
const [subcommand, ...rest] = args;
|
|
555
|
+
|
|
556
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
557
|
+
printHelp();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (subcommand === "ls") {
|
|
562
|
+
const parsed = parseCommandArgs(
|
|
563
|
+
rest,
|
|
564
|
+
{
|
|
565
|
+
project: { type: "string" },
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
570
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
571
|
+
const result = await issueClient.listRelations(issueRef.projectId, issueRef.issueId);
|
|
572
|
+
printData(result, {
|
|
573
|
+
...context.output,
|
|
574
|
+
render: renderIssueRelations,
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (subcommand === "add") {
|
|
580
|
+
const parsed = parseCommandArgs(
|
|
581
|
+
rest,
|
|
582
|
+
{
|
|
583
|
+
project: { type: "string" },
|
|
584
|
+
"relation-type": { type: "string" },
|
|
585
|
+
issues: { type: "string" },
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
590
|
+
ensureValue(parsed.values["relation-type"], "Relation type is required.");
|
|
591
|
+
ensureValue(parsed.values.issues, "At least one related issue ID is required.");
|
|
592
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
593
|
+
const result = await issueClient.createRelation(
|
|
594
|
+
issueRef.projectId,
|
|
595
|
+
issueRef.issueId,
|
|
596
|
+
buildIssueRelationPayload(parsed.values)
|
|
597
|
+
);
|
|
598
|
+
printData(result, context.output);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
throw new CliError(`Unknown issue relations subcommand: ${subcommand}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function uploadAttachmentBinary(uploadData, filePath, fileName, mimeType) {
|
|
606
|
+
const buffer = await readFile(filePath);
|
|
607
|
+
const form = new FormData();
|
|
608
|
+
|
|
609
|
+
for (const [key, value] of Object.entries(uploadData.fields || {})) {
|
|
610
|
+
form.append(key, value);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
form.append("file", new Blob([buffer], { type: mimeType }), fileName);
|
|
614
|
+
|
|
615
|
+
const response = await fetch(uploadData.url, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
body: form,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
if (!response.ok) {
|
|
621
|
+
throw new CliError(`Attachment upload failed: ${response.status} ${response.statusText}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function runIssueAttachmentsCommand(issueClient, args, context) {
|
|
626
|
+
const [subcommand, ...rest] = args;
|
|
627
|
+
|
|
628
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
629
|
+
printHelp();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (subcommand === "ls") {
|
|
634
|
+
const parsed = parseCommandArgs(
|
|
635
|
+
rest,
|
|
636
|
+
{
|
|
637
|
+
project: { type: "string" },
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
642
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
643
|
+
const result = await issueClient.listAttachments(issueRef.projectId, issueRef.issueId);
|
|
644
|
+
printData(result, {
|
|
645
|
+
...context.output,
|
|
646
|
+
render: renderIssueAttachments,
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (subcommand === "upload") {
|
|
652
|
+
const parsed = parseCommandArgs(
|
|
653
|
+
rest,
|
|
654
|
+
{
|
|
655
|
+
project: { type: "string" },
|
|
656
|
+
file: { type: "string" },
|
|
657
|
+
name: { type: "string" },
|
|
658
|
+
type: { type: "string" },
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
663
|
+
ensureValue(parsed.values.file, "File path is required.");
|
|
664
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
665
|
+
|
|
666
|
+
const filePath = parsed.values.file;
|
|
667
|
+
const fileInfo = await stat(filePath);
|
|
668
|
+
const fileName = parsed.values.name || basename(filePath);
|
|
669
|
+
const mimeType = parsed.values.type || inferAttachmentMimeType(filePath);
|
|
670
|
+
|
|
671
|
+
if (!mimeType) {
|
|
672
|
+
throw new CliError("Could not infer attachment MIME type. Pass --type explicitly.");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const uploadSession = await issueClient.createAttachmentUpload(issueRef.projectId, issueRef.issueId, {
|
|
676
|
+
name: fileName,
|
|
677
|
+
type: mimeType,
|
|
678
|
+
size: fileInfo.size,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
await uploadAttachmentBinary(uploadSession.upload_data, filePath, fileName, mimeType);
|
|
682
|
+
await issueClient.confirmAttachmentUpload(issueRef.projectId, issueRef.issueId, uploadSession.asset_id);
|
|
683
|
+
|
|
684
|
+
printData(
|
|
685
|
+
{
|
|
686
|
+
assetId: uploadSession.asset_id,
|
|
687
|
+
assetUrl: uploadSession.asset_url,
|
|
688
|
+
attachment: uploadSession.attachment,
|
|
689
|
+
uploaded: true,
|
|
690
|
+
},
|
|
691
|
+
context.output
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
throw new CliError(`Unknown issue attachments subcommand: ${subcommand}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
43
699
|
export async function runIssueCommand(args, context) {
|
|
44
700
|
const [subcommand, ...rest] = args;
|
|
45
701
|
|
|
@@ -49,7 +705,39 @@ export async function runIssueCommand(args, context) {
|
|
|
49
705
|
}
|
|
50
706
|
|
|
51
707
|
const config = await resolveRuntimeConfig();
|
|
52
|
-
const
|
|
708
|
+
const planeClient = new PlaneClient(config);
|
|
709
|
+
const issueClient = new IssueClient(planeClient);
|
|
710
|
+
const projectClient = new ProjectClient(planeClient);
|
|
711
|
+
|
|
712
|
+
if (subcommand === "labels") {
|
|
713
|
+
await runIssueLabelsCommand(issueClient, rest, context);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (subcommand === "comments") {
|
|
718
|
+
await runIssueCommentsCommand(issueClient, rest, context);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (subcommand === "activities") {
|
|
723
|
+
await runIssueActivitiesCommand(issueClient, rest, context);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (subcommand === "links") {
|
|
728
|
+
await runIssueLinksCommand(issueClient, rest, context);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (subcommand === "relations") {
|
|
733
|
+
await runIssueRelationsCommand(issueClient, rest, context);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (subcommand === "attachments") {
|
|
738
|
+
await runIssueAttachmentsCommand(issueClient, rest, context);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
53
741
|
|
|
54
742
|
if (subcommand === "ls") {
|
|
55
743
|
const parsed = parseCommandArgs(
|
|
@@ -101,14 +789,66 @@ export async function runIssueCommand(args, context) {
|
|
|
101
789
|
}
|
|
102
790
|
);
|
|
103
791
|
|
|
104
|
-
ensureValue(parsed.values.project, "Project ID is required.");
|
|
105
792
|
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
793
|
+
const issueRef = await resolveIssueTarget(issueClient, parsed.values.project, parsed.positionals[0]);
|
|
794
|
+
const result = await issueClient.get(issueRef.projectId, issueRef.issueId);
|
|
795
|
+
printData(result, context.output);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
106
798
|
|
|
107
|
-
|
|
799
|
+
if (subcommand === "key") {
|
|
800
|
+
const parsed = parseCommandArgs(
|
|
801
|
+
rest,
|
|
802
|
+
{
|
|
803
|
+
expand: { type: "string" },
|
|
804
|
+
fields: { type: "string" },
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
ensureValue(parsed.positionals[0], "Issue key is required.");
|
|
809
|
+
const { projectIdentifier, issueIdentifier } = parseIssueKey(parsed.positionals[0]);
|
|
810
|
+
const result = await issueClient.getByKey(
|
|
811
|
+
projectIdentifier,
|
|
812
|
+
issueIdentifier,
|
|
813
|
+
pickDefined({
|
|
814
|
+
expand: parsed.values.expand,
|
|
815
|
+
fields: parsed.values.fields,
|
|
816
|
+
})
|
|
817
|
+
);
|
|
108
818
|
printData(result, context.output);
|
|
109
819
|
return;
|
|
110
820
|
}
|
|
111
821
|
|
|
822
|
+
if (subcommand === "search") {
|
|
823
|
+
const parsed = parseCommandArgs(
|
|
824
|
+
rest,
|
|
825
|
+
{
|
|
826
|
+
query: { type: "string" },
|
|
827
|
+
project: { type: "string" },
|
|
828
|
+
limit: { type: "string" },
|
|
829
|
+
"workspace-search": { type: "boolean" },
|
|
830
|
+
},
|
|
831
|
+
false
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
ensureValue(parsed.values.query, "Search query is required.");
|
|
835
|
+
|
|
836
|
+
const result = await issueClient.search(
|
|
837
|
+
pickDefined({
|
|
838
|
+
search: parsed.values.query,
|
|
839
|
+
project_id: parsed.values.project,
|
|
840
|
+
limit: parsed.values.limit,
|
|
841
|
+
workspace_search: parsed.values["workspace-search"] ? "true" : parsed.values.project ? "false" : "true",
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
printData(result, {
|
|
846
|
+
...context.output,
|
|
847
|
+
render: renderIssueSearch,
|
|
848
|
+
});
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
112
852
|
if (subcommand === "create") {
|
|
113
853
|
const parsed = parseCommandArgs(
|
|
114
854
|
rest,
|
|
@@ -131,7 +871,13 @@ export async function runIssueCommand(args, context) {
|
|
|
131
871
|
ensureValue(parsed.values.project, "Project ID is required.");
|
|
132
872
|
ensureValue(parsed.values.name, "Issue name is required.");
|
|
133
873
|
|
|
134
|
-
const
|
|
874
|
+
const payload = buildIssuePayload(parsed.values);
|
|
875
|
+
if (payload.assignees) {
|
|
876
|
+
const members = await projectClient.listWorkspaceMembers();
|
|
877
|
+
payload.assignees = resolveAssigneeRefs(payload.assignees, members);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const result = await issueClient.create(parsed.values.project, payload);
|
|
135
881
|
printData(result, context.output);
|
|
136
882
|
return;
|
|
137
883
|
}
|
|
@@ -162,6 +908,11 @@ export async function runIssueCommand(args, context) {
|
|
|
162
908
|
throw new CliError("At least one update field is required.");
|
|
163
909
|
}
|
|
164
910
|
|
|
911
|
+
if (payload.assignees) {
|
|
912
|
+
const members = await projectClient.listWorkspaceMembers();
|
|
913
|
+
payload.assignees = resolveAssigneeRefs(payload.assignees, members);
|
|
914
|
+
}
|
|
915
|
+
|
|
165
916
|
const result = await issueClient.update(parsed.values.project, parsed.positionals[0], payload);
|
|
166
917
|
printData(result, context.output);
|
|
167
918
|
return;
|