@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.
@@ -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
  }
@@ -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 result = await projectClient.create(buildProjectPayload(parsed.values));
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
  }
@@ -51,6 +51,9 @@ export async function loadConfig() {
51
51
 
52
52
  try {
53
53
  const raw = await readFile(configPath, "utf8");
54
+ if (!raw.trim()) {
55
+ return {};
56
+ }
54
57
  return sanitizeConfig(JSON.parse(raw));
55
58
  } catch (error) {
56
59
  if (error && error.code === "ENOENT") {
@@ -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
  }