@ysicing/plane-cli 0.1.0 → 1.0.0

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.
@@ -16,6 +16,23 @@ function createProjectRender(data) {
16
16
  ]);
17
17
  }
18
18
 
19
+ const PROJECT_FEATURE_FIELDS = {
20
+ "issue-types": "is_issue_type_enabled",
21
+ epics: "is_epic_enabled",
22
+ milestones: "is_milestone_enabled",
23
+ "time-tracking": "is_time_tracking_enabled",
24
+ "auto-transition": "gaeaflow_auto_transition_enabled",
25
+ "auto-assign": "gaeaflow_auto_assign_enabled",
26
+ "auto-worklog": "gaeaflow_auto_worklog_enabled",
27
+ "require-worklog-before-completion": "require_worklog_before_completion_enabled",
28
+ };
29
+
30
+ const PROJECT_ROLE_MAP = {
31
+ admin: 20,
32
+ member: 15,
33
+ guest: 5,
34
+ };
35
+
19
36
  export function buildProjectPayload(values) {
20
37
  return pickDefined({
21
38
  name: values.name,
@@ -26,15 +43,230 @@ export function buildProjectPayload(values) {
26
43
  });
27
44
  }
28
45
 
46
+ export function splitProjectCreatePayload(values) {
47
+ const fullPayload = buildProjectPayload(values);
48
+ const createPayload = pickDefined({
49
+ name: fullPayload.name,
50
+ identifier: fullPayload.identifier,
51
+ });
52
+ const postCreateUpdatePayload = pickDefined({
53
+ description: fullPayload.description,
54
+ project_lead: fullPayload.project_lead,
55
+ default_assignee: fullPayload.default_assignee,
56
+ });
57
+
58
+ return { createPayload, postCreateUpdatePayload };
59
+ }
60
+
61
+ export function normalizeProjectRole(value) {
62
+ const normalized = String(value || "").trim().toLowerCase();
63
+ const role = PROJECT_ROLE_MAP[normalized];
64
+ if (!role) {
65
+ throw new CliError("Role must be one of: admin, member, guest.");
66
+ }
67
+ return role;
68
+ }
69
+
70
+ export function parseToggle(value, optionName) {
71
+ const normalized = String(value || "").trim().toLowerCase();
72
+ if (["on", "true", "1", "yes", "enable", "enabled"].includes(normalized)) return true;
73
+ if (["off", "false", "0", "no", "disable", "disabled"].includes(normalized)) return false;
74
+ throw new CliError(`Invalid value for ${optionName}: ${value}. Use on/off.`);
75
+ }
76
+
77
+ export function buildProjectFeaturesPayload(values) {
78
+ const payload = {};
79
+
80
+ for (const [option, field] of Object.entries(PROJECT_FEATURE_FIELDS)) {
81
+ if (values[option] !== undefined) {
82
+ payload[field] = parseToggle(values[option], `--${option}`);
83
+ }
84
+ }
85
+
86
+ return payload;
87
+ }
88
+
89
+ function pickProjectFeatures(project) {
90
+ return Object.fromEntries(Object.entries(PROJECT_FEATURE_FIELDS).map(([option, field]) => [field, project[field]]));
91
+ }
92
+
93
+ function renderProjectMembers(data) {
94
+ const rows = Array.isArray(data) ? data : [];
95
+ printTable(rows, [
96
+ { label: "User ID", get: (row) => row.id || "" },
97
+ { label: "Email", get: (row) => row.email || "" },
98
+ { label: "Name", get: (row) => `${row.first_name || ""} ${row.last_name || ""}`.trim() },
99
+ ]);
100
+ }
101
+
102
+ function renderWorkspaceMembers(data) {
103
+ const rows = Array.isArray(data) ? data : [];
104
+ printTable(rows, [
105
+ { label: "User ID", get: (row) => row.id || "" },
106
+ { label: "Email", get: (row) => row.email || "" },
107
+ { label: "Name", get: (row) => `${row.first_name || ""} ${row.last_name || ""}`.trim() },
108
+ { label: "Role", get: (row) => row.role ?? "" },
109
+ ]);
110
+ }
111
+
29
112
  function printHelp() {
30
113
  console.log(`Usage:
31
114
  plane project ls [--limit <n>] [--cursor <cursor>] [--order-by <field>]
32
115
  plane project get <project-id>
116
+ plane project summary <project-id> [--fields <members,states,labels,cycles,modules,issues,intakes,pages>]
117
+ plane project members ls --project <project-id>
118
+ plane project members workspace
119
+ plane project members add --project <project-id> --member <user-id> --role <admin|member|guest>
120
+ plane project features get <project-id>
121
+ 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]
122
+ plane project features enable-all <project-id>
33
123
  plane project create --name <name> --identifier <identifier> [--description <text>] [--project-lead <user-id>] [--default-assignee <user-id>]
34
124
  plane project update <project-id> [--name <name>] [--identifier <identifier>] [--description <text>] [--project-lead <user-id>] [--default-assignee <user-id>]
35
125
  `);
36
126
  }
37
127
 
128
+ async function runProjectMembersCommand(projectClient, args, context) {
129
+ const [subcommand, ...rest] = args;
130
+
131
+ if (!subcommand || subcommand === "--help" || subcommand === "help") {
132
+ printHelp();
133
+ return;
134
+ }
135
+
136
+ if (subcommand === "ls") {
137
+ const parsed = parseCommandArgs(
138
+ rest,
139
+ {
140
+ project: { type: "string" },
141
+ },
142
+ false
143
+ );
144
+
145
+ ensureValue(parsed.values.project, "Project ID is required.");
146
+ const result = await projectClient.listMembers(parsed.values.project);
147
+ printData(result, {
148
+ ...context.output,
149
+ render: renderProjectMembers,
150
+ });
151
+ return;
152
+ }
153
+
154
+ if (subcommand === "workspace") {
155
+ const result = await projectClient.listWorkspaceMembers();
156
+ printData(result, {
157
+ ...context.output,
158
+ render: renderWorkspaceMembers,
159
+ });
160
+ return;
161
+ }
162
+
163
+ if (subcommand === "add") {
164
+ const parsed = parseCommandArgs(
165
+ rest,
166
+ {
167
+ project: { type: "string" },
168
+ member: { type: "string" },
169
+ role: { type: "string" },
170
+ },
171
+ false
172
+ );
173
+
174
+ ensureValue(parsed.values.project, "Project ID is required.");
175
+ ensureValue(parsed.values.member, "Member user ID is required.");
176
+ ensureValue(parsed.values.role, "Role is required.");
177
+
178
+ const result = await projectClient.addMember(parsed.values.project, {
179
+ member: parsed.values.member,
180
+ role: normalizeProjectRole(parsed.values.role),
181
+ });
182
+ printData(result, context.output);
183
+ return;
184
+ }
185
+
186
+ throw new CliError(`Unknown project members subcommand: ${subcommand}`);
187
+ }
188
+
189
+ async function runProjectFeaturesCommand(projectClient, args, context) {
190
+ const [subcommand, ...rest] = args;
191
+
192
+ if (!subcommand || subcommand === "--help" || subcommand === "help") {
193
+ printHelp();
194
+ return;
195
+ }
196
+
197
+ if (subcommand === "get") {
198
+ const [projectId] = rest;
199
+ ensureValue(projectId, "Project ID is required.");
200
+ const project = await projectClient.get(projectId);
201
+ printData(
202
+ {
203
+ id: project.id,
204
+ identifier: project.identifier,
205
+ name: project.name,
206
+ ...pickProjectFeatures(project),
207
+ },
208
+ context.output
209
+ );
210
+ return;
211
+ }
212
+
213
+ if (subcommand === "enable-all") {
214
+ const [projectId] = rest;
215
+ ensureValue(projectId, "Project ID is required.");
216
+ const payload = Object.fromEntries(Object.values(PROJECT_FEATURE_FIELDS).map((field) => [field, true]));
217
+ const result = await projectClient.update(projectId, payload);
218
+ printData(
219
+ {
220
+ id: result.id,
221
+ identifier: result.identifier,
222
+ name: result.name,
223
+ ...pickProjectFeatures(result),
224
+ },
225
+ context.output
226
+ );
227
+ return;
228
+ }
229
+
230
+ if (subcommand === "set") {
231
+ const [projectId, ...optionArgs] = rest;
232
+ ensureValue(projectId, "Project ID is required.");
233
+
234
+ const parsed = parseCommandArgs(
235
+ optionArgs,
236
+ {
237
+ "issue-types": { type: "string" },
238
+ epics: { type: "string" },
239
+ milestones: { type: "string" },
240
+ "time-tracking": { type: "string" },
241
+ "auto-transition": { type: "string" },
242
+ "auto-assign": { type: "string" },
243
+ "auto-worklog": { type: "string" },
244
+ "require-worklog-before-completion": { type: "string" },
245
+ },
246
+ false
247
+ );
248
+
249
+ const payload = buildProjectFeaturesPayload(parsed.values);
250
+ if (Object.keys(payload).length === 0) {
251
+ throw new CliError("At least one feature flag is required.");
252
+ }
253
+
254
+ const result = await projectClient.update(projectId, payload);
255
+ printData(
256
+ {
257
+ id: result.id,
258
+ identifier: result.identifier,
259
+ name: result.name,
260
+ ...pickProjectFeatures(result),
261
+ },
262
+ context.output
263
+ );
264
+ return;
265
+ }
266
+
267
+ throw new CliError(`Unknown project features subcommand: ${subcommand}`);
268
+ }
269
+
38
270
  export async function runProjectCommand(args, context) {
39
271
  const [subcommand, ...rest] = args;
40
272
 
@@ -46,6 +278,16 @@ export async function runProjectCommand(args, context) {
46
278
  const config = await resolveRuntimeConfig();
47
279
  const projectClient = new ProjectClient(new PlaneClient(config));
48
280
 
281
+ if (subcommand === "members") {
282
+ await runProjectMembersCommand(projectClient, rest, context);
283
+ return;
284
+ }
285
+
286
+ if (subcommand === "features") {
287
+ await runProjectFeaturesCommand(projectClient, rest, context);
288
+ return;
289
+ }
290
+
49
291
  if (subcommand === "ls") {
50
292
  const parsed = parseCommandArgs(
51
293
  rest,
@@ -84,6 +326,27 @@ export async function runProjectCommand(args, context) {
84
326
  return;
85
327
  }
86
328
 
329
+ if (subcommand === "summary") {
330
+ const parsed = parseCommandArgs(
331
+ rest,
332
+ {
333
+ fields: { type: "string" },
334
+ }
335
+ );
336
+
337
+ const [projectId] = parsed.positionals;
338
+ ensureValue(projectId, "Project ID is required.");
339
+
340
+ const result = await projectClient.summary(
341
+ projectId,
342
+ pickDefined({
343
+ fields: parsed.values.fields,
344
+ })
345
+ );
346
+ printData(result, context.output);
347
+ return;
348
+ }
349
+
87
350
  if (subcommand === "create") {
88
351
  const parsed = parseCommandArgs(
89
352
  rest,
@@ -100,7 +363,12 @@ export async function runProjectCommand(args, context) {
100
363
  ensureValue(parsed.values.name, "Project name is required.");
101
364
  ensureValue(parsed.values.identifier, "Project identifier is required.");
102
365
 
103
- const result = await projectClient.create(buildProjectPayload(parsed.values));
366
+ const { createPayload, postCreateUpdatePayload } = splitProjectCreatePayload(parsed.values);
367
+ let result = await projectClient.create(createPayload);
368
+ if (Object.keys(postCreateUpdatePayload).length > 0) {
369
+ result = await projectClient.update(result.id, postCreateUpdatePayload);
370
+ }
371
+
104
372
  printData(result, context.output);
105
373
  return;
106
374
  }
@@ -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
- printJson(data);
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
  }