@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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.6",
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)
@@ -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
- ({ ref, state, priority, description, assignee, noAssignee }) =>
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
- ({ project, title, priority, state, description, assignee }) =>
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(
@@ -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,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
+ }