@aaronshaf/plane 0.1.6 → 0.1.8
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 +71 -4
- package/src/commands/issues.ts +45 -12
- package/src/config.ts +18 -0
- package/src/resolve.ts +37 -0
- package/tests/issue-commands.test.ts +407 -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,8 @@ import {
|
|
|
12
12
|
} from "../config.js";
|
|
13
13
|
import {
|
|
14
14
|
findIssueBySeq,
|
|
15
|
+
getEstimatePointId,
|
|
16
|
+
getLabelId,
|
|
15
17
|
getMemberId,
|
|
16
18
|
getStateId,
|
|
17
19
|
parseIssueRef,
|
|
@@ -48,6 +50,10 @@ const priorityOption = Options.optional(
|
|
|
48
50
|
Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
|
|
49
51
|
).pipe(Options.withDescription("Issue priority"));
|
|
50
52
|
|
|
53
|
+
const titleUpdateOption = Options.optional(Options.text("title")).pipe(
|
|
54
|
+
Options.withDescription("Issue title"),
|
|
55
|
+
);
|
|
56
|
+
|
|
51
57
|
const descriptionOption = Options.optional(Options.text("description")).pipe(
|
|
52
58
|
Options.withDescription("Issue description (plain text, stored as HTML)"),
|
|
53
59
|
);
|
|
@@ -56,6 +62,16 @@ const assigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
|
56
62
|
Options.withDescription("Assign to a member (display name, email, or UUID)"),
|
|
57
63
|
);
|
|
58
64
|
|
|
65
|
+
const labelOption = Options.optional(Options.text("label")).pipe(
|
|
66
|
+
Options.withDescription("Set issue label by name"),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const estimateOption = Options.optional(Options.text("estimate")).pipe(
|
|
70
|
+
Options.withDescription(
|
|
71
|
+
"Estimate point value (e.g. '3', 'Medium', 'L') — resolved to UUID from the project's estimate scheme",
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
59
75
|
const noAssigneeOption = Options.boolean("no-assignee").pipe(
|
|
60
76
|
Options.withDescription("Clear all assignees"),
|
|
61
77
|
Options.withDefault(false),
|
|
@@ -66,12 +82,25 @@ export const issueUpdate = Command.make(
|
|
|
66
82
|
{
|
|
67
83
|
state: stateOption,
|
|
68
84
|
priority: priorityOption,
|
|
85
|
+
title: titleUpdateOption,
|
|
69
86
|
description: descriptionOption,
|
|
70
87
|
assignee: assigneeOption,
|
|
88
|
+
label: labelOption,
|
|
89
|
+
estimate: estimateOption,
|
|
71
90
|
noAssignee: noAssigneeOption,
|
|
72
91
|
ref: refArg,
|
|
73
92
|
},
|
|
74
|
-
({
|
|
93
|
+
({
|
|
94
|
+
ref,
|
|
95
|
+
state,
|
|
96
|
+
priority,
|
|
97
|
+
title,
|
|
98
|
+
description,
|
|
99
|
+
assignee,
|
|
100
|
+
label,
|
|
101
|
+
estimate,
|
|
102
|
+
noAssignee,
|
|
103
|
+
}) =>
|
|
75
104
|
Effect.gen(function* () {
|
|
76
105
|
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
77
106
|
const issue = yield* findIssueBySeq(projectId, seq);
|
|
@@ -84,6 +113,9 @@ export const issueUpdate = Command.make(
|
|
|
84
113
|
if (priority._tag === "Some") {
|
|
85
114
|
body["priority"] = priority.value;
|
|
86
115
|
}
|
|
116
|
+
if (title._tag === "Some") {
|
|
117
|
+
body["name"] = title.value;
|
|
118
|
+
}
|
|
87
119
|
if (description._tag === "Some") {
|
|
88
120
|
const escaped = escapeHtmlText(description.value);
|
|
89
121
|
body["description_html"] = `<p>${escaped}</p>`;
|
|
@@ -94,11 +126,21 @@ export const issueUpdate = Command.make(
|
|
|
94
126
|
const memberId = yield* getMemberId(assignee.value);
|
|
95
127
|
body["assignees"] = [memberId];
|
|
96
128
|
}
|
|
129
|
+
if (label._tag === "Some") {
|
|
130
|
+
const labelId = yield* getLabelId(projectId, label.value);
|
|
131
|
+
body["label_ids"] = [labelId];
|
|
132
|
+
}
|
|
133
|
+
if (estimate._tag === "Some") {
|
|
134
|
+
body["estimate_point"] = yield* getEstimatePointId(
|
|
135
|
+
projectId,
|
|
136
|
+
estimate.value,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
97
139
|
|
|
98
140
|
if (Object.keys(body).length === 0) {
|
|
99
141
|
yield* Effect.fail(
|
|
100
142
|
new Error(
|
|
101
|
-
"Nothing to update. Specify --state, --priority, --description, --assignee, or --no-assignee",
|
|
143
|
+
"Nothing to update. Specify --state, --priority, --title, --description, --assignee, --label, --estimate, or --no-assignee",
|
|
102
144
|
),
|
|
103
145
|
);
|
|
104
146
|
}
|
|
@@ -114,7 +156,7 @@ export const issueUpdate = Command.make(
|
|
|
114
156
|
}),
|
|
115
157
|
).pipe(
|
|
116
158
|
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',
|
|
159
|
+
'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
160
|
),
|
|
119
161
|
);
|
|
120
162
|
|
|
@@ -170,6 +212,10 @@ const createAssigneeOption = Options.optional(Options.text("assignee")).pipe(
|
|
|
170
212
|
Options.withDescription("Assign to a member (display name, email, or UUID)"),
|
|
171
213
|
);
|
|
172
214
|
|
|
215
|
+
const createLabelOption = Options.optional(Options.text("label")).pipe(
|
|
216
|
+
Options.withDescription("Set issue label by name"),
|
|
217
|
+
);
|
|
218
|
+
|
|
173
219
|
export const issueCreate = Command.make(
|
|
174
220
|
"create",
|
|
175
221
|
{
|
|
@@ -177,10 +223,21 @@ export const issueCreate = Command.make(
|
|
|
177
223
|
state: createStateOption,
|
|
178
224
|
description: createDescriptionOption,
|
|
179
225
|
assignee: createAssigneeOption,
|
|
226
|
+
label: createLabelOption,
|
|
227
|
+
estimate: estimateOption,
|
|
180
228
|
project: projectRefArg,
|
|
181
229
|
title: titleArg,
|
|
182
230
|
},
|
|
183
|
-
({
|
|
231
|
+
({
|
|
232
|
+
project,
|
|
233
|
+
title,
|
|
234
|
+
priority,
|
|
235
|
+
state,
|
|
236
|
+
description,
|
|
237
|
+
assignee,
|
|
238
|
+
label,
|
|
239
|
+
estimate,
|
|
240
|
+
}) =>
|
|
184
241
|
Effect.gen(function* () {
|
|
185
242
|
const { key, id: projectId } = yield* resolveProject(project);
|
|
186
243
|
const body: Record<string, unknown> = { name: title };
|
|
@@ -195,6 +252,16 @@ export const issueCreate = Command.make(
|
|
|
195
252
|
const memberId = yield* getMemberId(assignee.value);
|
|
196
253
|
body["assignees"] = [memberId];
|
|
197
254
|
}
|
|
255
|
+
if (label._tag === "Some") {
|
|
256
|
+
const labelId = yield* getLabelId(projectId, label.value);
|
|
257
|
+
body["label_ids"] = [labelId];
|
|
258
|
+
}
|
|
259
|
+
if (estimate._tag === "Some") {
|
|
260
|
+
body["estimate_point"] = yield* getEstimatePointId(
|
|
261
|
+
projectId,
|
|
262
|
+
estimate.value,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
198
265
|
const raw = yield* api.post(`projects/${projectId}/issues/`, body);
|
|
199
266
|
const created = yield* decodeOrFail(IssueSchema, raw);
|
|
200
267
|
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.String)),
|
|
20
22
|
});
|
|
21
23
|
export type Issue = typeof IssueSchema.Type;
|
|
22
24
|
|
|
@@ -213,3 +215,19 @@ export type CycleIssue = typeof CycleIssueSchema.Type;
|
|
|
213
215
|
export const CycleIssuesResponseSchema = Schema.Struct({
|
|
214
216
|
results: Schema.Array(CycleIssueSchema),
|
|
215
217
|
});
|
|
218
|
+
|
|
219
|
+
export const EstimatePointSchema = Schema.Struct({
|
|
220
|
+
id: Schema.String,
|
|
221
|
+
value: Schema.String,
|
|
222
|
+
key: Schema.Number,
|
|
223
|
+
});
|
|
224
|
+
export type EstimatePoint = typeof EstimatePointSchema.Type;
|
|
225
|
+
|
|
226
|
+
export const EstimateSchema = Schema.Struct({
|
|
227
|
+
id: Schema.String,
|
|
228
|
+
points: Schema.Array(EstimatePointSchema),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export const EstimatesResponseSchema = Schema.Struct({
|
|
232
|
+
results: Schema.Array(EstimateSchema),
|
|
233
|
+
});
|
package/src/resolve.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { api, decodeOrFail } from "./api.js";
|
|
3
3
|
import {
|
|
4
|
+
EstimatesResponseSchema,
|
|
4
5
|
IssuesResponseSchema,
|
|
6
|
+
LabelsResponseSchema,
|
|
5
7
|
MembersResponseSchema,
|
|
6
8
|
ProjectsResponseSchema,
|
|
7
9
|
StatesResponseSchema,
|
|
@@ -109,3 +111,38 @@ export function getStateId(projectId: string, nameOrGroup: string) {
|
|
|
109
111
|
return state.id;
|
|
110
112
|
});
|
|
111
113
|
}
|
|
114
|
+
|
|
115
|
+
export function getEstimatePointId(projectId: string, value: string) {
|
|
116
|
+
return Effect.gen(function* () {
|
|
117
|
+
const raw = yield* api.get(`projects/${projectId}/estimates/`);
|
|
118
|
+
const { results } = yield* decodeOrFail(EstimatesResponseSchema, raw);
|
|
119
|
+
if (results.length === 0)
|
|
120
|
+
return yield* Effect.fail(
|
|
121
|
+
new Error("No estimate scheme configured for this project"),
|
|
122
|
+
);
|
|
123
|
+
const points = results[0].points;
|
|
124
|
+
const lower = value.toLowerCase();
|
|
125
|
+
const point = points.find(
|
|
126
|
+
(p) => p.value.toLowerCase() === lower || String(p.key) === value,
|
|
127
|
+
);
|
|
128
|
+
if (!point)
|
|
129
|
+
return yield* Effect.fail(
|
|
130
|
+
new Error(
|
|
131
|
+
`Estimate point not found: ${value}. Available: ${points.map((p) => p.value).join(", ")}`,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
return point.id;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getLabelId(projectId: string, name: string) {
|
|
139
|
+
return Effect.gen(function* () {
|
|
140
|
+
const raw = yield* api.get(`projects/${projectId}/labels/`);
|
|
141
|
+
const { results } = yield* decodeOrFail(LabelsResponseSchema, raw);
|
|
142
|
+
const lower = name.toLowerCase();
|
|
143
|
+
const label = results.find((l) => l.name.toLowerCase() === lower);
|
|
144
|
+
if (!label)
|
|
145
|
+
return yield* Effect.fail(new Error(`Label not found: ${name}`));
|
|
146
|
+
return label.id;
|
|
147
|
+
});
|
|
148
|
+
}
|