@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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.5",
6
+ "version": "0.1.7",
7
7
  "description": "CLI for the Plane project management API",
8
8
  "keywords": [
9
9
  "plane",
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 filter)
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)
@@ -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
- ({ ref, state, priority, description }) =>
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 --description",
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 description. 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 --description "New description" PROJ-29\n plane issue update --state started --priority medium OPS-3',
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
- ({ project, title, priority, state, description }) =>
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
 
@@ -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
- { state: stateOption, project: projectArg },
25
- ({ project, state }) =>
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
- const filtered =
32
- state._tag === "Some"
33
- ? results.filter((i) => {
34
- const s = i.state as State | string;
35
- if (typeof s !== "object") return false;
36
- const val = state.value.toLowerCase();
37
- return s.group === val || s.name.toLowerCase() === val;
38
- })
39
- : results;
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
- StatesResponseSchema,
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
+ }