@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.
package/README.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  基于 Plane 外部 API(`/api/v1`)的轻量命令行工具,使用 Node.js 原生能力实现,不依赖第三方包。
4
4
 
5
+ 默认输出偏人类可读;如果要给脚本、Agent 或其他自动化系统消费,统一加 `--json`:
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` 消费结果
26
+
5
27
  ## 安装
6
28
 
7
29
  ```bash
@@ -41,6 +63,20 @@ export PLANE_API_KEY=your-api-key
41
63
  export PLANE_WORKSPACE=your-workspace-slug
42
64
  ```
43
65
 
66
+ ## 快速开始
67
+
68
+ 如果已经有 API key,最短路径是:
69
+
70
+ ```bash
71
+ plane config set \
72
+ --base-url https://plane.example.com \
73
+ --api-key your-api-key \
74
+ --workspace your-workspace-slug
75
+
76
+ plane me
77
+ plane project ls
78
+ ```
79
+
44
80
  ## 登录并自动生成 API Token
45
81
 
46
82
  普通账号密码登录:
@@ -80,17 +116,73 @@ node ./src/cli.js workspace current
80
116
  node ./src/cli.js workspace use <slug>
81
117
  ```
82
118
 
83
- ## 示例
119
+ ## 常用命令
120
+
121
+ ### Workspace
84
122
 
85
123
  ```bash
86
- node ./src/cli.js me
87
- node ./src/cli.js project ls
88
- node ./src/cli.js project get <project-id>
89
- node ./src/cli.js project create --name Demo --identifier DEMO
90
- node ./src/cli.js issue ls --project <project-id>
91
- node ./src/cli.js issue get --project <project-id> <issue-id>
92
- node ./src/cli.js issue create --project <project-id> --name "First work item"
93
- node ./src/cli.js issue update --project <project-id> <issue-id> --priority high
124
+ plane me
125
+ plane workspace current
126
+ plane workspace ls
127
+ plane workspace use <slug>
128
+ ```
129
+
130
+ ### Project
131
+
132
+ ```bash
133
+ plane project ls
134
+ plane project get <project-id>
135
+ plane project summary <project-id>
136
+
137
+ plane project create --name Demo --identifier DEMO
138
+ plane project create --name Demo --identifier DEMO --project-lead <user-id> --default-assignee <user-id>
139
+ plane project update <project-id> --description 'updated description'
140
+
141
+ plane project members workspace
142
+ plane project members ls --project <project-id>
143
+ plane project members add --project <project-id> --member <user-id> --role member
144
+
145
+ plane project features get <project-id>
146
+ plane project features enable-all <project-id>
147
+ plane project features set <project-id> --epics on --milestones on --auto-transition on
148
+ ```
149
+
150
+ ### Issue / Work Item
151
+
152
+ ```bash
153
+ plane issue ls --project <project-id>
154
+ plane issue get --project <project-id> <issue-id>
155
+ plane issue key DEMO-123
156
+ plane issue get DEMO-123
157
+ plane issue search --query login --workspace-search
158
+
159
+ plane issue create --project <project-id> --name "First work item"
160
+ plane issue update --project <project-id> <issue-id> --priority high
161
+
162
+ plane issue labels ls --project <project-id>
163
+ plane issue labels create --project <project-id> --name backend --color '#ff6600'
164
+
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
173
+
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'
178
+
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>'
182
+
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
94
186
  ```
95
187
 
96
188
  `work-item` 是 `issue` 的别名:
@@ -99,6 +191,44 @@ node ./src/cli.js issue update --project <project-id> <issue-id> --priority high
99
191
  node ./src/cli.js work-item ls --project <project-id>
100
192
  ```
101
193
 
194
+ ## AI / Agent 友好约定
195
+
196
+ - 默认输出是人类可读格式
197
+ - 加 `--json` 或 `--format json` 后,成功结果和错误结果都会变成结构化 JSON
198
+ - 参数缺失、workspace 未选择、API 返回错误时,都会带明确 `message`
199
+
200
+ 示例:
201
+
202
+ ```bash
203
+ plane issue get --project <project-id> <issue-id> --json
204
+ plane project ls --json
205
+ ```
206
+
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。
214
+
215
+ ## 已验证命令
216
+
217
+ 在真实 Plane 实例上已验证:
218
+
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`
226
+ - `issue comments ls/add/update`
227
+ - `issue activities ls`
228
+ - `issue links ls/add/update`
229
+ - `issue relations ls/add`
230
+ - `issue attachments ls/upload`
231
+
102
232
  ## 发布
103
233
 
104
234
  当前包名使用 `@ysicing/plane-cli`,因为 `plane-cli` 已被 npm 占用。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ysicing/plane-cli",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Node.js CLI for managing Plane via the external API",
6
6
  "license": "UNLICENSED",
@@ -18,4 +18,77 @@ export class IssueClient {
18
18
  update(projectId, issueId, body) {
19
19
  return this.client.patch(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/`), body);
20
20
  }
21
+
22
+ getByKey(projectIdentifier, issueIdentifier, query = {}) {
23
+ return this.client.get(this.client.workspacePath(`/work-items/${projectIdentifier}-${issueIdentifier}/`), query);
24
+ }
25
+
26
+ search(query = {}) {
27
+ return this.client.get(this.client.workspacePath("/work-items/search/"), query);
28
+ }
29
+
30
+ listLabels(projectId, query = {}) {
31
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/labels/`), query);
32
+ }
33
+
34
+ createLabel(projectId, body) {
35
+ return this.client.post(this.client.workspacePath(`/projects/${projectId}/labels/`), body);
36
+ }
37
+
38
+ listComments(projectId, issueId, query = {}) {
39
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/comments/`), query);
40
+ }
41
+
42
+ createComment(projectId, issueId, body) {
43
+ return this.client.post(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/comments/`), body);
44
+ }
45
+
46
+ updateComment(projectId, issueId, commentId, body) {
47
+ return this.client.patch(
48
+ this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/comments/${commentId}/`),
49
+ body
50
+ );
51
+ }
52
+
53
+ listActivities(projectId, issueId, query = {}) {
54
+ return this.client.get(
55
+ this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/activities/`),
56
+ query
57
+ );
58
+ }
59
+
60
+ listLinks(projectId, issueId, query = {}) {
61
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/links/`), query);
62
+ }
63
+
64
+ createLink(projectId, issueId, body) {
65
+ return this.client.post(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/links/`), body);
66
+ }
67
+
68
+ updateLink(projectId, issueId, linkId, body) {
69
+ return this.client.patch(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/links/${linkId}/`), body);
70
+ }
71
+
72
+ listRelations(projectId, issueId) {
73
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/relations/`));
74
+ }
75
+
76
+ createRelation(projectId, issueId, body) {
77
+ return this.client.post(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/relations/`), body);
78
+ }
79
+
80
+ listAttachments(projectId, issueId) {
81
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/attachments/`));
82
+ }
83
+
84
+ createAttachmentUpload(projectId, issueId, body) {
85
+ return this.client.post(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/attachments/`), body);
86
+ }
87
+
88
+ confirmAttachmentUpload(projectId, issueId, attachmentId, body = { is_uploaded: true }) {
89
+ return this.client.patch(
90
+ this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/attachments/${attachmentId}/`),
91
+ body
92
+ );
93
+ }
21
94
  }
@@ -18,4 +18,20 @@ export class ProjectClient {
18
18
  update(projectId, body) {
19
19
  return this.client.patch(this.client.workspacePath(`/projects/${projectId}/`), body);
20
20
  }
21
+
22
+ summary(projectId, query = {}) {
23
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/summary/`), query);
24
+ }
25
+
26
+ listMembers(projectId) {
27
+ return this.client.get(this.client.workspacePath(`/projects/${projectId}/members/`));
28
+ }
29
+
30
+ addMember(projectId, body) {
31
+ return this.client.post(this.client.workspacePath(`/projects/${projectId}/members/`), body);
32
+ }
33
+
34
+ listWorkspaceMembers() {
35
+ return this.client.get(this.client.workspacePath("/members/"));
36
+ }
21
37
  }
package/src/cli.js CHANGED
@@ -11,14 +11,30 @@ import { CliError } from "./core/errors.js";
11
11
  function extractGlobalOptions(argv) {
12
12
  const args = [];
13
13
  const options = {
14
- json: false,
14
+ format: "human",
15
15
  };
16
16
 
17
- for (const arg of argv) {
17
+ for (let index = 0; index < argv.length; index += 1) {
18
+ const arg = argv[index];
19
+
18
20
  if (arg === "--json") {
19
- options.json = true;
21
+ options.format = "json";
20
22
  continue;
21
23
  }
24
+
25
+ if (arg === "--format") {
26
+ const value = argv[index + 1];
27
+ if (!value) {
28
+ throw new CliError("`--format` requires a value: human or json.");
29
+ }
30
+ if (!["human", "json"].includes(value)) {
31
+ throw new CliError(`Unsupported format: ${value}. Use human or json.`);
32
+ }
33
+ options.format = value;
34
+ index += 1;
35
+ continue;
36
+ }
37
+
22
38
  args.push(arg);
23
39
  }
24
40
 
@@ -41,7 +57,8 @@ Commands:
41
57
  workspace Manage selected workspace
42
58
 
43
59
  Global options:
44
- --json Print raw JSON
60
+ --format Output format: human|json
61
+ --json Alias of --format json
45
62
  --help Show help
46
63
  `);
47
64
  }
@@ -82,15 +99,57 @@ async function main() {
82
99
  }
83
100
  }
84
101
 
102
+ function wantsJsonOutput(argv) {
103
+ for (let index = 0; index < argv.length; index += 1) {
104
+ if (argv[index] === "--json") return true;
105
+ if (argv[index] === "--format" && argv[index + 1] === "json") return true;
106
+ }
107
+ return false;
108
+ }
109
+
85
110
  main().catch((error) => {
111
+ const wantsJson = wantsJsonOutput(process.argv.slice(2));
112
+
86
113
  if (error instanceof CliError) {
87
- console.error(`Error: ${error.message}`);
88
- if (error.details) {
89
- console.error(JSON.stringify(error.details, null, 2));
114
+ if (wantsJson) {
115
+ console.error(
116
+ JSON.stringify(
117
+ {
118
+ error: {
119
+ message: error.message,
120
+ details: error.details ?? null,
121
+ exitCode: error.exitCode,
122
+ },
123
+ },
124
+ null,
125
+ 2
126
+ )
127
+ );
128
+ } else {
129
+ console.error(`Error: ${error.message}`);
130
+ if (error.details) {
131
+ console.error(JSON.stringify(error.details, null, 2));
132
+ }
90
133
  }
91
134
  process.exit(error.exitCode);
92
135
  }
93
136
 
94
- console.error(error);
137
+ if (wantsJson) {
138
+ console.error(
139
+ JSON.stringify(
140
+ {
141
+ error: {
142
+ message: error?.message || String(error),
143
+ details: null,
144
+ exitCode: 1,
145
+ },
146
+ },
147
+ null,
148
+ 2
149
+ )
150
+ );
151
+ } else {
152
+ console.error(error);
153
+ }
95
154
  process.exit(1);
96
155
  });