@ysicing/plane-cli 0.1.0 → 1.0.1
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/README.md +139 -9
- package/package.json +1 -1
- package/src/api/issue-client.js +73 -0
- package/src/api/project-client.js +16 -0
- package/src/cli.js +67 -8
- package/src/commands/auth.js +2 -2
- package/src/commands/config.js +14 -4
- package/src/commands/issue.js +797 -7
- package/src/commands/me.js +1 -1
- package/src/commands/project.js +289 -2
- package/src/commands/workspace.js +2 -2
- package/src/core/config.js +3 -0
- package/src/core/output.js +48 -2
package/src/commands/me.js
CHANGED
|
@@ -5,7 +5,7 @@ import { PlaneClient } from "../core/http.js";
|
|
|
5
5
|
import { printData } from "../core/output.js";
|
|
6
6
|
|
|
7
7
|
export async function runMeCommand(args, context) {
|
|
8
|
-
if (args.includes("--help") || args.includes("help")) {
|
|
8
|
+
if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
|
|
9
9
|
console.log("Usage:\n plane me");
|
|
10
10
|
return;
|
|
11
11
|
}
|
package/src/commands/project.js
CHANGED
|
@@ -5,6 +5,10 @@ import { PlaneClient } from "../core/http.js";
|
|
|
5
5
|
import { ensureValue, parseCommandArgs, pickDefined } from "../core/options.js";
|
|
6
6
|
import { printData, printTable } from "../core/output.js";
|
|
7
7
|
|
|
8
|
+
function hasHelpFlag(args) {
|
|
9
|
+
return args.includes("--help") || args.includes("-h") || args.includes("help");
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
function createProjectRender(data) {
|
|
9
13
|
const rows = Array.isArray(data) ? data : data.results || [];
|
|
10
14
|
printTable(rows, [
|
|
@@ -16,6 +20,23 @@ function createProjectRender(data) {
|
|
|
16
20
|
]);
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
const PROJECT_FEATURE_FIELDS = {
|
|
24
|
+
"issue-types": "is_issue_type_enabled",
|
|
25
|
+
epics: "is_epic_enabled",
|
|
26
|
+
milestones: "is_milestone_enabled",
|
|
27
|
+
"time-tracking": "is_time_tracking_enabled",
|
|
28
|
+
"auto-transition": "gaeaflow_auto_transition_enabled",
|
|
29
|
+
"auto-assign": "gaeaflow_auto_assign_enabled",
|
|
30
|
+
"auto-worklog": "gaeaflow_auto_worklog_enabled",
|
|
31
|
+
"require-worklog-before-completion": "require_worklog_before_completion_enabled",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PROJECT_ROLE_MAP = {
|
|
35
|
+
admin: 20,
|
|
36
|
+
member: 15,
|
|
37
|
+
guest: 5,
|
|
38
|
+
};
|
|
39
|
+
|
|
19
40
|
export function buildProjectPayload(values) {
|
|
20
41
|
return pickDefined({
|
|
21
42
|
name: values.name,
|
|
@@ -26,19 +47,249 @@ export function buildProjectPayload(values) {
|
|
|
26
47
|
});
|
|
27
48
|
}
|
|
28
49
|
|
|
50
|
+
export function splitProjectCreatePayload(values) {
|
|
51
|
+
const fullPayload = buildProjectPayload(values);
|
|
52
|
+
const createPayload = pickDefined({
|
|
53
|
+
name: fullPayload.name,
|
|
54
|
+
identifier: fullPayload.identifier,
|
|
55
|
+
});
|
|
56
|
+
const postCreateUpdatePayload = pickDefined({
|
|
57
|
+
description: fullPayload.description,
|
|
58
|
+
project_lead: fullPayload.project_lead,
|
|
59
|
+
default_assignee: fullPayload.default_assignee,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { createPayload, postCreateUpdatePayload };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeProjectRole(value) {
|
|
66
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
67
|
+
const role = PROJECT_ROLE_MAP[normalized];
|
|
68
|
+
if (!role) {
|
|
69
|
+
throw new CliError("Role must be one of: admin, member, guest.");
|
|
70
|
+
}
|
|
71
|
+
return role;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseToggle(value, optionName) {
|
|
75
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
76
|
+
if (["on", "true", "1", "yes", "enable", "enabled"].includes(normalized)) return true;
|
|
77
|
+
if (["off", "false", "0", "no", "disable", "disabled"].includes(normalized)) return false;
|
|
78
|
+
throw new CliError(`Invalid value for ${optionName}: ${value}. Use on/off.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildProjectFeaturesPayload(values) {
|
|
82
|
+
const payload = {};
|
|
83
|
+
|
|
84
|
+
for (const [option, field] of Object.entries(PROJECT_FEATURE_FIELDS)) {
|
|
85
|
+
if (values[option] !== undefined) {
|
|
86
|
+
payload[field] = parseToggle(values[option], `--${option}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pickProjectFeatures(project) {
|
|
94
|
+
return Object.fromEntries(Object.entries(PROJECT_FEATURE_FIELDS).map(([option, field]) => [field, project[field]]));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderProjectMembers(data) {
|
|
98
|
+
const rows = Array.isArray(data) ? data : [];
|
|
99
|
+
printTable(rows, [
|
|
100
|
+
{ label: "User ID", get: (row) => row.id || "" },
|
|
101
|
+
{ label: "Email", get: (row) => row.email || "" },
|
|
102
|
+
{ label: "Name", get: (row) => `${row.first_name || ""} ${row.last_name || ""}`.trim() },
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderWorkspaceMembers(data) {
|
|
107
|
+
const rows = Array.isArray(data) ? data : [];
|
|
108
|
+
printTable(rows, [
|
|
109
|
+
{ label: "User ID", get: (row) => row.id || "" },
|
|
110
|
+
{ label: "Email", get: (row) => row.email || "" },
|
|
111
|
+
{ label: "Name", get: (row) => `${row.first_name || ""} ${row.last_name || ""}`.trim() },
|
|
112
|
+
{ label: "Role", get: (row) => row.role ?? "" },
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
|
|
29
116
|
function printHelp() {
|
|
30
117
|
console.log(`Usage:
|
|
31
118
|
plane project ls [--limit <n>] [--cursor <cursor>] [--order-by <field>]
|
|
32
119
|
plane project get <project-id>
|
|
120
|
+
plane project summary <project-id> [--fields <members,states,labels,cycles,modules,issues,intakes,pages>]
|
|
121
|
+
plane project members ls --project <project-id>
|
|
122
|
+
plane project members workspace
|
|
123
|
+
plane project members add --project <project-id> --member <user-id> --role <admin|member|guest>
|
|
124
|
+
plane project features get <project-id>
|
|
125
|
+
plane project features set <project-id> [--issue-types on|off] [--epics on|off] [--milestones on|off] [--time-tracking on|off] [--auto-transition on|off] [--auto-assign on|off] [--auto-worklog on|off] [--require-worklog-before-completion on|off]
|
|
126
|
+
plane project features enable-all <project-id>
|
|
33
127
|
plane project create --name <name> --identifier <identifier> [--description <text>] [--project-lead <user-id>] [--default-assignee <user-id>]
|
|
34
128
|
plane project update <project-id> [--name <name>] [--identifier <identifier>] [--description <text>] [--project-lead <user-id>] [--default-assignee <user-id>]
|
|
35
129
|
`);
|
|
36
130
|
}
|
|
37
131
|
|
|
132
|
+
async function runProjectMembersCommand(projectClient, args, context) {
|
|
133
|
+
const [subcommand, ...rest] = args;
|
|
134
|
+
|
|
135
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
136
|
+
printHelp();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hasHelpFlag(rest)) {
|
|
141
|
+
printHelp();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (subcommand === "ls") {
|
|
146
|
+
const parsed = parseCommandArgs(
|
|
147
|
+
rest,
|
|
148
|
+
{
|
|
149
|
+
project: { type: "string" },
|
|
150
|
+
},
|
|
151
|
+
false
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
155
|
+
const result = await projectClient.listMembers(parsed.values.project);
|
|
156
|
+
printData(result, {
|
|
157
|
+
...context.output,
|
|
158
|
+
render: renderProjectMembers,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (subcommand === "workspace") {
|
|
164
|
+
const result = await projectClient.listWorkspaceMembers();
|
|
165
|
+
printData(result, {
|
|
166
|
+
...context.output,
|
|
167
|
+
render: renderWorkspaceMembers,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (subcommand === "add") {
|
|
173
|
+
const parsed = parseCommandArgs(
|
|
174
|
+
rest,
|
|
175
|
+
{
|
|
176
|
+
project: { type: "string" },
|
|
177
|
+
member: { type: "string" },
|
|
178
|
+
role: { type: "string" },
|
|
179
|
+
},
|
|
180
|
+
false
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
184
|
+
ensureValue(parsed.values.member, "Member user ID is required.");
|
|
185
|
+
ensureValue(parsed.values.role, "Role is required.");
|
|
186
|
+
|
|
187
|
+
const result = await projectClient.addMember(parsed.values.project, {
|
|
188
|
+
member: parsed.values.member,
|
|
189
|
+
role: normalizeProjectRole(parsed.values.role),
|
|
190
|
+
});
|
|
191
|
+
printData(result, context.output);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw new CliError(`Unknown project members subcommand: ${subcommand}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runProjectFeaturesCommand(projectClient, args, context) {
|
|
199
|
+
const [subcommand, ...rest] = args;
|
|
200
|
+
|
|
201
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
202
|
+
printHelp();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (hasHelpFlag(rest)) {
|
|
207
|
+
printHelp();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (subcommand === "get") {
|
|
212
|
+
const [projectId] = rest;
|
|
213
|
+
ensureValue(projectId, "Project ID is required.");
|
|
214
|
+
const project = await projectClient.get(projectId);
|
|
215
|
+
printData(
|
|
216
|
+
{
|
|
217
|
+
id: project.id,
|
|
218
|
+
identifier: project.identifier,
|
|
219
|
+
name: project.name,
|
|
220
|
+
...pickProjectFeatures(project),
|
|
221
|
+
},
|
|
222
|
+
context.output
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (subcommand === "enable-all") {
|
|
228
|
+
const [projectId] = rest;
|
|
229
|
+
ensureValue(projectId, "Project ID is required.");
|
|
230
|
+
const payload = Object.fromEntries(Object.values(PROJECT_FEATURE_FIELDS).map((field) => [field, true]));
|
|
231
|
+
const result = await projectClient.update(projectId, payload);
|
|
232
|
+
printData(
|
|
233
|
+
{
|
|
234
|
+
id: result.id,
|
|
235
|
+
identifier: result.identifier,
|
|
236
|
+
name: result.name,
|
|
237
|
+
...pickProjectFeatures(result),
|
|
238
|
+
},
|
|
239
|
+
context.output
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (subcommand === "set") {
|
|
245
|
+
const [projectId, ...optionArgs] = rest;
|
|
246
|
+
ensureValue(projectId, "Project ID is required.");
|
|
247
|
+
|
|
248
|
+
const parsed = parseCommandArgs(
|
|
249
|
+
optionArgs,
|
|
250
|
+
{
|
|
251
|
+
"issue-types": { type: "string" },
|
|
252
|
+
epics: { type: "string" },
|
|
253
|
+
milestones: { type: "string" },
|
|
254
|
+
"time-tracking": { type: "string" },
|
|
255
|
+
"auto-transition": { type: "string" },
|
|
256
|
+
"auto-assign": { type: "string" },
|
|
257
|
+
"auto-worklog": { type: "string" },
|
|
258
|
+
"require-worklog-before-completion": { type: "string" },
|
|
259
|
+
},
|
|
260
|
+
false
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const payload = buildProjectFeaturesPayload(parsed.values);
|
|
264
|
+
if (Object.keys(payload).length === 0) {
|
|
265
|
+
throw new CliError("At least one feature flag is required.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await projectClient.update(projectId, payload);
|
|
269
|
+
printData(
|
|
270
|
+
{
|
|
271
|
+
id: result.id,
|
|
272
|
+
identifier: result.identifier,
|
|
273
|
+
name: result.name,
|
|
274
|
+
...pickProjectFeatures(result),
|
|
275
|
+
},
|
|
276
|
+
context.output
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new CliError(`Unknown project features subcommand: ${subcommand}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
38
284
|
export async function runProjectCommand(args, context) {
|
|
39
285
|
const [subcommand, ...rest] = args;
|
|
40
286
|
|
|
41
|
-
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
287
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
288
|
+
printHelp();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (hasHelpFlag(rest)) {
|
|
42
293
|
printHelp();
|
|
43
294
|
return;
|
|
44
295
|
}
|
|
@@ -46,6 +297,16 @@ export async function runProjectCommand(args, context) {
|
|
|
46
297
|
const config = await resolveRuntimeConfig();
|
|
47
298
|
const projectClient = new ProjectClient(new PlaneClient(config));
|
|
48
299
|
|
|
300
|
+
if (subcommand === "members") {
|
|
301
|
+
await runProjectMembersCommand(projectClient, rest, context);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (subcommand === "features") {
|
|
306
|
+
await runProjectFeaturesCommand(projectClient, rest, context);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
49
310
|
if (subcommand === "ls") {
|
|
50
311
|
const parsed = parseCommandArgs(
|
|
51
312
|
rest,
|
|
@@ -84,6 +345,27 @@ export async function runProjectCommand(args, context) {
|
|
|
84
345
|
return;
|
|
85
346
|
}
|
|
86
347
|
|
|
348
|
+
if (subcommand === "summary") {
|
|
349
|
+
const parsed = parseCommandArgs(
|
|
350
|
+
rest,
|
|
351
|
+
{
|
|
352
|
+
fields: { type: "string" },
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const [projectId] = parsed.positionals;
|
|
357
|
+
ensureValue(projectId, "Project ID is required.");
|
|
358
|
+
|
|
359
|
+
const result = await projectClient.summary(
|
|
360
|
+
projectId,
|
|
361
|
+
pickDefined({
|
|
362
|
+
fields: parsed.values.fields,
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
printData(result, context.output);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
87
369
|
if (subcommand === "create") {
|
|
88
370
|
const parsed = parseCommandArgs(
|
|
89
371
|
rest,
|
|
@@ -100,7 +382,12 @@ export async function runProjectCommand(args, context) {
|
|
|
100
382
|
ensureValue(parsed.values.name, "Project name is required.");
|
|
101
383
|
ensureValue(parsed.values.identifier, "Project identifier is required.");
|
|
102
384
|
|
|
103
|
-
const
|
|
385
|
+
const { createPayload, postCreateUpdatePayload } = splitProjectCreatePayload(parsed.values);
|
|
386
|
+
let result = await projectClient.create(createPayload);
|
|
387
|
+
if (Object.keys(postCreateUpdatePayload).length > 0) {
|
|
388
|
+
result = await projectClient.update(result.id, postCreateUpdatePayload);
|
|
389
|
+
}
|
|
390
|
+
|
|
104
391
|
printData(result, context.output);
|
|
105
392
|
return;
|
|
106
393
|
}
|
|
@@ -22,12 +22,12 @@ function workspaceRows(config) {
|
|
|
22
22
|
export async function runWorkspaceCommand(args, context) {
|
|
23
23
|
const [subcommand = "ls", ...rest] = args;
|
|
24
24
|
|
|
25
|
-
if (subcommand === "--help" || subcommand === "help") {
|
|
25
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
26
26
|
printHelp();
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
if (rest.includes("--help") || rest.includes("help")) {
|
|
30
|
+
if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
|
|
31
31
|
printHelp();
|
|
32
32
|
return;
|
|
33
33
|
}
|
package/src/core/config.js
CHANGED
package/src/core/output.js
CHANGED
|
@@ -4,10 +4,27 @@ function stringifyValue(value) {
|
|
|
4
4
|
return String(value);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export function printJson(data) {
|
|
8
12
|
console.log(JSON.stringify(data, null, 2));
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
export function printKeyValue(data) {
|
|
16
|
+
const entries = Object.entries(data);
|
|
17
|
+
if (!entries.length) {
|
|
18
|
+
console.log("(empty)");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const width = Math.max(...entries.map(([key]) => key.length));
|
|
23
|
+
for (const [key, value] of entries) {
|
|
24
|
+
console.log(`${key.padEnd(width, " ")} ${stringifyValue(value)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
export function printTable(rows, columns) {
|
|
12
29
|
if (!rows.length) {
|
|
13
30
|
console.log("(empty)");
|
|
@@ -37,7 +54,7 @@ export function printTable(rows, columns) {
|
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
export function printData(data, options = {}) {
|
|
40
|
-
if (options.json) {
|
|
57
|
+
if (options.format === "json") {
|
|
41
58
|
printJson(data);
|
|
42
59
|
return;
|
|
43
60
|
}
|
|
@@ -47,5 +64,34 @@ export function printData(data, options = {}) {
|
|
|
47
64
|
return;
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
if (Array.isArray(data)) {
|
|
68
|
+
if (!data.length) {
|
|
69
|
+
console.log("(empty)");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (data.every(isPlainObject)) {
|
|
74
|
+
const objectKeys = [...new Set(data.flatMap((item) => Object.keys(item)))];
|
|
75
|
+
printTable(
|
|
76
|
+
data,
|
|
77
|
+
objectKeys.map((key) => ({
|
|
78
|
+
label: key,
|
|
79
|
+
get: (row) => row[key],
|
|
80
|
+
}))
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const item of data) {
|
|
86
|
+
console.log(`- ${stringifyValue(item)}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isPlainObject(data)) {
|
|
92
|
+
printKeyValue(data);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(stringifyValue(data));
|
|
51
97
|
}
|