@ysicing/plane-cli 1.0.0 → 1.0.2

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 CHANGED
@@ -1,28 +1,15 @@
1
1
  # plane-cli
2
2
 
3
- 基于 Plane 外部 API(`/api/v1`)的轻量命令行工具,使用 Node.js 原生能力实现,不依赖第三方包。
3
+ `plane-cli` 是一个基于 Plane 外部 API(`/api/v1`)的命令行工具,用于在终端中管理 workspace、project 和 work item(issue)。
4
4
 
5
- 默认输出偏人类可读;如果要给脚本、Agent 或其他自动化系统消费,统一加 `--json`:
5
+ ## 特性
6
6
 
7
- ```bash
8
- plane workspace current
9
- plane workspace current --json
10
- plane issue get --project <project-id> <issue-id> --json
11
- ```
12
-
13
- ## 当前状态
14
-
15
- 这套 CLI 已经在真实 Plane 实例上做过 smoke 验证,当前重点覆盖:
16
-
17
- - 认证与 workspace 选择
18
- - `project` 的查询、创建、更新、成员管理、features 开关
19
- - `issue/work-item` 的查询、创建、更新
20
- - `issue` 的 labels、comments、activities、links、relations、attachments
21
-
22
- 适合:
23
-
24
- - 人类直接在终端里操作
25
- - Agent / 脚本通过 `--json` 消费结果
7
+ - 支持 API Key 配置与账号密码登录
8
+ - 支持多 workspace 选择与切换
9
+ - 支持人类可读输出与结构化 JSON 输出
10
+ - 支持 `project` 查询、创建、更新、成员管理、features 开关
11
+ - 支持 `issue` 查询、创建、更新、labels、comments、activities、links、relations、attachments
12
+ - 支持 `GAEA-25` 这类 issue key 自动解析
26
13
 
27
14
  ## 安装
28
15
 
@@ -30,104 +17,97 @@ plane issue get --project <project-id> <issue-id> --json
30
17
  npm install -g @ysicing/plane-cli
31
18
  ```
32
19
 
33
- 或直接用 `npx`:
20
+ 安装完成后可执行命令为:
34
21
 
35
22
  ```bash
36
- npx @ysicing/plane-cli --help
23
+ plane
37
24
  ```
38
25
 
39
- ## 约束
26
+ ## 环境要求
40
27
 
41
- - 鉴权方式:`X-Api-Key`
42
- - 默认面向 `project` 与 `work-item`
43
- - 暂不提供危险删除命令
28
+ - Node.js 22 或更高版本
44
29
 
45
- ## 要求
30
+ ## 输出格式
46
31
 
47
- - Node.js 22+
32
+ 默认输出为人类可读格式。
48
33
 
49
- ## 初始化
34
+ 如需供脚本、Agent 或流水线消费,可使用:
50
35
 
51
36
  ```bash
52
- node ./src/cli.js config set \
53
- --base-url https://plane.example.com \
54
- --api-key your-api-key \
55
- --workspace your-workspace-slug
56
- ```
57
-
58
- 也支持环境变量覆盖:
59
-
60
- ```bash
61
- export PLANE_BASE_URL=https://plane.example.com
62
- export PLANE_API_KEY=your-api-key
63
- export PLANE_WORKSPACE=your-workspace-slug
37
+ plane project ls --json
38
+ plane issue get GAEA-25 --format json
64
39
  ```
65
40
 
66
- ## 快速开始
41
+ ## 认证
67
42
 
68
- 如果已经有 API key,最短路径是:
43
+ ### 方式一:直接配置 API Key
69
44
 
70
45
  ```bash
71
46
  plane config set \
72
47
  --base-url https://plane.example.com \
73
48
  --api-key your-api-key \
74
49
  --workspace your-workspace-slug
75
-
76
- plane me
77
- plane project ls
78
50
  ```
79
51
 
80
- ## 登录并自动生成 API Token
52
+ 也可通过环境变量提供:
53
+
54
+ ```bash
55
+ export PLANE_BASE_URL=https://plane.example.com
56
+ export PLANE_API_KEY=your-api-key
57
+ export PLANE_WORKSPACE=your-workspace-slug
58
+ ```
81
59
 
82
- 普通账号密码登录:
60
+ ### 方式二:账号密码登录
83
61
 
84
62
  ```bash
85
- node ./src/cli.js auth login \
63
+ plane auth login \
86
64
  --base-url https://plane.example.com \
87
65
  --username you@example.com \
88
66
  --password 'your-password'
89
67
  ```
90
68
 
91
- LDAP 登录:
69
+ ### 方式三:LDAP 登录
92
70
 
93
71
  ```bash
94
- node ./src/cli.js auth login \
72
+ plane auth login \
95
73
  --base-url https://plane.example.com \
96
74
  --ldap \
97
- --username your-ldap-account \
75
+ --username your-ldap-user \
98
76
  --password 'your-password'
99
77
  ```
100
78
 
101
- 如果不想把密码放进命令行:
79
+ 如需避免在命令行中直接暴露密码:
102
80
 
103
81
  ```bash
104
- printf '%s' 'your-password' | node ./src/cli.js auth login \
82
+ printf '%s' 'your-password' | plane auth login \
105
83
  --base-url https://plane.example.com \
106
84
  --ldap \
107
- --username your-ldap-account \
85
+ --username your-ldap-user \
108
86
  --password-stdin
109
87
  ```
110
88
 
111
- 如果账号下有多个 workspace,且没有显式传 `--workspace`,CLI 会保存登录态生成的 API token,但不会自动选中 workspace。此时请先查看并选择:
89
+ ## Workspace 管理
90
+
91
+ 如账号下存在多个 workspace,建议先查看并选择默认 workspace:
112
92
 
113
93
  ```bash
114
- node ./src/cli.js workspace ls
115
- node ./src/cli.js workspace current
116
- node ./src/cli.js workspace use <slug>
94
+ plane workspace current
95
+ plane workspace ls
96
+ plane workspace use <slug>
117
97
  ```
118
98
 
119
- ## 常用命令
99
+ ## 命令概览
120
100
 
121
- ### Workspace
101
+ ### 基础命令
122
102
 
123
103
  ```bash
104
+ plane --help
124
105
  plane me
106
+ plane config list
125
107
  plane workspace current
126
- plane workspace ls
127
- plane workspace use <slug>
128
108
  ```
129
109
 
130
- ### Project
110
+ ### Project 命令
131
111
 
132
112
  ```bash
133
113
  plane project ls
@@ -143,111 +123,122 @@ plane project members ls --project <project-id>
143
123
  plane project members add --project <project-id> --member <user-id> --role member
144
124
 
145
125
  plane project features get <project-id>
146
- plane project features enable-all <project-id>
147
126
  plane project features set <project-id> --epics on --milestones on --auto-transition on
127
+ plane project features enable-all <project-id>
148
128
  ```
149
129
 
150
- ### Issue / Work Item
130
+ ### Issue / Work Item 命令
131
+
132
+ `work-item` 是 `issue` 的别名。
151
133
 
152
134
  ```bash
153
135
  plane issue ls --project <project-id>
154
136
  plane issue get --project <project-id> <issue-id>
155
- plane issue key DEMO-123
156
- plane issue get DEMO-123
137
+ plane issue get GAEA-25
138
+ plane issue key GAEA-25
157
139
  plane issue search --query login --workspace-search
158
140
 
159
141
  plane issue create --project <project-id> --name "First work item"
160
142
  plane issue update --project <project-id> <issue-id> --priority high
143
+ ```
144
+
145
+ ### Issue Labels
161
146
 
147
+ ```bash
162
148
  plane issue labels ls --project <project-id>
163
149
  plane issue labels create --project <project-id> --name backend --color '#ff6600'
150
+ ```
164
151
 
165
- plane issue comments ls --project <project-id> <issue-id>
166
- plane issue comments ls DEMO-123
167
- plane issue comments add --project <project-id> <issue-id> --html '<p>Need follow-up</p>'
168
- plane issue comments add DEMO-123 --html '<p>Need follow-up</p>'
169
- plane issue comments update --project <project-id> <issue-id> <comment-id> --html '<p>Updated</p>'
170
-
171
- plane issue activities ls --project <project-id> <issue-id>
172
- plane issue activities ls DEMO-123
152
+ ### Issue Comments
173
153
 
174
- plane issue links ls --project <project-id> <issue-id>
175
- plane issue links ls DEMO-123
176
- plane issue links add --project <project-id> <issue-id> --url 'https://example.com/doc'
177
- plane issue links update --project <project-id> <issue-id> <link-id> --url 'https://example.com/doc-v2'
154
+ ```bash
155
+ plane issue comments ls GAEA-25
156
+ plane issue comments add GAEA-25 --html '<p>Need follow-up</p>'
157
+ plane issue comments update GAEA-25 <comment-id> --html '<p>Updated</p>'
158
+ ```
178
159
 
179
- plane issue relations ls --project <project-id> <issue-id>
180
- plane issue relations ls DEMO-123
181
- plane issue relations add --project <project-id> <issue-id> --relation-type blocking --issues '<other-issue-id>'
160
+ ### Issue Activities
182
161
 
183
- plane issue attachments ls --project <project-id> <issue-id>
184
- plane issue attachments ls DEMO-123
185
- plane issue attachments upload --project <project-id> <issue-id> --file ./spec.pdf
162
+ ```bash
163
+ plane issue activities ls GAEA-25
186
164
  ```
187
165
 
188
- `work-item` `issue` 的别名:
166
+ ### Issue Links
189
167
 
190
168
  ```bash
191
- node ./src/cli.js work-item ls --project <project-id>
169
+ plane issue links ls GAEA-25
170
+ plane issue links add GAEA-25 --url 'https://example.com/doc'
171
+ plane issue links update GAEA-25 <link-id> --url 'https://example.com/doc-v2'
192
172
  ```
193
173
 
194
- ## AI / Agent 友好约定
174
+ ### Issue Relations
195
175
 
196
- - 默认输出是人类可读格式
197
- - `--json` `--format json` 后,成功结果和错误结果都会变成结构化 JSON
198
- - 参数缺失、workspace 未选择、API 返回错误时,都会带明确 `message`
176
+ ```bash
177
+ plane issue relations ls GAEA-25
178
+ plane issue relations add GAEA-25 --relation-type blocking --issues '<other-issue-id>'
179
+ ```
199
180
 
200
- 示例:
181
+ ### Issue Attachments
201
182
 
202
183
  ```bash
203
- plane issue get --project <project-id> <issue-id> --json
204
- plane project ls --json
184
+ plane issue attachments ls GAEA-25
185
+ plane issue attachments upload GAEA-25 --file ./spec.pdf
205
186
  ```
206
187
 
207
- ## 使用注意
208
-
209
- - 多 workspace 登录后,如果没有显式传 `--workspace`,CLI 不会自动替你选择默认 workspace。先执行 `plane workspace ls`,再执行 `plane workspace use <slug>`。
210
- - `issue create/update --assignees` 支持传用户 ID、邮箱、或精确全名。CLI 会先读取 workspace 成员再解析成后端需要的 member ID。
211
- - `project create` 带 `--project-lead` 和 `--default-assignee` 时,CLI 会自动采用“两段式创建”:先建项目,再补一次 update。这是为了兼容部分 Plane 实例的后端校验差异。
212
- - `project features set`、`issue attachments upload` 这类命令在真实实例上可能会遇到几秒钟的读副本延迟。写入成功后如果立刻回读不到,稍后再查一次通常即可。
213
- - 当前不提供危险删除命令,也不主动清理服务端 token。
188
+ ## 交互与参数约定
214
189
 
215
- ## 已验证命令
190
+ ### Issue Key 自动解析
216
191
 
217
- 在真实 Plane 实例上已验证:
192
+ 对于以下命令,若目标是已存在的 issue,可直接使用 `GAEA-25` 这类 key,而无需显式提供 `--project` 与 issue UUID:
218
193
 
219
- - `me`
220
- - `workspace current/ls/use`
221
- - `project ls/get/summary/create/update`
222
- - `project members workspace/ls/add`
223
- - `project features get/set/enable-all`
224
- - `issue ls/get/key/search/create/update`
225
- - `issue labels ls/create`
194
+ - `issue get`
226
195
  - `issue comments ls/add/update`
227
196
  - `issue activities ls`
228
197
  - `issue links ls/add/update`
229
198
  - `issue relations ls/add`
230
199
  - `issue attachments ls/upload`
231
200
 
232
- ## 发布
201
+ ### 指定 Assignee
202
+
203
+ `issue create` 与 `issue update` 的 `--assignees` 支持以下输入形式:
204
+
205
+ - 用户 ID
206
+ - 邮箱
207
+ - 精确全名
208
+
209
+ CLI 会自动根据 workspace 成员列表解析为后端所需的 member ID。
233
210
 
234
- 当前包名使用 `@ysicing/plane-cli`,因为 `plane-cli` 已被 npm 占用。
211
+ ## Help 体系
235
212
 
236
- 发布前检查:
213
+ 支持以下层级的帮助信息:
237
214
 
238
215
  ```bash
239
- npm test
240
- npm run pack:check
216
+ plane -h
217
+ plane project -h
218
+ plane project features set --help
219
+ plane issue comments add --help
220
+ plane issue attachments upload --help
241
221
  ```
242
222
 
243
- 发布:
223
+ ## 限制说明
224
+
225
+ - 当前不提供危险删除命令
226
+ - 当前不自动回收服务端 API Token
227
+ - `project members ls` 受限于 Plane `api/v1` 返回结构,仅返回用户资料,不包含完整的 `ProjectMember` 记录字段
228
+ - `project create` 在部分 Plane 实例上对 `project_lead/default_assignee` 的校验较严格,CLI 已采用“两段式创建”兼容该行为
229
+ - `project features set`、`issue attachments upload` 在启用读副本的实例上,可能存在短暂回读延迟
230
+
231
+ ## 发布
232
+
233
+ 当前包名为 `@ysicing/plane-cli`。发布前建议执行:
244
234
 
245
235
  ```bash
246
- npm publish
236
+ npm test
237
+ npm run pack:check
247
238
  ```
248
239
 
249
- 如果本机还没登录 npm:
240
+ 发布:
250
241
 
251
242
  ```bash
252
- npm login
243
+ npm publish
253
244
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ysicing/plane-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "Node.js CLI for managing Plane via the external API",
6
6
  "license": "UNLICENSED",
@@ -49,7 +49,7 @@ function resolveWorkspaceToSave({ explicitWorkspace, existingWorkspace, userWork
49
49
  export async function runAuthCommand(args, context) {
50
50
  const [subcommand, ...rest] = args;
51
51
 
52
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
52
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
53
53
  printHelp();
54
54
  return;
55
55
  }
@@ -58,7 +58,7 @@ export async function runAuthCommand(args, context) {
58
58
  throw new CliError(`Unknown auth subcommand: ${subcommand}`);
59
59
  }
60
60
 
61
- if (rest.includes("--help") || rest.includes("help")) {
61
+ if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
62
62
  printHelp();
63
63
  return;
64
64
  }
@@ -1,6 +1,6 @@
1
1
  import { loadConfig, maskApiKey, resolveConfigPath, saveConfig } from "../core/config.js";
2
2
  import { CliError } from "../core/errors.js";
3
- import { parseCommandArgs } from "../core/options.js";
3
+ import { parseCommandArgs, pickDefined } from "../core/options.js";
4
4
  import { printData } from "../core/output.js";
5
5
 
6
6
  function configSnapshot(config, path) {
@@ -21,11 +21,24 @@ function printHelp() {
21
21
  `);
22
22
  }
23
23
 
24
+ function printGetHelp() {
25
+ console.log(`Usage:
26
+ plane config get
27
+ plane config get <baseUrl|apiKey|workspace|knownWorkspaces>
28
+ `);
29
+ }
30
+
31
+ function printSetHelp() {
32
+ console.log(`Usage:
33
+ plane config set [--base-url <url>] [--api-key <key>] [--workspace <slug>]
34
+ `);
35
+ }
36
+
24
37
  export async function runConfigCommand(args, context) {
25
38
  const [subcommand = "list", ...rest] = args;
26
39
  const path = resolveConfigPath();
27
40
 
28
- if (subcommand === "--help" || subcommand === "help") {
41
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
29
42
  printHelp();
30
43
  return;
31
44
  }
@@ -37,6 +50,11 @@ export async function runConfigCommand(args, context) {
37
50
  }
38
51
 
39
52
  if (subcommand === "get") {
53
+ if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
54
+ printGetHelp();
55
+ return;
56
+ }
57
+
40
58
  const [key] = rest;
41
59
  const config = await loadConfig();
42
60
  const snapshot = configSnapshot(config, path);
@@ -55,6 +73,11 @@ export async function runConfigCommand(args, context) {
55
73
  }
56
74
 
57
75
  if (subcommand === "set") {
76
+ if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
77
+ printSetHelp();
78
+ return;
79
+ }
80
+
58
81
  const parsed = parseCommandArgs(
59
82
  rest,
60
83
  {
@@ -65,11 +88,11 @@ export async function runConfigCommand(args, context) {
65
88
  false
66
89
  );
67
90
 
68
- const update = {
91
+ const update = pickDefined({
69
92
  baseUrl: parsed.values["base-url"],
70
93
  apiKey: parsed.values["api-key"],
71
94
  workspace: parsed.values.workspace,
72
- };
95
+ });
73
96
 
74
97
  if (!update.baseUrl && !update.apiKey && !update.workspace) {
75
98
  throw new CliError("Nothing to update. Pass at least one of --base-url, --api-key, or --workspace.");
@@ -8,6 +8,10 @@ import { printData, printTable } from "../core/output.js";
8
8
  import { basename, extname } from "node:path";
9
9
  import { readFile, stat } from "node:fs/promises";
10
10
 
11
+ function hasHelpFlag(args) {
12
+ return args.includes("--help") || args.includes("-h") || args.includes("help");
13
+ }
14
+
11
15
  function renderIssueList(data) {
12
16
  const rows = Array.isArray(data) ? data : data.results || [];
13
17
  printTable(rows, [
@@ -291,14 +295,125 @@ function printHelp() {
291
295
  `);
292
296
  }
293
297
 
298
+ function printIssueCommentsHelp() {
299
+ console.log(`Usage:
300
+ plane issue comments ls --project <project-id> <issue-id>
301
+ plane issue comments ls GAEA-25
302
+ plane issue comments add --project <project-id> <issue-id> --html '<p>comment</p>' [--access <value>]
303
+ plane issue comments add GAEA-25 --html '<p>comment</p>' [--access <value>]
304
+ plane issue comments update --project <project-id> <issue-id> <comment-id> --html '<p>comment</p>' [--access <value>]
305
+ plane issue comments update GAEA-25 <comment-id> --html '<p>comment</p>' [--access <value>]
306
+ `);
307
+ }
308
+
309
+ function printIssueLinksHelp() {
310
+ console.log(`Usage:
311
+ plane issue links ls --project <project-id> <issue-id>
312
+ plane issue links ls GAEA-25
313
+ plane issue links add --project <project-id> <issue-id> --url <url> [--title <text>]
314
+ plane issue links add GAEA-25 --url <url> [--title <text>]
315
+ plane issue links update --project <project-id> <issue-id> <link-id> --url <url> [--title <text>]
316
+ plane issue links update GAEA-25 <link-id> --url <url> [--title <text>]
317
+ `);
318
+ }
319
+
320
+ function printIssueRelationsHelp() {
321
+ console.log(`Usage:
322
+ plane issue relations ls --project <project-id> <issue-id>
323
+ plane issue relations ls GAEA-25
324
+ plane issue relations add --project <project-id> <issue-id> --relation-type <blocking|blocked_by|duplicate|relates_to|start_before|start_after|finish_before|finish_after> --issues <id1,id2>
325
+ plane issue relations add GAEA-25 --relation-type <blocking|blocked_by|duplicate|relates_to|start_before|start_after|finish_before|finish_after> --issues <id1,id2>
326
+ `);
327
+ }
328
+
329
+ function printIssueAttachmentsHelp() {
330
+ console.log(`Usage:
331
+ plane issue attachments ls --project <project-id> <issue-id>
332
+ plane issue attachments ls GAEA-25
333
+ plane issue attachments upload --project <project-id> <issue-id> --file <path> [--name <filename>] [--type <mime>]
334
+ plane issue attachments upload GAEA-25 --file <path> [--name <filename>] [--type <mime>]
335
+ `);
336
+ }
337
+
338
+ function printIssueActivitiesHelp() {
339
+ console.log(`Usage:
340
+ plane issue activities ls --project <project-id> <issue-id>
341
+ plane issue activities ls GAEA-25
342
+ `);
343
+ }
344
+
345
+ function printIssueLabelsHelp() {
346
+ console.log(`Usage:
347
+ plane issue labels ls --project <project-id>
348
+ plane issue labels create --project <project-id> --name <name> [--color <hex>] [--description <text>] [--parent <label-id>] [--sort-order <n>]
349
+ `);
350
+ }
351
+
352
+ function printIssueLabelsCreateHelp() {
353
+ console.log(`Usage:
354
+ plane issue labels create --project <project-id> --name <name> [--color <hex>] [--description <text>] [--parent <label-id>] [--sort-order <n>]
355
+ `);
356
+ }
357
+
358
+ function printIssueCommentsAddHelp() {
359
+ console.log(`Usage:
360
+ plane issue comments add --project <project-id> <issue-id> --html '<p>comment</p>' [--access <value>]
361
+ plane issue comments add GAEA-25 --html '<p>comment</p>' [--access <value>]
362
+ `);
363
+ }
364
+
365
+ function printIssueCommentsUpdateHelp() {
366
+ console.log(`Usage:
367
+ plane issue comments update --project <project-id> <issue-id> <comment-id> --html '<p>comment</p>' [--access <value>]
368
+ plane issue comments update GAEA-25 <comment-id> --html '<p>comment</p>' [--access <value>]
369
+ `);
370
+ }
371
+
372
+ function printIssueLinksAddHelp() {
373
+ console.log(`Usage:
374
+ plane issue links add --project <project-id> <issue-id> --url <url> [--title <text>]
375
+ plane issue links add GAEA-25 --url <url> [--title <text>]
376
+ `);
377
+ }
378
+
379
+ function printIssueLinksUpdateHelp() {
380
+ console.log(`Usage:
381
+ plane issue links update --project <project-id> <issue-id> <link-id> --url <url> [--title <text>]
382
+ plane issue links update GAEA-25 <link-id> --url <url> [--title <text>]
383
+ `);
384
+ }
385
+
386
+ function printIssueRelationsAddHelp() {
387
+ console.log(`Usage:
388
+ plane issue relations add --project <project-id> <issue-id> --relation-type <blocking|blocked_by|duplicate|relates_to|start_before|start_after|finish_before|finish_after> --issues <id1,id2>
389
+ plane issue relations add GAEA-25 --relation-type <blocking|blocked_by|duplicate|relates_to|start_before|start_after|finish_before|finish_after> --issues <id1,id2>
390
+ `);
391
+ }
392
+
393
+ function printIssueAttachmentsUploadHelp() {
394
+ console.log(`Usage:
395
+ plane issue attachments upload --project <project-id> <issue-id> --file <path> [--name <filename>] [--type <mime>]
396
+ plane issue attachments upload GAEA-25 --file <path> [--name <filename>] [--type <mime>]
397
+ `);
398
+ }
399
+
294
400
  async function runIssueLabelsCommand(issueClient, args, context) {
295
401
  const [subcommand, ...rest] = args;
296
402
 
297
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
403
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
298
404
  printHelp();
299
405
  return;
300
406
  }
301
407
 
408
+ if (hasHelpFlag(rest)) {
409
+ if (subcommand === "create") {
410
+ printIssueLabelsCreateHelp();
411
+ return;
412
+ }
413
+ printIssueLabelsHelp();
414
+ return;
415
+ }
416
+
302
417
  if (subcommand === "ls") {
303
418
  const parsed = parseCommandArgs(
304
419
  rest,
@@ -353,11 +468,24 @@ async function runIssueLabelsCommand(issueClient, args, context) {
353
468
  async function runIssueCommentsCommand(issueClient, args, context) {
354
469
  const [subcommand, ...rest] = args;
355
470
 
356
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
471
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
357
472
  printHelp();
358
473
  return;
359
474
  }
360
475
 
476
+ if (hasHelpFlag(rest)) {
477
+ if (subcommand === "add") {
478
+ printIssueCommentsAddHelp();
479
+ return;
480
+ }
481
+ if (subcommand === "update") {
482
+ printIssueCommentsUpdateHelp();
483
+ return;
484
+ }
485
+ printIssueCommentsHelp();
486
+ return;
487
+ }
488
+
361
489
  if (subcommand === "ls") {
362
490
  const parsed = parseCommandArgs(
363
491
  rest,
@@ -439,11 +567,16 @@ async function runIssueCommentsCommand(issueClient, args, context) {
439
567
  async function runIssueActivitiesCommand(issueClient, args, context) {
440
568
  const [subcommand, ...rest] = args;
441
569
 
442
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
570
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
443
571
  printHelp();
444
572
  return;
445
573
  }
446
574
 
575
+ if (hasHelpFlag(rest)) {
576
+ printIssueActivitiesHelp();
577
+ return;
578
+ }
579
+
447
580
  if (subcommand !== "ls") {
448
581
  throw new CliError(`Unknown issue activities subcommand: ${subcommand}`);
449
582
  }
@@ -478,11 +611,24 @@ async function runIssueActivitiesCommand(issueClient, args, context) {
478
611
  async function runIssueLinksCommand(issueClient, args, context) {
479
612
  const [subcommand, ...rest] = args;
480
613
 
481
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
614
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
482
615
  printHelp();
483
616
  return;
484
617
  }
485
618
 
619
+ if (hasHelpFlag(rest)) {
620
+ if (subcommand === "add") {
621
+ printIssueLinksAddHelp();
622
+ return;
623
+ }
624
+ if (subcommand === "update") {
625
+ printIssueLinksUpdateHelp();
626
+ return;
627
+ }
628
+ printIssueLinksHelp();
629
+ return;
630
+ }
631
+
486
632
  if (subcommand === "ls") {
487
633
  const parsed = parseCommandArgs(
488
634
  rest,
@@ -553,11 +699,20 @@ async function runIssueLinksCommand(issueClient, args, context) {
553
699
  async function runIssueRelationsCommand(issueClient, args, context) {
554
700
  const [subcommand, ...rest] = args;
555
701
 
556
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
702
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
557
703
  printHelp();
558
704
  return;
559
705
  }
560
706
 
707
+ if (hasHelpFlag(rest)) {
708
+ if (subcommand === "add") {
709
+ printIssueRelationsAddHelp();
710
+ return;
711
+ }
712
+ printIssueRelationsHelp();
713
+ return;
714
+ }
715
+
561
716
  if (subcommand === "ls") {
562
717
  const parsed = parseCommandArgs(
563
718
  rest,
@@ -625,11 +780,20 @@ async function uploadAttachmentBinary(uploadData, filePath, fileName, mimeType)
625
780
  async function runIssueAttachmentsCommand(issueClient, args, context) {
626
781
  const [subcommand, ...rest] = args;
627
782
 
628
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
783
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
629
784
  printHelp();
630
785
  return;
631
786
  }
632
787
 
788
+ if (hasHelpFlag(rest)) {
789
+ if (subcommand === "upload") {
790
+ printIssueAttachmentsUploadHelp();
791
+ return;
792
+ }
793
+ printIssueAttachmentsHelp();
794
+ return;
795
+ }
796
+
633
797
  if (subcommand === "ls") {
634
798
  const parsed = parseCommandArgs(
635
799
  rest,
@@ -699,7 +863,36 @@ async function runIssueAttachmentsCommand(issueClient, args, context) {
699
863
  export async function runIssueCommand(args, context) {
700
864
  const [subcommand, ...rest] = args;
701
865
 
702
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
866
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
867
+ printHelp();
868
+ return;
869
+ }
870
+
871
+ if (hasHelpFlag(rest)) {
872
+ if (subcommand === "labels") {
873
+ printIssueLabelsHelp();
874
+ return;
875
+ }
876
+ if (subcommand === "comments") {
877
+ printIssueCommentsHelp();
878
+ return;
879
+ }
880
+ if (subcommand === "activities") {
881
+ printIssueActivitiesHelp();
882
+ return;
883
+ }
884
+ if (subcommand === "links") {
885
+ printIssueLinksHelp();
886
+ return;
887
+ }
888
+ if (subcommand === "relations") {
889
+ printIssueRelationsHelp();
890
+ return;
891
+ }
892
+ if (subcommand === "attachments") {
893
+ printIssueAttachmentsHelp();
894
+ return;
895
+ }
703
896
  printHelp();
704
897
  return;
705
898
  }
@@ -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, [
@@ -125,14 +129,63 @@ function printHelp() {
125
129
  `);
126
130
  }
127
131
 
132
+ function printProjectMembersHelp() {
133
+ console.log(`Usage:
134
+ plane project members ls --project <project-id>
135
+ plane project members workspace
136
+ plane project members add --project <project-id> --member <user-id> --role <admin|member|guest>
137
+ `);
138
+ }
139
+
140
+ function printProjectMembersAddHelp() {
141
+ console.log(`Usage:
142
+ plane project members add --project <project-id> --member <user-id> --role <admin|member|guest>
143
+ `);
144
+ }
145
+
146
+ function printProjectFeaturesHelp() {
147
+ console.log(`Usage:
148
+ plane project features get <project-id>
149
+ 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]
150
+ plane project features enable-all <project-id>
151
+ `);
152
+ }
153
+
154
+ function printProjectFeaturesSetHelp() {
155
+ console.log(`Usage:
156
+ 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]
157
+ `);
158
+ }
159
+
160
+ function printProjectCreateHelp() {
161
+ console.log(`Usage:
162
+ plane project create --name <name> --identifier <identifier> [--description <text>] [--project-lead <user-id>] [--default-assignee <user-id>]
163
+ `);
164
+ }
165
+
166
+ function printProjectUpdateHelp() {
167
+ console.log(`Usage:
168
+ plane project update <project-id> [--name <name>] [--identifier <identifier>] [--description <text>] [--project-lead <user-id>] [--default-assignee <user-id>]
169
+ `);
170
+ }
171
+
128
172
  async function runProjectMembersCommand(projectClient, args, context) {
129
173
  const [subcommand, ...rest] = args;
130
174
 
131
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
175
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
132
176
  printHelp();
133
177
  return;
134
178
  }
135
179
 
180
+ if (hasHelpFlag(rest)) {
181
+ if (subcommand === "add") {
182
+ printProjectMembersAddHelp();
183
+ return;
184
+ }
185
+ printProjectMembersHelp();
186
+ return;
187
+ }
188
+
136
189
  if (subcommand === "ls") {
137
190
  const parsed = parseCommandArgs(
138
191
  rest,
@@ -189,11 +242,20 @@ async function runProjectMembersCommand(projectClient, args, context) {
189
242
  async function runProjectFeaturesCommand(projectClient, args, context) {
190
243
  const [subcommand, ...rest] = args;
191
244
 
192
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
245
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
193
246
  printHelp();
194
247
  return;
195
248
  }
196
249
 
250
+ if (hasHelpFlag(rest)) {
251
+ if (subcommand === "set") {
252
+ printProjectFeaturesSetHelp();
253
+ return;
254
+ }
255
+ printProjectFeaturesHelp();
256
+ return;
257
+ }
258
+
197
259
  if (subcommand === "get") {
198
260
  const [projectId] = rest;
199
261
  ensureValue(projectId, "Project ID is required.");
@@ -270,7 +332,28 @@ async function runProjectFeaturesCommand(projectClient, args, context) {
270
332
  export async function runProjectCommand(args, context) {
271
333
  const [subcommand, ...rest] = args;
272
334
 
273
- if (!subcommand || subcommand === "--help" || subcommand === "help") {
335
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
336
+ printHelp();
337
+ return;
338
+ }
339
+
340
+ if (hasHelpFlag(rest)) {
341
+ if (subcommand === "members") {
342
+ printProjectMembersHelp();
343
+ return;
344
+ }
345
+ if (subcommand === "features") {
346
+ printProjectFeaturesHelp();
347
+ return;
348
+ }
349
+ if (subcommand === "create") {
350
+ printProjectCreateHelp();
351
+ return;
352
+ }
353
+ if (subcommand === "update") {
354
+ printProjectUpdateHelp();
355
+ return;
356
+ }
274
357
  printHelp();
275
358
  return;
276
359
  }
@@ -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") {