@aaronshaf/plane 0.1.7 → 0.1.10
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/commands/cycles.ts +86 -63
- package/src/commands/intake.ts +64 -44
- package/src/commands/issue.ts +386 -301
- package/src/commands/issues.ts +58 -45
- package/src/commands/labels.ts +6 -2
- package/src/commands/modules.ts +108 -74
- package/src/commands/pages.ts +42 -30
- package/src/config.ts +1 -1
- package/src/output.ts +12 -9
- package/src/resolve.ts +17 -5
- package/tests/api.test.ts +12 -8
- package/tests/cycles-extended.test.ts +15 -15
- package/tests/intake.test.ts +12 -12
- package/tests/issue-activity.test.ts +8 -16
- package/tests/issue-commands.test.ts +212 -278
- package/tests/issue-comments-worklogs.test.ts +31 -38
- package/tests/issue-links.test.ts +20 -23
- package/tests/modules.test.ts +17 -23
- package/tests/output.test.ts +11 -0
- package/tests/pages.test.ts +8 -8
package/src/commands/issue.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Options, Args } from "@effect/cli";
|
|
2
|
-
import { Console, Effect } from "effect";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
3
|
import { api, decodeOrFail } from "../api.js";
|
|
4
4
|
import {
|
|
5
5
|
IssueSchema,
|
|
@@ -24,23 +24,48 @@ import { escapeHtmlText } from "../format.js";
|
|
|
24
24
|
const refArg = Args.text({ name: "ref" }).pipe(
|
|
25
25
|
Args.withDescription("Issue reference, e.g. PROJ-29"),
|
|
26
26
|
);
|
|
27
|
-
|
|
27
|
+
// --- Typed payload interfaces ---
|
|
28
|
+
interface IssueUpdatePayload {
|
|
29
|
+
state?: string;
|
|
30
|
+
priority?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
description_html?: string;
|
|
33
|
+
assignees?: string[];
|
|
34
|
+
label_ids?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IssueCreatePayload {
|
|
38
|
+
name: string;
|
|
39
|
+
priority?: string;
|
|
40
|
+
state?: string;
|
|
41
|
+
description_html?: string;
|
|
42
|
+
assignees?: string[];
|
|
43
|
+
label_ids?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface WorklogPayload {
|
|
47
|
+
duration: number;
|
|
48
|
+
description?: string;
|
|
49
|
+
}
|
|
28
50
|
// --- issue get ---
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Effect.gen(function* () {
|
|
51
|
+
export function issueGetHandler({ ref }: { ref: string }) {
|
|
52
|
+
return Effect.gen(function* () {
|
|
32
53
|
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
33
54
|
const issue = yield* findIssueBySeq(projectId, seq);
|
|
34
55
|
yield* Console.log(JSON.stringify(issue, null, 2));
|
|
35
|
-
})
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const issueGet = Command.make(
|
|
60
|
+
"get",
|
|
61
|
+
{ ref: refArg },
|
|
62
|
+
issueGetHandler,
|
|
36
63
|
).pipe(
|
|
37
64
|
Command.withDescription(
|
|
38
65
|
"Print full JSON for an issue. Useful for inspecting all fields (state, priority, assignees, labels, etc.).",
|
|
39
66
|
),
|
|
40
67
|
);
|
|
41
|
-
|
|
42
68
|
// --- issue update ---
|
|
43
|
-
|
|
44
69
|
const stateOption = Options.optional(Options.text("state")).pipe(
|
|
45
70
|
Options.withDescription("State group or name (e.g. backlog, completed)"),
|
|
46
71
|
);
|
|
@@ -65,15 +90,81 @@ const labelOption = Options.optional(Options.text("label")).pipe(
|
|
|
65
90
|
Options.withDescription("Set issue label by name"),
|
|
66
91
|
);
|
|
67
92
|
|
|
68
|
-
const estimateOption = Options.optional(Options.integer("estimate")).pipe(
|
|
69
|
-
Options.withDescription("Estimate point (0–7)"),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
93
|
const noAssigneeOption = Options.boolean("no-assignee").pipe(
|
|
73
94
|
Options.withDescription("Clear all assignees"),
|
|
74
95
|
Options.withDefault(false),
|
|
75
96
|
);
|
|
76
97
|
|
|
98
|
+
export function issueUpdateHandler({
|
|
99
|
+
ref,
|
|
100
|
+
state,
|
|
101
|
+
priority,
|
|
102
|
+
title,
|
|
103
|
+
description,
|
|
104
|
+
assignee,
|
|
105
|
+
label,
|
|
106
|
+
noAssignee,
|
|
107
|
+
}: {
|
|
108
|
+
ref: string;
|
|
109
|
+
state: Option.Option<string>;
|
|
110
|
+
priority: Option.Option<string>;
|
|
111
|
+
title: Option.Option<string>;
|
|
112
|
+
description: Option.Option<string>;
|
|
113
|
+
assignee: Option.Option<string>;
|
|
114
|
+
label: Option.Option<string>;
|
|
115
|
+
noAssignee: boolean;
|
|
116
|
+
}) {
|
|
117
|
+
return Effect.gen(function* () {
|
|
118
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
119
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
120
|
+
|
|
121
|
+
const body: IssueUpdatePayload = {};
|
|
122
|
+
|
|
123
|
+
if (state._tag === "Some") {
|
|
124
|
+
body.state = yield* getStateId(projectId, state.value);
|
|
125
|
+
}
|
|
126
|
+
if (priority._tag === "Some") {
|
|
127
|
+
body.priority = priority.value;
|
|
128
|
+
}
|
|
129
|
+
if (title._tag === "Some") {
|
|
130
|
+
body.name = title.value;
|
|
131
|
+
}
|
|
132
|
+
if (description._tag === "Some") {
|
|
133
|
+
const escaped = escapeHtmlText(description.value);
|
|
134
|
+
body.description_html = `<p>${escaped}</p>`;
|
|
135
|
+
}
|
|
136
|
+
if (noAssignee) {
|
|
137
|
+
body.assignees = [];
|
|
138
|
+
} else if (assignee._tag === "Some") {
|
|
139
|
+
const memberId = yield* getMemberId(assignee.value);
|
|
140
|
+
body.assignees = [memberId];
|
|
141
|
+
}
|
|
142
|
+
if (label._tag === "Some") {
|
|
143
|
+
const labelId = yield* getLabelId(projectId, label.value);
|
|
144
|
+
body.label_ids = [labelId];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (Object.keys(body).length === 0) {
|
|
148
|
+
yield* Effect.fail(
|
|
149
|
+
new Error(
|
|
150
|
+
"Nothing to update. Specify --state, --priority, --title, --description, --assignee, --label, or --no-assignee",
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const raw = yield* api.patch(
|
|
156
|
+
`projects/${projectId}/issues/${issue.id}/`,
|
|
157
|
+
body,
|
|
158
|
+
);
|
|
159
|
+
const updated = yield* decodeOrFail(IssueSchema, raw);
|
|
160
|
+
const stateName =
|
|
161
|
+
typeof updated.state === "object" ? updated.state.name : updated.state;
|
|
162
|
+
yield* Console.log(
|
|
163
|
+
`Updated ${ref}: state=${stateName} priority=${updated.priority}`,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
77
168
|
export const issueUpdate = Command.make(
|
|
78
169
|
"update",
|
|
79
170
|
{
|
|
@@ -83,104 +174,48 @@ export const issueUpdate = Command.make(
|
|
|
83
174
|
description: descriptionOption,
|
|
84
175
|
assignee: assigneeOption,
|
|
85
176
|
label: labelOption,
|
|
86
|
-
estimate: estimateOption,
|
|
87
177
|
noAssignee: noAssigneeOption,
|
|
88
178
|
ref: refArg,
|
|
89
179
|
},
|
|
90
|
-
|
|
91
|
-
ref,
|
|
92
|
-
state,
|
|
93
|
-
priority,
|
|
94
|
-
title,
|
|
95
|
-
description,
|
|
96
|
-
assignee,
|
|
97
|
-
label,
|
|
98
|
-
estimate,
|
|
99
|
-
noAssignee,
|
|
100
|
-
}) =>
|
|
101
|
-
Effect.gen(function* () {
|
|
102
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
103
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
104
|
-
|
|
105
|
-
const body: Record<string, unknown> = {};
|
|
106
|
-
|
|
107
|
-
if (state._tag === "Some") {
|
|
108
|
-
body["state"] = yield* getStateId(projectId, state.value);
|
|
109
|
-
}
|
|
110
|
-
if (priority._tag === "Some") {
|
|
111
|
-
body["priority"] = priority.value;
|
|
112
|
-
}
|
|
113
|
-
if (title._tag === "Some") {
|
|
114
|
-
body["name"] = title.value;
|
|
115
|
-
}
|
|
116
|
-
if (description._tag === "Some") {
|
|
117
|
-
const escaped = escapeHtmlText(description.value);
|
|
118
|
-
body["description_html"] = `<p>${escaped}</p>`;
|
|
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
|
-
}
|
|
133
|
-
|
|
134
|
-
if (Object.keys(body).length === 0) {
|
|
135
|
-
yield* Effect.fail(
|
|
136
|
-
new Error(
|
|
137
|
-
"Nothing to update. Specify --state, --priority, --title, --description, --assignee, --label, --estimate, or --no-assignee",
|
|
138
|
-
),
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const raw = yield* api.patch(
|
|
143
|
-
`projects/${projectId}/issues/${issue.id}/`,
|
|
144
|
-
body,
|
|
145
|
-
);
|
|
146
|
-
const updated = yield* decodeOrFail(IssueSchema, raw);
|
|
147
|
-
yield* Console.log(
|
|
148
|
-
`Updated ${ref}: state=${String(updated.state)} priority=${updated.priority}`,
|
|
149
|
-
);
|
|
150
|
-
}),
|
|
180
|
+
issueUpdateHandler,
|
|
151
181
|
).pipe(
|
|
152
182
|
Command.withDescription(
|
|
153
183
|
'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',
|
|
154
184
|
),
|
|
155
185
|
);
|
|
156
|
-
|
|
157
186
|
// --- issue comment ---
|
|
158
|
-
|
|
159
187
|
const textArg = Args.text({ name: "text" }).pipe(
|
|
160
188
|
Args.withDescription("Comment text to add"),
|
|
161
189
|
);
|
|
162
190
|
|
|
191
|
+
export function issueCommentHandler({
|
|
192
|
+
ref,
|
|
193
|
+
text,
|
|
194
|
+
}: {
|
|
195
|
+
ref: string;
|
|
196
|
+
text: string;
|
|
197
|
+
}) {
|
|
198
|
+
return Effect.gen(function* () {
|
|
199
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
200
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
201
|
+
const escaped = escapeHtmlText(text);
|
|
202
|
+
yield* api.post(`projects/${projectId}/issues/${issue.id}/comments/`, {
|
|
203
|
+
comment_html: `<p>${escaped}</p>`,
|
|
204
|
+
});
|
|
205
|
+
yield* Console.log(`Comment added to ${ref}`);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
163
209
|
export const issueComment = Command.make(
|
|
164
210
|
"comment",
|
|
165
211
|
{ ref: refArg, text: textArg },
|
|
166
|
-
|
|
167
|
-
Effect.gen(function* () {
|
|
168
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
169
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
170
|
-
const escaped = escapeHtmlText(text);
|
|
171
|
-
yield* api.post(`projects/${projectId}/issues/${issue.id}/comments/`, {
|
|
172
|
-
comment_html: `<p>${escaped}</p>`,
|
|
173
|
-
});
|
|
174
|
-
yield* Console.log(`Comment added to ${ref}`);
|
|
175
|
-
}),
|
|
212
|
+
issueCommentHandler,
|
|
176
213
|
).pipe(
|
|
177
214
|
Command.withDescription(
|
|
178
215
|
'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"',
|
|
179
216
|
),
|
|
180
217
|
);
|
|
181
|
-
|
|
182
218
|
// --- issue create ---
|
|
183
|
-
|
|
184
219
|
const titleArg = Args.text({ name: "title" }).pipe(
|
|
185
220
|
Args.withDescription("Issue title"),
|
|
186
221
|
);
|
|
@@ -210,6 +245,49 @@ const createLabelOption = Options.optional(Options.text("label")).pipe(
|
|
|
210
245
|
Options.withDescription("Set issue label by name"),
|
|
211
246
|
);
|
|
212
247
|
|
|
248
|
+
export function issueCreateHandler({
|
|
249
|
+
project,
|
|
250
|
+
title,
|
|
251
|
+
priority,
|
|
252
|
+
state,
|
|
253
|
+
description,
|
|
254
|
+
assignee,
|
|
255
|
+
label,
|
|
256
|
+
}: {
|
|
257
|
+
project: string;
|
|
258
|
+
title: string;
|
|
259
|
+
priority: Option.Option<string>;
|
|
260
|
+
state: Option.Option<string>;
|
|
261
|
+
description: Option.Option<string>;
|
|
262
|
+
assignee: Option.Option<string>;
|
|
263
|
+
label: Option.Option<string>;
|
|
264
|
+
}) {
|
|
265
|
+
return Effect.gen(function* () {
|
|
266
|
+
const { key, id: projectId } = yield* resolveProject(project);
|
|
267
|
+
const body: IssueCreatePayload = { name: title };
|
|
268
|
+
if (priority._tag === "Some") body.priority = priority.value;
|
|
269
|
+
if (state._tag === "Some")
|
|
270
|
+
body.state = yield* getStateId(projectId, state.value);
|
|
271
|
+
if (description._tag === "Some") {
|
|
272
|
+
const escaped = escapeHtmlText(description.value);
|
|
273
|
+
body.description_html = `<p>${escaped}</p>`;
|
|
274
|
+
}
|
|
275
|
+
if (assignee._tag === "Some") {
|
|
276
|
+
const memberId = yield* getMemberId(assignee.value);
|
|
277
|
+
body.assignees = [memberId];
|
|
278
|
+
}
|
|
279
|
+
if (label._tag === "Some") {
|
|
280
|
+
const labelId = yield* getLabelId(projectId, label.value);
|
|
281
|
+
body.label_ids = [labelId];
|
|
282
|
+
}
|
|
283
|
+
const raw = yield* api.post(`projects/${projectId}/issues/`, body);
|
|
284
|
+
const created = yield* decodeOrFail(IssueSchema, raw);
|
|
285
|
+
yield* Console.log(
|
|
286
|
+
`Created ${key}-${created.sequence_id}: ${created.name}`,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
213
291
|
export const issueCreate = Command.make(
|
|
214
292
|
"create",
|
|
215
293
|
{
|
|
@@ -218,100 +296,62 @@ export const issueCreate = Command.make(
|
|
|
218
296
|
description: createDescriptionOption,
|
|
219
297
|
assignee: createAssigneeOption,
|
|
220
298
|
label: createLabelOption,
|
|
221
|
-
estimate: estimateOption,
|
|
222
299
|
project: projectRefArg,
|
|
223
300
|
title: titleArg,
|
|
224
301
|
},
|
|
225
|
-
|
|
226
|
-
project,
|
|
227
|
-
title,
|
|
228
|
-
priority,
|
|
229
|
-
state,
|
|
230
|
-
description,
|
|
231
|
-
assignee,
|
|
232
|
-
label,
|
|
233
|
-
estimate,
|
|
234
|
-
}) =>
|
|
235
|
-
Effect.gen(function* () {
|
|
236
|
-
const { key, id: projectId } = yield* resolveProject(project);
|
|
237
|
-
const body: Record<string, unknown> = { name: title };
|
|
238
|
-
if (priority._tag === "Some") body["priority"] = priority.value;
|
|
239
|
-
if (state._tag === "Some")
|
|
240
|
-
body["state"] = yield* getStateId(projectId, state.value);
|
|
241
|
-
if (description._tag === "Some") {
|
|
242
|
-
const escaped = escapeHtmlText(description.value);
|
|
243
|
-
body["description_html"] = `<p>${escaped}</p>`;
|
|
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
|
-
}
|
|
256
|
-
const raw = yield* api.post(`projects/${projectId}/issues/`, body);
|
|
257
|
-
const created = yield* decodeOrFail(IssueSchema, raw);
|
|
258
|
-
yield* Console.log(
|
|
259
|
-
`Created ${key}-${created.sequence_id}: ${created.name}`,
|
|
260
|
-
);
|
|
261
|
-
}),
|
|
302
|
+
issueCreateHandler,
|
|
262
303
|
).pipe(
|
|
263
304
|
Command.withDescription(
|
|
264
305
|
'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"',
|
|
265
306
|
),
|
|
266
307
|
);
|
|
267
|
-
|
|
268
308
|
// --- issue activity ---
|
|
309
|
+
export function issueActivityHandler({ ref }: { ref: string }) {
|
|
310
|
+
return Effect.gen(function* () {
|
|
311
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
312
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
313
|
+
const raw = yield* api.get(
|
|
314
|
+
`projects/${projectId}/issues/${issue.id}/activities/`,
|
|
315
|
+
);
|
|
316
|
+
const { results } = yield* decodeOrFail(ActivitiesResponseSchema, raw);
|
|
317
|
+
if (jsonMode) {
|
|
318
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (xmlMode) {
|
|
322
|
+
yield* Console.log(toXml(results));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (results.length === 0) {
|
|
326
|
+
yield* Console.log("No activity found");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const lines = results.map((a) => {
|
|
330
|
+
const who = a.actor_detail?.display_name ?? "?";
|
|
331
|
+
const when = a.created_at.slice(0, 16).replace("T", " ");
|
|
332
|
+
if (a.field) {
|
|
333
|
+
const from = a.old_value ?? "—";
|
|
334
|
+
const to = a.new_value ?? "—";
|
|
335
|
+
return `${when} ${who} ${a.field}: ${from} → ${to}`;
|
|
336
|
+
}
|
|
337
|
+
return `${when} ${who} ${a.verb ?? "updated"}`;
|
|
338
|
+
});
|
|
339
|
+
yield* Console.log(lines.join("\n"));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
269
342
|
|
|
270
343
|
export const issueActivity = Command.make(
|
|
271
344
|
"activity",
|
|
272
345
|
{ ref: refArg },
|
|
273
|
-
|
|
274
|
-
Effect.gen(function* () {
|
|
275
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
276
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
277
|
-
const raw = yield* api.get(
|
|
278
|
-
`projects/${projectId}/issues/${issue.id}/activities/`,
|
|
279
|
-
);
|
|
280
|
-
const { results } = yield* decodeOrFail(ActivitiesResponseSchema, raw);
|
|
281
|
-
if (jsonMode) {
|
|
282
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (xmlMode) {
|
|
286
|
-
yield* Console.log(toXml(results));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (results.length === 0) {
|
|
290
|
-
yield* Console.log("No activity found");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
const lines = results.map((a) => {
|
|
294
|
-
const who = a.actor_detail?.display_name ?? "?";
|
|
295
|
-
const when = a.created_at.slice(0, 16).replace("T", " ");
|
|
296
|
-
if (a.field) {
|
|
297
|
-
const from = a.old_value ?? "—";
|
|
298
|
-
const to = a.new_value ?? "—";
|
|
299
|
-
return `${when} ${who} ${a.field}: ${from} → ${to}`;
|
|
300
|
-
}
|
|
301
|
-
return `${when} ${who} ${a.verb ?? "updated"}`;
|
|
302
|
-
});
|
|
303
|
-
yield* Console.log(lines.join("\n"));
|
|
304
|
-
}),
|
|
346
|
+
issueActivityHandler,
|
|
305
347
|
).pipe(
|
|
306
348
|
Command.withDescription(
|
|
307
349
|
"Show audit trail for an issue — who changed what and when.\n\nExample:\n plane issue activity PROJ-29",
|
|
308
350
|
),
|
|
309
351
|
);
|
|
310
|
-
|
|
311
352
|
// --- issue link list ---
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
Effect.gen(function* () {
|
|
353
|
+
export function issueLinkListHandler({ ref }: { ref: string }) {
|
|
354
|
+
return Effect.gen(function* () {
|
|
315
355
|
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
316
356
|
const issue = yield* findIssueBySeq(projectId, seq);
|
|
317
357
|
const raw = yield* api.get(
|
|
@@ -334,11 +374,15 @@ export const issueLinkList = Command.make("list", { ref: refArg }, ({ ref }) =>
|
|
|
334
374
|
(l) => `${l.id} ${l.title ?? "(no title)"} ${l.url}`,
|
|
335
375
|
);
|
|
336
376
|
yield* Console.log(lines.join("\n"));
|
|
337
|
-
})
|
|
338
|
-
|
|
377
|
+
});
|
|
378
|
+
}
|
|
339
379
|
|
|
380
|
+
export const issueLinkList = Command.make(
|
|
381
|
+
"list",
|
|
382
|
+
{ ref: refArg },
|
|
383
|
+
issueLinkListHandler,
|
|
384
|
+
).pipe(Command.withDescription("List URL links attached to an issue."));
|
|
340
385
|
// --- issue link add ---
|
|
341
|
-
|
|
342
386
|
const urlArg = Args.text({ name: "url" }).pipe(
|
|
343
387
|
Args.withDescription("URL to link"),
|
|
344
388
|
);
|
|
@@ -346,140 +390,171 @@ const linkTitleOption = Options.optional(Options.text("title")).pipe(
|
|
|
346
390
|
Options.withDescription("Human-readable title for the link"),
|
|
347
391
|
);
|
|
348
392
|
|
|
393
|
+
export function issueLinkAddHandler({
|
|
394
|
+
ref,
|
|
395
|
+
url,
|
|
396
|
+
title,
|
|
397
|
+
}: {
|
|
398
|
+
ref: string;
|
|
399
|
+
url: string;
|
|
400
|
+
title: Option.Option<string>;
|
|
401
|
+
}) {
|
|
402
|
+
return Effect.gen(function* () {
|
|
403
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
404
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
405
|
+
const body: Record<string, string> = { url };
|
|
406
|
+
if (title._tag === "Some") body["title"] = title.value;
|
|
407
|
+
const raw = yield* api.post(
|
|
408
|
+
`projects/${projectId}/issues/${issue.id}/issue-links/`,
|
|
409
|
+
body,
|
|
410
|
+
);
|
|
411
|
+
const link = yield* decodeOrFail(IssueLinkSchema, raw);
|
|
412
|
+
yield* Console.log(`Link added: ${link.id} ${link.url}`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
349
416
|
export const issueLinkAdd = Command.make(
|
|
350
417
|
"add",
|
|
351
418
|
{ title: linkTitleOption, ref: refArg, url: urlArg },
|
|
352
|
-
|
|
353
|
-
Effect.gen(function* () {
|
|
354
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
355
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
356
|
-
const body: Record<string, string> = { url };
|
|
357
|
-
if (title._tag === "Some") body["title"] = title.value;
|
|
358
|
-
const raw = yield* api.post(
|
|
359
|
-
`projects/${projectId}/issues/${issue.id}/issue-links/`,
|
|
360
|
-
body,
|
|
361
|
-
);
|
|
362
|
-
const link = yield* decodeOrFail(IssueLinkSchema, raw);
|
|
363
|
-
yield* Console.log(`Link added: ${link.id} ${link.url}`);
|
|
364
|
-
}),
|
|
419
|
+
issueLinkAddHandler,
|
|
365
420
|
).pipe(
|
|
366
421
|
Command.withDescription(
|
|
367
422
|
'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',
|
|
368
423
|
),
|
|
369
424
|
);
|
|
370
|
-
|
|
371
425
|
// --- issue link remove ---
|
|
372
|
-
|
|
373
426
|
const linkIdArg = Args.text({ name: "link-id" }).pipe(
|
|
374
427
|
Args.withDescription("Link ID (from 'plane issue link list')"),
|
|
375
428
|
);
|
|
376
429
|
|
|
430
|
+
export function issueLinkRemoveHandler({
|
|
431
|
+
ref,
|
|
432
|
+
linkId,
|
|
433
|
+
}: {
|
|
434
|
+
ref: string;
|
|
435
|
+
linkId: string;
|
|
436
|
+
}) {
|
|
437
|
+
return Effect.gen(function* () {
|
|
438
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
439
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
440
|
+
yield* api.delete(
|
|
441
|
+
`projects/${projectId}/issues/${issue.id}/issue-links/${linkId}/`,
|
|
442
|
+
);
|
|
443
|
+
yield* Console.log(`Link ${linkId} removed from ${ref}`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
377
447
|
export const issueLinkRemove = Command.make(
|
|
378
448
|
"remove",
|
|
379
449
|
{ ref: refArg, linkId: linkIdArg },
|
|
380
|
-
|
|
381
|
-
Effect.gen(function* () {
|
|
382
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
383
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
384
|
-
yield* api.delete(
|
|
385
|
-
`projects/${projectId}/issues/${issue.id}/issue-links/${linkId}/`,
|
|
386
|
-
);
|
|
387
|
-
yield* Console.log(`Link ${linkId} removed from ${ref}`);
|
|
388
|
-
}),
|
|
450
|
+
issueLinkRemoveHandler,
|
|
389
451
|
).pipe(Command.withDescription("Remove a URL link from an issue by link ID."));
|
|
390
|
-
|
|
391
452
|
// --- issue link (parent) ---
|
|
392
|
-
|
|
393
453
|
export const issueLink = Command.make("link").pipe(
|
|
394
454
|
Command.withDescription(
|
|
395
455
|
"Manage URL links on an issue. Subcommands: list, add, remove",
|
|
396
456
|
),
|
|
397
457
|
Command.withSubcommands([issueLinkList, issueLinkAdd, issueLinkRemove]),
|
|
398
458
|
);
|
|
399
|
-
|
|
400
459
|
// --- issue comments list ---
|
|
460
|
+
export function issueCommentsListHandler({ ref }: { ref: string }) {
|
|
461
|
+
return Effect.gen(function* () {
|
|
462
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
463
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
464
|
+
const raw = yield* api.get(
|
|
465
|
+
`projects/${projectId}/issues/${issue.id}/comments/`,
|
|
466
|
+
);
|
|
467
|
+
const { results } = yield* decodeOrFail(CommentsResponseSchema, raw);
|
|
468
|
+
if (jsonMode) {
|
|
469
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (xmlMode) {
|
|
473
|
+
yield* Console.log(toXml(results));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (results.length === 0) {
|
|
477
|
+
yield* Console.log("No comments");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const lines = results.map((c) => {
|
|
481
|
+
const who = c.actor_detail?.display_name ?? "?";
|
|
482
|
+
const when = c.created_at.slice(0, 16).replace("T", " ");
|
|
483
|
+
const text = (c.comment_html ?? "").replace(/<[^>]+>/g, "").trim();
|
|
484
|
+
return `${c.id} ${when} ${who}: ${text}`;
|
|
485
|
+
});
|
|
486
|
+
yield* Console.log(lines.join("\n"));
|
|
487
|
+
});
|
|
488
|
+
}
|
|
401
489
|
|
|
402
490
|
export const issueCommentsList = Command.make(
|
|
403
491
|
"list",
|
|
404
492
|
{ ref: refArg },
|
|
405
|
-
|
|
406
|
-
Effect.gen(function* () {
|
|
407
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
408
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
409
|
-
const raw = yield* api.get(
|
|
410
|
-
`projects/${projectId}/issues/${issue.id}/comments/`,
|
|
411
|
-
);
|
|
412
|
-
const { results } = yield* decodeOrFail(CommentsResponseSchema, raw);
|
|
413
|
-
if (jsonMode) {
|
|
414
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (xmlMode) {
|
|
418
|
-
yield* Console.log(toXml(results));
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (results.length === 0) {
|
|
422
|
-
yield* Console.log("No comments");
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const lines = results.map((c) => {
|
|
426
|
-
const who = c.actor_detail?.display_name ?? "?";
|
|
427
|
-
const when = c.created_at.slice(0, 16).replace("T", " ");
|
|
428
|
-
const text = (c.comment_html ?? "").replace(/<[^>]+>/g, "").trim();
|
|
429
|
-
return `${c.id} ${when} ${who}: ${text}`;
|
|
430
|
-
});
|
|
431
|
-
yield* Console.log(lines.join("\n"));
|
|
432
|
-
}),
|
|
493
|
+
issueCommentsListHandler,
|
|
433
494
|
).pipe(
|
|
434
495
|
Command.withDescription(
|
|
435
496
|
"List comments on an issue. Shows comment ID, timestamp, author, and plain text.\n\nExample:\n plane issue comments list PROJ-29",
|
|
436
497
|
),
|
|
437
498
|
);
|
|
438
|
-
|
|
439
499
|
// --- issue comment update ---
|
|
440
|
-
|
|
441
500
|
const commentIdArg = Args.text({ name: "comment-id" }).pipe(
|
|
442
501
|
Args.withDescription("Comment ID (from 'plane issue comments list')"),
|
|
443
502
|
);
|
|
444
503
|
|
|
504
|
+
export function issueCommentUpdateHandler({
|
|
505
|
+
ref,
|
|
506
|
+
commentId,
|
|
507
|
+
text,
|
|
508
|
+
}: {
|
|
509
|
+
ref: string;
|
|
510
|
+
commentId: string;
|
|
511
|
+
text: string;
|
|
512
|
+
}) {
|
|
513
|
+
return Effect.gen(function* () {
|
|
514
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
515
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
516
|
+
const escaped = escapeHtmlText(text);
|
|
517
|
+
yield* api.patch(
|
|
518
|
+
`projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
|
|
519
|
+
{ comment_html: `<p>${escaped}</p>` },
|
|
520
|
+
);
|
|
521
|
+
yield* Console.log(`Comment ${commentId} updated`);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
445
525
|
export const issueCommentUpdate = Command.make(
|
|
446
526
|
"update",
|
|
447
527
|
{ ref: refArg, commentId: commentIdArg, text: textArg },
|
|
448
|
-
|
|
449
|
-
Effect.gen(function* () {
|
|
450
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
451
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
452
|
-
const escaped = escapeHtmlText(text);
|
|
453
|
-
yield* api.patch(
|
|
454
|
-
`projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
|
|
455
|
-
{ comment_html: `<p>${escaped}</p>` },
|
|
456
|
-
);
|
|
457
|
-
yield* Console.log(`Comment ${commentId} updated`);
|
|
458
|
-
}),
|
|
528
|
+
issueCommentUpdateHandler,
|
|
459
529
|
).pipe(
|
|
460
530
|
Command.withDescription(
|
|
461
531
|
'Edit an existing comment.\n\nExample:\n plane issue comments update PROJ-29 <comment-id> "Updated text"',
|
|
462
532
|
),
|
|
463
533
|
);
|
|
464
|
-
|
|
465
534
|
// --- issue comment delete ---
|
|
535
|
+
export function issueCommentDeleteHandler({
|
|
536
|
+
ref,
|
|
537
|
+
commentId,
|
|
538
|
+
}: {
|
|
539
|
+
ref: string;
|
|
540
|
+
commentId: string;
|
|
541
|
+
}) {
|
|
542
|
+
return Effect.gen(function* () {
|
|
543
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
544
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
545
|
+
yield* api.delete(
|
|
546
|
+
`projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
|
|
547
|
+
);
|
|
548
|
+
yield* Console.log(`Comment ${commentId} deleted`);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
466
551
|
|
|
467
552
|
export const issueCommentDelete = Command.make(
|
|
468
553
|
"delete",
|
|
469
554
|
{ ref: refArg, commentId: commentIdArg },
|
|
470
|
-
|
|
471
|
-
Effect.gen(function* () {
|
|
472
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
473
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
474
|
-
yield* api.delete(
|
|
475
|
-
`projects/${projectId}/issues/${issue.id}/comments/${commentId}/`,
|
|
476
|
-
);
|
|
477
|
-
yield* Console.log(`Comment ${commentId} deleted`);
|
|
478
|
-
}),
|
|
555
|
+
issueCommentDeleteHandler,
|
|
479
556
|
).pipe(Command.withDescription("Delete a comment from an issue."));
|
|
480
|
-
|
|
481
557
|
// --- issue comments (parent) ---
|
|
482
|
-
|
|
483
558
|
export const issueComments = Command.make("comments").pipe(
|
|
484
559
|
Command.withDescription(
|
|
485
560
|
"Manage comments on an issue. Subcommands: list, update, delete\n\nNote: use 'plane issue comment REF TEXT' to add a new comment.",
|
|
@@ -490,49 +565,48 @@ export const issueComments = Command.make("comments").pipe(
|
|
|
490
565
|
issueCommentDelete,
|
|
491
566
|
]),
|
|
492
567
|
);
|
|
493
|
-
|
|
494
568
|
// --- issue worklogs list ---
|
|
569
|
+
export function issueWorklogsListHandler({ ref }: { ref: string }) {
|
|
570
|
+
return Effect.gen(function* () {
|
|
571
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
572
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
573
|
+
const raw = yield* api.get(
|
|
574
|
+
`projects/${projectId}/issues/${issue.id}/worklogs/`,
|
|
575
|
+
);
|
|
576
|
+
const { results } = yield* decodeOrFail(WorklogsResponseSchema, raw);
|
|
577
|
+
if (jsonMode) {
|
|
578
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (xmlMode) {
|
|
582
|
+
yield* Console.log(toXml(results));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (results.length === 0) {
|
|
586
|
+
yield* Console.log("No worklogs");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const lines = results.map((w) => {
|
|
590
|
+
const who = w.logged_by_detail?.display_name ?? "?";
|
|
591
|
+
const when = w.created_at.slice(0, 10);
|
|
592
|
+
const hrs = (w.duration / 60).toFixed(1);
|
|
593
|
+
const desc = w.description ?? "";
|
|
594
|
+
return `${w.id} ${when} ${who} ${hrs}h ${desc}`;
|
|
595
|
+
});
|
|
596
|
+
yield* Console.log(lines.join("\n"));
|
|
597
|
+
});
|
|
598
|
+
}
|
|
495
599
|
|
|
496
600
|
export const issueWorklogsList = Command.make(
|
|
497
601
|
"list",
|
|
498
602
|
{ ref: refArg },
|
|
499
|
-
|
|
500
|
-
Effect.gen(function* () {
|
|
501
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
502
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
503
|
-
const raw = yield* api.get(
|
|
504
|
-
`projects/${projectId}/issues/${issue.id}/worklogs/`,
|
|
505
|
-
);
|
|
506
|
-
const { results } = yield* decodeOrFail(WorklogsResponseSchema, raw);
|
|
507
|
-
if (jsonMode) {
|
|
508
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
if (xmlMode) {
|
|
512
|
-
yield* Console.log(toXml(results));
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (results.length === 0) {
|
|
516
|
-
yield* Console.log("No worklogs");
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const lines = results.map((w) => {
|
|
520
|
-
const who = w.logged_by_detail?.display_name ?? "?";
|
|
521
|
-
const when = w.created_at.slice(0, 10);
|
|
522
|
-
const hrs = (w.duration / 60).toFixed(1);
|
|
523
|
-
const desc = w.description ?? "";
|
|
524
|
-
return `${w.id} ${when} ${who} ${hrs}h ${desc}`;
|
|
525
|
-
});
|
|
526
|
-
yield* Console.log(lines.join("\n"));
|
|
527
|
-
}),
|
|
603
|
+
issueWorklogsListHandler,
|
|
528
604
|
).pipe(
|
|
529
605
|
Command.withDescription(
|
|
530
606
|
"List time log entries for an issue. Duration shown in hours.\n\nExample:\n plane issue worklogs list PROJ-29",
|
|
531
607
|
),
|
|
532
608
|
);
|
|
533
|
-
|
|
534
609
|
// --- issue worklogs add ---
|
|
535
|
-
|
|
536
610
|
const durationArg = Args.integer({ name: "minutes" }).pipe(
|
|
537
611
|
Args.withDescription("Time spent in minutes"),
|
|
538
612
|
);
|
|
@@ -540,55 +614,66 @@ const worklogDescOption = Options.optional(Options.text("description")).pipe(
|
|
|
540
614
|
Options.withDescription("Optional description of work done"),
|
|
541
615
|
);
|
|
542
616
|
|
|
617
|
+
export function issueWorklogsAddHandler({
|
|
618
|
+
ref,
|
|
619
|
+
duration,
|
|
620
|
+
description,
|
|
621
|
+
}: {
|
|
622
|
+
ref: string;
|
|
623
|
+
duration: number;
|
|
624
|
+
description: Option.Option<string>;
|
|
625
|
+
}) {
|
|
626
|
+
return Effect.gen(function* () {
|
|
627
|
+
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
628
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
629
|
+
const body: WorklogPayload = { duration };
|
|
630
|
+
if (description._tag === "Some") body.description = description.value;
|
|
631
|
+
const raw = yield* api.post(
|
|
632
|
+
`projects/${projectId}/issues/${issue.id}/worklogs/`,
|
|
633
|
+
body,
|
|
634
|
+
);
|
|
635
|
+
const log = yield* decodeOrFail(WorklogSchema, raw);
|
|
636
|
+
const hrs = (log.duration / 60).toFixed(1);
|
|
637
|
+
yield* Console.log(`Logged ${hrs}h on ${ref} (${log.id})`);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
543
641
|
export const issueWorklogsAdd = Command.make(
|
|
544
642
|
"add",
|
|
545
643
|
{ description: worklogDescOption, ref: refArg, duration: durationArg },
|
|
546
|
-
|
|
547
|
-
Effect.gen(function* () {
|
|
548
|
-
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
549
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
550
|
-
const body: Record<string, unknown> = { duration };
|
|
551
|
-
if (description._tag === "Some") body["description"] = description.value;
|
|
552
|
-
const raw = yield* api.post(
|
|
553
|
-
`projects/${projectId}/issues/${issue.id}/worklogs/`,
|
|
554
|
-
body,
|
|
555
|
-
);
|
|
556
|
-
const log = yield* decodeOrFail(WorklogSchema, raw);
|
|
557
|
-
const hrs = (log.duration / 60).toFixed(1);
|
|
558
|
-
yield* Console.log(`Logged ${hrs}h on ${ref} (${log.id})`);
|
|
559
|
-
}),
|
|
644
|
+
issueWorklogsAddHandler,
|
|
560
645
|
).pipe(
|
|
561
646
|
Command.withDescription(
|
|
562
647
|
'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',
|
|
563
648
|
),
|
|
564
649
|
);
|
|
565
|
-
|
|
566
650
|
// --- issue worklogs (parent) ---
|
|
567
|
-
|
|
568
651
|
export const issueWorklogs = Command.make("worklogs").pipe(
|
|
569
652
|
Command.withDescription(
|
|
570
653
|
"Manage time logs for an issue. Subcommands: list, add",
|
|
571
654
|
),
|
|
572
655
|
Command.withSubcommands([issueWorklogsList, issueWorklogsAdd]),
|
|
573
656
|
);
|
|
574
|
-
|
|
575
657
|
// --- issue delete ---
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
Effect.gen(function* () {
|
|
658
|
+
export function issueDeleteHandler({ ref }: { ref: string }) {
|
|
659
|
+
return Effect.gen(function* () {
|
|
579
660
|
const { projectId, seq } = yield* parseIssueRef(ref);
|
|
580
661
|
const issue = yield* findIssueBySeq(projectId, seq);
|
|
581
662
|
yield* api.delete(`projects/${projectId}/issues/${issue.id}/`);
|
|
582
663
|
yield* Console.log(`Deleted ${ref}`);
|
|
583
|
-
})
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export const issueDelete = Command.make(
|
|
668
|
+
"delete",
|
|
669
|
+
{ ref: refArg },
|
|
670
|
+
issueDeleteHandler,
|
|
584
671
|
).pipe(
|
|
585
672
|
Command.withDescription(
|
|
586
673
|
"Permanently delete an issue. This cannot be undone.",
|
|
587
674
|
),
|
|
588
675
|
);
|
|
589
|
-
|
|
590
676
|
// --- issue (parent) ---
|
|
591
|
-
|
|
592
677
|
export const issue = Command.make("issue").pipe(
|
|
593
678
|
Command.withDescription(
|
|
594
679
|
"Manage individual issues. Use 'plane issue <subcommand> --help' for details.\n\nSubcommands: get, create, update, delete, comment, activity, link, comments, worklogs",
|