@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 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
- - 通过子命令(`context *`, `workitem *`, `auth *`)调用 PingCode API
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 / show / get / update` 子命令及完整参数;含共享安全规则 |
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` | 显示当前偏好和字典摘要 | `pingcode 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 show <id>` | 查看单个工作项(通过列表接口按 id 或 identifier 查询) | `pingcode workitem show SCR-123` |
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 show SCR-123
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.2",
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": {
@@ -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
- '--no-browser', '--no-token-cache', '--no-workspace-cache', '--dry-run',
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
- no_browser: false,
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
- const child = spawn(command, args, {
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) => reject(err));
145
- child.on('close', (code) => {
146
- if (code === 0) {
147
- resolve(true);
148
- } else {
149
- reject(new Error(`Browser process exited with code ${code}`));
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
- ' --no-browser Print the authorization URL and prompt for code',
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.no_browser) {
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 openBrowser(authUrl);
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 core.startAuthCallbackServer({
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
- if (subcommand !== 'login') {
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
- await runLogin(tokens.slice(1), inputFunc);
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 };