@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 +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/issue.js +757 -6
- package/src/commands/project.js +269 -1
- package/src/core/output.js +48 -2
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
package/src/api/issue-client.js
CHANGED
|
@@ -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
|
-
|
|
14
|
+
format: "human",
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
for (
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const arg = argv[index];
|
|
19
|
+
|
|
18
20
|
if (arg === "--json") {
|
|
19
|
-
options.
|
|
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
|
-
--
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
});
|