pingcode-cli-unofficial 1.7.1

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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "pingcode-cli-unofficial",
3
+ "version": "1.7.1",
4
+ "description": "Command-line client for the PingCode Open API",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/anine09/pingcode-cli-unofficial.git"
10
+ },
11
+ "bin": {
12
+ "pingcode": "dist/bin/pingcode.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "skills",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "test:coverage": "vitest run --coverage",
25
+ "catalog:sync": "node --experimental-strip-types scripts/catalog-sync.ts",
26
+ "catalog:check": "node --experimental-strip-types scripts/catalog-sync.ts --check",
27
+ "skill:install": "node --experimental-strip-types scripts/install-skill.ts",
28
+ "scan:secrets": "node --experimental-strip-types scripts/scan-secrets.ts",
29
+ "check:commits": "node --experimental-strip-types scripts/check-commits.ts",
30
+ "package:release": "node --experimental-strip-types scripts/package-release.ts",
31
+ "hooks:install": "node scripts/install-hooks.mjs",
32
+ "prepare": "node scripts/install-hooks.mjs",
33
+ "install:cli": "node scripts/install.mjs"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^12.1.0",
37
+ "picocolors": "^1.1.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.19.0",
41
+ "@vitest/coverage-v8": "^2.1.9",
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5.9.0",
44
+ "vitest": "^2.1.9"
45
+ },
46
+ "allowScripts": {
47
+ "esbuild@0.27.7": true,
48
+ "esbuild@0.21.5": true,
49
+ "fsevents@2.3.3": true
50
+ }
51
+ }
@@ -0,0 +1,431 @@
1
+ ---
2
+ name: pingcode
3
+ description: >-
4
+ Use the `pingcode` CLI to work with PingCode (研发管理): 敏捷项目管理 (pjm) projects and work items —
5
+ list and search work items (工作项), read a story/task/bug (需求/任务/缺陷), create one, update fields,
6
+ move it to another state (状态流转) — 产品管理 (ship) products, requirements (需求 / idea) and
7
+ tickets (工单): search, read, create, update and transition them — and 测试管理 (testhub) test
8
+ libraries (测试库), test cases (用例), test plans (测试计划) and runs (执行用例): create a library,
9
+ search cases, read one, create and update a case, create a plan, record a run result, and bulk
10
+ add/update/delete the runs of a plan. Also resolves project-, product- and library-scoped ids and
11
+ organisation members (项目/产品/测试库/迭代/成员). Triggers: pingcode, PingCode 工作项,
12
+ 创建任务, 更新状态, 迭代 sprint,
13
+ 产品 需求 工单, 测试用例 测试计划 执行用例 测试库, SCR-5 or SLC-1 style identifiers, a work-item or
14
+ idea URL from a PingCode instance. Do NOT use for Wiki pages, customers or external users, the
15
+ org chart beyond a member lookup, Insight/Goals/Flow, or webhooks — none of those are covered by
16
+ this CLI, and webhooks cannot be managed through the PingCode API at all (they live in PingCode
17
+ Flow's UI).
18
+ ---
19
+
20
+ # PingCode CLI
21
+
22
+ `pingcode` is a command-line client for the PingCode Open API. Its top level mirrors PingCode's own
23
+ GUI modules: **`product`** (产品管理 / ship — products, requirements/需求, tickets/工单),
24
+ **`project`** (项目管理 / pjm — projects, work items, sprints, releases), **`testhub`** (测试管理 —
25
+ test libraries, cases, plans and runs), **`scm`** (源码管理 — code hosting data), **`build`** and
26
+ **`release`** (构建与部署 — CI build and deployment records), **`settings`** (后台设置 — the
27
+ organisation directory) and **`auth`** (the CLI's own local credentials).
28
+
29
+ `scm`, `build` and `release` are the DevOps **write-back** surface: a CI/CD job tells PingCode what
30
+ happened, and PingCode links it to work items. None of the three reads your git server or your
31
+ pipeline.
32
+
33
+ **Two layers, and knowing which one you are on saves you a retry loop.** Every one of the **459**
34
+ documented `/v1` endpoints is callable through **`pingcode api`** — same auth, paging, `--dry-run`,
35
+ redaction and exit codes as anything else. **158** of them also have a named command with flags,
36
+ name→id resolution and a table. So: if a named command exists, use it; if it does not, the answer is
37
+ almost never "the CLI cannot do that" — it is `pingcode api list --search <word>` and then
38
+ `pingcode api <METHOD> <path>`. Only 7 endpoints are genuinely out of reach (they need a user token;
39
+ see [`modules/api.md`](modules/api.md)).
40
+
41
+ Each business module owns its resources *and* its id lookups, so a module's whole surface is one
42
+ `--help` away:
43
+
44
+ ```
45
+ auth login status logout
46
+ api GET|POST|PATCH|PUT|DELETE <path> · list · describe
47
+ resolve list · <kind> <name>
48
+ product list get · idea … · ticket … · plan … · meta …
49
+ project list get create update progress · work-item … · sprint … · version … · member … · meta …
50
+ testhub libraries … · cases … · plans … · runs … · meta …
51
+ scm platform … · platform-user … · repo … · branch … · commit … · ref … · pr … · review …
52
+ build list get create update delete
53
+ release env … · deploy …
54
+ settings users
55
+ ```
56
+
57
+ Four cross-object families — `relation`, `comment`, `attachment`, `activity` — are mounted under
58
+ five entities (`product idea`, `product ticket`, `project work-item`, `testhub cases`,
59
+ `testhub runs`), so e.g. `pingcode project work-item comment add SCR-5 --text "…"`. See
60
+ [`modules/crosscutting.md`](modules/crosscutting.md).
61
+
62
+ Everything below assumes `pingcode` is on `PATH` (`npm run build` produces `dist/bin/pingcode.js`).
63
+
64
+ ## 1. Authentication gate — do this first
65
+
66
+ If any command fails with exit code 3, or `pingcode auth status` reports no token, stop and get
67
+ credentials before retrying.
68
+
69
+ Credentials come from a PingCode application, not from a user account:
70
+
71
+ 1. In the PingCode enterprise console, open **后台管理 → 凭据管理** (Credential Management) and create an
72
+ application.
73
+ 2. Set 鉴权方式 (grant type) to **Client Credentials**.
74
+ 3. Grant these scopes:
75
+ - `pcp:read:pjm:project` — projects
76
+ - `pcp:write:pjm:project` — `project create` / `project update` / `project member add`. Grant
77
+ this one deliberately: **a project can never be deleted or archived** through this API, so
78
+ `project create` is irreversible
79
+ - `pcp:read:pjm:workitem` — work items, types, states, priorities, tags, relation types
80
+ - `pcp:write:pjm:workitem` — create/update/**delete** work items, plus links, tags and
81
+ `bulk-update`
82
+ - `pcp:read:global:team` — `pingcode settings users`
83
+ - `pcp:read:pjm:sprint` / `pcp:write:pjm:sprint` — `pingcode project meta sprints` (the sprint
84
+ list) and `project sprint get|create|update|bulk-create`. Grant the write half only if you intend to
85
+ plan sprints: **a sprint cannot be deleted**, by the API or by anyone
86
+ - `pcp:read:pjm:release` / `pcp:write:pjm:release` — `pingcode project version …` (发布, a
87
+ project release). Note the mismatch: the scope says *release*, the resource and the command say
88
+ *version*
89
+
90
+ For the ship (产品管理) commands, add:
91
+ - `pcp:read:ship:product` — `product list` / `product get` / `product meta members` /
92
+ `product plan list|get`, and every product name lookup, which every other ship command starts
93
+ with
94
+ - `pcp:read:ship:idea` — `product idea list` / `get` / `history list|get`, and `product meta idea-states|idea-priorities|idea-suites|idea-properties|idea-plans`
95
+ - `pcp:write:ship:idea` — `product idea create` / `update`
96
+ - `pcp:read:ship:ticket` — `product ticket list` / `get`, and `product meta ticket-states|ticket-priorities|ticket-types|ticket-channels|ticket-properties`
97
+ - `pcp:write:ship:ticket` — `product ticket create` / `update` / `transition`
98
+ - `pcp:read:ship:configuration` — **optional**, and only for `product ticket transition`: it is what
99
+ makes the state-plan pre-check possible. Without it the CLI warns on stderr and lets the
100
+ server judge the transition, so tickets stay movable either way.
101
+
102
+ The product-scoped metadata endpoints (`/v1/ship/idea/*`, `/v1/ship/ticket/*`) sit under the
103
+ ordinary read scopes above, **not** under `configuration` — only the ticket state plans and
104
+ flows need `configuration`.
105
+
106
+ For the testhub (测试管理) commands, add:
107
+ - `pcp:read:testhub:library` — `pingcode testhub libraries list` / `get`, `testhub meta suites`,
108
+ and the case-module (模块) tree behind `--suite`. Every other testhub command starts by
109
+ resolving a library, so this one is not optional
110
+ - `pcp:write:testhub:library` — `testhub libraries create`, and nothing else. Grant it only if
111
+ you intend to create libraries: they cannot be deleted afterwards
112
+ - `pcp:read:testhub:testcase` — `testhub cases list` / `get`, and `testhub meta case-types`
113
+ - `pcp:write:testhub:testcase` — `testhub cases create` / `update`
114
+ - `pcp:read:testhub:testplan` — `testhub plans list` / `get`, `testhub meta plan-types`, and
115
+ `testhub runs list`
116
+ - `pcp:write:testhub:testplan` — `testhub plans create`, `testhub runs update` / `bulk`
117
+ - `pcp:read:testhub:configuration` — **not optional, despite the name**: `testhub meta
118
+ case-states`, `testhub meta run-statuses` and `testhub meta important-levels` all sit behind
119
+ it, and they are the only source of a `state_id`, a `status_id` and an `important_level_id`.
120
+ Without it those three return a bare 403 while their sibling `case-types` keeps working, and
121
+ since `PATCH /runs/{id}` requires `status_id`, a token without this scope **cannot write a
122
+ run at all**. The CLI adds that explanation to the 403 for all three of those lookups.
123
+
124
+ `--executor` on a run and `--assignee` on a plan both resolve through the organisation
125
+ directory, so they also need `pcp:read:global:team`.
126
+
127
+ For the scm (源码管理) commands, add:
128
+ - `pcp:read:devops:code` — `pingcode scm platform|platform-user|repo list` / `get`, and every
129
+ platform/repository name lookup
130
+ - `pcp:write:devops:code` — `scm platform|platform-user|repo create` / `update`
131
+
132
+ The whole scm area is **企业令牌 only**, which is what `client_credentials` gives you, so no
133
+ extra grant type is needed — only the two scopes above.
134
+
135
+ For the build and deploy write-back commands, add:
136
+ - `pcp:read:devops:build` / `pcp:write:devops:build` — `pingcode build list|get` and
137
+ `build create|update|delete`. **Separate from `devops:code`**: a token that can write
138
+ commits cannot write builds, and the only symptom is exit 4
139
+ - `pcp:read:devops:deploy` / `pcp:write:devops:deploy` — `pingcode release env …` **and**
140
+ `pingcode release deploy …`; one pair covers both subgroups, there is no separate
141
+ environment scope
142
+
143
+ These two areas are 企业令牌 only as well, so again no extra grant type.
144
+ 4. Copy the `client_id` and `client_secret`.
145
+
146
+ Then:
147
+
148
+ ```bash
149
+ pingcode auth login --client-id <id> --client-secret <secret> --save
150
+ pingcode auth status --check
151
+ ```
152
+
153
+ - `--save` writes the client id/secret to `~/.pingcode/config.json` (mode `0600`). Without `--save`
154
+ only the token is stored, and a new login is needed once it expires.
155
+ - Credentials can also arrive as `PINGCODE_CLIENT_ID` / `PINGCODE_CLIENT_SECRET`, or interactively
156
+ when a terminal is attached.
157
+ - Self-hosted instances need `--host https://pingcode.example.com` (or `PINGCODE_HOST`); the API is
158
+ served from `<host>/open` there.
159
+ - `pingcode auth logout` removes the token, the stored credentials and the metadata cache.
160
+
161
+ **Security:** a Client Credentials token has organisation-wide system-administrator authority and is
162
+ valid for 30 days. It is not tied to a user. Never print it, never copy it into a file in a
163
+ repository, and treat `~/.pingcode/config.json` as a secret. The CLI redacts the secret and the token
164
+ from every URL, log line, dry-run plan and error message it emits.
165
+
166
+ ## 2. Output contract
167
+
168
+ - `--json` makes **stdout carry JSON only**. Logs, warnings, tables and notes go to stderr.
169
+ - In `--json` mode, timestamps stay raw unix **seconds**. Human mode renders local time.
170
+ - Three list shapes, by command family:
171
+ - `project list` / `project work-item list` (one page) → `{"page_index":0,"page_size":30,"total":123,"values":[…]}`,
172
+ and the same for `product`/`testhub` one-page lists
173
+ - any list with `--all` → `{"values":[…],"count":42,"all":true}`
174
+ - every `meta` lookup — `pingcode product meta …`, `pingcode project meta …`,
175
+ `pingcode testhub meta …`, and `pingcode settings users` (which still accepts
176
+ `--page`/`--page-size`) → `{"values":[…],"count":20}`
177
+ - Single-resource commands (`get`, `create`, `update`, `transition`) print the resource object.
178
+ - **Read keys defensively: an absent key means null or empty.** The CLI normalises `null` and `""`
179
+ to "not present", so a field the API returned as `null` (an unset `plan_at`, `score`, `solution`)
180
+ or as an empty string (a blank `description`) is simply missing from the JSON. It does **not**
181
+ distinguish the two, and a missing key is never evidence that the field does not exist. Use
182
+ `x?.y ?? fallback`, not `'y' in x`.
183
+ - `--dry-run` on a mutating command prints `{"dry_run":true,"request":{…}}` to stdout and exits 0
184
+ without sending anything. Read requests still run, so ids are really resolved.
185
+ - Errors in `--json` mode go to **stderr** as `{"error":{"kind":…,"message":…,"code":…,"exit":…}}`.
186
+ - **`pingcode api` is the exception to the shapes above**: stdout is the API's raw JSON, so `--json`
187
+ is a no-op on its five verbs and no key is normalised away. Its own `api list` / `api describe` are
188
+ local catalog views and do honour `--json`.
189
+ - Global flags (`--host`, `--json`, `--dry-run`, `--no-cache`, `--verbose`) may appear before or
190
+ after the subcommand.
191
+
192
+ ### Exit codes
193
+
194
+ | Exit | Meaning |
195
+ |---|---|
196
+ | 0 | success (including a printed dry-run plan) |
197
+ | 1 | unexpected internal error |
198
+ | 2 | usage error: bad flags, missing input, ambiguous or unresolvable name, empty update |
199
+ | 3 | authentication: no or invalid credentials |
200
+ | 4 | permission: 403, or a scope the app was not granted |
201
+ | 5 | not found: the work item, state or other resource does not exist |
202
+ | 6 | rate limited: 429 (200 requests/minute per token) |
203
+ | 7 | other API error, carrying the API's `{code, message}` |
204
+ | 8 | transport failure: DNS, TCP, TLS, timeout, unparseable body |
205
+
206
+ The API answers HTTP 400 for both "not found" and "bad credentials", so the CLI maps a few known API
207
+ codes onto exits 5 and 3 rather than trusting the status. Unknown codes stay on exit 7 with the raw
208
+ `code` in the error payload — read it before concluding anything.
209
+
210
+ ## 3. Commands
211
+
212
+ ### Where each module's commands are documented
213
+
214
+ The per-module surface — every flag, every id lookup, and the traps specific to that module — lives
215
+ one file per module, so a module can be revised without touching this one:
216
+
217
+ | Module | File | Covers |
218
+ |---|---|---|
219
+ | 项目管理 pjm | [`modules/pjm.md`](modules/pjm.md) | `project list/get/create/update/progress`, `project meta …`, `project work-item …` (incl. `link` / `tag` / `history`), `project sprint …`, `project version …`, `project member …`, and the pjm-only rules |
220
+ | 产品管理 ship | [`modules/ship.md`](modules/ship.md) | `product list/get`, `product meta …`, `product idea …`, `product ticket …`, `product plan …` (需求排期, read-only), and the ship-only traps |
221
+ | 测试管理 testhub | [`modules/testhub.md`](modules/testhub.md) | `testhub libraries/cases/plans/runs/meta …`, and the testhub-only traps |
222
+ | 源码管理 scm | [`modules/scm.md`](modules/scm.md) | `scm platform …`, `platform-user …`, `repo …`, `branch …`, `commit …`, `ref …`, `pr …`, `review …` — the DevOps write-back surface, 企业令牌 only |
223
+ | 构建与部署 | [`modules/cicd.md`](modules/cicd.md) | `build …` (CI build records) and `release env …` / `release deploy …` (deploy targets and deployments), 企业令牌 only |
224
+ | 跨对象资源 | [`modules/crosscutting.md`](modules/crosscutting.md) | `relation` / `comment` / `attachment` / `activity`, mounted under work items, ideas, tickets, cases and runs |
225
+ | 通用逃生舱 | [`modules/api.md`](modules/api.md) | `api GET/POST/PATCH/PUT/DELETE <path>` for every documented endpoint, plus `api list` / `api describe` |
226
+
227
+ A file marked *reserved* describes commands that **do not exist yet** — there are none at the
228
+ moment, every module document above is real. Do not suggest a command you have not seen in
229
+ `pingcode --help`; run it if you need to know what is actually installed.
230
+
231
+ `auth` and `settings` are documented here rather than in a module file: they are the CLI's own
232
+ credentials and a single directory lookup, not a business module.
233
+
234
+ ### Auth
235
+
236
+ ```bash
237
+ pingcode auth login --client-id <id> --client-secret <secret> --save
238
+ pingcode auth status
239
+ pingcode auth status --check # adds one live API call: GET /v1/pjm/projects?page_size=1
240
+ pingcode auth logout
241
+ ```
242
+
243
+ ### Settings — 后台设置
244
+
245
+ ```bash
246
+ pingcode settings users --keywords wang --json
247
+ ```
248
+
249
+ The organisation directory (`/v1/directory/users`, scope `pcp:read:global:team`) belongs to no
250
+ business module, which is why it sits here rather than under `product` or `project`. It is the
251
+ candidate set for pjm's `--assignee`; ship's `--assignee` uses `product meta members` instead.
252
+
253
+ ### Name → id resolution
254
+
255
+ `pingcode api` takes **ids only** — it understands no business names on purpose. `resolve` is the
256
+ missing half: one lookup, one id on stdout, nothing else.
257
+
258
+ ```bash
259
+ pingcode resolve list --json # every kind and the parent it needs
260
+ pingcode resolve project "移动端 App" --json # {"kind":"project","id":"5f2a…",…}
261
+ pingcode resolve ship-product SLC --json # identifier works as an alias
262
+ pingcode resolve ship-idea-state 已评审 --parent <product_id> --json
263
+ pingcode resolve testhub-library "研发测试库" --json
264
+ ```
265
+
266
+ - stdout under `--json` is the resolution itself, so it composes:
267
+ `pingcode api GET /v1/ship/idea/states --query product_id=$(pingcode resolve ship-product "智能客服" --json | jq -r .id)`.
268
+ - An **id is passed through** after being verified; a **name must match exactly** (case-insensitively)
269
+ and exactly once. Zero or several matches is exit 2 listing the candidates — it never picks one.
270
+ - `--parent` takes an **id**, not a name: chain a second `resolve` if you only have the name.
271
+ - Answers are cached for 24 h per (host, `client_id`, parent, kind); `--no-cache` bypasses it.
272
+ - Ticket state plans and their flows are absent from `resolve list`, because no name addresses them.
273
+
274
+ The refined commands (`project meta …`, `product meta …`, `testhub meta …`) already accept names
275
+ directly, so `resolve` is mainly for feeding the generic layer.
276
+
277
+ ### The two flag shapes, and how to tell which one a leaf uses
278
+
279
+ Every field that could be given either as a business name or as an id is spelled in **one of two
280
+ ways**, and the split is by module, not by field. It is not documented anywhere upstream, so read
281
+ `--help` rather than guessing — but these are the rules the CLI follows.
282
+
283
+ | Shape | Modules | Example |
284
+ |---|---|---|
285
+ | **Paired** — `--x <name\|id>` **plus** `--x-id <id>` | `testhub`, `scm`, `release` | `--library` / `--library-id`, `--platform` / `--platform-id`, `--repo` / `--repo-id`, `--env` / `--env-id` |
286
+ | **Single** — one `--x <name\|id>` that accepts both | `pjm` (`project`), `ship` (`product`) | `--project`, `--sprint`, `--release`, `--product` |
287
+
288
+ Both shapes accept a name or an id. What differs is only whether you can **force** the no-lookup
289
+ path:
290
+
291
+ - With a **pair**, `--x` triggers a name lookup (one extra GET, cached 24 h) and `--x-id` is sent
292
+ verbatim with no lookup at all. The two are mutually exclusive — passing both is exit 2. Reach for
293
+ `--x-id` when you already hold ids (it is faster, and it cannot be tripped by two objects sharing
294
+ a name).
295
+ - With a **single** flag, the CLI decides: it passes a value through if it looks like an id and
296
+ looks it up otherwise. There is no way to say "skip the lookup". It never validates an id's shape
297
+ — ids in this API are 24-hex, 32-hex *or* a bare slug like `task` / `story` / `bug`.
298
+
299
+ Three deliberate gaps in that scheme, so you do not read them as bugs:
300
+
301
+ 1. **`testhub runs list --case-id` has no `--case` twin**, although `runs create`,
302
+ `runs bulk-create` and `cases bulk-update` all pair on the same resource. The value goes straight
303
+ into the search filter, so a `short_id` arrives unresolved and the API answers 400 `100044`
304
+ (`'property_value'值不合法`). Pass a full case id, or get one from `testhub cases list`.
305
+ 2. **`scm branch|commit|pr list --work-item-id` accepts a 24-hex ObjectId only** — *not* an
306
+ identifier like `PLM-001`, which returns 400 `100003`. Note the asymmetry within one group: the
307
+ *write* paths' `--work-item` in the same commands **do** take `PLM-001`. Each leaf's `--help`
308
+ says which.
309
+ 3. **`project update --state-id` has no `--state`**, because there is no project-state resolver kind:
310
+ the lookup lives at `/v1/pjm/project/states`, which no `resolve` kind covers. Read the ids with
311
+ `pingcode api GET /v1/pjm/project/states --query project_id=<id>`.
312
+ ## 4. Rules that will bite you
313
+
314
+ These hold in **every** module. The rules that apply to one module only — pjm's
315
+ `--state` needs `--type`, ship's product scoping, testhub's library scoping, scm's platform hop —
316
+ live in that module's file, because there is no way to state them once and have them be true.
317
+
318
+ 1. **Resolve ids under the right parent first.** Ids are scoped: to a **project** in pjm, a
319
+ **product** in ship, a **test library** in testhub, a **hosting platform** in scm. The same state
320
+ name is a different id under a different parent, so run that module's `meta` lookup (or
321
+ `pingcode resolve <kind> <name> --parent <id>`) before a write. Never validate an id's shape: they
322
+ are 24-hex, 32-hex **or** bare slugs (`task`, `story`, `bug`).
323
+ 2. **`update` replaces, it does not merge.** Every field you pass overwrites the stored value, and
324
+ array fields and `properties` objects are replaced wholesale rather than merged. Read the item
325
+ first if you need to preserve anything.
326
+ 3. **There is no way to clear a field.** Only the fields you pass are sent; the CLI has no
327
+ `--clear-<field>`.
328
+ 4. **An update with no fields is exit 2**, not a no-op.
329
+ 5. **A 200 is not proof the field landed.** This API accepts unknown body fields, several read-only
330
+ fields, and whole unsupported properties, with a 200 and no warning. Where the CLI knows about a
331
+ specific case it refuses or warns on stderr; otherwise read the object back.
332
+ 6. **Every `delete` needs `--yes`, and every `PUT` needs `pingcode api`.** No named command issues a
333
+ `PUT`: it is full replacement and this API never documents what an omitted field does. See
334
+ [`modules/api.md`](modules/api.md).
335
+ 7. **No endpoint supports sorting.** Neither the CLI nor the API can order results. Sort what you
336
+ collected yourself, and remember that offset paging over unordered, changing data can duplicate or
337
+ skip rows.
338
+ 8. **`--all` is best effort, not a consistent snapshot.** It walks pages, de-duplicates by id, stops
339
+ at `--limit` (default 500), and gives up if the API stops honouring `page_index`.
340
+ 9. **Rate limit is 200 requests per minute per token.** Prefer one `--page-size 100` call over many
341
+ small ones, let the metadata cache work, and do not loop `--all` over large projects casually.
342
+ 10. **Timestamps are unix seconds.** Date flags accept `1730000000` or a calendar date — but read the
343
+ flag's own `--help`, because the families differ on purpose: `project --start-at` stores the
344
+ instant verbatim, while `project sprint` / `project version` / `testhub plans` snap `--start` to
345
+ `00:00:00` and `--end` to `23:59:59` of the date given.
346
+
347
+ ## 5. Agent workflow
348
+
349
+ 0. **Decide which layer you are on.** If the task maps onto a named command, use it. If it does not,
350
+ do not conclude the CLI cannot help: `pingcode api list --search <word> --json` finds the endpoint,
351
+ `pingcode api describe <id>` prints its fields, scope and warnings, and `pingcode resolve <kind>
352
+ <name> --json` turns a name into the id `api` needs. Never invent a command path — if you are
353
+ unsure it exists, run its `--help`.
354
+ 1. `pingcode auth status` — if it reports no token, ask the user for credentials (§1) instead of
355
+ guessing.
356
+ 2. Resolve the parent scope: `pingcode project list --json` for work items,
357
+ `pingcode product list --json` for ideas and tickets, `pingcode testhub libraries list --json`
358
+ for cases, plans and runs, or `pingcode scm platform list --json` for anything under `scm`.
359
+ 3. Resolve ids — each module's file has the full list; the short version:
360
+ - pjm: `pingcode project meta types --project <p> --json`, then
361
+ `project meta states --project <p> --type <t> --json`. Keep the type around: you need it again for
362
+ `--state <name>` on any write.
363
+ - ship: `pingcode product meta idea-states --product <p> --json` (or the `ticket-*` equivalents), plus
364
+ `product meta members --product <p> --json` before setting an assignee, and
365
+ `product meta ticket-types --product <p> --json` before creating a ticket — `--type` is required there.
366
+ - testhub: `pingcode testhub meta case-states --library <l> --json`, `case-types`,
367
+ `run-statuses` (the source of `--status`, needed by every run write), `suites` (the `PATH`
368
+ column is the spelling `--suite` takes) and the org-wide `important-levels`. Find the plan with
369
+ `pingcode testhub plans list --library <l> --json`, or create one — `testhub meta plan-types
370
+ --library <l> --json` first, since `--type` on `plans create` comes from there. If the tenant
371
+ has no usable library at all, `pingcode testhub libraries create` is the bootstrap; read
372
+ [`modules/testhub.md`](modules/testhub.md) §4c rule 15 first, because it cannot be undone.
373
+ - scm / CI: resolve the platform, then the repo (`pingcode scm repo list --platform <p> --json`);
374
+ branch ids are needed as *ids* by `scm pr create`, and work items are linked by
375
+ **identifier** (`PLM-1`), never by id.
376
+ 4. Read before writing: `pingcode project work-item get <ref> --json`, `pingcode product idea get <ref> --json`,
377
+ `pingcode product ticket get <ref> --json` or `pingcode testhub cases get <ref> --json` — and for a
378
+ step-level run edit, read the run's steps first, because `--step` must cover all of them.
379
+ 5. For any mutation, run it with `--dry-run --json` first, show the plan, get confirmation, then run
380
+ it again without `--dry-run`. Say what cannot be undone *before* creating it: **nothing in ship can
381
+ be deleted**, a project or sprint can never be deleted, a test library cannot be deleted, and
382
+ `scm` has no DELETE except branches.
383
+ 6. Always pass `--json` and parse stdout only; read stderr for warnings. Remember that
384
+ `pingcode api` prints the API's raw JSON regardless, and that a missing key means null *or* empty.
385
+ 7. On exit 2 read the message — it names the flag, the ambiguous name, the states a ticket can
386
+ actually move to, or the nearest documented paths. On exit 3 re-authenticate. On exit 4 name the
387
+ scope from `api describe`. On exit 6 wait a minute rather than retrying immediately.
388
+
389
+ ## 6. What has no named command
390
+
391
+ **Nothing on this list is out of reach** — it has no *refined* command, which means the answer is
392
+ `pingcode api`, not "the CLI cannot". Say that to the user rather than declaring a limitation.
393
+
394
+ | Area | Named commands | How to reach it |
395
+ |---|---|---|
396
+ | Wiki spaces and pages (19 endpoints) | none | `pingcode api list --module wiki`, then `api GET/POST/PATCH`. **`DELETE /v1/wiki/pages/{id}` has no recovery path** — never run it speculatively |
397
+ | 组织架构 beyond `settings users`: departments, teams, roles, jobs (22) | none | `pingcode api GET /v1/directory/departments` etc. |
398
+ | 工时 worklogs and worklog types (7) | none | `pingcode api GET /v1/workloads` |
399
+ | 评审 reviews (8) and 关注人 participants/followers (4) | none | `pingcode api GET /v1/reviews`, `api GET /v1/participants` — note `scm review` is a *different* resource |
400
+ | 权限 permission views (7) | none | 6 of the 7 need a user token and are refused; `pingcode api GET /v1/permission/points` works |
401
+ | 安全日志 login / audit logs (2) | none | `pingcode api GET /v1/security/audit_logs` |
402
+ | Nexus / CES app storage (5) | none | `pingcode api list --module nexus` |
403
+ | pjm 工作项配置 schemes (42) and 看板 boards (15) | only the project-scoped read lookups, as `project meta …` | `pingcode api list --module pjm --search 工作项配置` |
404
+ | ship customers, external users, product tags, suite and member writes | none | `pingcode api list --module ship` |
405
+ | all 10 `PUT` endpoints | none, on purpose | `pingcode api PUT …` — read the warning in [`modules/api.md`](modules/api.md) first |
406
+
407
+ Genuinely unavailable, as opposed to merely unrefined:
408
+
409
+ - **Webhooks.** PingCode has no REST API for them at all; they are configured in PingCode Flow's UI.
410
+ - **Insight / Goals / Flow.** No API surface.
411
+ - **The 7 user-token endpoints** (`/v1/myself`, `/v1/permission/my/*`, `/v1/permission/check/*`) —
412
+ they need the OAuth2 authorization-code flow, which this CLI does not implement. They are refused
413
+ before any request, with an explanation.
414
+ - **Uploading a file as an attachment.** Only the JSON code-snippet form of `POST /v1/attachments`
415
+ exists; the `multipart/form-data` form is not expressible in either layer. See
416
+ [`modules/crosscutting.md`](modules/crosscutting.md).
417
+
418
+ A module file also records the omissions *inside* that module and how to reach them — testhub's are a
419
+ section of its own in [`modules/testhub.md`](modules/testhub.md) §5, and every other module states
420
+ them inline next to the family they belong to. If a module document does not mention something, check
421
+ `pingcode api list --module <m>` before telling the user it is impossible.
422
+
423
+ ## 7. Installing this skill elsewhere
424
+
425
+ From a checkout of the CLI repository:
426
+
427
+ ```bash
428
+ npm run skill:install -- --dry-run # show the destinations
429
+ npm run skill:install # copy to ~/.claude/skills and ./.opencode/skills
430
+ npm run skill:install -- --force # overwrite existing copies
431
+ ```
@@ -0,0 +1,116 @@
1
+ # 通用逃生舱 (the generic executor) — `api`
2
+
3
+ > Part of the `pingcode` skill. Read [`../SKILL.md`](../SKILL.md) first — it carries the
4
+ > authentication gate, the stdout/stderr contract, the exit-code table and the rules that
5
+ > apply to every module. This file is only the generic executor and its discovery commands.
6
+
7
+ `pingcode api` calls **any** of the 459 documented `/v1` endpoints directly. Use it whenever the
8
+ refined command groups do not cover what you need — that is most of the API, and it is not a
9
+ second-class path: the request goes through the same transport, the same auth, the same
10
+ `--dry-run` gate and the same exit codes as every other command.
11
+
12
+ What it deliberately does **not** do: no name→id resolution, no response shaping, no
13
+ per-endpoint flags. Ids go in as ids, JSON comes out as JSON.
14
+
15
+ ## 1. The two rules that surprise people
16
+
17
+ 1. **stdout is always the API's raw JSON, so `--json` is a no-op on the five verbs.** There is no
18
+ table, no localised time, no column clipping — nothing to switch off. (`api list` and
19
+ `api describe` are local catalog views and *do* honour `--json`.)
20
+ 2. **`DELETE` requires `--yes`.** Without it: exit 2, nothing sent, and the message shows what
21
+ would have been sent. `pingcode api list --method DELETE` enumerates all 49 deletable
22
+ endpoints; two of them have no recovery path at all — a wiki page and a code branch.
23
+
24
+ ## 2. Find the endpoint first
25
+
26
+ ```bash
27
+ pingcode api list --search commit # substring over path, title and group
28
+ pingcode api list --module scm # 36 endpoints
29
+ pingcode api list --token ENT # 61 endpoints only a machine identity may call
30
+ pingcode api list --method DELETE # the auditable danger surface, 49 rows
31
+ pingcode api describe scm.commits.get # every documented field, scope, token, paging
32
+ pingcode api describe GET /v1/scm/commits/{commit_id_or_sha}
33
+ ```
34
+
35
+ Both are **local**: they read a bundled catalog and never make a request. Prefer them over
36
+ guessing a path, and prefer them over the documentation site (which is a client-rendered SPA
37
+ whose per-endpoint anchors are ~200 characters of percent-encoding).
38
+
39
+ `api describe` also prints the warnings that matter for that one endpoint: that a `PUT` is a
40
+ full replacement, that a `DELETE` is irreversible, that an endpoint needs a user token, or that
41
+ the docs declare no scope for it.
42
+
43
+ ## 3. Call it
44
+
45
+ ```bash
46
+ pingcode api GET /v1/scm/commits/9f3c1ab
47
+ pingcode api GET /v1/relations --query principal_type=work_item --query principal_id=<id>
48
+ pingcode api GET /v1/directory/users --page 2 --page-size 100
49
+ pingcode api GET /v1/directory/users --all --limit 200
50
+ pingcode api POST /v1/comments --set principal_type=work_item --set principal_id=<id> --set content="CI #123 failed"
51
+ pingcode api PATCH /v1/build/builds/<id> --set status=success
52
+ pingcode api POST /v1/wiki/pages --body-file page.json
53
+ pingcode api POST /v1/pjm/work_items/search --body '{"mode":"query","payload":{"keywords":"login"}}'
54
+ pingcode api DELETE /v1/pjm/projects/<id>/versions/<id> --yes
55
+ ```
56
+
57
+ - the path carries **substituted ids**, never `{placeholders}` and never a `?query` — a query
58
+ string in the path is exit 2, because silently dropping it would be worse;
59
+ - `--query k=v` is repeatable; repeating a key sends it as CSV, which is this API's convention;
60
+ - body: `--set k=v` (repeatable, flat, values sent **verbatim**), `--body-file <path>`,
61
+ `--body '<json>'`, or `--body -` to read stdin. The four are mutually exclusive;
62
+ - `--set` values are never type-guessed: a select-typed field wants the option's `_id`, not its
63
+ display text;
64
+ - `--set` is **scalar-only**. Its value is sent verbatim as a JSON *string*, so an array or object
65
+ field needs `--body` / `--body-file`. A few fields are genuinely type-checked upstream —
66
+ `--set version_ids='["<id>"]'` on a work item is `100006 'version_ids'不是有效的数组`, exit 7 —
67
+ but do not count on that everywhere: this API's default is to accept a wrong-typed field with 200
68
+ and store nothing;
69
+ - paging: `--page` / `--page-size` (max 100) are forwarded **only when you pass them**; `--all`
70
+ walks the pages and prints `{"values":[…],"count":N,"all":true}`. On an endpoint that is not a
71
+ collection, any paging flag is exit 2 rather than silently ignored.
72
+
73
+ ### The five `POST …/search` endpoints
74
+
75
+ `pjm/work_items`, `ship/ideas`, `ship/tickets`, `testhub/cases`, `testhub/runs`. They are **reads
76
+ wearing a mutating verb**, so: they execute even under `--dry-run`, their body is
77
+ `{"mode":"query","payload":{…}}` (`mode` has exactly one legal value), and the paging flags are
78
+ written into `payload.page_index` / `payload.page_size` for you. stdout is the page envelope.
79
+
80
+ ### `PUT` — prefer `PATCH`
81
+
82
+ All 10 `PUT` endpoints are reachable **only** here, on purpose. `PUT` replaces the whole object
83
+ and this API never documents what an omitted field does; one module was measured clearing a
84
+ field that its `PATCH` sibling preserves. Unless you truly mean "replace everything", use
85
+ `PATCH`.
86
+
87
+ ## 4. What fails before anything is sent (all exit 2, zero requests)
88
+
89
+ | Situation | What you get |
90
+ |---|---|
91
+ | path not in the catalog | the three nearest documented paths ("did you mean …") |
92
+ | method the path does not support | the methods that path *does* support |
93
+ | a required query/body field missing | the field named, by kind |
94
+ | `{placeholder}` or `?query` still in the path | told which one |
95
+ | `DELETE` without `--yes` | the request it refused to send |
96
+ | paging flags on a non-collection | told that the endpoint does not page |
97
+ | `/oauth2/authorize` | told it is the browser authorization page, not a REST endpoint |
98
+ | a **user-token-only** endpoint | told this CLI holds an enterprise token |
99
+
100
+ The seven user-token endpoints — `/v1/myself`, `/v1/permission/my/*` (3) and
101
+ `/v1/permission/check/*` (3) — are the only part of the API this CLI cannot reach at all: they
102
+ need the OAuth2 authorization-code flow, which is not implemented. Note that
103
+ `GET /v1/permission/points` **does** work. Everything else, including all 61 enterprise-token-only
104
+ DevOps endpoints, is reachable with the credentials from `pingcode auth login`.
105
+
106
+ ## 5. Everything else behaves exactly as it does elsewhere
107
+
108
+ `--dry-run` halts every write (search reads excepted, above) and prints the full request plan
109
+ with the `Authorization` header and any `client_secret` redacted. A `401` re-acquires the token
110
+ and replays once. A `429` honours `x-pc-retry-after` once. Any `2xx` is success. The exit-code
111
+ table in [`../SKILL.md`](../SKILL.md) applies unchanged; on a `403` the scope the docs declare
112
+ for that endpoint is printed to stderr (or an honest "the docs declare no scope" when they
113
+ declare none).
114
+
115
+ The rate limit is shared with everything else, so a `--all` sweep over a large collection spends
116
+ the same budget any other command would.