@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.
@@ -0,0 +1,556 @@
1
+ 'use strict';
2
+
3
+ const core = require('../core');
4
+ const shared = require('./shared');
5
+
6
+ function isIdentifier(arg) {
7
+ return /^[A-Z]{3,6}-\d+$/.test(arg);
8
+ }
9
+
10
+ // ── Comment-specific compact helper ────────────────────────────────────
11
+
12
+ function compactComment(item) {
13
+ const fields = ['id', 'content', 'created_at', 'is_deleted', 'is_reply_comment'];
14
+ const compact = {};
15
+ for (const key of fields) {
16
+ if (!(key in item)) continue;
17
+ const value = item[key];
18
+ if (value !== null && value !== undefined) {
19
+ compact[key] = value;
20
+ }
21
+ }
22
+ const createdBy = item.created_by;
23
+ if (createdBy && typeof createdBy === 'object' && !Array.isArray(createdBy)) {
24
+ compact.created_by = createdBy.display_name || createdBy.name || createdBy.id;
25
+ }
26
+ const repliedComment = item.replied_comment;
27
+ if (repliedComment && typeof repliedComment === 'object' && !Array.isArray(repliedComment)) {
28
+ compact.replied_comment = {
29
+ id: repliedComment.id,
30
+ content: repliedComment.content,
31
+ is_deleted: repliedComment.is_deleted,
32
+ };
33
+ }
34
+ return compact;
35
+ }
36
+
37
+ function compactCommentResponse(payload) {
38
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
39
+ return payload;
40
+ }
41
+ const values = core.pageValues(payload);
42
+ if (values.length > 0) {
43
+ const result = {
44
+ page_size: payload.page_size,
45
+ page_index: payload.page_index,
46
+ total: payload.total,
47
+ count: values.length,
48
+ values: values.map(compactComment),
49
+ };
50
+ return Object.fromEntries(Object.entries(result).filter(([, v]) => v !== null && v !== undefined));
51
+ }
52
+ return compactComment(payload);
53
+ }
54
+
55
+ // ── Create subcommand ─────────────────────────────────────────────────
56
+
57
+ function parseCreateArgs(tokens) {
58
+ const args = {
59
+ content: null,
60
+ reply_to: null,
61
+ };
62
+ const stringFlags = {
63
+ '--content': 'content',
64
+ '--reply-to': 'reply_to',
65
+ };
66
+ const positionals = [];
67
+
68
+ for (let i = 0; i < tokens.length; i++) {
69
+ const arg = tokens[i];
70
+ if (!arg.startsWith('--')) {
71
+ positionals.push(arg);
72
+ continue;
73
+ }
74
+ if (arg === '--help' || arg === '-h') {
75
+ positionals.push(arg);
76
+ continue;
77
+ }
78
+ if (arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS) continue;
79
+ if (arg in stringFlags) {
80
+ if (i + 1 >= tokens.length) {
81
+ throw new core.PingCodeError(`Flag ${arg} requires a value`);
82
+ }
83
+ args[stringFlags[arg]] = tokens[i + 1];
84
+ i += 1;
85
+ continue;
86
+ }
87
+ const eqIndex = arg.indexOf('=');
88
+ if (eqIndex !== -1) {
89
+ const flag = arg.slice(0, eqIndex);
90
+ const value = arg.slice(eqIndex + 1);
91
+ if (flag in stringFlags) {
92
+ args[stringFlags[flag]] = value;
93
+ } else if (flag in shared.BASE_GLOBAL_STRING_FLAGS) {
94
+ // pass through
95
+ } else {
96
+ throw new core.PingCodeError(`Unknown option: ${flag}`);
97
+ }
98
+ } else if (!(arg in shared.BASE_GLOBAL_STRING_FLAGS)) {
99
+ throw new core.PingCodeError(`Unknown option: ${arg}. Use comment create --help for usage.`);
100
+ } else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
101
+ i += 1; // skip value for known global string flags
102
+ }
103
+ }
104
+ return { args, positionals };
105
+ }
106
+
107
+ async function runCreate(client, opts, args, positionals) {
108
+ if (positionals.length === 0) {
109
+ throw new core.PingCodeError('A work item id or identifier is required. Use comment create --help for usage.');
110
+ }
111
+ if (positionals.length > 1) {
112
+ throw new core.PingCodeError(`Unexpected argument: ${positionals[1]}. Use comment create --help for usage.`);
113
+ }
114
+
115
+ const workItemRef = positionals[0];
116
+
117
+ if (typeof args.content !== 'string' || !args.content.trim()) {
118
+ throw new core.PingCodeError('--content is required and must be non-empty. Use comment create --help for usage.');
119
+ }
120
+
121
+ const body = {
122
+ principal_type: 'work_item',
123
+ content: args.content,
124
+ };
125
+
126
+ if (args.reply_to) {
127
+ body.reply_comment_id = args.reply_to;
128
+ }
129
+
130
+ if (isIdentifier(workItemRef)) {
131
+ if (opts.dry_run) {
132
+ return {
133
+ dry_run: true,
134
+ resolution: {
135
+ method: 'GET',
136
+ path: '/v1/project/work_items',
137
+ params: { identifier: workItemRef },
138
+ },
139
+ post: {
140
+ method: 'POST',
141
+ path: '/v1/comments',
142
+ json: { ...body, principal_id: '{id}' },
143
+ },
144
+ };
145
+ }
146
+
147
+ const resolvedId = await core.resolveWorkItemIdentifier(client, workItemRef);
148
+ body.principal_id = resolvedId;
149
+ return await client.request(
150
+ 'POST',
151
+ '/v1/comments',
152
+ null,
153
+ body,
154
+ { dry_run: false, use_workspace_cache: true },
155
+ );
156
+ }
157
+
158
+ body.principal_id = workItemRef;
159
+ return await client.request(
160
+ 'POST',
161
+ '/v1/comments',
162
+ null,
163
+ body,
164
+ { dry_run: opts.dry_run, use_workspace_cache: true },
165
+ );
166
+ }
167
+
168
+ // ── List subcommand ───────────────────────────────────────────────────
169
+
170
+ function parseListArgs(tokens) {
171
+ const positionals = [];
172
+ for (let i = 0; i < tokens.length; i++) {
173
+ const arg = tokens[i];
174
+ if (!arg.startsWith('--')) {
175
+ positionals.push(arg);
176
+ continue;
177
+ }
178
+ if (arg === '--help' || arg === '-h') {
179
+ positionals.push(arg);
180
+ continue;
181
+ }
182
+ if (arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS) continue;
183
+ if (arg in shared.BASE_GLOBAL_STRING_FLAGS) {
184
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
185
+ i += 1;
186
+ }
187
+ continue;
188
+ }
189
+ const eqIndex = arg.indexOf('=');
190
+ if (eqIndex !== -1) {
191
+ const flag = arg.slice(0, eqIndex);
192
+ // skip value
193
+ } else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
194
+ i += 1; // skip value
195
+ }
196
+ }
197
+ return { positionals };
198
+ }
199
+
200
+ async function runList(client, opts, positionals) {
201
+ if (positionals.length === 0) {
202
+ throw new core.PingCodeError('A work item id or identifier is required. Use comment list --help for usage.');
203
+ }
204
+ if (positionals.length > 1) {
205
+ throw new core.PingCodeError(`Unexpected argument: ${positionals[1]}. Use comment list --help for usage.`);
206
+ }
207
+
208
+ const workItemRef = positionals[0];
209
+
210
+ if (isIdentifier(workItemRef)) {
211
+ if (opts.dry_run) {
212
+ return {
213
+ dry_run: true,
214
+ resolution: {
215
+ method: 'GET',
216
+ path: '/v1/project/work_items',
217
+ params: { identifier: workItemRef },
218
+ },
219
+ get: {
220
+ method: 'GET',
221
+ path: '/v1/comments',
222
+ params: {
223
+ principal_id: '{id}',
224
+ principal_type: 'work_item',
225
+ },
226
+ },
227
+ };
228
+ }
229
+
230
+ const resolvedId = await core.resolveWorkItemIdentifier(client, workItemRef);
231
+ return await client.request(
232
+ 'GET',
233
+ '/v1/comments',
234
+ { principal_type: 'work_item', principal_id: resolvedId },
235
+ null,
236
+ { dry_run: false, use_workspace_cache: true },
237
+ );
238
+ }
239
+
240
+ return await client.request(
241
+ 'GET',
242
+ '/v1/comments',
243
+ { principal_type: 'work_item', principal_id: workItemRef },
244
+ null,
245
+ { dry_run: opts.dry_run, use_workspace_cache: true },
246
+ );
247
+ }
248
+
249
+ // ── Get subcommand ────────────────────────────────────────────────────
250
+
251
+ function parseGetArgs(tokens) {
252
+ const positionals = [];
253
+ for (let i = 0; i < tokens.length; i++) {
254
+ const arg = tokens[i];
255
+ if (!arg.startsWith('--')) {
256
+ positionals.push(arg);
257
+ continue;
258
+ }
259
+ if (arg === '--help' || arg === '-h') {
260
+ positionals.push(arg);
261
+ continue;
262
+ }
263
+ if (arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS) continue;
264
+ if (arg in shared.BASE_GLOBAL_STRING_FLAGS) {
265
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
266
+ i += 1;
267
+ }
268
+ continue;
269
+ }
270
+ const eqIndex = arg.indexOf('=');
271
+ if (eqIndex !== -1) {
272
+ const flag = arg.slice(0, eqIndex);
273
+ } else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
274
+ i += 1;
275
+ }
276
+ }
277
+ return { positionals };
278
+ }
279
+
280
+ async function runGet(client, opts, positionals) {
281
+ if (positionals.length < 2) {
282
+ throw new core.PingCodeError('A comment id and work item id/identifier are required. Use comment get --help for usage.');
283
+ }
284
+ if (positionals.length > 2) {
285
+ throw new core.PingCodeError(`Unexpected argument: ${positionals[2]}. Use comment get --help for usage.`);
286
+ }
287
+
288
+ const commentId = positionals[0];
289
+ const workItemRef = positionals[1];
290
+
291
+ if (isIdentifier(workItemRef)) {
292
+ if (opts.dry_run) {
293
+ return {
294
+ dry_run: true,
295
+ resolution: {
296
+ method: 'GET',
297
+ path: '/v1/project/work_items',
298
+ params: { identifier: workItemRef },
299
+ },
300
+ get: {
301
+ method: 'GET',
302
+ path: `/v1/comments/${commentId}`,
303
+ params: {
304
+ principal_id: '{id}',
305
+ principal_type: 'work_item',
306
+ },
307
+ },
308
+ };
309
+ }
310
+
311
+ const resolvedId = await core.resolveWorkItemIdentifier(client, workItemRef);
312
+ return await client.request(
313
+ 'GET',
314
+ `/v1/comments/${commentId}`,
315
+ { principal_type: 'work_item', principal_id: resolvedId },
316
+ null,
317
+ { dry_run: false, use_workspace_cache: true },
318
+ );
319
+ }
320
+
321
+ return await client.request(
322
+ 'GET',
323
+ `/v1/comments/${commentId}`,
324
+ { principal_type: 'work_item', principal_id: workItemRef },
325
+ null,
326
+ { dry_run: opts.dry_run, use_workspace_cache: true },
327
+ );
328
+ }
329
+
330
+ // ── Delete subcommand ─────────────────────────────────────────────────
331
+
332
+ function parseDeleteArgs(tokens) {
333
+ const positionals = [];
334
+ for (let i = 0; i < tokens.length; i++) {
335
+ const arg = tokens[i];
336
+ if (!arg.startsWith('--')) {
337
+ positionals.push(arg);
338
+ continue;
339
+ }
340
+ if (arg === '--help' || arg === '-h') {
341
+ positionals.push(arg);
342
+ continue;
343
+ }
344
+ if (arg in shared.BASE_GLOBAL_BOOLEAN_FLAGS) continue;
345
+ if (arg in shared.BASE_GLOBAL_STRING_FLAGS) {
346
+ if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
347
+ i += 1;
348
+ }
349
+ continue;
350
+ }
351
+ const eqIndex = arg.indexOf('=');
352
+ if (eqIndex !== -1) {
353
+ const flag = arg.slice(0, eqIndex);
354
+ } else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
355
+ i += 1;
356
+ }
357
+ }
358
+ return { positionals };
359
+ }
360
+
361
+ async function runDelete(client, opts, positionals) {
362
+ if (positionals.length < 2) {
363
+ throw new core.PingCodeError('A comment id and work item id/identifier are required. Use comment delete --help for usage.');
364
+ }
365
+ if (positionals.length > 2) {
366
+ throw new core.PingCodeError(`Unexpected argument: ${positionals[2]}. Use comment delete --help for usage.`);
367
+ }
368
+
369
+ const commentId = positionals[0];
370
+ const workItemRef = positionals[1];
371
+
372
+ if (isIdentifier(workItemRef)) {
373
+ if (opts.dry_run) {
374
+ return {
375
+ dry_run: true,
376
+ resolution: {
377
+ method: 'GET',
378
+ path: '/v1/project/work_items',
379
+ params: { identifier: workItemRef },
380
+ },
381
+ delete: {
382
+ method: 'DELETE',
383
+ path: `/v1/comments/${commentId}`,
384
+ params: {
385
+ principal_id: '{id}',
386
+ principal_type: 'work_item',
387
+ },
388
+ },
389
+ };
390
+ }
391
+
392
+ const resolvedId = await core.resolveWorkItemIdentifier(client, workItemRef);
393
+ return await client.request(
394
+ 'DELETE',
395
+ `/v1/comments/${commentId}`,
396
+ { principal_type: 'work_item', principal_id: resolvedId },
397
+ null,
398
+ { dry_run: false, use_workspace_cache: true },
399
+ );
400
+ }
401
+
402
+ return await client.request(
403
+ 'DELETE',
404
+ `/v1/comments/${commentId}`,
405
+ { principal_type: 'work_item', principal_id: workItemRef },
406
+ null,
407
+ { dry_run: opts.dry_run, use_workspace_cache: true },
408
+ );
409
+ }
410
+
411
+ // ── Help ──────────────────────────────────────────────────────────────
412
+
413
+ function printHelp() {
414
+ console.log([
415
+ 'PingCode comment — Manage work-item comments',
416
+ '',
417
+ 'Usage: pingcode comment <subcommand> [options]',
418
+ '',
419
+ 'Subcommands:',
420
+ ' create <id|identifier> Create a comment on a work item',
421
+ ' --content TEXT Comment content (required)',
422
+ ' --reply-to COMMENT_ID Reply to an existing comment',
423
+ '',
424
+ ' list <id|identifier> List comments on a work item',
425
+ '',
426
+ ' get <comment-id> <id|identifier>',
427
+ ' Get a single comment',
428
+ '',
429
+ ' delete <comment-id> <id|identifier>',
430
+ ' Delete a comment',
431
+ '',
432
+ 'Global options:',
433
+ ' --base-url URL PingCode base URL',
434
+ ' --client-id ID OAuth client ID',
435
+ ' --client-secret SECRET OAuth client secret',
436
+ ' --token TOKEN Bearer token (skip OAuth)',
437
+ ' --user-id ID Current user ID',
438
+ ' --user-name NAME Current user name',
439
+ ' --workspace-cache PATH Workspace cache file path',
440
+ ' --no-workspace-cache Disable workspace cache',
441
+ ' --no-token-cache Disable token cache',
442
+ ' --dry-run Show API request without executing',
443
+ ' --compact Compact output',
444
+ ' --grant-type TYPE OAuth grant type: client_credentials, authorization_code, or auto (default; uses cached token type)',
445
+ ' --help Show this help',
446
+ ].join('\n'));
447
+ }
448
+
449
+ function printSubcommandHelp(subcommand) {
450
+ switch (subcommand) {
451
+ case 'create':
452
+ console.log([
453
+ 'Usage: pingcode comment create <id|identifier> --content TEXT [options]',
454
+ '',
455
+ 'Create a comment on a work item. principal_type is always work_item.',
456
+ '',
457
+ 'Options:',
458
+ ' --content TEXT Comment content (required, non-empty)',
459
+ ' --reply-to COMMENT_ID Reply to an existing comment',
460
+ ].join('\n'));
461
+ break;
462
+ case 'list':
463
+ console.log([
464
+ 'Usage: pingcode comment list <id|identifier>',
465
+ '',
466
+ 'List all comments on a work item.',
467
+ ].join('\n'));
468
+ break;
469
+ case 'get':
470
+ console.log([
471
+ 'Usage: pingcode comment get <comment-id> <id|identifier>',
472
+ '',
473
+ 'Get a single comment by its id and work item reference.',
474
+ ].join('\n'));
475
+ break;
476
+ case 'delete':
477
+ console.log([
478
+ 'Usage: pingcode comment delete <comment-id> <id|identifier>',
479
+ '',
480
+ 'Delete a comment by its id and work item reference.',
481
+ ].join('\n'));
482
+ break;
483
+ default:
484
+ printHelp();
485
+ }
486
+ }
487
+
488
+ // ── Main dispatcher ───────────────────────────────────────────────────
489
+
490
+ async function run(argv) {
491
+ const tokens = argv || [];
492
+
493
+ if (tokens.length === 0 || tokens[0] === '--help' || tokens[0] === '-h') {
494
+ printHelp();
495
+ return;
496
+ }
497
+
498
+ const subcommand = tokens[0];
499
+ const remaining = tokens.slice(1);
500
+
501
+ if (remaining.includes('--help') || remaining.includes('-h')) {
502
+ printSubcommandHelp(subcommand);
503
+ return;
504
+ }
505
+
506
+ const { opts, remaining: subArgs } = shared.parseGlobalOptions(remaining);
507
+ const client = shared.clientFromOpts(opts);
508
+
509
+ try {
510
+ let result;
511
+ switch (subcommand) {
512
+ case 'create': {
513
+ const { args, positionals } = parseCreateArgs(subArgs);
514
+ result = await runCreate(client, opts, args, positionals);
515
+ break;
516
+ }
517
+ case 'list': {
518
+ const { positionals } = parseListArgs(subArgs);
519
+ result = await runList(client, opts, positionals);
520
+ break;
521
+ }
522
+ case 'get': {
523
+ const { positionals } = parseGetArgs(subArgs);
524
+ result = await runGet(client, opts, positionals);
525
+ break;
526
+ }
527
+ case 'delete': {
528
+ const { positionals } = parseDeleteArgs(subArgs);
529
+ result = await runDelete(client, opts, positionals);
530
+ break;
531
+ }
532
+ default:
533
+ throw new core.PingCodeError(`Unknown comment subcommand: ${subcommand}. Use comment --help for usage.`);
534
+ }
535
+
536
+ if (opts.dry_run) {
537
+ core.printJson(result);
538
+ } else if (result !== null && result !== undefined) {
539
+ if (opts.compact) {
540
+ core.printJson(compactCommentResponse(result));
541
+ } else {
542
+ core.printJson(result);
543
+ }
544
+ }
545
+ } catch (exc) {
546
+ throw exc;
547
+ }
548
+ }
549
+
550
+ shared.registerModule('comment', {
551
+ name: 'comment',
552
+ description: 'Manage work-item comments',
553
+ run,
554
+ });
555
+
556
+ module.exports = { run, printHelp };