@aaronshaf/plane 0.1.6 → 0.1.7
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 +1 -1
- package/src/app.ts +1 -1
- package/src/commands/issue.ts +62 -4
- package/src/commands/issues.ts +45 -12
- package/src/config.ts +2 -0
- package/src/resolve.ts +13 -0
- package/tests/issue-commands.test.ts +390 -0
- package/tests/json-output.test.ts +353 -0
- package/tests/resolve.test.ts +37 -0
- package/tests/xml-output.test.ts +342 -0
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -38,7 +38,7 @@ CONCEPTS
|
|
|
38
38
|
ALL SUBCOMMANDS
|
|
39
39
|
init Set up config interactively
|
|
40
40
|
projects list List all projects
|
|
41
|
-
issues list List issues (supports --state
|
|
41
|
+
issues list List issues (supports --state, --assignee, --priority)
|
|
42
42
|
issue get | create | update | delete | comment | activity |
|
|
43
43
|
link | comments | worklogs
|
|
44
44
|
cycles list | issues (list, add)
|
package/src/commands/issue.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../config.js";
|
|
13
13
|
import {
|
|
14
14
|
findIssueBySeq,
|
|
15
|
+
getLabelId,
|
|
15
16
|
getMemberId,
|
|
16
17
|
getStateId,
|
|
17
18
|
parseIssueRef,
|
|
@@ -48,6 +49,10 @@ const priorityOption = Options.optional(
|
|
|
48
49
|
Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
|
|
49
50
|
).pipe(Options.withDescription("Issue priority"));
|
|
50
51
|
|
|
52
|
+
const titleUpdateOption = Options.optional(Options.text("title")).pipe(
|
|
53
|
+
Options.withDescription("Issue title"),
|
|
54
|
+
);
|
|
55
|
+
|
|
51
56
|
const descriptionOption = Options.optional(Options.text("description")).pipe(
|
|
52
57
|
Options.withDescription("Issue description (plain text, stored as HTML)"),
|
|
53
58
|
);
|
|
@@ -56,6 +61,14 @@ const assigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
|
56
61
|
Options.withDescription("Assign to a member (display name, email, or UUID)"),
|
|
57
62
|
);
|
|
58
63
|
|
|
64
|
+
const labelOption = Options.optional(Options.text("label")).pipe(
|
|
65
|
+
Options.withDescription("Set issue label by name"),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const estimateOption = Options.optional(Options.integer("estimate")).pipe(
|
|
69
|
+
Options.withDescription("Estimate point (0–7)"),
|
|
70
|
+
);
|
|
71
|
+
|
|
59
72
|
const noAssigneeOption = Options.boolean("no-assignee").pipe(
|
|
60
73
|
Options.withDescription("Clear all assignees"),
|
|
61
74
|
Options.withDefault(false),
|
|
@@ -66,12 +79,25 @@ export const issueUpdate = Command.make(
|
|
|
66
79
|
{
|
|
67
80
|
state: stateOption,
|
|
68
81
|
priority: priorityOption,
|
|
82
|
+
title: titleUpdateOption,
|
|
69
83
|
description: descriptionOption,
|
|
70
84
|
assignee: assigneeOption,
|
|
85
|
+
label: labelOption,
|
|
86
|
+
estimate: estimateOption,
|
|
71
87
|
noAssignee: noAssigneeOption,
|
|
72
88
|
ref: refArg,
|
|
73
89
|
},
|
|
74
|
-
({
|
|
90
|
+
({
|
|
91
|
+
ref,
|
|
92
|
+
state,
|
|
93
|
+
priority,
|
|
94
|
+
title,
|
|
95
|
+
description,
|
|
96
|
+
assignee,
|
|
97
|
+
label,
|
|
98
|
+
estimate,
|
|
99
|
+
noAssignee,
|
|
100
|
+
}) =>
|
|
75
101
|
Effect.gen(function* () {
|
|
76
102
|
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
77
103
|
const issue = yield* findIssueBySeq(projectId, seq);
|
|
@@ -84,6 +110,9 @@ export const issueUpdate = Command.make(
|
|
|
84
110
|
if (priority._tag === "Some") {
|
|
85
111
|
body["priority"] = priority.value;
|
|
86
112
|
}
|
|
113
|
+
if (title._tag === "Some") {
|
|
114
|
+
body["name"] = title.value;
|
|
115
|
+
}
|
|
87
116
|
if (description._tag === "Some") {
|
|
88
117
|
const escaped = escapeHtmlText(description.value);
|
|
89
118
|
body["description_html"] = `<p>${escaped}</p>`;
|
|
@@ -94,11 +123,18 @@ export const issueUpdate = Command.make(
|
|
|
94
123
|
const memberId = yield* getMemberId(assignee.value);
|
|
95
124
|
body["assignees"] = [memberId];
|
|
96
125
|
}
|
|
126
|
+
if (label._tag === "Some") {
|
|
127
|
+
const labelId = yield* getLabelId(projectId, label.value);
|
|
128
|
+
body["label_ids"] = [labelId];
|
|
129
|
+
}
|
|
130
|
+
if (estimate._tag === "Some") {
|
|
131
|
+
body["estimate_point"] = estimate.value;
|
|
132
|
+
}
|
|
97
133
|
|
|
98
134
|
if (Object.keys(body).length === 0) {
|
|
99
135
|
yield* Effect.fail(
|
|
100
136
|
new Error(
|
|
101
|
-
"Nothing to update. Specify --state, --priority, --description, --assignee, or --no-assignee",
|
|
137
|
+
"Nothing to update. Specify --state, --priority, --title, --description, --assignee, --label, --estimate, or --no-assignee",
|
|
102
138
|
),
|
|
103
139
|
);
|
|
104
140
|
}
|
|
@@ -114,7 +150,7 @@ export const issueUpdate = Command.make(
|
|
|
114
150
|
}),
|
|
115
151
|
).pipe(
|
|
116
152
|
Command.withDescription(
|
|
117
|
-
'Update an issue\'s state, priority, description, or assignee. Options must come before the REF argument.\n\nExamples:\n plane issue update --state completed PROJ-29\n plane issue update --priority high WEB-5\n plane issue update --assignee "Jane Doe" PROJ-29\n plane issue update --no-assignee PROJ-29\n plane issue update --description "New description" PROJ-29',
|
|
153
|
+
'Update an issue\'s state, priority, title, description, or assignee. Options must come before the REF argument.\n\nExamples:\n plane issue update --state completed PROJ-29\n plane issue update --priority high WEB-5\n plane issue update --title "New issue title" PROJ-29\n plane issue update --assignee "Jane Doe" PROJ-29\n plane issue update --no-assignee PROJ-29\n plane issue update --description "New description" PROJ-29',
|
|
118
154
|
),
|
|
119
155
|
);
|
|
120
156
|
|
|
@@ -170,6 +206,10 @@ const createAssigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
|
170
206
|
Options.withDescription("Assign to a member (display name, email, or UUID)"),
|
|
171
207
|
);
|
|
172
208
|
|
|
209
|
+
const createLabelOption = Options.optional(Options.text("label")).pipe(
|
|
210
|
+
Options.withDescription("Set issue label by name"),
|
|
211
|
+
);
|
|
212
|
+
|
|
173
213
|
export const issueCreate = Command.make(
|
|
174
214
|
"create",
|
|
175
215
|
{
|
|
@@ -177,10 +217,21 @@ export const issueCreate = Command.make(
|
|
|
177
217
|
state: createStateOption,
|
|
178
218
|
description: createDescriptionOption,
|
|
179
219
|
assignee: createAssigneeOption,
|
|
220
|
+
label: createLabelOption,
|
|
221
|
+
estimate: estimateOption,
|
|
180
222
|
project: projectRefArg,
|
|
181
223
|
title: titleArg,
|
|
182
224
|
},
|
|
183
|
-
({
|
|
225
|
+
({
|
|
226
|
+
project,
|
|
227
|
+
title,
|
|
228
|
+
priority,
|
|
229
|
+
state,
|
|
230
|
+
description,
|
|
231
|
+
assignee,
|
|
232
|
+
label,
|
|
233
|
+
estimate,
|
|
234
|
+
}) =>
|
|
184
235
|
Effect.gen(function* () {
|
|
185
236
|
const { key, id: projectId } = yield* resolveProject(project);
|
|
186
237
|
const body: Record<string, unknown> = { name: title };
|
|
@@ -195,6 +246,13 @@ export const issueCreate = Command.make(
|
|
|
195
246
|
const memberId = yield* getMemberId(assignee.value);
|
|
196
247
|
body["assignees"] = [memberId];
|
|
197
248
|
}
|
|
249
|
+
if (label._tag === "Some") {
|
|
250
|
+
const labelId = yield* getLabelId(projectId, label.value);
|
|
251
|
+
body["label_ids"] = [labelId];
|
|
252
|
+
}
|
|
253
|
+
if (estimate._tag === "Some") {
|
|
254
|
+
body["estimate_point"] = estimate.value;
|
|
255
|
+
}
|
|
198
256
|
const raw = yield* api.post(`projects/${projectId}/issues/`, body);
|
|
199
257
|
const created = yield* decodeOrFail(IssueSchema, raw);
|
|
200
258
|
yield* Console.log(
|
package/src/commands/issues.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Console, Effect } from "effect";
|
|
|
3
3
|
import { api, decodeOrFail } from "../api.js";
|
|
4
4
|
import { IssuesResponseSchema } from "../config.js";
|
|
5
5
|
import { formatIssue } from "../format.js";
|
|
6
|
-
import { resolveProject } from "../resolve.js";
|
|
6
|
+
import { getMemberId, resolveProject } from "../resolve.js";
|
|
7
7
|
import type { State } from "../config.js";
|
|
8
8
|
import { jsonMode, xmlMode, toXml } from "../output.js";
|
|
9
9
|
|
|
@@ -19,24 +19,57 @@ const stateOption = Options.optional(Options.text("state")).pipe(
|
|
|
19
19
|
),
|
|
20
20
|
);
|
|
21
21
|
|
|
22
|
+
const assigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
23
|
+
Options.withDescription(
|
|
24
|
+
"Filter by assignee (display name, email, or member UUID)",
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const priorityOption = Options.optional(
|
|
29
|
+
Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
|
|
30
|
+
).pipe(Options.withDescription("Filter by priority"));
|
|
31
|
+
|
|
22
32
|
export const issuesList = Command.make(
|
|
23
33
|
"list",
|
|
24
|
-
{
|
|
25
|
-
|
|
34
|
+
{
|
|
35
|
+
state: stateOption,
|
|
36
|
+
assignee: assigneeOption,
|
|
37
|
+
priority: priorityOption,
|
|
38
|
+
project: projectArg,
|
|
39
|
+
},
|
|
40
|
+
({ project, state, assignee, priority }) =>
|
|
26
41
|
Effect.gen(function* () {
|
|
27
42
|
const { key, id } = yield* resolveProject(project);
|
|
28
43
|
const raw = yield* api.get(`projects/${id}/issues/?order_by=sequence_id`);
|
|
29
44
|
const { results } = yield* decodeOrFail(IssuesResponseSchema, raw);
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
let filtered = results;
|
|
47
|
+
|
|
48
|
+
if (state._tag === "Some") {
|
|
49
|
+
filtered = filtered.filter((i) => {
|
|
50
|
+
const s = i.state as State | string;
|
|
51
|
+
if (typeof s !== "object") return false;
|
|
52
|
+
const val = state.value.toLowerCase();
|
|
53
|
+
return s.group === val || s.name.toLowerCase() === val;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (assignee._tag === "Some") {
|
|
58
|
+
const isUuid =
|
|
59
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
60
|
+
assignee.value,
|
|
61
|
+
);
|
|
62
|
+
const memberId = isUuid
|
|
63
|
+
? assignee.value
|
|
64
|
+
: yield* getMemberId(assignee.value);
|
|
65
|
+
filtered = filtered.filter(
|
|
66
|
+
(i) => Array.isArray(i.assignees) && i.assignees.includes(memberId),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (priority._tag === "Some") {
|
|
71
|
+
filtered = filtered.filter((i) => i.priority === priority.value);
|
|
72
|
+
}
|
|
40
73
|
|
|
41
74
|
if (jsonMode) {
|
|
42
75
|
yield* Console.log(JSON.stringify(filtered, null, 2));
|
package/src/config.ts
CHANGED
|
@@ -16,7 +16,9 @@ export const IssueSchema = Schema.Struct({
|
|
|
16
16
|
name: Schema.String,
|
|
17
17
|
priority: Schema.String,
|
|
18
18
|
state: Schema.Union(Schema.String, StateSchema),
|
|
19
|
+
assignees: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
19
20
|
description_html: Schema.optional(Schema.NullOr(Schema.String)),
|
|
21
|
+
estimate_point: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
20
22
|
});
|
|
21
23
|
export type Issue = typeof IssueSchema.Type;
|
|
22
24
|
|
package/src/resolve.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { api, decodeOrFail } from "./api.js";
|
|
3
3
|
import {
|
|
4
4
|
IssuesResponseSchema,
|
|
5
|
+
LabelsResponseSchema,
|
|
5
6
|
MembersResponseSchema,
|
|
6
7
|
ProjectsResponseSchema,
|
|
7
8
|
StatesResponseSchema,
|
|
@@ -109,3 +110,15 @@ export function getStateId(projectId: string, nameOrGroup: string) {
|
|
|
109
110
|
return state.id;
|
|
110
111
|
});
|
|
111
112
|
}
|
|
113
|
+
|
|
114
|
+
export function getLabelId(projectId: string, name: string) {
|
|
115
|
+
return Effect.gen(function* () {
|
|
116
|
+
const raw = yield* api.get(`projects/${projectId}/labels/`);
|
|
117
|
+
const { results } = yield* decodeOrFail(LabelsResponseSchema, raw);
|
|
118
|
+
const lower = name.toLowerCase();
|
|
119
|
+
const label = results.find((l) => l.name.toLowerCase() === lower);
|
|
120
|
+
if (!label)
|
|
121
|
+
return yield* Effect.fail(new Error(`Label not found: ${name}`));
|
|
122
|
+
return label.id;
|
|
123
|
+
});
|
|
124
|
+
}
|