@aaronshaf/plane 0.1.5 → 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 +91 -6
- package/src/commands/issues.ts +45 -12
- package/src/config.ts +2 -0
- package/src/resolve.ts +36 -1
- package/tests/issue-commands.test.ts +552 -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
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
WorklogSchema,
|
|
12
12
|
} from "../config.js";
|
|
13
13
|
import {
|
|
14
|
-
parseIssueRef,
|
|
15
14
|
findIssueBySeq,
|
|
15
|
+
getLabelId,
|
|
16
|
+
getMemberId,
|
|
16
17
|
getStateId,
|
|
18
|
+
parseIssueRef,
|
|
17
19
|
resolveProject,
|
|
18
20
|
} from "../resolve.js";
|
|
19
21
|
import { jsonMode, xmlMode, toXml } from "../output.js";
|
|
@@ -47,19 +49,55 @@ const priorityOption = Options.optional(
|
|
|
47
49
|
Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
|
|
48
50
|
).pipe(Options.withDescription("Issue priority"));
|
|
49
51
|
|
|
52
|
+
const titleUpdateOption = Options.optional(Options.text("title")).pipe(
|
|
53
|
+
Options.withDescription("Issue title"),
|
|
54
|
+
);
|
|
55
|
+
|
|
50
56
|
const descriptionOption = Options.optional(Options.text("description")).pipe(
|
|
51
57
|
Options.withDescription("Issue description (plain text, stored as HTML)"),
|
|
52
58
|
);
|
|
53
59
|
|
|
60
|
+
const assigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
61
|
+
Options.withDescription("Assign to a member (display name, email, or UUID)"),
|
|
62
|
+
);
|
|
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
|
+
|
|
72
|
+
const noAssigneeOption = Options.boolean("no-assignee").pipe(
|
|
73
|
+
Options.withDescription("Clear all assignees"),
|
|
74
|
+
Options.withDefault(false),
|
|
75
|
+
);
|
|
76
|
+
|
|
54
77
|
export const issueUpdate = Command.make(
|
|
55
78
|
"update",
|
|
56
79
|
{
|
|
57
80
|
state: stateOption,
|
|
58
81
|
priority: priorityOption,
|
|
82
|
+
title: titleUpdateOption,
|
|
59
83
|
description: descriptionOption,
|
|
84
|
+
assignee: assigneeOption,
|
|
85
|
+
label: labelOption,
|
|
86
|
+
estimate: estimateOption,
|
|
87
|
+
noAssignee: noAssigneeOption,
|
|
60
88
|
ref: refArg,
|
|
61
89
|
},
|
|
62
|
-
({
|
|
90
|
+
({
|
|
91
|
+
ref,
|
|
92
|
+
state,
|
|
93
|
+
priority,
|
|
94
|
+
title,
|
|
95
|
+
description,
|
|
96
|
+
assignee,
|
|
97
|
+
label,
|
|
98
|
+
estimate,
|
|
99
|
+
noAssignee,
|
|
100
|
+
}) =>
|
|
63
101
|
Effect.gen(function* () {
|
|
64
102
|
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
65
103
|
const issue = yield* findIssueBySeq(projectId, seq);
|
|
@@ -72,15 +110,31 @@ export const issueUpdate = Command.make(
|
|
|
72
110
|
if (priority._tag === "Some") {
|
|
73
111
|
body["priority"] = priority.value;
|
|
74
112
|
}
|
|
113
|
+
if (title._tag === "Some") {
|
|
114
|
+
body["name"] = title.value;
|
|
115
|
+
}
|
|
75
116
|
if (description._tag === "Some") {
|
|
76
117
|
const escaped = escapeHtmlText(description.value);
|
|
77
118
|
body["description_html"] = `<p>${escaped}</p>`;
|
|
78
119
|
}
|
|
120
|
+
if (noAssignee) {
|
|
121
|
+
body["assignees"] = [];
|
|
122
|
+
} else if (assignee._tag === "Some") {
|
|
123
|
+
const memberId = yield* getMemberId(assignee.value);
|
|
124
|
+
body["assignees"] = [memberId];
|
|
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
|
+
}
|
|
79
133
|
|
|
80
134
|
if (Object.keys(body).length === 0) {
|
|
81
135
|
yield* Effect.fail(
|
|
82
136
|
new Error(
|
|
83
|
-
"Nothing to update. Specify --state, --priority, or --
|
|
137
|
+
"Nothing to update. Specify --state, --priority, --title, --description, --assignee, --label, --estimate, or --no-assignee",
|
|
84
138
|
),
|
|
85
139
|
);
|
|
86
140
|
}
|
|
@@ -96,7 +150,7 @@ export const issueUpdate = Command.make(
|
|
|
96
150
|
}),
|
|
97
151
|
).pipe(
|
|
98
152
|
Command.withDescription(
|
|
99
|
-
'Update an issue\'s state, priority, or
|
|
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',
|
|
100
154
|
),
|
|
101
155
|
);
|
|
102
156
|
|
|
@@ -148,16 +202,36 @@ const createDescriptionOption = Options.optional(
|
|
|
148
202
|
Options.withDescription("Issue description (plain text, stored as HTML)"),
|
|
149
203
|
);
|
|
150
204
|
|
|
205
|
+
const createAssigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
206
|
+
Options.withDescription("Assign to a member (display name, email, or UUID)"),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const createLabelOption = Options.optional(Options.text("label")).pipe(
|
|
210
|
+
Options.withDescription("Set issue label by name"),
|
|
211
|
+
);
|
|
212
|
+
|
|
151
213
|
export const issueCreate = Command.make(
|
|
152
214
|
"create",
|
|
153
215
|
{
|
|
154
216
|
priority: createPriorityOption,
|
|
155
217
|
state: createStateOption,
|
|
156
218
|
description: createDescriptionOption,
|
|
219
|
+
assignee: createAssigneeOption,
|
|
220
|
+
label: createLabelOption,
|
|
221
|
+
estimate: estimateOption,
|
|
157
222
|
project: projectRefArg,
|
|
158
223
|
title: titleArg,
|
|
159
224
|
},
|
|
160
|
-
({
|
|
225
|
+
({
|
|
226
|
+
project,
|
|
227
|
+
title,
|
|
228
|
+
priority,
|
|
229
|
+
state,
|
|
230
|
+
description,
|
|
231
|
+
assignee,
|
|
232
|
+
label,
|
|
233
|
+
estimate,
|
|
234
|
+
}) =>
|
|
161
235
|
Effect.gen(function* () {
|
|
162
236
|
const { key, id: projectId } = yield* resolveProject(project);
|
|
163
237
|
const body: Record<string, unknown> = { name: title };
|
|
@@ -168,6 +242,17 @@ export const issueCreate = Command.make(
|
|
|
168
242
|
const escaped = escapeHtmlText(description.value);
|
|
169
243
|
body["description_html"] = `<p>${escaped}</p>`;
|
|
170
244
|
}
|
|
245
|
+
if (assignee._tag === "Some") {
|
|
246
|
+
const memberId = yield* getMemberId(assignee.value);
|
|
247
|
+
body["assignees"] = [memberId];
|
|
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
|
+
}
|
|
171
256
|
const raw = yield* api.post(`projects/${projectId}/issues/`, body);
|
|
172
257
|
const created = yield* decodeOrFail(IssueSchema, raw);
|
|
173
258
|
yield* Console.log(
|
|
@@ -176,7 +261,7 @@ export const issueCreate = Command.make(
|
|
|
176
261
|
}),
|
|
177
262
|
).pipe(
|
|
178
263
|
Command.withDescription(
|
|
179
|
-
'Create a new issue in a project.\n\nExamples:\n plane issue create PROJ "Migrate Button component"\n plane issue create --priority high --state started PROJ "Fix lint pipeline"\n plane issue create --description "Detailed context here" PROJ "Add dark mode"',
|
|
264
|
+
'Create a new issue in a project.\n\nExamples:\n plane issue create PROJ "Migrate Button component"\n plane issue create --priority high --state started PROJ "Fix lint pipeline"\n plane issue create --description "Detailed context here" PROJ "Add dark mode"\n plane issue create --assignee "Jane Doe" PROJ "Onboarding bug"',
|
|
180
265
|
),
|
|
181
266
|
);
|
|
182
267
|
|
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,8 +2,10 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { api, decodeOrFail } from "./api.js";
|
|
3
3
|
import {
|
|
4
4
|
IssuesResponseSchema,
|
|
5
|
-
|
|
5
|
+
LabelsResponseSchema,
|
|
6
|
+
MembersResponseSchema,
|
|
6
7
|
ProjectsResponseSchema,
|
|
8
|
+
StatesResponseSchema,
|
|
7
9
|
} from "./config.js";
|
|
8
10
|
|
|
9
11
|
// Cache project list within a process invocation
|
|
@@ -74,6 +76,27 @@ export function findIssueBySeq(projectId: string, seq: number) {
|
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
export function getMemberId(nameEmailOrId: string) {
|
|
80
|
+
return Effect.gen(function* () {
|
|
81
|
+
const results = yield* decodeOrFail(
|
|
82
|
+
MembersResponseSchema,
|
|
83
|
+
yield* api.get("members/"),
|
|
84
|
+
);
|
|
85
|
+
const lower = nameEmailOrId.toLowerCase();
|
|
86
|
+
const member = results.find(
|
|
87
|
+
(m) =>
|
|
88
|
+
m.id === nameEmailOrId ||
|
|
89
|
+
m.display_name.toLowerCase() === lower ||
|
|
90
|
+
(m.email ?? "").toLowerCase() === lower,
|
|
91
|
+
);
|
|
92
|
+
if (!member)
|
|
93
|
+
return yield* Effect.fail(
|
|
94
|
+
new Error(`Member not found: ${nameEmailOrId}`),
|
|
95
|
+
);
|
|
96
|
+
return member.id;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
77
100
|
export function getStateId(projectId: string, nameOrGroup: string) {
|
|
78
101
|
return Effect.gen(function* () {
|
|
79
102
|
const raw = yield* api.get(`projects/${projectId}/states/`);
|
|
@@ -87,3 +110,15 @@ export function getStateId(projectId: string, nameOrGroup: string) {
|
|
|
87
110
|
return state.id;
|
|
88
111
|
});
|
|
89
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
|
+
}
|