@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
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const core = require('../core');
|
|
4
|
+
const shared = require('./shared');
|
|
5
|
+
|
|
6
|
+
// ── Identifier helpers ─────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function isIdentifier(arg) {
|
|
9
|
+
return /^[A-Z]{2,}-\d+$/.test(arg);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRawId(arg) {
|
|
13
|
+
return /^[a-fA-F0-9]{24,32}$/.test(arg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Help ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function printHelp() {
|
|
19
|
+
console.log([
|
|
20
|
+
'PingCode idea — Manage ideas (requirements)',
|
|
21
|
+
'',
|
|
22
|
+
'Usage: pingcode idea <subcommand> [options]',
|
|
23
|
+
'',
|
|
24
|
+
'Subcommands:',
|
|
25
|
+
' create Create an idea',
|
|
26
|
+
' update Update an idea',
|
|
27
|
+
' list List ideas',
|
|
28
|
+
' get Get a single idea by id or identifier',
|
|
29
|
+
' search Search ideas',
|
|
30
|
+
' states List idea states',
|
|
31
|
+
' properties List idea properties',
|
|
32
|
+
' suites List idea suites',
|
|
33
|
+
' plans List idea plans',
|
|
34
|
+
' priorities List idea priorities',
|
|
35
|
+
' transition-history Get transition history of an idea',
|
|
36
|
+
' transition-histories List transition histories of ideas',
|
|
37
|
+
'',
|
|
38
|
+
'Global options:',
|
|
39
|
+
' --base-url URL PingCode base URL',
|
|
40
|
+
' --client-id ID OAuth client ID',
|
|
41
|
+
' --client-secret SECRET OAuth client secret',
|
|
42
|
+
' --token TOKEN Bearer token (skip OAuth)',
|
|
43
|
+
' --user-id ID Current user ID',
|
|
44
|
+
' --user-name NAME Current user name',
|
|
45
|
+
' --workspace-cache PATH Workspace cache file path',
|
|
46
|
+
' --no-workspace-cache Disable workspace cache',
|
|
47
|
+
' --no-token-cache Disable token cache',
|
|
48
|
+
' --dry-run Show API request without executing',
|
|
49
|
+
' --compact Compact output',
|
|
50
|
+
' --grant-type TYPE OAuth grant type: client_credentials, authorization_code, or auto (default; uses cached token type)',
|
|
51
|
+
' --help Show this help',
|
|
52
|
+
].join('\n'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printSubcommandHelp(subcommand) {
|
|
56
|
+
switch (subcommand) {
|
|
57
|
+
case 'create':
|
|
58
|
+
console.log([
|
|
59
|
+
'Usage: pingcode idea create [options]',
|
|
60
|
+
'',
|
|
61
|
+
'Create an idea.',
|
|
62
|
+
'',
|
|
63
|
+
'Options:',
|
|
64
|
+
' --product ID (required) Raw product id',
|
|
65
|
+
' --title TEXT (required) Idea title (max 255 chars)',
|
|
66
|
+
' --assignee NAME Assignee (name resolvable from workspace cache)',
|
|
67
|
+
' --description TEXT Description',
|
|
68
|
+
' --suite ID Raw suite id',
|
|
69
|
+
' --priority ID Raw priority id',
|
|
70
|
+
' --properties JSON Properties as JSON object',
|
|
71
|
+
'',
|
|
72
|
+
'Examples:',
|
|
73
|
+
' pingcode idea create --product 6422711c3f12e6c1e46d40e9 --title "New feature"',
|
|
74
|
+
' pingcode idea create --product 6422711c3f12e6c1e46d40e9 --title "New" --assignee john --priority 5cb9466afda1ce4ca0090005 --dry-run',
|
|
75
|
+
].join('\n'));
|
|
76
|
+
break;
|
|
77
|
+
case 'update':
|
|
78
|
+
console.log([
|
|
79
|
+
'Usage: pingcode idea update <id|identifier> [options]',
|
|
80
|
+
'',
|
|
81
|
+
'Update an idea.',
|
|
82
|
+
'',
|
|
83
|
+
'Options:',
|
|
84
|
+
' --title TEXT New title',
|
|
85
|
+
' --description TEXT New description',
|
|
86
|
+
' --state ID Raw state id',
|
|
87
|
+
' --priority ID Raw priority id',
|
|
88
|
+
' --assignee NAME Assignee (name resolvable from workspace cache)',
|
|
89
|
+
' --progress NUM Progress (0 to 1, at most 2 decimal places)',
|
|
90
|
+
' --plan-at JSON Plan time range as JSON: {"from":<ts>,"to":<ts>,"granularity":"day"}',
|
|
91
|
+
' --real-at JSON Real time range as JSON (same format as --plan-at)',
|
|
92
|
+
' --plan ID Raw plan id',
|
|
93
|
+
' --suite ID Raw suite id',
|
|
94
|
+
' --properties JSON Properties as JSON object',
|
|
95
|
+
'',
|
|
96
|
+
'Examples:',
|
|
97
|
+
' pingcode idea update SLC-1 --title "Updated title"',
|
|
98
|
+
' pingcode idea update SLC-1 --state 63e1bf51898a0be5a2d21b2a --progress 0.5 --dry-run',
|
|
99
|
+
].join('\n'));
|
|
100
|
+
break;
|
|
101
|
+
case 'list':
|
|
102
|
+
console.log([
|
|
103
|
+
'Usage: pingcode idea list [options]',
|
|
104
|
+
'',
|
|
105
|
+
'Options:',
|
|
106
|
+
' --product ID Filter by raw product id',
|
|
107
|
+
' --state ID Filter by raw state id',
|
|
108
|
+
' --priority ID Filter by raw priority id',
|
|
109
|
+
' --keywords TEXT Filter by keywords (matches identifier and title)',
|
|
110
|
+
' --include-public-image-token Include image access token in response',
|
|
111
|
+
].join('\n'));
|
|
112
|
+
break;
|
|
113
|
+
case 'get':
|
|
114
|
+
console.log([
|
|
115
|
+
'Usage: pingcode idea get <id|identifier>',
|
|
116
|
+
'',
|
|
117
|
+
'Get a single idea by raw id or identifier.',
|
|
118
|
+
'For identifiers (format PROJ-123), first resolves to a raw id via keyword search.',
|
|
119
|
+
'',
|
|
120
|
+
'Options:',
|
|
121
|
+
' --include-public-image-token Include image access token in response',
|
|
122
|
+
].join('\n'));
|
|
123
|
+
break;
|
|
124
|
+
case 'search':
|
|
125
|
+
console.log([
|
|
126
|
+
'Usage: pingcode idea search [options]',
|
|
127
|
+
'',
|
|
128
|
+
'Search ideas using structured query filters.',
|
|
129
|
+
'',
|
|
130
|
+
'Options:',
|
|
131
|
+
' --filter JSON Filter conditions as JSON object (MongoDB-like query syntax)',
|
|
132
|
+
' --keywords TEXT Keywords to filter by (matches identifier and title)',
|
|
133
|
+
' --limit NUM Number of results per page (1-100, default 30)',
|
|
134
|
+
' --page-index NUM Page index starting from 0 (default 0)',
|
|
135
|
+
' --include-public-image-token Include image access token in response',
|
|
136
|
+
'',
|
|
137
|
+
'Examples:',
|
|
138
|
+
' pingcode idea search --keywords "login" --compact',
|
|
139
|
+
' pingcode idea search --filter \'{"title":{"contains":"账号"}}\' --dry-run',
|
|
140
|
+
' pingcode idea search --filter \'{"product.id":{"in":["6422711c3f12e6c1e46d40e9"]}}\' --limit 10',
|
|
141
|
+
].join('\n'));
|
|
142
|
+
break;
|
|
143
|
+
case 'states':
|
|
144
|
+
console.log([
|
|
145
|
+
'Usage: pingcode idea states [options]',
|
|
146
|
+
'',
|
|
147
|
+
'List idea states for a product.',
|
|
148
|
+
'',
|
|
149
|
+
'Options:',
|
|
150
|
+
' --product ID (required) Raw product id',
|
|
151
|
+
'',
|
|
152
|
+
'Examples:',
|
|
153
|
+
' pingcode idea states --product 6422711c3f12e6c1e46d40e9 --compact',
|
|
154
|
+
].join('\n'));
|
|
155
|
+
break;
|
|
156
|
+
case 'properties':
|
|
157
|
+
console.log([
|
|
158
|
+
'Usage: pingcode idea properties [options]',
|
|
159
|
+
'',
|
|
160
|
+
'List idea properties for a product.',
|
|
161
|
+
'',
|
|
162
|
+
'Options:',
|
|
163
|
+
' --product ID (required) Raw product id',
|
|
164
|
+
'',
|
|
165
|
+
'Examples:',
|
|
166
|
+
' pingcode idea properties --product 6422711c3f12e6c1e46d40e9 --compact',
|
|
167
|
+
].join('\n'));
|
|
168
|
+
break;
|
|
169
|
+
case 'suites':
|
|
170
|
+
console.log([
|
|
171
|
+
'Usage: pingcode idea suites [options]',
|
|
172
|
+
'',
|
|
173
|
+
'List idea suites (modules) for a product.',
|
|
174
|
+
'',
|
|
175
|
+
'Options:',
|
|
176
|
+
' --product ID (required) Raw product id',
|
|
177
|
+
'',
|
|
178
|
+
'Examples:',
|
|
179
|
+
' pingcode idea suites --product 6422711c3f12e6c1e46d40e9 --compact',
|
|
180
|
+
].join('\n'));
|
|
181
|
+
break;
|
|
182
|
+
case 'plans':
|
|
183
|
+
console.log([
|
|
184
|
+
'Usage: pingcode idea plans [options]',
|
|
185
|
+
'',
|
|
186
|
+
'List idea plans for a product.',
|
|
187
|
+
'',
|
|
188
|
+
'Options:',
|
|
189
|
+
' --product ID (required) Raw product id',
|
|
190
|
+
'',
|
|
191
|
+
'Examples:',
|
|
192
|
+
' pingcode idea plans --product 6422711c3f12e6c1e46d40e9 --compact',
|
|
193
|
+
].join('\n'));
|
|
194
|
+
break;
|
|
195
|
+
case 'priorities':
|
|
196
|
+
console.log([
|
|
197
|
+
'Usage: pingcode idea priorities [options]',
|
|
198
|
+
'',
|
|
199
|
+
'List idea priorities for a product.',
|
|
200
|
+
'',
|
|
201
|
+
'Options:',
|
|
202
|
+
' --product ID (required) Raw product id',
|
|
203
|
+
'',
|
|
204
|
+
'Examples:',
|
|
205
|
+
' pingcode idea priorities --product 6422711c3f12e6c1e46d40e9 --compact',
|
|
206
|
+
].join('\n'));
|
|
207
|
+
break;
|
|
208
|
+
case 'transition-history':
|
|
209
|
+
console.log([
|
|
210
|
+
'Usage: pingcode idea transition-history <transition_history_id> <id|identifier>',
|
|
211
|
+
'',
|
|
212
|
+
'Get a single transition history record of an idea.',
|
|
213
|
+
'',
|
|
214
|
+
'The first argument is the transition history id; the second is the idea id or identifier.',
|
|
215
|
+
'For identifiers (format PROJ-123), first resolves to a raw id via keyword search.',
|
|
216
|
+
'',
|
|
217
|
+
'Examples:',
|
|
218
|
+
' pingcode idea transition-history 64c3676c983bb9481ee1eea5 64b4d70ba368e6594360ea24',
|
|
219
|
+
' pingcode idea transition-history 64c3676c983bb9481ee1eea5 SLC-1 --dry-run',
|
|
220
|
+
].join('\n'));
|
|
221
|
+
break;
|
|
222
|
+
case 'transition-histories':
|
|
223
|
+
console.log([
|
|
224
|
+
'Usage: pingcode idea transition-histories <id|identifier>',
|
|
225
|
+
'',
|
|
226
|
+
'List transition history records of an idea.',
|
|
227
|
+
'',
|
|
228
|
+
'For identifiers (format PROJ-123), first resolves to a raw id via keyword search.',
|
|
229
|
+
'',
|
|
230
|
+
'Examples:',
|
|
231
|
+
' pingcode idea transition-histories 64b4d70ba368e6594360ea24 --compact',
|
|
232
|
+
' pingcode idea transition-histories SLC-1 --dry-run',
|
|
233
|
+
].join('\n'));
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
printHelp();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── List subcommand ───────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function parseListArgs(tokens) {
|
|
243
|
+
const args = {
|
|
244
|
+
product: null,
|
|
245
|
+
state: null,
|
|
246
|
+
priority: null,
|
|
247
|
+
keywords: null,
|
|
248
|
+
include_public_image_token: false,
|
|
249
|
+
};
|
|
250
|
+
const stringFlags = {
|
|
251
|
+
'--product': 'product',
|
|
252
|
+
'--state': 'state',
|
|
253
|
+
'--priority': 'priority',
|
|
254
|
+
'--keywords': 'keywords',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
258
|
+
const arg = tokens[i];
|
|
259
|
+
if (arg in stringFlags) {
|
|
260
|
+
if (i + 1 >= tokens.length) {
|
|
261
|
+
throw new core.PingCodeError(`Flag ${arg} requires a value`);
|
|
262
|
+
}
|
|
263
|
+
args[stringFlags[arg]] = tokens[i + 1];
|
|
264
|
+
i += 1;
|
|
265
|
+
} else if (arg.startsWith('--')) {
|
|
266
|
+
if (arg === '--include-public-image-token') {
|
|
267
|
+
args.include_public_image_token = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const eqIndex = arg.indexOf('=');
|
|
271
|
+
if (eqIndex !== -1) {
|
|
272
|
+
const flag = arg.slice(0, eqIndex);
|
|
273
|
+
const value = arg.slice(eqIndex + 1);
|
|
274
|
+
if (flag in stringFlags) {
|
|
275
|
+
args[stringFlags[flag]] = value;
|
|
276
|
+
} else {
|
|
277
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
278
|
+
}
|
|
279
|
+
} else if (!(arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS)) {
|
|
280
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use idea list --help for usage.`);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea list --help for usage.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return args;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runList(client, opts, args) {
|
|
290
|
+
const params = {};
|
|
291
|
+
if (args.product) {
|
|
292
|
+
if (!isRawId(args.product)) {
|
|
293
|
+
throw new core.PingCodeError('--product must be a raw id');
|
|
294
|
+
}
|
|
295
|
+
params.product_id = args.product;
|
|
296
|
+
}
|
|
297
|
+
if (args.state) {
|
|
298
|
+
if (!isRawId(args.state)) {
|
|
299
|
+
throw new core.PingCodeError('--state must be a raw id');
|
|
300
|
+
}
|
|
301
|
+
params.state_id = args.state;
|
|
302
|
+
}
|
|
303
|
+
if (args.priority) {
|
|
304
|
+
if (!isRawId(args.priority)) {
|
|
305
|
+
throw new core.PingCodeError('--priority must be a raw id');
|
|
306
|
+
}
|
|
307
|
+
params.priority_id = args.priority;
|
|
308
|
+
}
|
|
309
|
+
if (args.keywords) {
|
|
310
|
+
params.keywords = args.keywords;
|
|
311
|
+
}
|
|
312
|
+
if (args.include_public_image_token) {
|
|
313
|
+
params.include_public_image_token = 'description';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return await client.request(
|
|
317
|
+
'GET',
|
|
318
|
+
'/v1/ship/ideas',
|
|
319
|
+
params,
|
|
320
|
+
null,
|
|
321
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Get subcommand ───────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
function parseGetArgs(tokens) {
|
|
328
|
+
let target = null;
|
|
329
|
+
let includePublicImageToken = false;
|
|
330
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
331
|
+
const arg = tokens[i];
|
|
332
|
+
if (!arg.startsWith('--')) {
|
|
333
|
+
if (target === null) {
|
|
334
|
+
target = arg;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea get --help for usage.`);
|
|
338
|
+
}
|
|
339
|
+
if (arg === '--include-public-image-token') {
|
|
340
|
+
includePublicImageToken = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (shared.BASE_GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
|
|
344
|
+
if (shared.BASE_GLOBAL_STRING_FLAGS[arg]) {
|
|
345
|
+
i += 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!target) {
|
|
350
|
+
throw new core.PingCodeError('An idea id or identifier is required. Use idea get --help for usage.');
|
|
351
|
+
}
|
|
352
|
+
return { target, include_public_image_token: includePublicImageToken };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function runGet(client, opts, args) {
|
|
356
|
+
const params = {};
|
|
357
|
+
if (args.include_public_image_token) {
|
|
358
|
+
params.include_public_image_token = 'description';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (isIdentifier(args.target)) {
|
|
362
|
+
const searchParams = { keywords: args.target };
|
|
363
|
+
if (args.include_public_image_token) {
|
|
364
|
+
searchParams.include_public_image_token = 'description';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (opts.dry_run) {
|
|
368
|
+
return {
|
|
369
|
+
dry_run: true,
|
|
370
|
+
get: {
|
|
371
|
+
method: 'GET',
|
|
372
|
+
path: '/v1/ship/ideas',
|
|
373
|
+
params: searchParams,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = await client.request(
|
|
379
|
+
'GET',
|
|
380
|
+
'/v1/ship/ideas',
|
|
381
|
+
searchParams,
|
|
382
|
+
null,
|
|
383
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
384
|
+
);
|
|
385
|
+
const values = core.pageValues(result);
|
|
386
|
+
if (values.length === 0) {
|
|
387
|
+
throw new core.PingCodeError(`No idea found with identifier ${args.target}`);
|
|
388
|
+
}
|
|
389
|
+
return values[0];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return await client.request(
|
|
393
|
+
'GET',
|
|
394
|
+
`/v1/ship/ideas/${args.target}`,
|
|
395
|
+
Object.keys(params).length > 0 ? params : null,
|
|
396
|
+
null,
|
|
397
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Transition Histories subcommand (list) ────────────────────────────
|
|
402
|
+
|
|
403
|
+
function parseTransitionHistoriesArgs(tokens) {
|
|
404
|
+
let target = null;
|
|
405
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
406
|
+
const arg = tokens[i];
|
|
407
|
+
if (!arg.startsWith('--')) {
|
|
408
|
+
if (target === null) {
|
|
409
|
+
target = arg;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea transition-histories --help for usage.`);
|
|
413
|
+
}
|
|
414
|
+
if (shared.BASE_GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
|
|
415
|
+
if (shared.BASE_GLOBAL_STRING_FLAGS[arg]) {
|
|
416
|
+
i += 1;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!target) {
|
|
421
|
+
throw new core.PingCodeError('An idea id or identifier is required. Use idea transition-histories --help for usage.');
|
|
422
|
+
}
|
|
423
|
+
return { target };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function runTransitionHistories(client, opts, args) {
|
|
427
|
+
if (isIdentifier(args.target)) {
|
|
428
|
+
if (opts.dry_run) {
|
|
429
|
+
return {
|
|
430
|
+
dry_run: true,
|
|
431
|
+
resolution: {
|
|
432
|
+
method: 'GET',
|
|
433
|
+
path: '/v1/ship/ideas',
|
|
434
|
+
params: { keywords: args.target },
|
|
435
|
+
},
|
|
436
|
+
list: {
|
|
437
|
+
method: 'GET',
|
|
438
|
+
path: '/v1/ship/ideas/{id}/transition_histories',
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const resolved = await client.request(
|
|
444
|
+
'GET',
|
|
445
|
+
'/v1/ship/ideas',
|
|
446
|
+
{ keywords: args.target },
|
|
447
|
+
null,
|
|
448
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
449
|
+
);
|
|
450
|
+
const values = core.pageValues(resolved);
|
|
451
|
+
if (values.length === 0) {
|
|
452
|
+
throw new core.PingCodeError(`No idea found with identifier ${args.target}`);
|
|
453
|
+
}
|
|
454
|
+
const ideaId = values[0].id;
|
|
455
|
+
return await client.request(
|
|
456
|
+
'GET',
|
|
457
|
+
`/v1/ship/ideas/${ideaId}/transition_histories`,
|
|
458
|
+
null,
|
|
459
|
+
null,
|
|
460
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return await client.request(
|
|
465
|
+
'GET',
|
|
466
|
+
`/v1/ship/ideas/${args.target}/transition_histories`,
|
|
467
|
+
null,
|
|
468
|
+
null,
|
|
469
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ── Transition History subcommand (single) ────────────────────────────
|
|
474
|
+
|
|
475
|
+
function parseTransitionHistoryArgs(tokens) {
|
|
476
|
+
const positionals = [];
|
|
477
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
478
|
+
const arg = tokens[i];
|
|
479
|
+
if (!arg.startsWith('--')) {
|
|
480
|
+
positionals.push(arg);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (shared.BASE_GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
|
|
484
|
+
if (shared.BASE_GLOBAL_STRING_FLAGS[arg]) {
|
|
485
|
+
i += 1;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return { positionals };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function runTransitionHistory(client, opts, positionals) {
|
|
493
|
+
if (positionals.length < 2) {
|
|
494
|
+
throw new core.PingCodeError('A transition history id and an idea id/identifier are required. Use idea transition-history --help for usage.');
|
|
495
|
+
}
|
|
496
|
+
if (positionals.length > 2) {
|
|
497
|
+
throw new core.PingCodeError(`Unexpected argument: ${positionals[2]}. Use idea transition-history --help for usage.`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const historyId = positionals[0];
|
|
501
|
+
const ideaRef = positionals[1];
|
|
502
|
+
|
|
503
|
+
if (isIdentifier(ideaRef)) {
|
|
504
|
+
if (opts.dry_run) {
|
|
505
|
+
return {
|
|
506
|
+
dry_run: true,
|
|
507
|
+
resolution: {
|
|
508
|
+
method: 'GET',
|
|
509
|
+
path: '/v1/ship/ideas',
|
|
510
|
+
params: { keywords: ideaRef },
|
|
511
|
+
},
|
|
512
|
+
get: {
|
|
513
|
+
method: 'GET',
|
|
514
|
+
path: `/v1/ship/ideas/{id}/transition_histories/${historyId}`,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const resolved = await client.request(
|
|
520
|
+
'GET',
|
|
521
|
+
'/v1/ship/ideas',
|
|
522
|
+
{ keywords: ideaRef },
|
|
523
|
+
null,
|
|
524
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
525
|
+
);
|
|
526
|
+
const values = core.pageValues(resolved);
|
|
527
|
+
if (values.length === 0) {
|
|
528
|
+
throw new core.PingCodeError(`No idea found with identifier ${ideaRef}`);
|
|
529
|
+
}
|
|
530
|
+
const ideaId = values[0].id;
|
|
531
|
+
return await client.request(
|
|
532
|
+
'GET',
|
|
533
|
+
`/v1/ship/ideas/${ideaId}/transition_histories/${historyId}`,
|
|
534
|
+
null,
|
|
535
|
+
null,
|
|
536
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return await client.request(
|
|
541
|
+
'GET',
|
|
542
|
+
`/v1/ship/ideas/${ideaRef}/transition_histories/${historyId}`,
|
|
543
|
+
null,
|
|
544
|
+
null,
|
|
545
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Create subcommand ──────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
function parseCreateArgs(tokens) {
|
|
552
|
+
const args = {
|
|
553
|
+
product: null,
|
|
554
|
+
title: null,
|
|
555
|
+
assignee: null,
|
|
556
|
+
description: null,
|
|
557
|
+
suite: null,
|
|
558
|
+
priority: null,
|
|
559
|
+
properties: null,
|
|
560
|
+
};
|
|
561
|
+
const stringFlags = {
|
|
562
|
+
'--product': 'product',
|
|
563
|
+
'--title': 'title',
|
|
564
|
+
'--assignee': 'assignee',
|
|
565
|
+
'--description': 'description',
|
|
566
|
+
'--suite': 'suite',
|
|
567
|
+
'--priority': 'priority',
|
|
568
|
+
'--properties': 'properties',
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
572
|
+
const arg = tokens[i];
|
|
573
|
+
if (arg in stringFlags) {
|
|
574
|
+
if (i + 1 >= tokens.length) {
|
|
575
|
+
throw new core.PingCodeError(`Flag ${arg} requires a value`);
|
|
576
|
+
}
|
|
577
|
+
args[stringFlags[arg]] = tokens[i + 1];
|
|
578
|
+
i += 1;
|
|
579
|
+
} else if (arg.startsWith('--')) {
|
|
580
|
+
const eqIndex = arg.indexOf('=');
|
|
581
|
+
if (eqIndex !== -1) {
|
|
582
|
+
const flag = arg.slice(0, eqIndex);
|
|
583
|
+
const value = arg.slice(eqIndex + 1);
|
|
584
|
+
if (flag in stringFlags) {
|
|
585
|
+
args[stringFlags[flag]] = value;
|
|
586
|
+
} else {
|
|
587
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
588
|
+
}
|
|
589
|
+
} else if (!(arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS)) {
|
|
590
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use idea create --help for usage.`);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea create --help for usage.`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return args;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function runCreate(client, opts, args) {
|
|
600
|
+
// Validate required fields
|
|
601
|
+
if (typeof args.product !== 'string' || !args.product.trim()) {
|
|
602
|
+
throw new core.PingCodeError('--product is required and must be a raw id. Use idea create --help for usage.');
|
|
603
|
+
}
|
|
604
|
+
if (!isRawId(args.product)) {
|
|
605
|
+
throw new core.PingCodeError('--product must be a raw id');
|
|
606
|
+
}
|
|
607
|
+
if (typeof args.title !== 'string' || !args.title.trim()) {
|
|
608
|
+
throw new core.PingCodeError('--title is required and must be non-empty. Use idea create --help for usage.');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Build body
|
|
612
|
+
const body = {
|
|
613
|
+
product_id: args.product,
|
|
614
|
+
title: args.title,
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// Description
|
|
618
|
+
if (args.description) {
|
|
619
|
+
body.description = args.description;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Suite (raw ID)
|
|
623
|
+
if (args.suite) {
|
|
624
|
+
if (!isRawId(args.suite)) {
|
|
625
|
+
throw new core.PingCodeError('--suite must be a raw id');
|
|
626
|
+
}
|
|
627
|
+
body.suite_id = args.suite;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Priority (raw ID)
|
|
631
|
+
if (args.priority) {
|
|
632
|
+
if (!isRawId(args.priority)) {
|
|
633
|
+
throw new core.PingCodeError('--priority must be a raw id');
|
|
634
|
+
}
|
|
635
|
+
body.priority_id = args.priority;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Properties (JSON object)
|
|
639
|
+
if (args.properties) {
|
|
640
|
+
body.properties = core.parseJsonObject(args.properties, '--properties');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Assignee (name resolvable from cache)
|
|
644
|
+
if (args.assignee) {
|
|
645
|
+
const cache = client.workspaceCache;
|
|
646
|
+
const userId = core.cachedUserId(args.assignee, cache);
|
|
647
|
+
body.assignee_id = userId;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return await client.request(
|
|
651
|
+
'POST',
|
|
652
|
+
'/v1/ship/ideas',
|
|
653
|
+
null,
|
|
654
|
+
body,
|
|
655
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ── Update subcommand ──────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
function parseUpdateArgs(tokens) {
|
|
662
|
+
const args = {
|
|
663
|
+
target: null,
|
|
664
|
+
title: null,
|
|
665
|
+
description: null,
|
|
666
|
+
state: null,
|
|
667
|
+
priority: null,
|
|
668
|
+
assignee: null,
|
|
669
|
+
progress: null,
|
|
670
|
+
planAt: null,
|
|
671
|
+
realAt: null,
|
|
672
|
+
plan: null,
|
|
673
|
+
suite: null,
|
|
674
|
+
properties: null,
|
|
675
|
+
};
|
|
676
|
+
const stringFlags = {
|
|
677
|
+
'--title': 'title',
|
|
678
|
+
'--description': 'description',
|
|
679
|
+
'--state': 'state',
|
|
680
|
+
'--priority': 'priority',
|
|
681
|
+
'--assignee': 'assignee',
|
|
682
|
+
'--progress': 'progress',
|
|
683
|
+
'--plan-at': 'planAt',
|
|
684
|
+
'--real-at': 'realAt',
|
|
685
|
+
'--plan': 'plan',
|
|
686
|
+
'--suite': 'suite',
|
|
687
|
+
'--properties': 'properties',
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
691
|
+
const arg = tokens[i];
|
|
692
|
+
if (!arg.startsWith('--')) {
|
|
693
|
+
if (args.target === null) {
|
|
694
|
+
args.target = arg;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea update --help for usage.`);
|
|
698
|
+
}
|
|
699
|
+
if (arg in stringFlags) {
|
|
700
|
+
if (i + 1 >= tokens.length) {
|
|
701
|
+
throw new core.PingCodeError(`Flag ${arg} requires a value`);
|
|
702
|
+
}
|
|
703
|
+
args[stringFlags[arg]] = tokens[i + 1];
|
|
704
|
+
i += 1;
|
|
705
|
+
} else if (arg.startsWith('--')) {
|
|
706
|
+
const eqIndex = arg.indexOf('=');
|
|
707
|
+
if (eqIndex !== -1) {
|
|
708
|
+
const flag = arg.slice(0, eqIndex);
|
|
709
|
+
const value = arg.slice(eqIndex + 1);
|
|
710
|
+
if (flag in stringFlags) {
|
|
711
|
+
args[stringFlags[flag]] = value;
|
|
712
|
+
} else {
|
|
713
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
714
|
+
}
|
|
715
|
+
} else if (!(arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS)) {
|
|
716
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use idea update --help for usage.`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!args.target) {
|
|
722
|
+
throw new core.PingCodeError('An idea id or identifier is required. Use idea update --help for usage.');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const hasUpdateField = Object.entries(args).some(
|
|
726
|
+
([key, value]) => key !== 'target' && value !== null,
|
|
727
|
+
);
|
|
728
|
+
if (!hasUpdateField) {
|
|
729
|
+
throw new core.PingCodeError('At least one field to update is required. Use idea update --help for usage.');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return args;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function validateProgress(value) {
|
|
736
|
+
const num = Number(value);
|
|
737
|
+
if (Number.isNaN(num)) {
|
|
738
|
+
throw new core.PingCodeError('--progress must be a number between 0 and 1');
|
|
739
|
+
}
|
|
740
|
+
if (num < 0 || num > 1) {
|
|
741
|
+
throw new core.PingCodeError('--progress must be between 0 and 1');
|
|
742
|
+
}
|
|
743
|
+
// At most 2 decimal places
|
|
744
|
+
const str = String(value);
|
|
745
|
+
const dotIndex = str.indexOf('.');
|
|
746
|
+
if (dotIndex !== -1 && str.length - dotIndex - 1 > 2) {
|
|
747
|
+
throw new core.PingCodeError('--progress must have at most 2 decimal places');
|
|
748
|
+
}
|
|
749
|
+
return num;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function validatePlanAt(raw, label) {
|
|
753
|
+
const obj = core.parseJsonObject(raw, label);
|
|
754
|
+
if (!obj) return null;
|
|
755
|
+
if (typeof obj.from === 'undefined' || typeof obj.to === 'undefined' || typeof obj.granularity === 'undefined') {
|
|
756
|
+
throw new core.PingCodeError(`${label} must include from, to, and granularity fields`);
|
|
757
|
+
}
|
|
758
|
+
const validGranularities = new Set(['year', 'quarter', 'month', 'day']);
|
|
759
|
+
if (!validGranularities.has(obj.granularity)) {
|
|
760
|
+
throw new core.PingCodeError(`${label} granularity must be one of: year, quarter, month, day`);
|
|
761
|
+
}
|
|
762
|
+
return obj;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function runUpdate(client, opts, args) {
|
|
766
|
+
const body = {};
|
|
767
|
+
|
|
768
|
+
if (args.title) body.title = args.title;
|
|
769
|
+
if (args.description) body.description = args.description;
|
|
770
|
+
|
|
771
|
+
if (args.state) {
|
|
772
|
+
if (!isRawId(args.state)) {
|
|
773
|
+
throw new core.PingCodeError('--state must be a raw id');
|
|
774
|
+
}
|
|
775
|
+
body.state_id = args.state;
|
|
776
|
+
}
|
|
777
|
+
if (args.priority) {
|
|
778
|
+
if (!isRawId(args.priority)) {
|
|
779
|
+
throw new core.PingCodeError('--priority must be a raw id');
|
|
780
|
+
}
|
|
781
|
+
body.priority_id = args.priority;
|
|
782
|
+
}
|
|
783
|
+
if (args.assignee) {
|
|
784
|
+
const cache = client.workspaceCache;
|
|
785
|
+
const userId = core.cachedUserId(args.assignee, cache);
|
|
786
|
+
body.assignee_id = userId;
|
|
787
|
+
}
|
|
788
|
+
if (args.progress) {
|
|
789
|
+
body.progress = validateProgress(args.progress);
|
|
790
|
+
}
|
|
791
|
+
if (args.planAt) {
|
|
792
|
+
body.plan_at = validatePlanAt(args.planAt, '--plan-at');
|
|
793
|
+
}
|
|
794
|
+
if (args.realAt) {
|
|
795
|
+
body.real_at = validatePlanAt(args.realAt, '--real-at');
|
|
796
|
+
}
|
|
797
|
+
if (args.plan) {
|
|
798
|
+
if (!isRawId(args.plan)) {
|
|
799
|
+
throw new core.PingCodeError('--plan must be a raw id');
|
|
800
|
+
}
|
|
801
|
+
body.plan_id = args.plan;
|
|
802
|
+
}
|
|
803
|
+
if (args.suite) {
|
|
804
|
+
if (!isRawId(args.suite)) {
|
|
805
|
+
throw new core.PingCodeError('--suite must be a raw id');
|
|
806
|
+
}
|
|
807
|
+
body.suite_id = args.suite;
|
|
808
|
+
}
|
|
809
|
+
if (args.properties) {
|
|
810
|
+
body.properties = core.parseJsonObject(args.properties, '--properties');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Resolve identifier to raw id if needed
|
|
814
|
+
let targetId = args.target;
|
|
815
|
+
if (isIdentifier(args.target)) {
|
|
816
|
+
if (opts.dry_run) {
|
|
817
|
+
return {
|
|
818
|
+
dry_run: true,
|
|
819
|
+
resolution: {
|
|
820
|
+
method: 'GET',
|
|
821
|
+
path: '/v1/ship/ideas',
|
|
822
|
+
params: { keywords: args.target },
|
|
823
|
+
},
|
|
824
|
+
update: {
|
|
825
|
+
method: 'PATCH',
|
|
826
|
+
path: '/v1/ship/ideas/{id}',
|
|
827
|
+
json: body,
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const resolved = await client.request(
|
|
833
|
+
'GET',
|
|
834
|
+
'/v1/ship/ideas',
|
|
835
|
+
{ keywords: args.target },
|
|
836
|
+
null,
|
|
837
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
838
|
+
);
|
|
839
|
+
const values = core.pageValues(resolved);
|
|
840
|
+
if (values.length === 0) {
|
|
841
|
+
throw new core.PingCodeError(`No idea found with identifier ${args.target}`);
|
|
842
|
+
}
|
|
843
|
+
targetId = values[0].id;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return await client.request(
|
|
847
|
+
'PATCH',
|
|
848
|
+
`/v1/ship/ideas/${targetId}`,
|
|
849
|
+
null,
|
|
850
|
+
body,
|
|
851
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── Search subcommand ────────────────────────────────────────────────────
|
|
856
|
+
|
|
857
|
+
function parseSearchArgs(tokens) {
|
|
858
|
+
const args = {
|
|
859
|
+
filter: null,
|
|
860
|
+
keywords: null,
|
|
861
|
+
page_size: null,
|
|
862
|
+
page_index: null,
|
|
863
|
+
include_public_image_token: false,
|
|
864
|
+
};
|
|
865
|
+
const stringFlags = {
|
|
866
|
+
'--filter': 'filter',
|
|
867
|
+
'--keywords': 'keywords',
|
|
868
|
+
'--limit': 'page_size',
|
|
869
|
+
'--page-index': 'page_index',
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
873
|
+
const arg = tokens[i];
|
|
874
|
+
if (arg in stringFlags) {
|
|
875
|
+
if (i + 1 >= tokens.length) {
|
|
876
|
+
throw new core.PingCodeError(`Flag ${arg} requires a value`);
|
|
877
|
+
}
|
|
878
|
+
args[stringFlags[arg]] = tokens[i + 1];
|
|
879
|
+
i += 1;
|
|
880
|
+
} else if (arg.startsWith('--')) {
|
|
881
|
+
if (arg === '--include-public-image-token') {
|
|
882
|
+
args.include_public_image_token = true;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const eqIndex = arg.indexOf('=');
|
|
886
|
+
if (eqIndex !== -1) {
|
|
887
|
+
const flag = arg.slice(0, eqIndex);
|
|
888
|
+
const value = arg.slice(eqIndex + 1);
|
|
889
|
+
if (flag in stringFlags) {
|
|
890
|
+
args[stringFlags[flag]] = value;
|
|
891
|
+
} else {
|
|
892
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
893
|
+
}
|
|
894
|
+
} else if (!(arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS)) {
|
|
895
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use idea search --help for usage.`);
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea search --help for usage.`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Validate and convert page_size / page_index to numbers
|
|
903
|
+
if (args.page_size !== null) {
|
|
904
|
+
const num = Number(args.page_size);
|
|
905
|
+
if (Number.isNaN(num) || num < 1 || num > 100) {
|
|
906
|
+
throw new core.PingCodeError('--limit must be a number between 1 and 100');
|
|
907
|
+
}
|
|
908
|
+
args.page_size = num;
|
|
909
|
+
}
|
|
910
|
+
if (args.page_index !== null) {
|
|
911
|
+
const num = Number(args.page_index);
|
|
912
|
+
if (Number.isNaN(num) || num < 0) {
|
|
913
|
+
throw new core.PingCodeError('--page-index must be a non-negative number');
|
|
914
|
+
}
|
|
915
|
+
args.page_index = num;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Parse filter as JSON object
|
|
919
|
+
if (args.filter !== null) {
|
|
920
|
+
args.filter = core.parseJsonObject(args.filter, '--filter');
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return args;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function runSearch(client, opts, args) {
|
|
927
|
+
const payload = {};
|
|
928
|
+
|
|
929
|
+
if (args.filter !== null) {
|
|
930
|
+
payload.filter = args.filter;
|
|
931
|
+
}
|
|
932
|
+
if (args.keywords !== null) {
|
|
933
|
+
payload.keywords = args.keywords;
|
|
934
|
+
}
|
|
935
|
+
if (args.page_size !== null) {
|
|
936
|
+
payload.page_size = args.page_size;
|
|
937
|
+
}
|
|
938
|
+
if (args.page_index !== null) {
|
|
939
|
+
payload.page_index = args.page_index;
|
|
940
|
+
}
|
|
941
|
+
if (args.include_public_image_token) {
|
|
942
|
+
payload.include_public_image_token = 'description';
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const body = {
|
|
946
|
+
mode: 'query',
|
|
947
|
+
payload: payload,
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
return await client.request(
|
|
951
|
+
'POST',
|
|
952
|
+
'/v1/ship/ideas/search',
|
|
953
|
+
null,
|
|
954
|
+
body,
|
|
955
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ── Dictionary subcommands (states, properties, suites, plans, priorities)
|
|
960
|
+
|
|
961
|
+
function parseDictionaryArgs(tokens) {
|
|
962
|
+
const args = { product: null };
|
|
963
|
+
const stringFlags = { '--product': 'product' };
|
|
964
|
+
|
|
965
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
966
|
+
const arg = tokens[i];
|
|
967
|
+
if (arg in stringFlags) {
|
|
968
|
+
if (i + 1 >= tokens.length) {
|
|
969
|
+
throw new core.PingCodeError(`Flag ${arg} requires a value`);
|
|
970
|
+
}
|
|
971
|
+
args[stringFlags[arg]] = tokens[i + 1];
|
|
972
|
+
i += 1;
|
|
973
|
+
} else if (arg.startsWith('--')) {
|
|
974
|
+
const eqIndex = arg.indexOf('=');
|
|
975
|
+
if (eqIndex !== -1) {
|
|
976
|
+
const flag = arg.slice(0, eqIndex);
|
|
977
|
+
const value = arg.slice(eqIndex + 1);
|
|
978
|
+
if (flag in stringFlags) {
|
|
979
|
+
args[stringFlags[flag]] = value;
|
|
980
|
+
} else {
|
|
981
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
982
|
+
}
|
|
983
|
+
} else if (!(arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS)) {
|
|
984
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use idea <subcommand> --help for usage.`);
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
987
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use idea <subcommand> --help for usage.`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return args;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function runDictionary(client, opts, args, resource) {
|
|
995
|
+
if (!args.product) {
|
|
996
|
+
throw new core.PingCodeError('--product is required and must be a raw id. Use idea <subcommand> --help for usage.');
|
|
997
|
+
}
|
|
998
|
+
if (!isRawId(args.product)) {
|
|
999
|
+
throw new core.PingCodeError('--product must be a raw id');
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const params = { product_id: args.product };
|
|
1003
|
+
|
|
1004
|
+
return await client.request(
|
|
1005
|
+
'GET',
|
|
1006
|
+
`/v1/ship/idea/${resource}`,
|
|
1007
|
+
params,
|
|
1008
|
+
null,
|
|
1009
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// ── Main dispatcher ───────────────────────────────────────────────────────
|
|
1014
|
+
|
|
1015
|
+
async function run(argv) {
|
|
1016
|
+
const tokens = argv || [];
|
|
1017
|
+
|
|
1018
|
+
if (tokens.length === 0 || tokens[0] === '--help' || tokens[0] === '-h') {
|
|
1019
|
+
printHelp();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const subcommand = tokens[0];
|
|
1024
|
+
const remaining = tokens.slice(1);
|
|
1025
|
+
|
|
1026
|
+
if (remaining.includes('--help') || remaining.includes('-h')) {
|
|
1027
|
+
printSubcommandHelp(subcommand);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const { opts, remaining: subArgs } = shared.parseGlobalOptions(remaining);
|
|
1032
|
+
const client = shared.clientFromOpts(opts);
|
|
1033
|
+
|
|
1034
|
+
try {
|
|
1035
|
+
let result;
|
|
1036
|
+
switch (subcommand) {
|
|
1037
|
+
case 'list': {
|
|
1038
|
+
const listArgs = parseListArgs(subArgs);
|
|
1039
|
+
result = await runList(client, opts, listArgs);
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
case 'get': {
|
|
1043
|
+
const getArgs = parseGetArgs(subArgs);
|
|
1044
|
+
result = await runGet(client, opts, getArgs);
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
case 'create': {
|
|
1048
|
+
const createArgs = parseCreateArgs(subArgs);
|
|
1049
|
+
result = await runCreate(client, opts, createArgs);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
case 'update': {
|
|
1053
|
+
const updateArgs = parseUpdateArgs(subArgs);
|
|
1054
|
+
result = await runUpdate(client, opts, updateArgs);
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
case 'search': {
|
|
1058
|
+
const searchArgs = parseSearchArgs(subArgs);
|
|
1059
|
+
result = await runSearch(client, opts, searchArgs);
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
case 'states': {
|
|
1063
|
+
const statesArgs = parseDictionaryArgs(subArgs);
|
|
1064
|
+
result = await runDictionary(client, opts, statesArgs, 'states');
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case 'properties': {
|
|
1068
|
+
const propsArgs = parseDictionaryArgs(subArgs);
|
|
1069
|
+
result = await runDictionary(client, opts, propsArgs, 'properties');
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case 'suites': {
|
|
1073
|
+
const suitesArgs = parseDictionaryArgs(subArgs);
|
|
1074
|
+
result = await runDictionary(client, opts, suitesArgs, 'suites');
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
case 'plans': {
|
|
1078
|
+
const plansArgs = parseDictionaryArgs(subArgs);
|
|
1079
|
+
result = await runDictionary(client, opts, plansArgs, 'plans');
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
case 'priorities': {
|
|
1083
|
+
const prioritiesArgs = parseDictionaryArgs(subArgs);
|
|
1084
|
+
result = await runDictionary(client, opts, prioritiesArgs, 'priorities');
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
case 'transition-histories': {
|
|
1088
|
+
const thListArgs = parseTransitionHistoriesArgs(subArgs);
|
|
1089
|
+
result = await runTransitionHistories(client, opts, thListArgs);
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case 'transition-history': {
|
|
1093
|
+
const thArgs = parseTransitionHistoryArgs(subArgs);
|
|
1094
|
+
result = await runTransitionHistory(client, opts, thArgs.positionals);
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
default:
|
|
1098
|
+
throw new core.PingCodeError(`Unknown idea subcommand: ${subcommand}. Use idea --help for usage.`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (opts.dry_run) {
|
|
1102
|
+
core.printJson(result);
|
|
1103
|
+
} else if (result !== null && result !== undefined) {
|
|
1104
|
+
if (opts.compact) {
|
|
1105
|
+
core.printJson(core.compactResponse(result));
|
|
1106
|
+
} else {
|
|
1107
|
+
core.printJson(result);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
} catch (exc) {
|
|
1111
|
+
throw exc;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
shared.registerModule('idea', {
|
|
1116
|
+
name: 'idea',
|
|
1117
|
+
description: 'Manage PingCode ideas (requirements)',
|
|
1118
|
+
run,
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
module.exports = { run, printHelp };
|