@ysicing/plane-cli 0.1.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 +123 -0
- package/package.json +42 -0
- package/src/api/issue-client.js +21 -0
- package/src/api/project-client.js +21 -0
- package/src/api/user-client.js +9 -0
- package/src/cli.js +96 -0
- package/src/commands/auth.js +136 -0
- package/src/commands/config.js +84 -0
- package/src/commands/issue.js +171 -0
- package/src/commands/me.js +23 -0
- package/src/commands/project.js +135 -0
- package/src/commands/workspace.js +102 -0
- package/src/core/config.js +103 -0
- package/src/core/errors.js +8 -0
- package/src/core/http.js +109 -0
- package/src/core/options.js +33 -0
- package/src/core/output.js +51 -0
- package/src/core/session.js +181 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# plane-cli
|
|
2
|
+
|
|
3
|
+
基于 Plane 外部 API(`/api/v1`)的轻量命令行工具,使用 Node.js 原生能力实现,不依赖第三方包。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @ysicing/plane-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或直接用 `npx`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @ysicing/plane-cli --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 约束
|
|
18
|
+
|
|
19
|
+
- 鉴权方式:`X-Api-Key`
|
|
20
|
+
- 默认面向 `project` 与 `work-item`
|
|
21
|
+
- 暂不提供危险删除命令
|
|
22
|
+
|
|
23
|
+
## 要求
|
|
24
|
+
|
|
25
|
+
- Node.js 22+
|
|
26
|
+
|
|
27
|
+
## 初始化
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node ./src/cli.js config set \
|
|
31
|
+
--base-url https://plane.example.com \
|
|
32
|
+
--api-key your-api-key \
|
|
33
|
+
--workspace your-workspace-slug
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
也支持环境变量覆盖:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export PLANE_BASE_URL=https://plane.example.com
|
|
40
|
+
export PLANE_API_KEY=your-api-key
|
|
41
|
+
export PLANE_WORKSPACE=your-workspace-slug
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 登录并自动生成 API Token
|
|
45
|
+
|
|
46
|
+
普通账号密码登录:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node ./src/cli.js auth login \
|
|
50
|
+
--base-url https://plane.example.com \
|
|
51
|
+
--username you@example.com \
|
|
52
|
+
--password 'your-password'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
LDAP 登录:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node ./src/cli.js auth login \
|
|
59
|
+
--base-url https://plane.example.com \
|
|
60
|
+
--ldap \
|
|
61
|
+
--username your-ldap-account \
|
|
62
|
+
--password 'your-password'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
如果不想把密码放进命令行:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
printf '%s' 'your-password' | node ./src/cli.js auth login \
|
|
69
|
+
--base-url https://plane.example.com \
|
|
70
|
+
--ldap \
|
|
71
|
+
--username your-ldap-account \
|
|
72
|
+
--password-stdin
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
如果账号下有多个 workspace,且没有显式传 `--workspace`,CLI 会保存登录态生成的 API token,但不会自动选中 workspace。此时请先查看并选择:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node ./src/cli.js workspace ls
|
|
79
|
+
node ./src/cli.js workspace current
|
|
80
|
+
node ./src/cli.js workspace use <slug>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 示例
|
|
84
|
+
|
|
85
|
+
```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
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`work-item` 是 `issue` 的别名:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
node ./src/cli.js work-item ls --project <project-id>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 发布
|
|
103
|
+
|
|
104
|
+
当前包名使用 `@ysicing/plane-cli`,因为 `plane-cli` 已被 npm 占用。
|
|
105
|
+
|
|
106
|
+
发布前检查:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm test
|
|
110
|
+
npm run pack:check
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
发布:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm publish
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
如果本机还没登录 npm:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm login
|
|
123
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ysicing/plane-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Node.js CLI for managing Plane via the external API",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+ssh://git@github.com/ysicing/plane-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ysicing/plane-cli#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ysicing/plane-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"plane",
|
|
17
|
+
"cli",
|
|
18
|
+
"project-management",
|
|
19
|
+
"ldap",
|
|
20
|
+
"api"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"plane": "./src/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"start": "node ./src/cli.js",
|
|
35
|
+
"test": "node --test",
|
|
36
|
+
"pack:check": "npm pack --dry-run",
|
|
37
|
+
"prepublishOnly": "npm test && npm run pack:check"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class IssueClient {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
list(projectId, query = {}) {
|
|
7
|
+
return this.client.get(this.client.workspacePath(`/projects/${projectId}/work-items/`), query);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get(projectId, issueId) {
|
|
11
|
+
return this.client.get(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/`));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
create(projectId, body) {
|
|
15
|
+
return this.client.post(this.client.workspacePath(`/projects/${projectId}/work-items/`), body);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
update(projectId, issueId, body) {
|
|
19
|
+
return this.client.patch(this.client.workspacePath(`/projects/${projectId}/work-items/${issueId}/`), body);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class ProjectClient {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
list(query = {}) {
|
|
7
|
+
return this.client.get(this.client.workspacePath("/projects/"), query);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get(projectId) {
|
|
11
|
+
return this.client.get(this.client.workspacePath(`/projects/${projectId}/`));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
create(body) {
|
|
15
|
+
return this.client.post(this.client.workspacePath("/projects/"), body);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
update(projectId, body) {
|
|
19
|
+
return this.client.patch(this.client.workspacePath(`/projects/${projectId}/`), body);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runAuthCommand } from "./commands/auth.js";
|
|
4
|
+
import { runConfigCommand } from "./commands/config.js";
|
|
5
|
+
import { runIssueCommand } from "./commands/issue.js";
|
|
6
|
+
import { runMeCommand } from "./commands/me.js";
|
|
7
|
+
import { runProjectCommand } from "./commands/project.js";
|
|
8
|
+
import { runWorkspaceCommand } from "./commands/workspace.js";
|
|
9
|
+
import { CliError } from "./core/errors.js";
|
|
10
|
+
|
|
11
|
+
function extractGlobalOptions(argv) {
|
|
12
|
+
const args = [];
|
|
13
|
+
const options = {
|
|
14
|
+
json: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
for (const arg of argv) {
|
|
18
|
+
if (arg === "--json") {
|
|
19
|
+
options.json = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
args.push(arg);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { args, options };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printHelp() {
|
|
29
|
+
console.log(`plane-cli
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
plane <command> [options]
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
auth Sign in and bootstrap API credentials
|
|
36
|
+
config Manage local CLI config
|
|
37
|
+
me Show current authenticated user
|
|
38
|
+
project Manage projects
|
|
39
|
+
issue Manage work items through issue commands
|
|
40
|
+
work-item Alias of issue
|
|
41
|
+
workspace Manage selected workspace
|
|
42
|
+
|
|
43
|
+
Global options:
|
|
44
|
+
--json Print raw JSON
|
|
45
|
+
--help Show help
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
const { args, options } = extractGlobalOptions(process.argv.slice(2));
|
|
51
|
+
const [command, ...rest] = args;
|
|
52
|
+
|
|
53
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
54
|
+
printHelp();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const context = { output: options };
|
|
59
|
+
|
|
60
|
+
switch (command) {
|
|
61
|
+
case "auth":
|
|
62
|
+
await runAuthCommand(rest, context);
|
|
63
|
+
return;
|
|
64
|
+
case "config":
|
|
65
|
+
await runConfigCommand(rest, context);
|
|
66
|
+
return;
|
|
67
|
+
case "me":
|
|
68
|
+
await runMeCommand(rest, context);
|
|
69
|
+
return;
|
|
70
|
+
case "project":
|
|
71
|
+
await runProjectCommand(rest, context);
|
|
72
|
+
return;
|
|
73
|
+
case "workspace":
|
|
74
|
+
await runWorkspaceCommand(rest, context);
|
|
75
|
+
return;
|
|
76
|
+
case "issue":
|
|
77
|
+
case "work-item":
|
|
78
|
+
await runIssueCommand(rest, context);
|
|
79
|
+
return;
|
|
80
|
+
default:
|
|
81
|
+
throw new CliError(`Unknown command: ${command}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch((error) => {
|
|
86
|
+
if (error instanceof CliError) {
|
|
87
|
+
console.error(`Error: ${error.message}`);
|
|
88
|
+
if (error.details) {
|
|
89
|
+
console.error(JSON.stringify(error.details, null, 2));
|
|
90
|
+
}
|
|
91
|
+
process.exit(error.exitCode);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.error(error);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { loadConfig, maskApiKey, saveConfig } from "../core/config.js";
|
|
4
|
+
import { CliError } from "../core/errors.js";
|
|
5
|
+
import { ensureValue, parseCommandArgs } from "../core/options.js";
|
|
6
|
+
import { printData } from "../core/output.js";
|
|
7
|
+
import { PlaneSessionClient } from "../core/session.js";
|
|
8
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
console.log(`Usage:
|
|
11
|
+
plane auth login --base-url <plane-url> [--ldap] (--username <value> | --email <value>) [--password <value> | --password-stdin] [--workspace <slug>] [--token-label <label>]
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function defaultTokenLabel() {
|
|
16
|
+
return `plane-cli-${new Date().toISOString()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readPasswordFromStdin() {
|
|
20
|
+
const content = await readFile(0, "utf8");
|
|
21
|
+
return content.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveWorkspaceToSave({ explicitWorkspace, existingWorkspace, userWorkspaces }) {
|
|
25
|
+
const known = Array.isArray(userWorkspaces) ? userWorkspaces.map((item) => item.slug) : [];
|
|
26
|
+
|
|
27
|
+
if (explicitWorkspace) {
|
|
28
|
+
if (!known.includes(explicitWorkspace)) {
|
|
29
|
+
throw new CliError(`Workspace not found for this account: ${explicitWorkspace}`, {
|
|
30
|
+
details: {
|
|
31
|
+
knownWorkspaces: known,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return explicitWorkspace;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (known.length === 1) {
|
|
39
|
+
return known[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (known.length > 1) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return existingWorkspace || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runAuthCommand(args, context) {
|
|
50
|
+
const [subcommand, ...rest] = args;
|
|
51
|
+
|
|
52
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
53
|
+
printHelp();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (subcommand !== "login") {
|
|
58
|
+
throw new CliError(`Unknown auth subcommand: ${subcommand}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (rest.includes("--help") || rest.includes("help")) {
|
|
62
|
+
printHelp();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parsed = parseCommandArgs(
|
|
67
|
+
rest,
|
|
68
|
+
{
|
|
69
|
+
"base-url": { type: "string" },
|
|
70
|
+
ldap: { type: "boolean" },
|
|
71
|
+
username: { type: "string" },
|
|
72
|
+
email: { type: "string" },
|
|
73
|
+
password: { type: "string" },
|
|
74
|
+
"password-stdin": { type: "boolean" },
|
|
75
|
+
workspace: { type: "string" },
|
|
76
|
+
"token-label": { type: "string" },
|
|
77
|
+
},
|
|
78
|
+
false
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const existingConfig = await loadConfig();
|
|
82
|
+
const baseUrl = parsed.values["base-url"] || existingConfig.baseUrl;
|
|
83
|
+
ensureValue(baseUrl, "Base URL is required. Pass --base-url or set it with `plane config set --base-url ...`.");
|
|
84
|
+
|
|
85
|
+
const username = parsed.values.username || parsed.values.email;
|
|
86
|
+
ensureValue(username, "Username is required. Pass --username or --email.");
|
|
87
|
+
|
|
88
|
+
let password = parsed.values.password;
|
|
89
|
+
if (!password && parsed.values["password-stdin"]) {
|
|
90
|
+
password = await readPasswordFromStdin();
|
|
91
|
+
}
|
|
92
|
+
ensureValue(password, "Password is required. Pass --password or --password-stdin.");
|
|
93
|
+
|
|
94
|
+
const sessionClient = new PlaneSessionClient(baseUrl);
|
|
95
|
+
await sessionClient.login({
|
|
96
|
+
username,
|
|
97
|
+
password,
|
|
98
|
+
ldap: Boolean(parsed.values.ldap),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const tokenLabel = parsed.values["token-label"] || defaultTokenLabel();
|
|
102
|
+
const token = await sessionClient.createApiToken({
|
|
103
|
+
label: tokenLabel,
|
|
104
|
+
description: "Created by plane-cli auth login",
|
|
105
|
+
});
|
|
106
|
+
const workspaces = await sessionClient.listUserWorkspaces();
|
|
107
|
+
const knownWorkspaces = Array.isArray(workspaces) ? workspaces.map((item) => item.slug) : [];
|
|
108
|
+
|
|
109
|
+
const workspace = resolveWorkspaceToSave({
|
|
110
|
+
explicitWorkspace: parsed.values.workspace,
|
|
111
|
+
existingWorkspace: existingConfig.workspace,
|
|
112
|
+
userWorkspaces: workspaces,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const saved = await saveConfig({
|
|
116
|
+
baseUrl,
|
|
117
|
+
apiKey: token.token,
|
|
118
|
+
workspace,
|
|
119
|
+
knownWorkspaces,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
printData(
|
|
123
|
+
{
|
|
124
|
+
path: saved.path,
|
|
125
|
+
baseUrl: saved.config.baseUrl,
|
|
126
|
+
workspace: saved.config.workspace || "",
|
|
127
|
+
apiKey: maskApiKey(saved.config.apiKey),
|
|
128
|
+
loginMode: parsed.values.ldap ? "ldap" : "password",
|
|
129
|
+
tokenId: token.id,
|
|
130
|
+
tokenLabel: token.label,
|
|
131
|
+
detectedWorkspaces: knownWorkspaces,
|
|
132
|
+
workspaceSelectionRequired: !saved.config.workspace && knownWorkspaces.length > 1,
|
|
133
|
+
},
|
|
134
|
+
context.output
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { loadConfig, maskApiKey, resolveConfigPath, saveConfig } from "../core/config.js";
|
|
2
|
+
import { CliError } from "../core/errors.js";
|
|
3
|
+
import { parseCommandArgs } from "../core/options.js";
|
|
4
|
+
import { printData } from "../core/output.js";
|
|
5
|
+
|
|
6
|
+
function configSnapshot(config, path) {
|
|
7
|
+
return {
|
|
8
|
+
path,
|
|
9
|
+
baseUrl: config.baseUrl || "",
|
|
10
|
+
apiKey: maskApiKey(config.apiKey),
|
|
11
|
+
workspace: config.workspace || "",
|
|
12
|
+
knownWorkspaces: config.knownWorkspaces || [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`Usage:
|
|
18
|
+
plane config list
|
|
19
|
+
plane config get [baseUrl|apiKey|workspace|knownWorkspaces]
|
|
20
|
+
plane config set [--base-url <url>] [--api-key <key>] [--workspace <slug>]
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runConfigCommand(args, context) {
|
|
25
|
+
const [subcommand = "list", ...rest] = args;
|
|
26
|
+
const path = resolveConfigPath();
|
|
27
|
+
|
|
28
|
+
if (subcommand === "--help" || subcommand === "help") {
|
|
29
|
+
printHelp();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (subcommand === "list") {
|
|
34
|
+
const config = await loadConfig();
|
|
35
|
+
printData(configSnapshot(config, path), context.output);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (subcommand === "get") {
|
|
40
|
+
const [key] = rest;
|
|
41
|
+
const config = await loadConfig();
|
|
42
|
+
const snapshot = configSnapshot(config, path);
|
|
43
|
+
|
|
44
|
+
if (!key) {
|
|
45
|
+
printData(snapshot, context.output);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!(key in snapshot)) {
|
|
50
|
+
throw new CliError(`Unknown config key: ${key}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
printData({ [key]: snapshot[key] }, context.output);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (subcommand === "set") {
|
|
58
|
+
const parsed = parseCommandArgs(
|
|
59
|
+
rest,
|
|
60
|
+
{
|
|
61
|
+
"base-url": { type: "string" },
|
|
62
|
+
"api-key": { type: "string" },
|
|
63
|
+
workspace: { type: "string" },
|
|
64
|
+
},
|
|
65
|
+
false
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const update = {
|
|
69
|
+
baseUrl: parsed.values["base-url"],
|
|
70
|
+
apiKey: parsed.values["api-key"],
|
|
71
|
+
workspace: parsed.values.workspace,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!update.baseUrl && !update.apiKey && !update.workspace) {
|
|
75
|
+
throw new CliError("Nothing to update. Pass at least one of --base-url, --api-key, or --workspace.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await saveConfig(update);
|
|
79
|
+
printData(configSnapshot(result.config, result.path), context.output);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new CliError(`Unknown config subcommand: ${subcommand}`);
|
|
84
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { IssueClient } from "../api/issue-client.js";
|
|
2
|
+
import { resolveRuntimeConfig } from "../core/config.js";
|
|
3
|
+
import { CliError } from "../core/errors.js";
|
|
4
|
+
import { PlaneClient } from "../core/http.js";
|
|
5
|
+
import { ensureValue, parseCommandArgs, pickDefined, splitCsv } from "../core/options.js";
|
|
6
|
+
import { printData, printTable } from "../core/output.js";
|
|
7
|
+
|
|
8
|
+
function renderIssueList(data) {
|
|
9
|
+
const rows = Array.isArray(data) ? data : data.results || [];
|
|
10
|
+
printTable(rows, [
|
|
11
|
+
{ label: "ID", get: (row) => row.id },
|
|
12
|
+
{ label: "Seq", get: (row) => row.sequence_id || "" },
|
|
13
|
+
{ label: "Name", get: (row) => row.name },
|
|
14
|
+
{ label: "Priority", get: (row) => row.priority || "" },
|
|
15
|
+
{ label: "State", get: (row) => row.state || row.state_id || "" },
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildIssuePayload(values) {
|
|
20
|
+
return pickDefined({
|
|
21
|
+
name: values.name,
|
|
22
|
+
description_html: values["description-html"],
|
|
23
|
+
state: values.state,
|
|
24
|
+
priority: values.priority,
|
|
25
|
+
start_date: values["start-date"],
|
|
26
|
+
target_date: values["target-date"],
|
|
27
|
+
parent: values.parent,
|
|
28
|
+
type_id: values["type-id"],
|
|
29
|
+
assignees: splitCsv(values.assignees),
|
|
30
|
+
labels: splitCsv(values.labels),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printHelp() {
|
|
35
|
+
console.log(`Usage:
|
|
36
|
+
plane issue ls --project <project-id> [--limit <n>] [--cursor <cursor>] [--order-by <field>] [--state <state-id>] [--priority <value>] [--assignees <id1,id2>] [--expand <field1,field2>]
|
|
37
|
+
plane issue get --project <project-id> <issue-id>
|
|
38
|
+
plane issue create --project <project-id> --name <name> [--description-html <html>] [--state <state-id>] [--priority <value>] [--assignees <id1,id2>] [--labels <id1,id2>]
|
|
39
|
+
plane issue update --project <project-id> <issue-id> [--name <name>] [--description-html <html>] [--state <state-id>] [--priority <value>] [--assignees <id1,id2>] [--labels <id1,id2>]
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runIssueCommand(args, context) {
|
|
44
|
+
const [subcommand, ...rest] = args;
|
|
45
|
+
|
|
46
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
47
|
+
printHelp();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = await resolveRuntimeConfig();
|
|
52
|
+
const issueClient = new IssueClient(new PlaneClient(config));
|
|
53
|
+
|
|
54
|
+
if (subcommand === "ls") {
|
|
55
|
+
const parsed = parseCommandArgs(
|
|
56
|
+
rest,
|
|
57
|
+
{
|
|
58
|
+
project: { type: "string" },
|
|
59
|
+
limit: { type: "string" },
|
|
60
|
+
cursor: { type: "string" },
|
|
61
|
+
"order-by": { type: "string" },
|
|
62
|
+
state: { type: "string" },
|
|
63
|
+
priority: { type: "string" },
|
|
64
|
+
assignees: { type: "string" },
|
|
65
|
+
labels: { type: "string" },
|
|
66
|
+
expand: { type: "string" },
|
|
67
|
+
fields: { type: "string" },
|
|
68
|
+
},
|
|
69
|
+
false
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
73
|
+
|
|
74
|
+
const result = await issueClient.list(
|
|
75
|
+
parsed.values.project,
|
|
76
|
+
pickDefined({
|
|
77
|
+
per_page: parsed.values.limit,
|
|
78
|
+
cursor: parsed.values.cursor,
|
|
79
|
+
order_by: parsed.values["order-by"],
|
|
80
|
+
state: parsed.values.state,
|
|
81
|
+
priority: parsed.values.priority,
|
|
82
|
+
assignees: parsed.values.assignees,
|
|
83
|
+
labels: parsed.values.labels,
|
|
84
|
+
expand: parsed.values.expand,
|
|
85
|
+
fields: parsed.values.fields,
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
printData(result, {
|
|
90
|
+
...context.output,
|
|
91
|
+
render: renderIssueList,
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (subcommand === "get") {
|
|
97
|
+
const parsed = parseCommandArgs(
|
|
98
|
+
rest,
|
|
99
|
+
{
|
|
100
|
+
project: { type: "string" },
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
105
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
106
|
+
|
|
107
|
+
const result = await issueClient.get(parsed.values.project, parsed.positionals[0]);
|
|
108
|
+
printData(result, context.output);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (subcommand === "create") {
|
|
113
|
+
const parsed = parseCommandArgs(
|
|
114
|
+
rest,
|
|
115
|
+
{
|
|
116
|
+
project: { type: "string" },
|
|
117
|
+
name: { type: "string" },
|
|
118
|
+
"description-html": { type: "string" },
|
|
119
|
+
state: { type: "string" },
|
|
120
|
+
priority: { type: "string" },
|
|
121
|
+
assignees: { type: "string" },
|
|
122
|
+
labels: { type: "string" },
|
|
123
|
+
"start-date": { type: "string" },
|
|
124
|
+
"target-date": { type: "string" },
|
|
125
|
+
parent: { type: "string" },
|
|
126
|
+
"type-id": { type: "string" },
|
|
127
|
+
},
|
|
128
|
+
false
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
132
|
+
ensureValue(parsed.values.name, "Issue name is required.");
|
|
133
|
+
|
|
134
|
+
const result = await issueClient.create(parsed.values.project, buildIssuePayload(parsed.values));
|
|
135
|
+
printData(result, context.output);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (subcommand === "update") {
|
|
140
|
+
const parsed = parseCommandArgs(
|
|
141
|
+
rest,
|
|
142
|
+
{
|
|
143
|
+
project: { type: "string" },
|
|
144
|
+
name: { type: "string" },
|
|
145
|
+
"description-html": { type: "string" },
|
|
146
|
+
state: { type: "string" },
|
|
147
|
+
priority: { type: "string" },
|
|
148
|
+
assignees: { type: "string" },
|
|
149
|
+
labels: { type: "string" },
|
|
150
|
+
"start-date": { type: "string" },
|
|
151
|
+
"target-date": { type: "string" },
|
|
152
|
+
parent: { type: "string" },
|
|
153
|
+
"type-id": { type: "string" },
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
ensureValue(parsed.values.project, "Project ID is required.");
|
|
158
|
+
ensureValue(parsed.positionals[0], "Issue ID is required.");
|
|
159
|
+
|
|
160
|
+
const payload = buildIssuePayload(parsed.values);
|
|
161
|
+
if (Object.keys(payload).length === 0) {
|
|
162
|
+
throw new CliError("At least one update field is required.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await issueClient.update(parsed.values.project, parsed.positionals[0], payload);
|
|
166
|
+
printData(result, context.output);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new CliError(`Unknown issue subcommand: ${subcommand}`);
|
|
171
|
+
}
|