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