@metaphorli/pingcode-cli 0.3.2

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.
@@ -0,0 +1,1039 @@
1
+ 'use strict';
2
+
3
+ const core = require('../core');
4
+ const shared = require('./shared');
5
+
6
+ // ── Global option defaults ────────────────────────────────────────────
7
+ const GLOBAL_BOOLEAN_FLAGS = new Set([
8
+ '--dry-run', '--compact', '--no-token-cache', '--no-workspace-cache',
9
+ '--all-users', '--all-projects', '--all-sprints',
10
+ ]);
11
+
12
+ const GLOBAL_STRING_FLAGS = {
13
+ '--base-url': 'base_url',
14
+ '--client-id': 'client_id',
15
+ '--client-secret': 'client_secret',
16
+ '--token': 'token',
17
+ '--user-id': 'user_id',
18
+ '--user-name': 'user_name',
19
+ '--workspace-cache': 'workspace_cache',
20
+ '--grant-type': 'grant_type',
21
+ };
22
+
23
+ function defaultGlobalOpts() {
24
+ return {
25
+ base_url: process.env.PINGCODE_BASE_URL || core.DEFAULT_BASE_URL,
26
+ client_id: process.env.PINGCODE_CLIENT_ID || null,
27
+ client_secret: process.env.PINGCODE_CLIENT_SECRET || null,
28
+ token: process.env.PINGCODE_ACCESS_TOKEN || null,
29
+ user_id: process.env.PINGCODE_USER_ID || null,
30
+ user_name: process.env.PINGCODE_USER_NAME || null,
31
+ no_token_cache: false,
32
+ workspace_cache: process.env.PINGCODE_WORKSPACE_CACHE || core.DEFAULT_WORKSPACE_CACHE,
33
+ no_workspace_cache: false,
34
+ dry_run: false,
35
+ compact: false,
36
+ all_users: false,
37
+ all_projects: false,
38
+ all_sprints: false,
39
+ grant_type: 'auto',
40
+ };
41
+ }
42
+
43
+ function parseGlobalOptions(tokens) {
44
+ const opts = defaultGlobalOpts();
45
+ const remaining = [];
46
+ for (let i = 0; i < tokens.length; i++) {
47
+ const arg = tokens[i];
48
+ if (arg === '--help' || arg === '-h') {
49
+ remaining.push(arg);
50
+ continue;
51
+ }
52
+ if (GLOBAL_BOOLEAN_FLAGS.has(arg)) {
53
+ const key = arg.replace(/^--/, '').replace(/-/g, '_');
54
+ opts[key] = true;
55
+ continue;
56
+ }
57
+ const eqIndex = arg.indexOf('=');
58
+ let flag, value, consumedNext = false;
59
+ if (eqIndex !== -1) {
60
+ flag = arg.slice(0, eqIndex);
61
+ value = arg.slice(eqIndex + 1);
62
+ } else if (arg.startsWith('--')) {
63
+ flag = arg;
64
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
65
+ value = tokens[i + 1];
66
+ consumedNext = true;
67
+ } else {
68
+ // Boolean-like flag with no value — could be a subcommand flag
69
+ remaining.push(arg);
70
+ continue;
71
+ }
72
+ } else {
73
+ remaining.push(arg);
74
+ continue;
75
+ }
76
+ if (flag in GLOBAL_STRING_FLAGS) {
77
+ opts[GLOBAL_STRING_FLAGS[flag]] = value;
78
+ if (consumedNext) i += 1;
79
+ } else {
80
+ // Not a recognized global flag — pass through as subcommand arg
81
+ remaining.push(arg);
82
+ if (consumedNext) remaining.push(value);
83
+ if (consumedNext) i += 1;
84
+ }
85
+ }
86
+ return { opts, remaining };
87
+ }
88
+
89
+ function clientFromOpts(opts) {
90
+ const tokenCache = opts.no_token_cache ? null : (process.env.PINGCODE_TOKEN_CACHE || core.DEFAULT_TOKEN_CACHE);
91
+ const workspaceCache = opts.no_workspace_cache ? null : opts.workspace_cache;
92
+ return new core.PingCodeClient({
93
+ base_url: opts.base_url,
94
+ client_id: opts.client_id,
95
+ client_secret: opts.client_secret,
96
+ token: opts.token,
97
+ token_cache: tokenCache,
98
+ workspace_cache: workspaceCache,
99
+ grant_type: opts.grant_type,
100
+ });
101
+ }
102
+
103
+ // ── Cache lookup helpers ──────────────────────────────────────────────
104
+
105
+ function findAllCachedStates(cache) {
106
+ const all = [];
107
+ const seen = new Set();
108
+ const states = cache.work_item_states || {};
109
+ for (const key of Object.keys(states)) {
110
+ const payload = states[key];
111
+ for (const s of core.pageValues(payload)) {
112
+ if (s.id && !seen.has(s.id)) {
113
+ seen.add(s.id);
114
+ all.push(s);
115
+ }
116
+ }
117
+ }
118
+ return all;
119
+ }
120
+
121
+ function findCachedWorkItemType(cache, query) {
122
+ // Types are keyed by project_id. Search across all projects.
123
+ const types = cache.work_item_types || {};
124
+ const seen = new Set();
125
+ const all = [];
126
+ for (const key of Object.keys(types)) {
127
+ for (const t of core.pageValues(types[key])) {
128
+ if (t.id && !seen.has(t.id)) {
129
+ seen.add(t.id);
130
+ all.push(t);
131
+ }
132
+ }
133
+ }
134
+ return core.findCachedItem(all, query, 'work item type');
135
+ }
136
+
137
+ function findCachedPriority(cache, query) {
138
+ const priorities = cache.work_item_priorities || {};
139
+ const seen = new Set();
140
+ const all = [];
141
+ for (const key of Object.keys(priorities)) {
142
+ for (const p of core.pageValues(priorities[key])) {
143
+ if (p.id && !seen.has(p.id)) {
144
+ seen.add(p.id);
145
+ all.push(p);
146
+ }
147
+ }
148
+ }
149
+ return core.findCachedItem(all, query, 'priority');
150
+ }
151
+
152
+ function findCachedUser(cache, query) {
153
+ const users = core.pageValues(cache.users);
154
+ if (query === '@me') {
155
+ return { id: core.currentUserId(null, cache) };
156
+ }
157
+ return core.findCachedItem(users, query, 'user');
158
+ }
159
+
160
+ function findCachedProject(cache, query) {
161
+ const projects = core.pageValues(cache.projects);
162
+ return core.findCachedItem(projects, query, 'project');
163
+ }
164
+
165
+ function findCachedSprint(cache, query) {
166
+ const sprints = cache.sprints || {};
167
+ const seen = new Set();
168
+ const all = [];
169
+ for (const key of Object.keys(sprints)) {
170
+ for (const s of core.pageValues(sprints[key])) {
171
+ if (s.id && !seen.has(s.id)) {
172
+ seen.add(s.id);
173
+ all.push(s);
174
+ }
175
+ }
176
+ }
177
+ return core.findCachedItem(all, query, 'sprint');
178
+ }
179
+
180
+ // Resolve a name-or-id string to an id. If the value is already a raw id
181
+ // (no letters that suggest a name), return it as-is without cache lookup.
182
+ function resolveId(value, resolver, cache, label) {
183
+ if (!value) return null;
184
+ // Check if the value looks like a raw id (no spaces, no CJK, but could be a name)
185
+ // We always try cache lookup first; if it fails, treat as raw id.
186
+ try {
187
+ const item = resolver(cache, value);
188
+ const itemId = item.id;
189
+ if (typeof itemId === 'string' && itemId) return itemId;
190
+ return value; // fallback: use value as raw id
191
+ } catch (exc) {
192
+ // If cache lookup fails, use the raw value as id
193
+ return value;
194
+ }
195
+ }
196
+
197
+ // ── List subcommand ───────────────────────────────────────────────────
198
+
199
+ function parseListArgs(tokens) {
200
+ const args = {
201
+ state: null,
202
+ type: null,
203
+ assignee: null,
204
+ project: null,
205
+ sprint: null,
206
+ limit: null,
207
+ keywords: null,
208
+ };
209
+ const stringFlags = {
210
+ '--state': 'state',
211
+ '--type': 'type',
212
+ '--assignee': 'assignee',
213
+ '--project': 'project',
214
+ '--sprint': 'sprint',
215
+ '--limit': 'limit',
216
+ '--keywords': 'keywords',
217
+ };
218
+
219
+ for (let i = 0; i < tokens.length; i++) {
220
+ const arg = tokens[i];
221
+ if (arg in stringFlags) {
222
+ if (i + 1 >= tokens.length) {
223
+ throw new core.PingCodeError(`Flag ${arg} requires a value`);
224
+ }
225
+ args[stringFlags[arg]] = tokens[i + 1];
226
+ i += 1;
227
+ } else if (arg.startsWith('--')) {
228
+ const eqIndex = arg.indexOf('=');
229
+ if (eqIndex !== -1) {
230
+ const flag = arg.slice(0, eqIndex);
231
+ const value = arg.slice(eqIndex + 1);
232
+ if (flag in stringFlags) {
233
+ args[stringFlags[flag]] = value;
234
+ } else {
235
+ throw new core.PingCodeError(`Unknown option: ${flag}`);
236
+ }
237
+ } else if (!(arg in GLOBAL_BOOLEAN_FLAGS)) {
238
+ throw new core.PingCodeError(`Unknown option: ${arg}. Use workitem list --help for usage.`);
239
+ }
240
+ } else {
241
+ throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem list --help for usage.`);
242
+ }
243
+ }
244
+ return args;
245
+ }
246
+
247
+ async function runList(client, opts, args) {
248
+ const params = {};
249
+ const cache = client.workspaceCache;
250
+ const defaultToCurrentUser = !opts.all_users && client.resolveGrantType() !== 'authorization_code';
251
+
252
+ const filtered = core.applyDefaultWorkItemFilters(
253
+ '/v1/project/work_items',
254
+ params,
255
+ client,
256
+ opts.user_id,
257
+ opts.user_name,
258
+ defaultToCurrentUser,
259
+ opts.all_projects,
260
+ opts.all_sprints,
261
+ );
262
+
263
+ // Resolve explicit filters
264
+ if (args.state) {
265
+ const stateItem = core.findCachedItem(findAllCachedStates(cache), args.state, 'state');
266
+ filtered.state_id = stateItem.id;
267
+ }
268
+ if (args.type) {
269
+ const typeItem = findCachedWorkItemType(cache, args.type);
270
+ filtered.type_ids = typeItem.id;
271
+ }
272
+ if (args.assignee) {
273
+ const userItem = findCachedUser(cache, args.assignee);
274
+ filtered.assignee_ids = userItem.id;
275
+ }
276
+ if (args.project) {
277
+ const projectItem = findCachedProject(cache, args.project);
278
+ filtered.project_ids = projectItem.id;
279
+ }
280
+ if (args.sprint) {
281
+ const sprintItem = findCachedSprint(cache, args.sprint);
282
+ filtered.sprint_ids = sprintItem.id;
283
+ }
284
+ if (args.limit) {
285
+ filtered.page_size = String(args.limit);
286
+ }
287
+ if (args.keywords) {
288
+ filtered.keywords = args.keywords;
289
+ }
290
+
291
+ return await client.request(
292
+ 'GET',
293
+ '/v1/project/work_items',
294
+ filtered,
295
+ null,
296
+ { dry_run: opts.dry_run, use_workspace_cache: true },
297
+ );
298
+ }
299
+
300
+ // ── Create subcommand ─────────────────────────────────────────────────
301
+
302
+ function parseCreateArgs(tokens) {
303
+ const args = {
304
+ title: null,
305
+ type: null,
306
+ project: null,
307
+ sprint: null,
308
+ assignee: null,
309
+ state: null,
310
+ priority: null,
311
+ description: null,
312
+ parent: null,
313
+ };
314
+ const stringFlags = {
315
+ '--title': 'title',
316
+ '--type': 'type',
317
+ '--project': 'project',
318
+ '--sprint': 'sprint',
319
+ '--assignee': 'assignee',
320
+ '--state': 'state',
321
+ '--priority': 'priority',
322
+ '--description': 'description',
323
+ '--parent': 'parent',
324
+ };
325
+
326
+ for (let i = 0; i < tokens.length; i++) {
327
+ const arg = tokens[i];
328
+ if (arg in stringFlags) {
329
+ if (i + 1 >= tokens.length) {
330
+ throw new core.PingCodeError(`Flag ${arg} requires a value`);
331
+ }
332
+ args[stringFlags[arg]] = tokens[i + 1];
333
+ i += 1;
334
+ } else if (arg.startsWith('--')) {
335
+ const eqIndex = arg.indexOf('=');
336
+ if (eqIndex !== -1) {
337
+ const flag = arg.slice(0, eqIndex);
338
+ const value = arg.slice(eqIndex + 1);
339
+ if (flag in stringFlags) {
340
+ args[stringFlags[flag]] = value;
341
+ } else {
342
+ throw new core.PingCodeError(`Unknown option: ${flag}`);
343
+ }
344
+ } else if (!(arg in GLOBAL_BOOLEAN_FLAGS)) {
345
+ throw new core.PingCodeError(`Unknown option: ${arg}. Use workitem create --help for usage.`);
346
+ }
347
+ } else {
348
+ throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem create --help for usage.`);
349
+ }
350
+ }
351
+ return args;
352
+ }
353
+
354
+ async function runCreate(client, opts, args) {
355
+ if (typeof args.title !== 'string' || !args.title.trim()) {
356
+ throw new core.PingCodeError('--title is required and must be non-empty. Use workitem create --help for usage.');
357
+ }
358
+
359
+ // Ensure workspace context for defaults
360
+ core.ensureWorkItemWorkspaceContext(
361
+ '/v1/project/work_items',
362
+ client,
363
+ 'POST',
364
+ !opts.all_users,
365
+ opts.all_projects,
366
+ opts.all_sprints,
367
+ );
368
+
369
+ const cache = client.workspaceCache;
370
+
371
+ // Build body
372
+ const body = { title: args.title };
373
+
374
+ // Resolve project
375
+ if (args.project) {
376
+ const projectItem = findCachedProject(cache, args.project);
377
+ body.project_id = projectItem.id;
378
+ } else {
379
+ const projectId = (cache.preferences || {}).current_project_id;
380
+ if (typeof projectId === 'string' && projectId) {
381
+ body.project_id = projectId;
382
+ }
383
+ }
384
+
385
+ // Resolve type
386
+ if (args.type) {
387
+ const typeItem = findCachedWorkItemType(cache, args.type);
388
+ body.type_id = typeItem.id;
389
+ }
390
+
391
+ // Resolve sprint
392
+ if (args.sprint) {
393
+ const sprintItem = findCachedSprint(cache, args.sprint);
394
+ body.sprint_id = sprintItem.id;
395
+ }
396
+
397
+ // Resolve state
398
+ if (args.state) {
399
+ const stateItem = core.findCachedItem(findAllCachedStates(cache), args.state, 'state');
400
+ body.state_id = stateItem.id;
401
+ }
402
+
403
+ // Resolve priority
404
+ if (args.priority) {
405
+ const priorityItem = findCachedPriority(cache, args.priority);
406
+ body.priority_id = priorityItem.id;
407
+ }
408
+
409
+ // Description
410
+ if (args.description) {
411
+ body.description = args.description;
412
+ }
413
+
414
+ // Parent
415
+ if (args.parent) {
416
+ body.parent_id = args.parent;
417
+ }
418
+
419
+ // Resolve assignee
420
+ if (args.assignee) {
421
+ const userItem = findCachedUser(cache, args.assignee);
422
+ body.assignee_id = userItem.id;
423
+ }
424
+
425
+ // Apply default assignee (unless --all-users or explicit --assignee)
426
+ const withDefaults = core.applyDefaultWorkItemCreateBody(
427
+ 'POST',
428
+ '/v1/project/work_items',
429
+ body,
430
+ client,
431
+ opts.user_id,
432
+ !opts.all_users && !args.assignee,
433
+ );
434
+
435
+ return await client.request(
436
+ 'POST',
437
+ '/v1/project/work_items',
438
+ null,
439
+ withDefaults,
440
+ { dry_run: opts.dry_run, use_workspace_cache: true },
441
+ );
442
+ }
443
+
444
+ // ── Show subcommand ──────────────────────────────────────────────────
445
+
446
+ function parseShowArgs(tokens) {
447
+ // First non-flag token is the id/identifier
448
+ let target = null;
449
+ for (let i = 0; i < tokens.length; i++) {
450
+ const arg = tokens[i];
451
+ if (!arg.startsWith('--')) {
452
+ if (target === null) {
453
+ target = arg;
454
+ continue;
455
+ }
456
+ throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem show --help for usage.`);
457
+ }
458
+ // Consume flag+value for global options (already parsed)
459
+ if (GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
460
+ if (GLOBAL_STRING_FLAGS[arg]) {
461
+ i += 1;
462
+ continue;
463
+ }
464
+ // For --help (already handled in parseGlobalOptions)
465
+ }
466
+ if (!target) {
467
+ throw new core.PingCodeError('A work item id or identifier is required. Use workitem show --help for usage.');
468
+ }
469
+ return { target };
470
+ }
471
+
472
+ async function runShow(client, opts, args) {
473
+ const params = {};
474
+ // Detect if target is an identifier (uppercase letters + dash + number)
475
+ const isIdFormat = /^[A-Z]{3,6}-\d+$/.test(args.target);
476
+ if (isIdFormat) {
477
+ params.identifier = args.target;
478
+ } else {
479
+ params.id = args.target;
480
+ }
481
+
482
+ const result = await client.request(
483
+ 'GET',
484
+ '/v1/project/work_items',
485
+ params,
486
+ null,
487
+ { dry_run: opts.dry_run, use_workspace_cache: true },
488
+ );
489
+
490
+ if (opts.dry_run) return result;
491
+
492
+ // For real results, compact if requested, otherwise return first match
493
+ if (opts.compact) {
494
+ return core.compactResponse(result);
495
+ }
496
+ return result;
497
+ }
498
+
499
+ // ── Get subcommand ───────────────────────────────────────────────────
500
+
501
+ function parseGetArgs(tokens) {
502
+ let workItemId = null;
503
+ for (let i = 0; i < tokens.length; i++) {
504
+ const arg = tokens[i];
505
+ if (!arg.startsWith('--')) {
506
+ if (workItemId === null) {
507
+ workItemId = arg;
508
+ continue;
509
+ }
510
+ throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem get --help for usage.`);
511
+ }
512
+ if (GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
513
+ if (GLOBAL_STRING_FLAGS[arg]) {
514
+ i += 1;
515
+ continue;
516
+ }
517
+ }
518
+ if (!workItemId) {
519
+ throw new core.PingCodeError('A work item id or identifier is required. Use workitem get --help for usage.');
520
+ }
521
+ return { work_item_id: workItemId };
522
+ }
523
+
524
+ async function runGet(client, opts, args) {
525
+ if (isIdentifier(args.work_item_id)) {
526
+ const resolutionParams = { identifier: args.work_item_id };
527
+
528
+ if (opts.dry_run) {
529
+ return {
530
+ dry_run: true,
531
+ resolution: {
532
+ method: 'GET',
533
+ path: '/v1/project/work_items',
534
+ params: resolutionParams,
535
+ },
536
+ get: {
537
+ method: 'GET',
538
+ path: '/v1/project/work_items/{id}',
539
+ },
540
+ };
541
+ }
542
+
543
+ const resolved = await client.request(
544
+ 'GET',
545
+ '/v1/project/work_items',
546
+ resolutionParams,
547
+ null,
548
+ { dry_run: false, use_workspace_cache: true },
549
+ );
550
+ const values = core.pageValues(resolved);
551
+ if (values.length === 0) {
552
+ throw new core.PingCodeError(`No work item found with identifier ${args.work_item_id}`);
553
+ }
554
+ const workItemId = values[0].id;
555
+ return await client.request(
556
+ 'GET',
557
+ `/v1/project/work_items/${workItemId}`,
558
+ null,
559
+ null,
560
+ { dry_run: false, use_workspace_cache: true },
561
+ );
562
+ }
563
+
564
+ return await client.request(
565
+ 'GET',
566
+ `/v1/project/work_items/${args.work_item_id}`,
567
+ null,
568
+ null,
569
+ { dry_run: opts.dry_run, use_workspace_cache: true },
570
+ );
571
+ }
572
+
573
+ // ── Update subcommand ────────────────────────────────────────────────
574
+
575
+ function parseUpdateArgs(tokens) {
576
+ const args = {
577
+ target: null,
578
+ title: null,
579
+ description: null,
580
+ type: null,
581
+ project: null,
582
+ sprint: null,
583
+ state: null,
584
+ priority: null,
585
+ assignee: null,
586
+ parent: null,
587
+ version: null,
588
+ board: null,
589
+ entry: null,
590
+ swimlane: null,
591
+ startAt: null,
592
+ endAt: null,
593
+ participants: null,
594
+ storyPoints: null,
595
+ estimatedWorkload: null,
596
+ remainingWorkload: null,
597
+ properties: null,
598
+ };
599
+ const stringFlags = {
600
+ '--title': 'title',
601
+ '--description': 'description',
602
+ '--type': 'type',
603
+ '--project': 'project',
604
+ '--sprint': 'sprint',
605
+ '--state': 'state',
606
+ '--priority': 'priority',
607
+ '--assignee': 'assignee',
608
+ '--parent': 'parent',
609
+ '--version': 'version',
610
+ '--board': 'board',
611
+ '--entry': 'entry',
612
+ '--swimlane': 'swimlane',
613
+ '--start-at': 'startAt',
614
+ '--end-at': 'endAt',
615
+ '--participants': 'participants',
616
+ '--story-points': 'storyPoints',
617
+ '--estimated-workload': 'estimatedWorkload',
618
+ '--remaining-workload': 'remainingWorkload',
619
+ '--properties': 'properties',
620
+ };
621
+
622
+ for (let i = 0; i < tokens.length; i++) {
623
+ const arg = tokens[i];
624
+ if (!arg.startsWith('--')) {
625
+ if (args.target === null) {
626
+ args.target = arg;
627
+ continue;
628
+ }
629
+ throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem update --help for usage.`);
630
+ }
631
+ if (arg in stringFlags) {
632
+ if (i + 1 >= tokens.length) {
633
+ throw new core.PingCodeError(`Flag ${arg} requires a value`);
634
+ }
635
+ args[stringFlags[arg]] = tokens[i + 1];
636
+ i += 1;
637
+ } else if (arg.startsWith('--')) {
638
+ const eqIndex = arg.indexOf('=');
639
+ if (eqIndex !== -1) {
640
+ const flag = arg.slice(0, eqIndex);
641
+ const value = arg.slice(eqIndex + 1);
642
+ if (flag in stringFlags) {
643
+ args[stringFlags[flag]] = value;
644
+ } else {
645
+ if (!(flag in GLOBAL_BOOLEAN_FLAGS) && !(flag in GLOBAL_STRING_FLAGS)) {
646
+ throw new core.PingCodeError(`Unknown option: ${flag}`);
647
+ }
648
+ }
649
+ } else if (!(arg in GLOBAL_BOOLEAN_FLAGS)) {
650
+ throw new core.PingCodeError(`Unknown option: ${arg}. Use workitem update --help for usage.`);
651
+ }
652
+ }
653
+ }
654
+
655
+ if (!args.target) {
656
+ throw new core.PingCodeError('A work item id or identifier is required. Use workitem update --help for usage.');
657
+ }
658
+
659
+ const hasUpdateField = Object.entries(args).some(
660
+ ([key, value]) => key !== 'target' && value !== null,
661
+ );
662
+ if (!hasUpdateField) {
663
+ throw new core.PingCodeError('At least one field to update is required. Use workitem update --help for usage.');
664
+ }
665
+
666
+ return args;
667
+ }
668
+
669
+ function isIdentifier(arg) {
670
+ // PingCode identifiers look like: PROJECT_KEY-NUMBER (e.g., SCR-1, TASK-42)
671
+ // Project keys are typically 3-6 uppercase letters.
672
+ return /^[A-Z]{3,6}-\d+$/.test(arg);
673
+ }
674
+
675
+ function parseNumber(value, label) {
676
+ const parsed = Number(value);
677
+ if (Number.isNaN(parsed)) {
678
+ throw new core.PingCodeError(`${label} must be a number`);
679
+ }
680
+ return parsed;
681
+ }
682
+
683
+ function parseTimestamp(value, label) {
684
+ const trimmed = String(value).trim();
685
+ if (/^\d+$/.test(trimmed)) {
686
+ return parseInt(trimmed, 10);
687
+ }
688
+ const date = new Date(trimmed);
689
+ if (Number.isNaN(date.getTime())) {
690
+ throw new core.PingCodeError(`${label} must be a Unix timestamp or ISO date string`);
691
+ }
692
+ return Math.floor(date.getTime() / 1000);
693
+ }
694
+
695
+ function resolveParticipants(value, cache) {
696
+ const items = value.split(',').map((s) => s.trim()).filter(Boolean);
697
+ return items.map((item) => {
698
+ if (item.startsWith('@user:')) {
699
+ return core.cachedUserId(item.slice(6), cache);
700
+ }
701
+ const user = findCachedUser(cache, item);
702
+ return user.id;
703
+ });
704
+ }
705
+
706
+ async function runUpdate(client, opts, args) {
707
+ const cache = client.workspaceCache;
708
+ const body = {};
709
+
710
+ if (args.title) body.title = args.title;
711
+ if (args.description) body.description = args.description;
712
+
713
+ if (args.project) {
714
+ const projectItem = findCachedProject(cache, args.project);
715
+ body.project_id = projectItem.id;
716
+ }
717
+ if (args.type) {
718
+ const typeItem = findCachedWorkItemType(cache, args.type);
719
+ body.type_id = typeItem.id;
720
+ }
721
+ if (args.sprint) {
722
+ const sprintItem = findCachedSprint(cache, args.sprint);
723
+ body.sprint_id = sprintItem.id;
724
+ }
725
+ if (args.state) {
726
+ const stateItem = core.findCachedItem(findAllCachedStates(cache), args.state, 'state');
727
+ body.state_id = stateItem.id;
728
+ }
729
+ if (args.priority) {
730
+ const priorityItem = findCachedPriority(cache, args.priority);
731
+ body.priority_id = priorityItem.id;
732
+ }
733
+ if (args.assignee) {
734
+ const userItem = findCachedUser(cache, args.assignee);
735
+ body.assignee_id = userItem.id;
736
+ }
737
+ if (args.parent) body.parent_id = args.parent;
738
+ if (args.version) body.version_id = args.version;
739
+ if (args.board) body.board_id = args.board;
740
+ if (args.entry) body.entry_id = args.entry;
741
+ if (args.swimlane) body.swimlane_id = args.swimlane;
742
+ if (args.startAt) body.start_at = parseTimestamp(args.startAt, 'start_at');
743
+ if (args.endAt) body.end_at = parseTimestamp(args.endAt, 'end_at');
744
+ if (args.participants) body.participant_ids = resolveParticipants(args.participants, cache);
745
+ if (args.storyPoints) body.story_points = parseNumber(args.storyPoints, 'story_points');
746
+ if (args.estimatedWorkload) body.estimated_workload = parseNumber(args.estimatedWorkload, 'estimated_workload');
747
+ if (args.remainingWorkload) body.remaining_workload = parseNumber(args.remainingWorkload, 'remaining_workload');
748
+ if (args.properties) body.properties = core.parseJsonObject(args.properties, 'properties');
749
+
750
+ // Sort keys for deterministic dry-run output
751
+ const sortedBody = {};
752
+ for (const k of Object.keys(body).sort()) sortedBody[k] = body[k];
753
+
754
+ // Determine if target is an identifier needing resolution, or a direct work item id
755
+ if (isIdentifier(args.target)) {
756
+ const resolutionParams = { identifier: args.target };
757
+
758
+ if (opts.dry_run) {
759
+ return {
760
+ dry_run: true,
761
+ resolution: {
762
+ method: 'GET',
763
+ path: '/v1/project/work_items',
764
+ params: resolutionParams,
765
+ },
766
+ patch: {
767
+ method: 'PATCH',
768
+ path: '/v1/project/work_items/{id}',
769
+ json: sortedBody,
770
+ },
771
+ };
772
+ }
773
+
774
+ // Non-dry-run: resolve the identifier to get the work item id
775
+ const resolved = await client.request(
776
+ 'GET',
777
+ '/v1/project/work_items',
778
+ resolutionParams,
779
+ null,
780
+ { dry_run: false, use_workspace_cache: true },
781
+ );
782
+ const values = core.pageValues(resolved);
783
+ if (values.length === 0) {
784
+ throw new core.PingCodeError(`No work item found with identifier ${args.target}`);
785
+ }
786
+ const workItemId = values[0].id;
787
+ return await client.request(
788
+ 'PATCH',
789
+ `/v1/project/work_items/${workItemId}`,
790
+ null,
791
+ body,
792
+ { dry_run: false, use_workspace_cache: false },
793
+ );
794
+ }
795
+
796
+ // Direct ID: patch immediately
797
+ return await client.request(
798
+ 'PATCH',
799
+ `/v1/project/work_items/${args.target}`,
800
+ null,
801
+ body,
802
+ { dry_run: opts.dry_run, use_workspace_cache: false },
803
+ );
804
+ }
805
+
806
+ // ── Help ──────────────────────────────────────────────────────────────
807
+
808
+ function printHelp() {
809
+ console.log([
810
+ 'PingCode workitem — Manage work items',
811
+ '',
812
+ 'Usage: pingcode workitem <subcommand> [options]',
813
+ '',
814
+ 'Subcommands:',
815
+ ' list [options] List work items',
816
+ ' --state <name|id> Filter by state',
817
+ ' --type <name|id> Filter by type',
818
+ ' --assignee <name|id|@me> Filter by assignee',
819
+ ' --project <id|name> Filter by project',
820
+ ' --sprint <id|name> Filter by sprint',
821
+ ' --keywords <text> Search keywords (title, identifier, etc.)',
822
+ ' --limit N Max results per page',
823
+ '',
824
+ ' create --title TITLE Create a new work item',
825
+ ' --type <name|id> Work item type',
826
+ ' --project <id|name> Target project',
827
+ ' --sprint <id|name> Target sprint',
828
+ ' --assignee <name|id|@me> Assignee (defaults to @me)',
829
+ ' --state <name|id> Initial state',
830
+ ' --priority <name|id> Priority',
831
+ ' --description TEXT Description text',
832
+ ' --parent <id|identifier> Parent work item',
833
+ '',
834
+ ' show <id|identifier> Show a single work item',
835
+ '',
836
+ ' get <id|identifier> Get a single work item by id or identifier',
837
+ '',
838
+ ' update <id|identifier> Update a work item',
839
+ ' --title TEXT New title',
840
+ ' --description TEXT New description',
841
+ ' --type <name|id> New work item type',
842
+ ' --project <id|name> New project',
843
+ ' --sprint <id|name> New sprint',
844
+ ' --state <name|id> New state',
845
+ ' --priority <name|id> New priority',
846
+ ' --assignee <name|id|@me> New assignee',
847
+ ' --parent <id|identifier> New parent work item',
848
+ ' --version ID New version id',
849
+ ' --board ID New board id',
850
+ ' --entry ID New entry id',
851
+ ' --swimlane ID New swimlane id',
852
+ ' --start-at TIMESTAMP New start time (Unix timestamp or ISO date)',
853
+ ' --end-at TIMESTAMP New end time (Unix timestamp or ISO date)',
854
+ ' --participants LIST Comma-separated participant user names/ids',
855
+ ' --story-points NUMBER New story points',
856
+ ' --estimated-workload NUM New estimated workload',
857
+ ' --remaining-workload NUM New remaining workload',
858
+ ' --properties JSON New custom properties as JSON object',
859
+ '',
860
+ 'Global options:',
861
+ ' --base-url URL PingCode base URL',
862
+ ' --client-id ID OAuth client ID',
863
+ ' --client-secret SECRET OAuth client secret',
864
+ ' --token TOKEN Bearer token (skip OAuth)',
865
+ ' --user-id ID Current user ID',
866
+ ' --user-name NAME Current user name',
867
+ ' --workspace-cache PATH Workspace cache file path',
868
+ ' --no-workspace-cache Disable workspace cache',
869
+ ' --no-token-cache Disable token cache',
870
+ ' --all-users Show work items from all users (skip current-user filter)',
871
+ ' --all-projects Show work items from all projects (skip current-project filter)',
872
+ ' --all-sprints Show work items from all sprints (skip current-sprint filter)',
873
+ ' --dry-run Show API request without executing',
874
+ ' --compact Compact output',
875
+ ' --grant-type TYPE OAuth grant type: client_credentials, authorization_code, or auto (default; uses cached token type)',
876
+ ' --help Show this help',
877
+ ].join('\n'));
878
+ }
879
+
880
+ function printSubcommandHelp(subcommand) {
881
+ switch (subcommand) {
882
+ case 'list':
883
+ console.log([
884
+ 'Usage: pingcode workitem list [options]',
885
+ '',
886
+ 'List work items from the current project/sprint/assignee.',
887
+ '',
888
+ 'Options:',
889
+ ' --state <name|id> Filter by state',
890
+ ' --type <name|id> Filter by type',
891
+ ' --assignee <name|id|@me> Filter by assignee',
892
+ ' --project <id|name> Filter by project',
893
+ ' --sprint <id|name> Filter by sprint',
894
+ ' --keywords <text> Search keywords (title, identifier, etc.)',
895
+ ' --limit N Max results per page',
896
+ ].join('\n'));
897
+ break;
898
+ case 'create':
899
+ console.log([
900
+ 'Usage: pingcode workitem create --title TITLE [options]',
901
+ '',
902
+ 'Create a new work item.',
903
+ '',
904
+ 'Options:',
905
+ ' --title TITLE Title (required)',
906
+ ' --type <name|id> Work item type',
907
+ ' --project <id|name> Target project',
908
+ ' --sprint <id|name> Target sprint',
909
+ ' --assignee <name|id|@me> Assignee (defaults to @me)',
910
+ ' --state <name|id> Initial state',
911
+ ' --priority <name|id> Priority',
912
+ ' --description TEXT Description text',
913
+ ' --parent <id|identifier> Parent work item',
914
+ ].join('\n'));
915
+ break;
916
+ case 'show':
917
+ console.log([
918
+ 'Usage: pingcode workitem show <id|identifier>',
919
+ '',
920
+ 'Show a single work item by id or identifier.',
921
+ ].join('\n'));
922
+ break;
923
+ case 'get':
924
+ console.log([
925
+ 'Usage: pingcode workitem get <id|identifier>',
926
+ '',
927
+ 'Get a single work item by id or identifier.',
928
+ ].join('\n'));
929
+ break;
930
+ case 'update':
931
+ console.log([
932
+ 'Usage: pingcode workitem update <id|identifier> [options]',
933
+ '',
934
+ 'Update a work item. At least one option must be provided.',
935
+ '',
936
+ 'Options:',
937
+ ' --title TEXT New title',
938
+ ' --description TEXT New description',
939
+ ' --type <name|id> New work item type',
940
+ ' --project <id|name> New project',
941
+ ' --sprint <id|name> New sprint',
942
+ ' --state <name|id> New state',
943
+ ' --priority <name|id> New priority',
944
+ ' --assignee <name|id|@me> New assignee',
945
+ ' --parent <id|identifier> New parent work item',
946
+ ' --version ID New version id',
947
+ ' --board ID New board id',
948
+ ' --entry ID New entry id',
949
+ ' --swimlane ID New swimlane id',
950
+ ' --start-at TIMESTAMP New start time (Unix timestamp or ISO date)',
951
+ ' --end-at TIMESTAMP New end time (Unix timestamp or ISO date)',
952
+ ' --participants LIST Comma-separated participant user names/ids',
953
+ ' --story-points NUMBER New story points',
954
+ ' --estimated-workload NUM New estimated workload',
955
+ ' --remaining-workload NUM New remaining workload',
956
+ ' --properties JSON New custom properties as JSON object',
957
+ ].join('\n'));
958
+ break;
959
+ default:
960
+ printHelp();
961
+ }
962
+ }
963
+
964
+ // ── Main dispatcher ───────────────────────────────────────────────────
965
+
966
+ async function run(argv) {
967
+ const tokens = argv || [];
968
+
969
+ if (tokens.length === 0 || tokens[0] === '--help' || tokens[0] === '-h') {
970
+ printHelp();
971
+ return;
972
+ }
973
+
974
+ const subcommand = tokens[0];
975
+ const remaining = tokens.slice(1);
976
+
977
+ if (remaining.includes('--help') || remaining.includes('-h')) {
978
+ printSubcommandHelp(subcommand);
979
+ return;
980
+ }
981
+
982
+ // Parse global options from remaining tokens
983
+ const { opts, remaining: subArgs } = parseGlobalOptions(remaining);
984
+
985
+ const client = clientFromOpts(opts);
986
+
987
+ try {
988
+ let result;
989
+ switch (subcommand) {
990
+ case 'list': {
991
+ const args = parseListArgs(subArgs);
992
+ result = await runList(client, opts, args);
993
+ break;
994
+ }
995
+ case 'create': {
996
+ const args = parseCreateArgs(subArgs);
997
+ result = await runCreate(client, opts, args);
998
+ break;
999
+ }
1000
+ case 'show': {
1001
+ const args = parseShowArgs(subArgs);
1002
+ result = await runShow(client, opts, args);
1003
+ break;
1004
+ }
1005
+ case 'get': {
1006
+ const args = parseGetArgs(subArgs);
1007
+ result = await runGet(client, opts, args);
1008
+ break;
1009
+ }
1010
+ case 'update': {
1011
+ const args = parseUpdateArgs(subArgs);
1012
+ result = await runUpdate(client, opts, args);
1013
+ break;
1014
+ }
1015
+ default:
1016
+ throw new core.PingCodeError(`Unknown workitem subcommand: ${subcommand}. Use workitem --help for usage.`);
1017
+ }
1018
+
1019
+ if (opts.dry_run) {
1020
+ core.printJson(result);
1021
+ } else if (result !== null && result !== undefined) {
1022
+ if (opts.compact) {
1023
+ core.printJson(core.compactResponse(result));
1024
+ } else {
1025
+ core.printJson(result);
1026
+ }
1027
+ }
1028
+ } catch (exc) {
1029
+ throw exc;
1030
+ }
1031
+ }
1032
+
1033
+ shared.registerModule('workitem', {
1034
+ name: 'workitem',
1035
+ description: 'Manage PingCode work items',
1036
+ run,
1037
+ });
1038
+
1039
+ module.exports = { run, printHelp };