@aaronshaf/plane 0.1.3 → 0.1.5

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.
@@ -1,418 +1,522 @@
1
- import { Command, Options, Args } from "@effect/cli"
2
- import { Console, Effect } from "effect"
3
- import { api, decodeOrFail } from "../api.js"
1
+ import { Command, Options, Args } from "@effect/cli";
2
+ import { Console, Effect } from "effect";
3
+ import { api, decodeOrFail } from "../api.js";
4
4
  import {
5
- IssueSchema,
6
- ActivitiesResponseSchema,
7
- IssueLinksResponseSchema,
8
- IssueLinkSchema,
9
- CommentsResponseSchema,
10
- WorklogsResponseSchema,
11
- WorklogSchema,
12
- } from "../config.js"
13
- import { parseIssueRef, findIssueBySeq, getStateId, resolveProject } from "../resolve.js"
14
- import { jsonMode, xmlMode, toXml } from "../output.js"
5
+ IssueSchema,
6
+ ActivitiesResponseSchema,
7
+ IssueLinksResponseSchema,
8
+ IssueLinkSchema,
9
+ CommentsResponseSchema,
10
+ WorklogsResponseSchema,
11
+ WorklogSchema,
12
+ } from "../config.js";
13
+ import {
14
+ parseIssueRef,
15
+ findIssueBySeq,
16
+ getStateId,
17
+ resolveProject,
18
+ } from "../resolve.js";
19
+ import { jsonMode, xmlMode, toXml } from "../output.js";
20
+ import { escapeHtmlText } from "../format.js";
15
21
 
16
22
  const refArg = Args.text({ name: "ref" }).pipe(
17
- Args.withDescription("Issue reference, e.g. PROJ-29"),
18
- )
23
+ Args.withDescription("Issue reference, e.g. PROJ-29"),
24
+ );
19
25
 
20
26
  // --- issue get ---
21
27
 
22
28
  export const issueGet = Command.make("get", { ref: refArg }, ({ ref }) =>
23
- Effect.gen(function* () {
24
- const { projectId, seq } = yield* parseIssueRef(ref)
25
- const issue = yield* findIssueBySeq(projectId, seq)
26
- yield* Console.log(JSON.stringify(issue, null, 2))
27
- }),
29
+ Effect.gen(function* () {
30
+ const { projectId, seq } = yield* parseIssueRef(ref);
31
+ const issue = yield* findIssueBySeq(projectId, seq);
32
+ yield* Console.log(JSON.stringify(issue, null, 2));
33
+ }),
28
34
  ).pipe(
29
- Command.withDescription(
30
- "Print full JSON for an issue. Useful for inspecting all fields (state, priority, assignees, labels, etc.).",
31
- ),
32
- )
35
+ Command.withDescription(
36
+ "Print full JSON for an issue. Useful for inspecting all fields (state, priority, assignees, labels, etc.).",
37
+ ),
38
+ );
33
39
 
34
40
  // --- issue update ---
35
41
 
36
42
  const stateOption = Options.optional(Options.text("state")).pipe(
37
- Options.withDescription("State group or name (e.g. backlog, completed)"),
38
- )
43
+ Options.withDescription("State group or name (e.g. backlog, completed)"),
44
+ );
39
45
 
40
46
  const priorityOption = Options.optional(
41
- Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
42
- ).pipe(Options.withDescription("Issue priority"))
47
+ Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
48
+ ).pipe(Options.withDescription("Issue priority"));
49
+
50
+ const descriptionOption = Options.optional(Options.text("description")).pipe(
51
+ Options.withDescription("Issue description (plain text, stored as HTML)"),
52
+ );
43
53
 
44
54
  export const issueUpdate = Command.make(
45
- "update",
46
- { state: stateOption, priority: priorityOption, ref: refArg },
47
- ({ ref, state, priority }) =>
48
- Effect.gen(function* () {
49
- const { projectId, seq } = yield* parseIssueRef(ref)
50
- const issue = yield* findIssueBySeq(projectId, seq)
51
-
52
- const body: Record<string, unknown> = {}
53
-
54
- if (state._tag === "Some") {
55
- body["state"] = yield* getStateId(projectId, state.value)
56
- }
57
- if (priority._tag === "Some") {
58
- body["priority"] = priority.value
59
- }
60
-
61
- if (Object.keys(body).length === 0) {
62
- yield* Effect.fail(new Error("Nothing to update. Specify --state or --priority"))
63
- }
64
-
65
- const raw = yield* api.patch(`projects/${projectId}/issues/${issue.id}/`, body)
66
- const updated = yield* decodeOrFail(IssueSchema, raw)
67
- yield* Console.log(
68
- `Updated ${ref}: state=${String(updated.state)} priority=${updated.priority}`,
69
- )
70
- }),
55
+ "update",
56
+ {
57
+ state: stateOption,
58
+ priority: priorityOption,
59
+ description: descriptionOption,
60
+ ref: refArg,
61
+ },
62
+ ({ ref, state, priority, description }) =>
63
+ Effect.gen(function* () {
64
+ const { projectId, seq } = yield* parseIssueRef(ref);
65
+ const issue = yield* findIssueBySeq(projectId, seq);
66
+
67
+ const body: Record<string, unknown> = {};
68
+
69
+ if (state._tag === "Some") {
70
+ body["state"] = yield* getStateId(projectId, state.value);
71
+ }
72
+ if (priority._tag === "Some") {
73
+ body["priority"] = priority.value;
74
+ }
75
+ if (description._tag === "Some") {
76
+ const escaped = escapeHtmlText(description.value);
77
+ body["description_html"] = `<p>${escaped}</p>`;
78
+ }
79
+
80
+ if (Object.keys(body).length === 0) {
81
+ yield* Effect.fail(
82
+ new Error(
83
+ "Nothing to update. Specify --state, --priority, or --description",
84
+ ),
85
+ );
86
+ }
87
+
88
+ const raw = yield* api.patch(
89
+ `projects/${projectId}/issues/${issue.id}/`,
90
+ body,
91
+ );
92
+ const updated = yield* decodeOrFail(IssueSchema, raw);
93
+ yield* Console.log(
94
+ `Updated ${ref}: state=${String(updated.state)} priority=${updated.priority}`,
95
+ );
96
+ }),
71
97
  ).pipe(
72
- Command.withDescription(
73
- "Update an issue's state or priority. 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 --state started --priority medium OPS-3",
74
- ),
75
- )
98
+ 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',
100
+ ),
101
+ );
76
102
 
77
103
  // --- issue comment ---
78
104
 
79
105
  const textArg = Args.text({ name: "text" }).pipe(
80
- Args.withDescription("Comment text to add"),
81
- )
106
+ Args.withDescription("Comment text to add"),
107
+ );
82
108
 
83
109
  export const issueComment = Command.make(
84
- "comment",
85
- { ref: refArg, text: textArg },
86
- ({ ref, text }) =>
87
- Effect.gen(function* () {
88
- const { projectId, seq } = yield* parseIssueRef(ref)
89
- const issue = yield* findIssueBySeq(projectId, seq)
90
- const escaped = text.replace(/</g, "&lt;").replace(/>/g, "&gt;")
91
- yield* api.post(`projects/${projectId}/issues/${issue.id}/comments/`, {
92
- comment_html: `<p>${escaped}</p>`,
93
- })
94
- yield* Console.log(`Comment added to ${ref}`)
95
- }),
110
+ "comment",
111
+ { ref: refArg, text: textArg },
112
+ ({ ref, text }) =>
113
+ Effect.gen(function* () {
114
+ const { projectId, seq } = yield* parseIssueRef(ref);
115
+ const issue = yield* findIssueBySeq(projectId, seq);
116
+ const escaped = escapeHtmlText(text);
117
+ yield* api.post(`projects/${projectId}/issues/${issue.id}/comments/`, {
118
+ comment_html: `<p>${escaped}</p>`,
119
+ });
120
+ yield* Console.log(`Comment added to ${ref}`);
121
+ }),
96
122
  ).pipe(
97
- Command.withDescription(
98
- "Add a comment to an issue. The text is wrapped in <p> tags and HTML-escaped.\n\nExample:\n plane issue comment PROJ-29 \"Fixed in latest build\"",
99
- ),
100
- )
123
+ Command.withDescription(
124
+ 'Add a comment to an issue. The text is wrapped in <p> tags and HTML-escaped.\n\nExample:\n plane issue comment PROJ-29 "Fixed in latest build"',
125
+ ),
126
+ );
101
127
 
102
128
  // --- issue create ---
103
129
 
104
130
  const titleArg = Args.text({ name: "title" }).pipe(
105
- Args.withDescription("Issue title"),
106
- )
131
+ Args.withDescription("Issue title"),
132
+ );
107
133
  const projectRefArg = Args.text({ name: "project" }).pipe(
108
- Args.withDescription("Project identifier (e.g. PROJ)"),
109
- )
134
+ Args.withDescription("Project identifier (e.g. PROJ)"),
135
+ );
110
136
 
111
137
  const createPriorityOption = Options.optional(
112
- Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
113
- ).pipe(Options.withDescription("Issue priority"))
138
+ Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
139
+ ).pipe(Options.withDescription("Issue priority"));
114
140
 
115
141
  const createStateOption = Options.optional(Options.text("state")).pipe(
116
- Options.withDescription("Initial state group or name"),
117
- )
142
+ Options.withDescription("Initial state group or name"),
143
+ );
144
+
145
+ const createDescriptionOption = Options.optional(
146
+ Options.text("description"),
147
+ ).pipe(
148
+ Options.withDescription("Issue description (plain text, stored as HTML)"),
149
+ );
118
150
 
119
151
  export const issueCreate = Command.make(
120
- "create",
121
- { priority: createPriorityOption, state: createStateOption, project: projectRefArg, title: titleArg },
122
- ({ project, title, priority, state }) =>
123
- Effect.gen(function* () {
124
- const { key, id: projectId } = yield* resolveProject(project)
125
- const body: Record<string, unknown> = { name: title }
126
- if (priority._tag === "Some") body["priority"] = priority.value
127
- if (state._tag === "Some") body["state"] = yield* getStateId(projectId, state.value)
128
- const raw = yield* api.post(`projects/${projectId}/issues/`, body)
129
- const created = yield* decodeOrFail(IssueSchema, raw)
130
- yield* Console.log(`Created ${key}-${created.sequence_id}: ${created.name}`)
131
- }),
152
+ "create",
153
+ {
154
+ priority: createPriorityOption,
155
+ state: createStateOption,
156
+ description: createDescriptionOption,
157
+ project: projectRefArg,
158
+ title: titleArg,
159
+ },
160
+ ({ project, title, priority, state, description }) =>
161
+ Effect.gen(function* () {
162
+ const { key, id: projectId } = yield* resolveProject(project);
163
+ const body: Record<string, unknown> = { name: title };
164
+ if (priority._tag === "Some") body["priority"] = priority.value;
165
+ if (state._tag === "Some")
166
+ body["state"] = yield* getStateId(projectId, state.value);
167
+ if (description._tag === "Some") {
168
+ const escaped = escapeHtmlText(description.value);
169
+ body["description_html"] = `<p>${escaped}</p>`;
170
+ }
171
+ const raw = yield* api.post(`projects/${projectId}/issues/`, body);
172
+ const created = yield* decodeOrFail(IssueSchema, raw);
173
+ yield* Console.log(
174
+ `Created ${key}-${created.sequence_id}: ${created.name}`,
175
+ );
176
+ }),
132
177
  ).pipe(
133
- Command.withDescription(
134
- "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\"",
135
- ),
136
- )
178
+ 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"',
180
+ ),
181
+ );
137
182
 
138
183
  // --- issue activity ---
139
184
 
140
- export const issueActivity = Command.make("activity", { ref: refArg }, ({ ref }) =>
141
- Effect.gen(function* () {
142
- const { projectId, seq } = yield* parseIssueRef(ref)
143
- const issue = yield* findIssueBySeq(projectId, seq)
144
- const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/activities/`)
145
- const { results } = yield* decodeOrFail(ActivitiesResponseSchema, raw)
146
- if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
147
- if (xmlMode) { yield* Console.log(toXml(results)); return }
148
- if (results.length === 0) {
149
- yield* Console.log("No activity found")
150
- return
151
- }
152
- const lines = results.map((a) => {
153
- const who = a.actor_detail?.display_name ?? "?"
154
- const when = a.created_at.slice(0, 16).replace("T", " ")
155
- if (a.field) {
156
- const from = a.old_value ?? "—"
157
- const to = a.new_value ?? "—"
158
- return `${when} ${who} ${a.field}: ${from} → ${to}`
159
- }
160
- return `${when} ${who} ${a.verb ?? "updated"}`
161
- })
162
- yield* Console.log(lines.join("\n"))
163
- }),
185
+ export const issueActivity = Command.make(
186
+ "activity",
187
+ { ref: refArg },
188
+ ({ ref }) =>
189
+ Effect.gen(function* () {
190
+ const { projectId, seq } = yield* parseIssueRef(ref);
191
+ const issue = yield* findIssueBySeq(projectId, seq);
192
+ const raw = yield* api.get(
193
+ `projects/${projectId}/issues/${issue.id}/activities/`,
194
+ );
195
+ const { results } = yield* decodeOrFail(ActivitiesResponseSchema, raw);
196
+ if (jsonMode) {
197
+ yield* Console.log(JSON.stringify(results, null, 2));
198
+ return;
199
+ }
200
+ if (xmlMode) {
201
+ yield* Console.log(toXml(results));
202
+ return;
203
+ }
204
+ if (results.length === 0) {
205
+ yield* Console.log("No activity found");
206
+ return;
207
+ }
208
+ const lines = results.map((a) => {
209
+ const who = a.actor_detail?.display_name ?? "?";
210
+ const when = a.created_at.slice(0, 16).replace("T", " ");
211
+ if (a.field) {
212
+ const from = a.old_value ?? "—";
213
+ const to = a.new_value ?? "—";
214
+ return `${when} ${who} ${a.field}: ${from} → ${to}`;
215
+ }
216
+ return `${when} ${who} ${a.verb ?? "updated"}`;
217
+ });
218
+ yield* Console.log(lines.join("\n"));
219
+ }),
164
220
  ).pipe(
165
- Command.withDescription(
166
- "Show audit trail for an issue — who changed what and when.\n\nExample:\n plane issue activity PROJ-29",
167
- ),
168
- )
221
+ Command.withDescription(
222
+ "Show audit trail for an issue — who changed what and when.\n\nExample:\n plane issue activity PROJ-29",
223
+ ),
224
+ );
169
225
 
170
226
  // --- issue link list ---
171
227
 
172
228
  export const issueLinkList = Command.make("list", { ref: refArg }, ({ ref }) =>
173
- Effect.gen(function* () {
174
- const { projectId, seq } = yield* parseIssueRef(ref)
175
- const issue = yield* findIssueBySeq(projectId, seq)
176
- const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/issue-links/`)
177
- const { results } = yield* decodeOrFail(IssueLinksResponseSchema, raw)
178
- if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
179
- if (xmlMode) { yield* Console.log(toXml(results)); return }
180
- if (results.length === 0) {
181
- yield* Console.log("No links")
182
- return
183
- }
184
- const lines = results.map((l) => `${l.id} ${l.title ?? "(no title)"} ${l.url}`)
185
- yield* Console.log(lines.join("\n"))
186
- }),
187
- ).pipe(Command.withDescription("List URL links attached to an issue."))
229
+ Effect.gen(function* () {
230
+ const { projectId, seq } = yield* parseIssueRef(ref);
231
+ const issue = yield* findIssueBySeq(projectId, seq);
232
+ const raw = yield* api.get(
233
+ `projects/${projectId}/issues/${issue.id}/issue-links/`,
234
+ );
235
+ const { results } = yield* decodeOrFail(IssueLinksResponseSchema, raw);
236
+ if (jsonMode) {
237
+ yield* Console.log(JSON.stringify(results, null, 2));
238
+ return;
239
+ }
240
+ if (xmlMode) {
241
+ yield* Console.log(toXml(results));
242
+ return;
243
+ }
244
+ if (results.length === 0) {
245
+ yield* Console.log("No links");
246
+ return;
247
+ }
248
+ const lines = results.map(
249
+ (l) => `${l.id} ${l.title ?? "(no title)"} ${l.url}`,
250
+ );
251
+ yield* Console.log(lines.join("\n"));
252
+ }),
253
+ ).pipe(Command.withDescription("List URL links attached to an issue."));
188
254
 
189
255
  // --- issue link add ---
190
256
 
191
- const urlArg = Args.text({ name: "url" }).pipe(Args.withDescription("URL to link"))
257
+ const urlArg = Args.text({ name: "url" }).pipe(
258
+ Args.withDescription("URL to link"),
259
+ );
192
260
  const linkTitleOption = Options.optional(Options.text("title")).pipe(
193
- Options.withDescription("Human-readable title for the link"),
194
- )
261
+ Options.withDescription("Human-readable title for the link"),
262
+ );
195
263
 
196
264
  export const issueLinkAdd = Command.make(
197
- "add",
198
- { title: linkTitleOption, ref: refArg, url: urlArg },
199
- ({ ref, url, title }) =>
200
- Effect.gen(function* () {
201
- const { projectId, seq } = yield* parseIssueRef(ref)
202
- const issue = yield* findIssueBySeq(projectId, seq)
203
- const body: Record<string, string> = { url }
204
- if (title._tag === "Some") body["title"] = title.value
205
- const raw = yield* api.post(
206
- `projects/${projectId}/issues/${issue.id}/issue-links/`,
207
- body,
208
- )
209
- const link = yield* decodeOrFail(IssueLinkSchema, raw)
210
- yield* Console.log(`Link added: ${link.id} ${link.url}`)
211
- }),
265
+ "add",
266
+ { title: linkTitleOption, ref: refArg, url: urlArg },
267
+ ({ ref, url, title }) =>
268
+ Effect.gen(function* () {
269
+ const { projectId, seq } = yield* parseIssueRef(ref);
270
+ const issue = yield* findIssueBySeq(projectId, seq);
271
+ const body: Record<string, string> = { url };
272
+ if (title._tag === "Some") body["title"] = title.value;
273
+ const raw = yield* api.post(
274
+ `projects/${projectId}/issues/${issue.id}/issue-links/`,
275
+ body,
276
+ );
277
+ const link = yield* decodeOrFail(IssueLinkSchema, raw);
278
+ yield* Console.log(`Link added: ${link.id} ${link.url}`);
279
+ }),
212
280
  ).pipe(
213
- Command.withDescription(
214
- "Attach a URL link to an issue.\n\nExamples:\n plane issue link add PROJ-29 https://github.com/org/repo/pull/42\n plane issue link add --title \"Design doc\" PROJ-29 https://docs.example.com",
215
- ),
216
- )
281
+ Command.withDescription(
282
+ 'Attach a URL link to an issue.\n\nExamples:\n plane issue link add PROJ-29 https://github.com/org/repo/pull/42\n plane issue link add --title "Design doc" PROJ-29 https://docs.example.com',
283
+ ),
284
+ );
217
285
 
218
286
  // --- issue link remove ---
219
287
 
220
288
  const linkIdArg = Args.text({ name: "link-id" }).pipe(
221
- Args.withDescription("Link ID (from 'plane issue link list')"),
222
- )
289
+ Args.withDescription("Link ID (from 'plane issue link list')"),
290
+ );
223
291
 
224
292
  export const issueLinkRemove = Command.make(
225
- "remove",
226
- { ref: refArg, linkId: linkIdArg },
227
- ({ ref, linkId }) =>
228
- Effect.gen(function* () {
229
- const { projectId, seq } = yield* parseIssueRef(ref)
230
- const issue = yield* findIssueBySeq(projectId, seq)
231
- yield* api.delete(`projects/${projectId}/issues/${issue.id}/issue-links/${linkId}/`)
232
- yield* Console.log(`Link ${linkId} removed from ${ref}`)
233
- }),
234
- ).pipe(Command.withDescription("Remove a URL link from an issue by link ID."))
293
+ "remove",
294
+ { ref: refArg, linkId: linkIdArg },
295
+ ({ ref, linkId }) =>
296
+ Effect.gen(function* () {
297
+ const { projectId, seq } = yield* parseIssueRef(ref);
298
+ const issue = yield* findIssueBySeq(projectId, seq);
299
+ yield* api.delete(
300
+ `projects/${projectId}/issues/${issue.id}/issue-links/${linkId}/`,
301
+ );
302
+ yield* Console.log(`Link ${linkId} removed from ${ref}`);
303
+ }),
304
+ ).pipe(Command.withDescription("Remove a URL link from an issue by link ID."));
235
305
 
236
306
  // --- issue link (parent) ---
237
307
 
238
308
  export const issueLink = Command.make("link").pipe(
239
- Command.withDescription(
240
- "Manage URL links on an issue. Subcommands: list, add, remove",
241
- ),
242
- Command.withSubcommands([issueLinkList, issueLinkAdd, issueLinkRemove]),
243
- )
309
+ Command.withDescription(
310
+ "Manage URL links on an issue. Subcommands: list, add, remove",
311
+ ),
312
+ Command.withSubcommands([issueLinkList, issueLinkAdd, issueLinkRemove]),
313
+ );
244
314
 
245
315
  // --- issue comments list ---
246
316
 
247
- export const issueCommentsList = Command.make("list", { ref: refArg }, ({ ref }) =>
248
- Effect.gen(function* () {
249
- const { projectId, seq } = yield* parseIssueRef(ref)
250
- const issue = yield* findIssueBySeq(projectId, seq)
251
- const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/comments/`)
252
- const { results } = yield* decodeOrFail(CommentsResponseSchema, raw)
253
- if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
254
- if (xmlMode) { yield* Console.log(toXml(results)); return }
255
- if (results.length === 0) {
256
- yield* Console.log("No comments")
257
- return
258
- }
259
- const lines = results.map((c) => {
260
- const who = c.actor_detail?.display_name ?? "?"
261
- const when = c.created_at.slice(0, 16).replace("T", " ")
262
- const text = (c.comment_html ?? "").replace(/<[^>]+>/g, "").trim()
263
- return `${c.id} ${when} ${who}: ${text}`
264
- })
265
- yield* Console.log(lines.join("\n"))
266
- }),
317
+ export const issueCommentsList = Command.make(
318
+ "list",
319
+ { ref: refArg },
320
+ ({ ref }) =>
321
+ Effect.gen(function* () {
322
+ const { projectId, seq } = yield* parseIssueRef(ref);
323
+ const issue = yield* findIssueBySeq(projectId, seq);
324
+ const raw = yield* api.get(
325
+ `projects/${projectId}/issues/${issue.id}/comments/`,
326
+ );
327
+ const { results } = yield* decodeOrFail(CommentsResponseSchema, raw);
328
+ if (jsonMode) {
329
+ yield* Console.log(JSON.stringify(results, null, 2));
330
+ return;
331
+ }
332
+ if (xmlMode) {
333
+ yield* Console.log(toXml(results));
334
+ return;
335
+ }
336
+ if (results.length === 0) {
337
+ yield* Console.log("No comments");
338
+ return;
339
+ }
340
+ const lines = results.map((c) => {
341
+ const who = c.actor_detail?.display_name ?? "?";
342
+ const when = c.created_at.slice(0, 16).replace("T", " ");
343
+ const text = (c.comment_html ?? "").replace(/<[^>]+>/g, "").trim();
344
+ return `${c.id} ${when} ${who}: ${text}`;
345
+ });
346
+ yield* Console.log(lines.join("\n"));
347
+ }),
267
348
  ).pipe(
268
- Command.withDescription(
269
- "List comments on an issue. Shows comment ID, timestamp, author, and plain text.\n\nExample:\n plane issue comments list PROJ-29",
270
- ),
271
- )
349
+ Command.withDescription(
350
+ "List comments on an issue. Shows comment ID, timestamp, author, and plain text.\n\nExample:\n plane issue comments list PROJ-29",
351
+ ),
352
+ );
272
353
 
273
354
  // --- issue comment update ---
274
355
 
275
356
  const commentIdArg = Args.text({ name: "comment-id" }).pipe(
276
- Args.withDescription("Comment ID (from 'plane issue comments list')"),
277
- )
357
+ Args.withDescription("Comment ID (from 'plane issue comments list')"),
358
+ );
278
359
 
279
360
  export const issueCommentUpdate = Command.make(
280
- "update",
281
- { ref: refArg, commentId: commentIdArg, text: textArg },
282
- ({ ref, commentId, text }) =>
283
- Effect.gen(function* () {
284
- const { projectId, seq } = yield* parseIssueRef(ref)
285
- const issue = yield* findIssueBySeq(projectId, seq)
286
- const escaped = text.replace(/</g, "&lt;").replace(/>/g, "&gt;")
287
- yield* api.patch(
288
- `projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
289
- { comment_html: `<p>${escaped}</p>` },
290
- )
291
- yield* Console.log(`Comment ${commentId} updated`)
292
- }),
361
+ "update",
362
+ { ref: refArg, commentId: commentIdArg, text: textArg },
363
+ ({ ref, commentId, text }) =>
364
+ Effect.gen(function* () {
365
+ const { projectId, seq } = yield* parseIssueRef(ref);
366
+ const issue = yield* findIssueBySeq(projectId, seq);
367
+ const escaped = escapeHtmlText(text);
368
+ yield* api.patch(
369
+ `projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
370
+ { comment_html: `<p>${escaped}</p>` },
371
+ );
372
+ yield* Console.log(`Comment ${commentId} updated`);
373
+ }),
293
374
  ).pipe(
294
- Command.withDescription(
295
- "Edit an existing comment.\n\nExample:\n plane issue comments update PROJ-29 <comment-id> \"Updated text\"",
296
- ),
297
- )
375
+ Command.withDescription(
376
+ 'Edit an existing comment.\n\nExample:\n plane issue comments update PROJ-29 <comment-id> "Updated text"',
377
+ ),
378
+ );
298
379
 
299
380
  // --- issue comment delete ---
300
381
 
301
382
  export const issueCommentDelete = Command.make(
302
- "delete",
303
- { ref: refArg, commentId: commentIdArg },
304
- ({ ref, commentId }) =>
305
- Effect.gen(function* () {
306
- const { projectId, seq } = yield* parseIssueRef(ref)
307
- const issue = yield* findIssueBySeq(projectId, seq)
308
- yield* api.delete(
309
- `projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
310
- )
311
- yield* Console.log(`Comment ${commentId} deleted`)
312
- }),
313
- ).pipe(Command.withDescription("Delete a comment from an issue."))
383
+ "delete",
384
+ { ref: refArg, commentId: commentIdArg },
385
+ ({ ref, commentId }) =>
386
+ Effect.gen(function* () {
387
+ const { projectId, seq } = yield* parseIssueRef(ref);
388
+ const issue = yield* findIssueBySeq(projectId, seq);
389
+ yield* api.delete(
390
+ `projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
391
+ );
392
+ yield* Console.log(`Comment ${commentId} deleted`);
393
+ }),
394
+ ).pipe(Command.withDescription("Delete a comment from an issue."));
314
395
 
315
396
  // --- issue comments (parent) ---
316
397
 
317
398
  export const issueComments = Command.make("comments").pipe(
318
- Command.withDescription("Manage comments on an issue. Subcommands: list, update, delete\n\nNote: use 'plane issue comment REF TEXT' to add a new comment."),
319
- Command.withSubcommands([issueCommentsList, issueCommentUpdate, issueCommentDelete]),
320
- )
399
+ Command.withDescription(
400
+ "Manage comments on an issue. Subcommands: list, update, delete\n\nNote: use 'plane issue comment REF TEXT' to add a new comment.",
401
+ ),
402
+ Command.withSubcommands([
403
+ issueCommentsList,
404
+ issueCommentUpdate,
405
+ issueCommentDelete,
406
+ ]),
407
+ );
321
408
 
322
409
  // --- issue worklogs list ---
323
410
 
324
- export const issueWorklogsList = Command.make("list", { ref: refArg }, ({ ref }) =>
325
- Effect.gen(function* () {
326
- const { projectId, seq } = yield* parseIssueRef(ref)
327
- const issue = yield* findIssueBySeq(projectId, seq)
328
- const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/worklogs/`)
329
- const { results } = yield* decodeOrFail(WorklogsResponseSchema, raw)
330
- if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
331
- if (xmlMode) { yield* Console.log(toXml(results)); return }
332
- if (results.length === 0) {
333
- yield* Console.log("No worklogs")
334
- return
335
- }
336
- const lines = results.map((w) => {
337
- const who = w.logged_by_detail?.display_name ?? "?"
338
- const when = w.created_at.slice(0, 10)
339
- const hrs = (w.duration / 60).toFixed(1)
340
- const desc = w.description ?? ""
341
- return `${w.id} ${when} ${who} ${hrs}h ${desc}`
342
- })
343
- yield* Console.log(lines.join("\n"))
344
- }),
411
+ export const issueWorklogsList = Command.make(
412
+ "list",
413
+ { ref: refArg },
414
+ ({ ref }) =>
415
+ Effect.gen(function* () {
416
+ const { projectId, seq } = yield* parseIssueRef(ref);
417
+ const issue = yield* findIssueBySeq(projectId, seq);
418
+ const raw = yield* api.get(
419
+ `projects/${projectId}/issues/${issue.id}/worklogs/`,
420
+ );
421
+ const { results } = yield* decodeOrFail(WorklogsResponseSchema, raw);
422
+ if (jsonMode) {
423
+ yield* Console.log(JSON.stringify(results, null, 2));
424
+ return;
425
+ }
426
+ if (xmlMode) {
427
+ yield* Console.log(toXml(results));
428
+ return;
429
+ }
430
+ if (results.length === 0) {
431
+ yield* Console.log("No worklogs");
432
+ return;
433
+ }
434
+ const lines = results.map((w) => {
435
+ const who = w.logged_by_detail?.display_name ?? "?";
436
+ const when = w.created_at.slice(0, 10);
437
+ const hrs = (w.duration / 60).toFixed(1);
438
+ const desc = w.description ?? "";
439
+ return `${w.id} ${when} ${who} ${hrs}h ${desc}`;
440
+ });
441
+ yield* Console.log(lines.join("\n"));
442
+ }),
345
443
  ).pipe(
346
- Command.withDescription(
347
- "List time log entries for an issue. Duration shown in hours.\n\nExample:\n plane issue worklogs list PROJ-29",
348
- ),
349
- )
444
+ Command.withDescription(
445
+ "List time log entries for an issue. Duration shown in hours.\n\nExample:\n plane issue worklogs list PROJ-29",
446
+ ),
447
+ );
350
448
 
351
449
  // --- issue worklogs add ---
352
450
 
353
451
  const durationArg = Args.integer({ name: "minutes" }).pipe(
354
- Args.withDescription("Time spent in minutes"),
355
- )
452
+ Args.withDescription("Time spent in minutes"),
453
+ );
356
454
  const worklogDescOption = Options.optional(Options.text("description")).pipe(
357
- Options.withDescription("Optional description of work done"),
358
- )
455
+ Options.withDescription("Optional description of work done"),
456
+ );
359
457
 
360
458
  export const issueWorklogsAdd = Command.make(
361
- "add",
362
- { description: worklogDescOption, ref: refArg, duration: durationArg },
363
- ({ ref, duration, description }) =>
364
- Effect.gen(function* () {
365
- const { projectId, seq } = yield* parseIssueRef(ref)
366
- const issue = yield* findIssueBySeq(projectId, seq)
367
- const body: Record<string, unknown> = { duration }
368
- if (description._tag === "Some") body["description"] = description.value
369
- const raw = yield* api.post(
370
- `projects/${projectId}/issues/${issue.id}/worklogs/`,
371
- body,
372
- )
373
- const log = yield* decodeOrFail(WorklogSchema, raw)
374
- const hrs = (log.duration / 60).toFixed(1)
375
- yield* Console.log(`Logged ${hrs}h on ${ref} (${log.id})`)
376
- }),
459
+ "add",
460
+ { description: worklogDescOption, ref: refArg, duration: durationArg },
461
+ ({ ref, duration, description }) =>
462
+ Effect.gen(function* () {
463
+ const { projectId, seq } = yield* parseIssueRef(ref);
464
+ const issue = yield* findIssueBySeq(projectId, seq);
465
+ const body: Record<string, unknown> = { duration };
466
+ if (description._tag === "Some") body["description"] = description.value;
467
+ const raw = yield* api.post(
468
+ `projects/${projectId}/issues/${issue.id}/worklogs/`,
469
+ body,
470
+ );
471
+ const log = yield* decodeOrFail(WorklogSchema, raw);
472
+ const hrs = (log.duration / 60).toFixed(1);
473
+ yield* Console.log(`Logged ${hrs}h on ${ref} (${log.id})`);
474
+ }),
377
475
  ).pipe(
378
- Command.withDescription(
379
- "Log time spent on an issue (duration in minutes).\n\nExamples:\n plane issue worklogs add PROJ-29 90\n plane issue worklogs add --description \"code review\" PROJ-29 30",
380
- ),
381
- )
476
+ Command.withDescription(
477
+ 'Log time spent on an issue (duration in minutes).\n\nExamples:\n plane issue worklogs add PROJ-29 90\n plane issue worklogs add --description "code review" PROJ-29 30',
478
+ ),
479
+ );
382
480
 
383
481
  // --- issue worklogs (parent) ---
384
482
 
385
483
  export const issueWorklogs = Command.make("worklogs").pipe(
386
- Command.withDescription("Manage time logs for an issue. Subcommands: list, add"),
387
- Command.withSubcommands([issueWorklogsList, issueWorklogsAdd]),
388
- )
484
+ Command.withDescription(
485
+ "Manage time logs for an issue. Subcommands: list, add",
486
+ ),
487
+ Command.withSubcommands([issueWorklogsList, issueWorklogsAdd]),
488
+ );
389
489
 
390
490
  // --- issue delete ---
391
491
 
392
492
  export const issueDelete = Command.make("delete", { ref: refArg }, ({ ref }) =>
393
- Effect.gen(function* () {
394
- const { projectId, seq } = yield* parseIssueRef(ref)
395
- const issue = yield* findIssueBySeq(projectId, seq)
396
- yield* api.delete(`projects/${projectId}/issues/${issue.id}/`)
397
- yield* Console.log(`Deleted ${ref}`)
398
- }),
399
- ).pipe(Command.withDescription("Permanently delete an issue. This cannot be undone."))
493
+ Effect.gen(function* () {
494
+ const { projectId, seq } = yield* parseIssueRef(ref);
495
+ const issue = yield* findIssueBySeq(projectId, seq);
496
+ yield* api.delete(`projects/${projectId}/issues/${issue.id}/`);
497
+ yield* Console.log(`Deleted ${ref}`);
498
+ }),
499
+ ).pipe(
500
+ Command.withDescription(
501
+ "Permanently delete an issue. This cannot be undone.",
502
+ ),
503
+ );
400
504
 
401
505
  // --- issue (parent) ---
402
506
 
403
507
  export const issue = Command.make("issue").pipe(
404
- Command.withDescription(
405
- "Manage individual issues. Use 'plane issue <subcommand> --help' for details.\n\nSubcommands: get, create, update, delete, comment, activity, link, comments, worklogs",
406
- ),
407
- Command.withSubcommands([
408
- issueGet,
409
- issueCreate,
410
- issueUpdate,
411
- issueDelete,
412
- issueComment,
413
- issueActivity,
414
- issueLink,
415
- issueComments,
416
- issueWorklogs,
417
- ]),
418
- )
508
+ Command.withDescription(
509
+ "Manage individual issues. Use 'plane issue <subcommand> --help' for details.\n\nSubcommands: get, create, update, delete, comment, activity, link, comments, worklogs",
510
+ ),
511
+ Command.withSubcommands([
512
+ issueGet,
513
+ issueCreate,
514
+ issueUpdate,
515
+ issueDelete,
516
+ issueComment,
517
+ issueActivity,
518
+ issueLink,
519
+ issueComments,
520
+ issueWorklogs,
521
+ ]),
522
+ );