@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.
- package/package.json +1 -1
- package/scripts/check-coverage.ts +2 -2
- package/src/api.ts +67 -59
- package/src/app.ts +78 -0
- package/src/bin.ts +6 -71
- package/src/commands/cycles.ts +104 -85
- package/src/commands/init.ts +57 -55
- package/src/commands/intake.ts +82 -65
- package/src/commands/issue.ts +418 -314
- package/src/commands/issues.ts +51 -43
- package/src/commands/labels.ts +52 -43
- package/src/commands/members.ts +25 -19
- package/src/commands/modules.ts +136 -99
- package/src/commands/pages.ts +58 -49
- package/src/commands/projects.ts +28 -22
- package/src/commands/states.ts +31 -25
- package/src/config.ts +152 -154
- package/src/format.ts +15 -8
- package/src/output.ts +28 -28
- package/src/resolve.ts +66 -53
- package/tests/api.test.ts +178 -155
- package/tests/cycles-extended.test.ts +205 -162
- package/tests/format.test.ts +72 -54
- package/tests/helpers/mock-api.ts +16 -14
- package/tests/intake.test.ts +173 -139
- package/tests/issue-activity.test.ts +191 -158
- package/tests/issue-commands.test.ts +587 -304
- package/tests/issue-comments-worklogs.test.ts +337 -265
- package/tests/issue-links.test.ts +229 -193
- package/tests/modules.test.ts +283 -239
- package/tests/new-schemas.test.ts +203 -183
- package/tests/new-schemas2.test.ts +195 -183
- package/tests/output.test.ts +66 -64
- package/tests/pages.test.ts +122 -108
- package/tests/resolve.test.ts +186 -156
- package/tests/schemas.test.ts +215 -177
package/src/commands/issue.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "../config.js"
|
|
13
|
-
import {
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
)
|
|
43
|
+
Options.withDescription("State group or name (e.g. backlog, completed)"),
|
|
44
|
+
);
|
|
39
45
|
|
|
40
46
|
const priorityOption = Options.optional(
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
)
|
|
106
|
+
Args.withDescription("Comment text to add"),
|
|
107
|
+
);
|
|
82
108
|
|
|
83
109
|
export const issueComment = Command.make(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
)
|
|
131
|
+
Args.withDescription("Issue title"),
|
|
132
|
+
);
|
|
107
133
|
const projectRefArg = Args.text({ name: "project" }).pipe(
|
|
108
|
-
|
|
109
|
-
)
|
|
134
|
+
Args.withDescription("Project identifier (e.g. PROJ)"),
|
|
135
|
+
);
|
|
110
136
|
|
|
111
137
|
const createPriorityOption = Options.optional(
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(
|
|
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
|
-
|
|
194
|
-
)
|
|
261
|
+
Options.withDescription("Human-readable title for the link"),
|
|
262
|
+
);
|
|
195
263
|
|
|
196
264
|
export const issueLinkAdd = Command.make(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
222
|
-
)
|
|
289
|
+
Args.withDescription("Link ID (from 'plane issue link list')"),
|
|
290
|
+
);
|
|
223
291
|
|
|
224
292
|
export const issueLinkRemove = Command.make(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
277
|
-
)
|
|
357
|
+
Args.withDescription("Comment ID (from 'plane issue comments list')"),
|
|
358
|
+
);
|
|
278
359
|
|
|
279
360
|
export const issueCommentUpdate = Command.make(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
355
|
-
)
|
|
452
|
+
Args.withDescription("Time spent in minutes"),
|
|
453
|
+
);
|
|
356
454
|
const worklogDescOption = Options.optional(Options.text("description")).pipe(
|
|
357
|
-
|
|
358
|
-
)
|
|
455
|
+
Options.withDescription("Optional description of work done"),
|
|
456
|
+
);
|
|
359
457
|
|
|
360
458
|
export const issueWorklogsAdd = Command.make(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
).pipe(
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
);
|