@metaphorli/pingcode-cli 0.3.2 → 0.3.3
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 +72 -9
- package/package.json +2 -2
- package/scripts/commands/auth.js +221 -24
- package/scripts/commands/comment.js +556 -0
- package/scripts/commands/context.js +15 -111
- package/scripts/commands/idea.js +1121 -0
- package/scripts/commands/product.js +236 -0
- package/scripts/commands/shared.js +107 -2
- package/scripts/commands/workitem.js +24 -179
- package/scripts/core.js +17 -0
- package/scripts/pingcode.js +3 -0
- package/skills/pingcode/SKILL.md +23 -8
- package/skills/pingcode/references/auth.md +55 -1
- package/skills/pingcode/references/comment.md +52 -0
- package/skills/pingcode/references/ctx.md +3 -3
- package/skills/pingcode/references/idea.md +70 -0
- package/skills/pingcode/references/product.md +48 -0
- package/skills/pingcode/references/workitem.md +2 -4
package/README.md
CHANGED
|
@@ -15,10 +15,12 @@ npx @metaphorli/pingcode-cli@latest
|
|
|
15
15
|
~/.codex/skills/pingcode/references/auth.md
|
|
16
16
|
~/.codex/skills/pingcode/references/ctx.md
|
|
17
17
|
~/.codex/skills/pingcode/references/workitem.md
|
|
18
|
+
~/.codex/skills/pingcode/references/comment.md
|
|
18
19
|
~/.config/opencode/skills/pingcode
|
|
19
20
|
~/.config/opencode/skills/pingcode/references/auth.md
|
|
20
21
|
~/.config/opencode/skills/pingcode/references/ctx.md
|
|
21
22
|
~/.config/opencode/skills/pingcode/references/workitem.md
|
|
23
|
+
~/.config/opencode/skills/pingcode/references/comment.md
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
默认会进入交互式安装,先选择“全局 / 项目级”,再选择要安装的 Agent;在 CI 或脚本中可以使用 `--non-interactive` 保持旧的静默自动安装行为。任何一个已选择目录写入失败(权限、磁盘等问题)不会阻断其他目录,安装结束时会打印每个目录的成功/失败/跳过摘要。
|
|
@@ -120,9 +122,12 @@ npx @metaphorli/pingcode-cli@latest --target ".opencode/skills" --force
|
|
|
120
122
|
- 查询项目、迭代、看板、工作项类型、状态、优先级
|
|
121
123
|
- 查询、创建、更新工作项(含状态更新,支持 `--dry-run` 试运行)
|
|
122
124
|
- 在故事下创建子工作项(通过 `--parent`)
|
|
123
|
-
-
|
|
125
|
+
- 创建、查看、删除工作项评论(通过 `--reply-to` 支持回复)
|
|
126
|
+
- 查询、创建、更新需求(idea)
|
|
127
|
+
- 查询产品列表和产品详情(`product list / get`)
|
|
128
|
+
- 通过子命令(`context *`, `workitem *`, `auth *`, `comment *`, `idea *`, `product *`)调用 PingCode API
|
|
124
129
|
|
|
125
|
-
以上能力由同一个 `pingcode` skill 统一提供,并通过 `references/` 下的 `auth.md`、`ctx.md`、`workitem.md`
|
|
130
|
+
以上能力由同一个 `pingcode` skill 统一提供,并通过 `references/` 下的 `auth.md`、`ctx.md`、`workitem.md`、`comment.md` 分别补充用户令牌登录、工作区上下文初始化、工作项操作、评论操作的详细参考。
|
|
126
131
|
|
|
127
132
|
## Skill 结构
|
|
128
133
|
|
|
@@ -133,7 +138,10 @@ npx @metaphorli/pingcode-cli@latest --target ".opencode/skills" --force
|
|
|
133
138
|
| `pingcode/SKILL.md` | 路由入口,统一提供 PingCode CLI 的各项能力 |
|
|
134
139
|
| `pingcode/references/auth.md` | 用户令牌登录:`pingcode auth login`、grant-type 自动识别与覆盖、令牌类型说明 |
|
|
135
140
|
| `pingcode/references/ctx.md` | 工作区上下文初始化:在 Agent 前台按编号选择当前项目、迭代、用户并写入缓存 |
|
|
136
|
-
| `pingcode/references/workitem.md` | 工作项操作:`workitem list / create /
|
|
141
|
+
| `pingcode/references/workitem.md` | 工作项操作:`workitem list / create / get / update` 子命令及完整参数;含共享安全规则 |
|
|
142
|
+
| `pingcode/references/idea.md` | 需求操作:`idea list / create / update / get / search` 等子命令及安全规则 |
|
|
143
|
+
| `pingcode/references/product.md` | 产品操作:`product list / get` 子命令及安全规则 |
|
|
144
|
+
| `pingcode/references/comment.md` | 评论操作:`comment create / list / get / delete` 子命令及安全规则 |
|
|
137
145
|
|
|
138
146
|
## 子命令
|
|
139
147
|
|
|
@@ -144,7 +152,7 @@ PingCode CLI 通过子命令管理配置和工作项。
|
|
|
144
152
|
| 子命令 | 说明 | 示例 |
|
|
145
153
|
|---|---|---|
|
|
146
154
|
| `context init` | 交互式初始化工作区上下文 | `pingcode context init` |
|
|
147
|
-
| `context list` |
|
|
155
|
+
| `context list` | 以 JSON 输出当前偏好和字典条目数量 | `pingcode context list` |
|
|
148
156
|
| `context set-current-user <id>` | 设置当前用户 | `pingcode context set-current-user @me` |
|
|
149
157
|
| `context set-current-project <id>` | 设置当前项目 | `pingcode context set-current-project PROJECT_ID` |
|
|
150
158
|
| `context set-current-sprint <id>` | 设置当前迭代 | `pingcode context set-current-sprint SPRINT_ID` |
|
|
@@ -166,8 +174,7 @@ pingcode context set-current-project my-project
|
|
|
166
174
|
|---|---|---|
|
|
167
175
|
| `workitem list` | 列出工作项(自动加当前用户/项目/迭代过滤) | `pingcode workitem list --assignee @me --state 进行中` |
|
|
168
176
|
| `workitem create` | 创建工作项 | `pingcode workitem create --title "新任务" --type task` |
|
|
169
|
-
| `workitem
|
|
170
|
-
| `workitem get <id|identifier>` | 获取单个工作项(官方单个工作项接口;identifier 会先解析为 id) | `pingcode workitem get WORK_ITEM_ID` |
|
|
177
|
+
| `workitem get <id|identifier>` | 获取单个工作项(官方单个工作项接口;identifier 会先解析为 id) | `pingcode workitem get SCR-123` |
|
|
171
178
|
| `workitem update <id>` | 更新工作项 | `pingcode workitem update SCR-123 --state 已完成` |
|
|
172
179
|
|
|
173
180
|
```bash
|
|
@@ -183,8 +190,8 @@ pingcode workitem list --keywords "登录页面" --compact
|
|
|
183
190
|
# 创建工作项(默认负责人为当前用户)
|
|
184
191
|
pingcode workitem create --title "实现登录页面" --type task --project "Core" --sprint "Sprint 1"
|
|
185
192
|
|
|
186
|
-
#
|
|
187
|
-
pingcode workitem
|
|
193
|
+
# 通过编号获取工作项
|
|
194
|
+
pingcode workitem get SCR-123
|
|
188
195
|
|
|
189
196
|
# 通过 id 获取单个工作项(官方单个工作项接口)
|
|
190
197
|
pingcode workitem get WORK_ITEM_ID
|
|
@@ -203,6 +210,58 @@ pingcode workitem create --title "test" --type task --dry-run
|
|
|
203
210
|
pingcode workitem update SCR-123 --state 已完成 --dry-run
|
|
204
211
|
```
|
|
205
212
|
|
|
213
|
+
### 产品管理 (`product`)
|
|
214
|
+
|
|
215
|
+
| 子命令 | 说明 | 示例 |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `product list` | 列出产品 | `pingcode product list --compact` |
|
|
218
|
+
| `product get <id>` | 获取单个产品详情 | `pingcode product get PRODUCT_ID` |
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# 查看产品列表
|
|
222
|
+
pingcode product list --compact
|
|
223
|
+
|
|
224
|
+
# 按关键词搜索产品
|
|
225
|
+
pingcode product list --keywords "核心产品" --compact
|
|
226
|
+
|
|
227
|
+
# 获取产品详情
|
|
228
|
+
pingcode product get PRODUCT_ID
|
|
229
|
+
|
|
230
|
+
# 试运行(预览 API 请求,不发送)
|
|
231
|
+
pingcode product list --dry-run
|
|
232
|
+
pingcode product get PRODUCT_ID --dry-run
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 评论管理 (`comment`)
|
|
236
|
+
|
|
237
|
+
| 子命令 | 说明 | 示例 |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `comment create <id\|identifier>` | 创建工作项评论 | `pingcode comment create SCR-123 --content "需要优化性能"` |
|
|
240
|
+
| `comment list <id\|identifier>` | 列出工作项评论 | `pingcode comment list SCR-123 --compact` |
|
|
241
|
+
| `comment get <comment-id> <id\|identifier>` | 获取单条评论 | `pingcode comment get cmt-456 SCR-123` |
|
|
242
|
+
| `comment delete <comment-id> <id\|identifier>` | 删除评论 | `pingcode comment delete cmt-456 SCR-123` |
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# 创建工作项评论
|
|
246
|
+
pingcode comment create SCR-123 --content "这个工作项需要考虑性能优化"
|
|
247
|
+
|
|
248
|
+
# 创建回复评论
|
|
249
|
+
pingcode comment create SCR-123 --content "已确认,稍后处理" --reply-to cmt-456
|
|
250
|
+
|
|
251
|
+
# 查看评论列表
|
|
252
|
+
pingcode comment list SCR-123 --compact
|
|
253
|
+
|
|
254
|
+
# 获取单条评论
|
|
255
|
+
pingcode comment get cmt-456 SCR-123
|
|
256
|
+
|
|
257
|
+
# 删除评论
|
|
258
|
+
pingcode comment delete cmt-456 SCR-123
|
|
259
|
+
|
|
260
|
+
# 试运行(预览 API 请求,不发送)
|
|
261
|
+
pingcode comment create SCR-123 --content "hello" --dry-run
|
|
262
|
+
pingcode comment delete cmt-456 SCR-123 --dry-run
|
|
263
|
+
```
|
|
264
|
+
|
|
206
265
|
## 凭证配置
|
|
207
266
|
|
|
208
267
|
在 PingCode 企业后台创建应用,配置数据访问范围,然后设置环境变量:
|
|
@@ -231,9 +290,12 @@ export PINGCODE_USER_ID="你的 PingCode 用户 ID"
|
|
|
231
290
|
除了 `client_credentials` 企业令牌,CLI 也支持通过 OAuth2 `authorization_code` 获取用户令牌。用户令牌代表具体的人类用户,适合需要以个人身份操作 PingCode 的场景。
|
|
232
291
|
|
|
233
292
|
```bash
|
|
234
|
-
#
|
|
293
|
+
# 默认行为:打印授权 URL 并提示粘贴授权码
|
|
235
294
|
pingcode auth login --client-id ID --client-secret SECRET
|
|
236
295
|
|
|
296
|
+
# 自动打开浏览器完成授权
|
|
297
|
+
pingcode auth login --client-id ID --client-secret SECRET --browser
|
|
298
|
+
|
|
237
299
|
# 使用用户令牌查询工作项
|
|
238
300
|
# --grant-type 会自动从缓存中识别,且 workitem list 不再默认按当前用户过滤
|
|
239
301
|
pingcode workitem list --state 进行中 --compact
|
|
@@ -290,5 +352,6 @@ pingcode context init
|
|
|
290
352
|
- 用户令牌登录:[skills/pingcode/references/auth.md](skills/pingcode/references/auth.md)
|
|
291
353
|
- 工作区上下文:[skills/pingcode/references/ctx.md](skills/pingcode/references/ctx.md)
|
|
292
354
|
- 工作项操作:[skills/pingcode/references/workitem.md](skills/pingcode/references/workitem.md)
|
|
355
|
+
- 评论操作:[skills/pingcode/references/comment.md](skills/pingcode/references/comment.md)
|
|
293
356
|
- 官方文档:https://open.pingcode.com/
|
|
294
357
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaphorli/pingcode-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "PingCode CLI and Codex skill, written in Node.js.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"bin/install.js"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node --test tests/test_pingcode.js tests/test_install.js tests/test_context.js tests/test_workitem.js tests/test_auth.js",
|
|
29
|
+
"test": "node --test tests/test_pingcode.js tests/test_install.js tests/test_context.js tests/test_workitem.js tests/test_auth.js tests/test_comment.js tests/test_idea.js tests/test_product.js",
|
|
30
30
|
"pack:check": "npm pack --dry-run"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
package/scripts/commands/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
4
5
|
const os = require('node:os');
|
|
5
6
|
const readline = require('node:readline');
|
|
6
7
|
const { spawn } = require('node:child_process');
|
|
@@ -16,7 +17,7 @@ const DEFAULT_PORT = 8765;
|
|
|
16
17
|
// ── Flag maps ──────────────────────────────────────────────────────────
|
|
17
18
|
|
|
18
19
|
const BOOLEAN_FLAGS = new Set([
|
|
19
|
-
'--
|
|
20
|
+
'--browser', '--no-token-cache', '--no-workspace-cache', '--dry-run',
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
const STRING_FLAGS = {
|
|
@@ -31,6 +32,20 @@ const STRING_FLAGS = {
|
|
|
31
32
|
'--workspace-cache': 'workspace_cache',
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
const STATUS_BOOLEAN_FLAGS = new Set([
|
|
36
|
+
'--no-token-cache', '--no-workspace-cache', '--dry-run', '--compact',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const STATUS_STRING_FLAGS = {
|
|
40
|
+
'--grant-type': 'grant_type',
|
|
41
|
+
'--client-id': 'client_id',
|
|
42
|
+
'--client-secret': 'client_secret',
|
|
43
|
+
'--base-url': 'base_url',
|
|
44
|
+
'--token-cache': 'token_cache',
|
|
45
|
+
'--workspace-cache': 'workspace_cache',
|
|
46
|
+
'--token': 'token',
|
|
47
|
+
};
|
|
48
|
+
|
|
34
49
|
// ── Parser ─────────────────────────────────────────────────────────────
|
|
35
50
|
|
|
36
51
|
function parseLoginArgs(tokens) {
|
|
@@ -46,7 +61,7 @@ function parseLoginArgs(tokens) {
|
|
|
46
61
|
base_url: process.env.PINGCODE_BASE_URL || core.DEFAULT_BASE_URL,
|
|
47
62
|
token_cache: process.env.PINGCODE_TOKEN_CACHE || core.DEFAULT_TOKEN_CACHE,
|
|
48
63
|
workspace_cache: process.env.PINGCODE_WORKSPACE_CACHE || core.DEFAULT_WORKSPACE_CACHE,
|
|
49
|
-
|
|
64
|
+
browser: true,
|
|
50
65
|
no_token_cache: false,
|
|
51
66
|
no_workspace_cache: false,
|
|
52
67
|
dry_run: false,
|
|
@@ -60,6 +75,10 @@ function parseLoginArgs(tokens) {
|
|
|
60
75
|
helpRequested = true;
|
|
61
76
|
continue;
|
|
62
77
|
}
|
|
78
|
+
if (arg === '--no-browser') {
|
|
79
|
+
opts.browser = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
63
82
|
if (BOOLEAN_FLAGS.has(arg)) {
|
|
64
83
|
const key = arg.replace(/^--/, '').replace(/-/g, '_');
|
|
65
84
|
opts[key] = true;
|
|
@@ -104,6 +123,64 @@ function parseLoginArgs(tokens) {
|
|
|
104
123
|
return { opts, helpRequested };
|
|
105
124
|
}
|
|
106
125
|
|
|
126
|
+
function parseStatusArgs(tokens) {
|
|
127
|
+
const opts = {
|
|
128
|
+
base_url: process.env.PINGCODE_BASE_URL || core.DEFAULT_BASE_URL,
|
|
129
|
+
client_id: process.env.PINGCODE_CLIENT_ID || null,
|
|
130
|
+
client_secret: process.env.PINGCODE_CLIENT_SECRET || null,
|
|
131
|
+
grant_type: process.env.PINGCODE_GRANT_TYPE || 'auto',
|
|
132
|
+
token_cache: process.env.PINGCODE_TOKEN_CACHE || core.DEFAULT_TOKEN_CACHE,
|
|
133
|
+
workspace_cache: process.env.PINGCODE_WORKSPACE_CACHE || core.DEFAULT_WORKSPACE_CACHE,
|
|
134
|
+
token: process.env.PINGCODE_ACCESS_TOKEN || null,
|
|
135
|
+
no_token_cache: false,
|
|
136
|
+
no_workspace_cache: false,
|
|
137
|
+
dry_run: false,
|
|
138
|
+
compact: false,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let helpRequested = false;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
144
|
+
const arg = tokens[i];
|
|
145
|
+
if (arg === '--help' || arg === '-h') {
|
|
146
|
+
helpRequested = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (STATUS_BOOLEAN_FLAGS.has(arg)) {
|
|
150
|
+
const key = arg.replace(/^--/, '').replace(/-/g, '_');
|
|
151
|
+
opts[key] = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg.startsWith('--')) {
|
|
155
|
+
const eqIndex = arg.indexOf('=');
|
|
156
|
+
let flag, value;
|
|
157
|
+
if (eqIndex !== -1) {
|
|
158
|
+
flag = arg.slice(0, eqIndex);
|
|
159
|
+
value = arg.slice(eqIndex + 1);
|
|
160
|
+
} else {
|
|
161
|
+
flag = arg;
|
|
162
|
+
if (!(flag in STATUS_STRING_FLAGS)) {
|
|
163
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
164
|
+
}
|
|
165
|
+
if (i + 1 < tokens.length) {
|
|
166
|
+
value = tokens[i + 1];
|
|
167
|
+
i += 1;
|
|
168
|
+
} else {
|
|
169
|
+
throw new core.PingCodeError(`Flag ${flag} requires a value`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!(flag in STATUS_STRING_FLAGS)) {
|
|
173
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
174
|
+
}
|
|
175
|
+
opts[STATUS_STRING_FLAGS[flag]] = value;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use auth status --help for usage.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { opts, helpRequested };
|
|
182
|
+
}
|
|
183
|
+
|
|
107
184
|
// ── Client ─────────────────────────────────────────────────────────────
|
|
108
185
|
|
|
109
186
|
function createClient(opts) {
|
|
@@ -113,6 +190,7 @@ function createClient(opts) {
|
|
|
113
190
|
base_url: opts.base_url,
|
|
114
191
|
client_id: opts.client_id,
|
|
115
192
|
client_secret: opts.client_secret,
|
|
193
|
+
token: opts.token || null,
|
|
116
194
|
token_cache: tokenCache,
|
|
117
195
|
workspace_cache: workspaceCache,
|
|
118
196
|
grant_type: opts.grant_type,
|
|
@@ -121,7 +199,7 @@ function createClient(opts) {
|
|
|
121
199
|
|
|
122
200
|
// ── Browser opening ────────────────────────────────────────────────────
|
|
123
201
|
|
|
124
|
-
function openBrowser(url) {
|
|
202
|
+
function openBrowser(url, spawner = spawn) {
|
|
125
203
|
const platform = os.platform();
|
|
126
204
|
let command, args;
|
|
127
205
|
|
|
@@ -137,17 +215,20 @@ function openBrowser(url) {
|
|
|
137
215
|
}
|
|
138
216
|
|
|
139
217
|
return new Promise((resolve, reject) => {
|
|
140
|
-
|
|
218
|
+
let settled = false;
|
|
219
|
+
const child = spawner(command, args, {
|
|
141
220
|
stdio: 'ignore',
|
|
142
221
|
detached: true,
|
|
143
222
|
});
|
|
144
|
-
child.on('error', (err) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
223
|
+
child.on('error', (err) => {
|
|
224
|
+
if (settled) return;
|
|
225
|
+
settled = true;
|
|
226
|
+
reject(err);
|
|
227
|
+
});
|
|
228
|
+
child.on('spawn', () => {
|
|
229
|
+
if (settled) return;
|
|
230
|
+
settled = true;
|
|
231
|
+
resolve(true);
|
|
151
232
|
});
|
|
152
233
|
child.unref();
|
|
153
234
|
});
|
|
@@ -163,6 +244,7 @@ function printHelp() {
|
|
|
163
244
|
'',
|
|
164
245
|
'Subcommands:',
|
|
165
246
|
' login Authenticate with your PingCode user account (OAuth2 authorization_code)',
|
|
247
|
+
' status Show current authentication status',
|
|
166
248
|
'',
|
|
167
249
|
'Run `pingcode auth <subcommand> --help` for subcommand-specific usage.',
|
|
168
250
|
].join('\n'));
|
|
@@ -178,7 +260,8 @@ function printLoginHelp() {
|
|
|
178
260
|
' --redirect-uri URI Redirect URI for OAuth callback',
|
|
179
261
|
` (default: ${DEFAULT_REDIRECT_URI})`,
|
|
180
262
|
` --port PORT Local callback server port (default: ${DEFAULT_PORT})`,
|
|
181
|
-
' --
|
|
263
|
+
' --browser Open browser for authorization (default: true)',
|
|
264
|
+
' --no-browser Print the authorization URL and prompt for code instead of opening browser',
|
|
182
265
|
' --code CODE Authorization code (skip browser and callback)',
|
|
183
266
|
' --grant-type TYPE OAuth grant type (default: authorization_code)',
|
|
184
267
|
' --client-id ID OAuth client ID',
|
|
@@ -202,6 +285,36 @@ function printLoginHelp() {
|
|
|
202
285
|
].join('\n'));
|
|
203
286
|
}
|
|
204
287
|
|
|
288
|
+
function printStatusHelp() {
|
|
289
|
+
console.log([
|
|
290
|
+
'PingCode auth status — Show current authentication status',
|
|
291
|
+
'',
|
|
292
|
+
'Usage: pingcode auth status [options]',
|
|
293
|
+
'',
|
|
294
|
+
'Options:',
|
|
295
|
+
' --grant-type TYPE OAuth grant type (default: auto)',
|
|
296
|
+
' --client-id ID OAuth client ID',
|
|
297
|
+
' --client-secret SECRET OAuth client secret',
|
|
298
|
+
' --base-url URL PingCode base URL',
|
|
299
|
+
' --token-cache PATH Token cache file path',
|
|
300
|
+
' --no-token-cache Disable token cache',
|
|
301
|
+
' --workspace-cache PATH Workspace cache file path',
|
|
302
|
+
' --no-workspace-cache Disable workspace cache',
|
|
303
|
+
' --token TOKEN Use an explicit access token (not saved)',
|
|
304
|
+
' --dry-run Show planned actions without executing',
|
|
305
|
+
' --compact Show compact output',
|
|
306
|
+
' --help Show this help',
|
|
307
|
+
'',
|
|
308
|
+
'Credentials can also be set via environment variables:',
|
|
309
|
+
' PINGCODE_CLIENT_ID OAuth client ID',
|
|
310
|
+
' PINGCODE_CLIENT_SECRET OAuth client secret',
|
|
311
|
+
' PINGCODE_BASE_URL PingCode base URL',
|
|
312
|
+
' PINGCODE_ACCESS_TOKEN Explicit access token (not saved)',
|
|
313
|
+
' PINGCODE_TOKEN_CACHE Token cache file path',
|
|
314
|
+
' PINGCODE_WORKSPACE_CACHE Workspace cache file path',
|
|
315
|
+
].join('\n'));
|
|
316
|
+
}
|
|
317
|
+
|
|
205
318
|
// ── Helper: prompt for code from stdin ──────────────────────────────────
|
|
206
319
|
|
|
207
320
|
function promptForCode(inputFunc) {
|
|
@@ -254,8 +367,10 @@ function buildDryRunExchange(opts, code) {
|
|
|
254
367
|
|
|
255
368
|
// ── Main ────────────────────────────────────────────────────────────────
|
|
256
369
|
|
|
257
|
-
async function runLogin(argv, inputFunc) {
|
|
370
|
+
async function runLogin(argv, inputFunc, deps = {}) {
|
|
258
371
|
const tokens = argv || [];
|
|
372
|
+
const openBrowserImpl = deps.openBrowser || openBrowser;
|
|
373
|
+
const startAuthCallbackServerImpl = deps.startAuthCallbackServer || core.startAuthCallbackServer;
|
|
259
374
|
|
|
260
375
|
// No args → help
|
|
261
376
|
if (tokens.length === 0) {
|
|
@@ -314,7 +429,7 @@ async function runLogin(argv, inputFunc) {
|
|
|
314
429
|
const authUrl = client.buildAuthorizationUrl(opts.redirect_uri, state);
|
|
315
430
|
const callbackPath = extractCallbackPath(opts.redirect_uri);
|
|
316
431
|
|
|
317
|
-
if (opts.
|
|
432
|
+
if (!opts.browser) {
|
|
318
433
|
// Print URL and prompt for code
|
|
319
434
|
console.log('Open this URL in your browser to authorize:');
|
|
320
435
|
console.log(authUrl);
|
|
@@ -330,7 +445,7 @@ async function runLogin(argv, inputFunc) {
|
|
|
330
445
|
// Try to open the browser; fall back to URL + prompt on failure
|
|
331
446
|
let browserOpened = false;
|
|
332
447
|
try {
|
|
333
|
-
await
|
|
448
|
+
await openBrowserImpl(authUrl);
|
|
334
449
|
browserOpened = true;
|
|
335
450
|
} catch (_) {
|
|
336
451
|
// Browser spawn failed; fall back to printing the URL
|
|
@@ -348,19 +463,94 @@ async function runLogin(argv, inputFunc) {
|
|
|
348
463
|
|
|
349
464
|
if (browserOpened) {
|
|
350
465
|
// Start callback server and wait for the redirect
|
|
351
|
-
const result = await
|
|
466
|
+
const result = await startAuthCallbackServerImpl({
|
|
352
467
|
port: opts.port,
|
|
353
468
|
path: callbackPath,
|
|
354
469
|
state: state,
|
|
355
470
|
});
|
|
471
|
+
console.log('Authorization code received; exchanging for token...');
|
|
356
472
|
await client.exchangeAuthorizationCode(result.code, opts.redirect_uri);
|
|
357
473
|
console.log(`User token saved for grant_type ${opts.grant_type}`);
|
|
358
474
|
}
|
|
359
475
|
}
|
|
360
476
|
|
|
477
|
+
// ── Status ───────────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
async function runStatus(argv) {
|
|
480
|
+
const tokens = argv || [];
|
|
481
|
+
|
|
482
|
+
if (tokens.length === 1 && (tokens[0] === '--help' || tokens[0] === '-h')) {
|
|
483
|
+
printStatusHelp();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const { opts, helpRequested } = parseStatusArgs(tokens);
|
|
488
|
+
|
|
489
|
+
if (helpRequested) {
|
|
490
|
+
printStatusHelp();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const client = createClient(opts);
|
|
495
|
+
const tokenCachePath = client.tokenCache;
|
|
496
|
+
const workspaceCachePath = client.workspaceCachePath;
|
|
497
|
+
|
|
498
|
+
let rawToken = null;
|
|
499
|
+
let validToken = null;
|
|
500
|
+
if (tokenCachePath) {
|
|
501
|
+
rawToken = core.readRawTokenCache(tokenCachePath);
|
|
502
|
+
validToken = core.loadCachedToken(tokenCachePath);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const hasExplicitToken = typeof opts.token === 'string' && opts.token;
|
|
506
|
+
const tokenValid = !!hasExplicitToken || (validToken !== null);
|
|
507
|
+
const tokenCacheExists = tokenCachePath ? fs.existsSync(tokenCachePath) : false;
|
|
508
|
+
const workspaceCacheExists = workspaceCachePath ? fs.existsSync(workspaceCachePath) : false;
|
|
509
|
+
|
|
510
|
+
const grantType = tokenValid
|
|
511
|
+
? client.resolveGrantType()
|
|
512
|
+
: (rawToken && typeof rawToken.grant_type === 'string' ? rawToken.grant_type : opts.grant_type);
|
|
513
|
+
|
|
514
|
+
const expiresAt = rawToken && typeof rawToken.expires_at === 'number' ? rawToken.expires_at : null;
|
|
515
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
516
|
+
const expiresIn = expiresAt && expiresAt > nowSeconds ? expiresAt - nowSeconds : null;
|
|
517
|
+
|
|
518
|
+
const result = {
|
|
519
|
+
authenticated: tokenValid,
|
|
520
|
+
grant_type: grantType,
|
|
521
|
+
base_url: client.baseUrl,
|
|
522
|
+
token_cache: tokenCachePath,
|
|
523
|
+
token_cache_exists: tokenCacheExists,
|
|
524
|
+
token_valid: tokenValid,
|
|
525
|
+
token_expires_at: expiresAt,
|
|
526
|
+
token_expires_in: expiresIn,
|
|
527
|
+
credentials: {
|
|
528
|
+
client_id_configured: !!(opts.client_id),
|
|
529
|
+
client_secret_configured: !!(opts.client_secret),
|
|
530
|
+
},
|
|
531
|
+
workspace_cache: workspaceCachePath,
|
|
532
|
+
workspace_cache_exists: workspaceCacheExists,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (opts.dry_run) {
|
|
536
|
+
result.dry_run = true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (opts.compact) {
|
|
540
|
+
core.printJson({
|
|
541
|
+
authenticated: result.authenticated,
|
|
542
|
+
grant_type: result.grant_type,
|
|
543
|
+
token_valid: result.token_valid,
|
|
544
|
+
credentials: result.credentials,
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
core.printJson(result);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
361
551
|
// ── Register ───────────────────────────────────────────────────────────
|
|
362
552
|
|
|
363
|
-
const SUBCOMMANDS = ['login'];
|
|
553
|
+
const SUBCOMMANDS = ['login', 'status'];
|
|
364
554
|
|
|
365
555
|
async function run(argv, inputFunc) {
|
|
366
556
|
const tokens = argv || [];
|
|
@@ -371,13 +561,20 @@ async function run(argv, inputFunc) {
|
|
|
371
561
|
return;
|
|
372
562
|
}
|
|
373
563
|
|
|
374
|
-
|
|
375
|
-
throw new core.PingCodeError(
|
|
376
|
-
`Unknown auth subcommand: ${subcommand}. Valid subcommands: ${SUBCOMMANDS.join(', ')}.`
|
|
377
|
-
);
|
|
378
|
-
}
|
|
564
|
+
const remaining = tokens.slice(1);
|
|
379
565
|
|
|
380
|
-
|
|
566
|
+
switch (subcommand) {
|
|
567
|
+
case 'login':
|
|
568
|
+
await runLogin(remaining, inputFunc);
|
|
569
|
+
return;
|
|
570
|
+
case 'status':
|
|
571
|
+
await runStatus(remaining);
|
|
572
|
+
return;
|
|
573
|
+
default:
|
|
574
|
+
throw new core.PingCodeError(
|
|
575
|
+
`Unknown auth subcommand: ${subcommand}. Use auth --help for usage.`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
381
578
|
}
|
|
382
579
|
|
|
383
580
|
shared.registerModule('auth', {
|
|
@@ -386,4 +583,4 @@ shared.registerModule('auth', {
|
|
|
386
583
|
run,
|
|
387
584
|
});
|
|
388
585
|
|
|
389
|
-
module.exports = { run, runLogin, printHelp, printLoginHelp, parseLoginArgs, createClient, buildDryRunExchange };
|
|
586
|
+
module.exports = { run, runLogin, runStatus, printHelp, printLoginHelp, printStatusHelp, parseLoginArgs, parseStatusArgs, createClient, buildDryRunExchange, openBrowser };
|