jiradc-cli 1.0.43 → 1.0.44

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.
Files changed (3) hide show
  1. package/README.md +62 -21
  2. package/dist/index.js +84 -49
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -31,27 +31,68 @@ All commands output JSON. Add `--pretty` to pretty-print.
31
31
 
32
32
  ### issue
33
33
 
34
- | Command | Description |
35
- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
36
- | `jiradc issue get <key>` | One issue. `--fields` selects fields by id or name, `--all-fields` returns everything Jira holds, `--expand` adds changelog/names |
37
- | `jiradc issue search <jql>` | Issues matching a JQL query. `--all` collects every match (capped at 1,000); `description` and `comment` are excluded unless asked for via `--fields` |
38
- | `jiradc issue create` | The new issue's key and id. Requires `--project`, `--type`, `--summary`; `--fields <json>` sets anything else |
39
- | `jiradc issue update <key>` | Applies field changes. Shortcuts for `--summary`/`--description`/`--priority`/`--assignee`, list flags for `--labels`/`--components`/`--fix-versions` |
40
- | `jiradc issue delete <key>` | Deletes the issue. `--delete-subtasks` to take its subtasks with it |
41
- | `jiradc issue assign <key>` | Sets the assignee. `--assignee` takes a username, `me`, or `none` to unassign |
42
- | `jiradc issue transition <key>` | Moves the issue. `--to` accepts a transition id, a transition name, or the target status name; `--fields` supplies screen fields, `--comment` a note |
43
- | `jiradc issue transitions <key>` | The transitions available from the current status, each with the screen fields it requires |
44
- | `jiradc issue createmeta` | The fields a new issue accepts and the values they allow. Requires `--project` and `--type` |
45
- | `jiradc issue editmeta <key>` | The fields this issue's edit screen accepts, with their allowed values |
46
- | `jiradc issue link` | Links two issues. Requires `--from`, `--to` and `--type` (a link type name) |
47
- | `jiradc issue unlink <id>` | Removes one issue link by its link id |
48
- | `jiradc issue link-types` | The link type names this instance accepts, for `issue link --type` |
49
- | `jiradc issue link-epic <keys...>` | Links one or more issues to `--epic <epicKey>`; reports a `failed` array if any link is rejected |
50
- | `jiradc issue changelog <key>` | The issue's change history |
51
- | `jiradc issue batch-changelog <keys>` | Change history for several issues at once, from a comma-separated key list |
52
- | `jiradc issue clone <key>` | Creates a duplicate. `--include-links`, `--include-attachments`, `--summary` and `--project` control what carries over |
53
- | `jiradc issue batch-create` | Creates many issues from `--issues <json>` (an array of field objects) |
54
- | `jiradc issue dev-status <key>` | Branches, pull requests, commits and builds linked to the issue. `--detail` for the full records |
34
+ | Command | Description |
35
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `jiradc issue get <key>` | One issue, with every dimension hanging off it (see below). `--fields` selects fields by id or name, `--all-fields` returns everything Jira holds, `--expand` adds changelog/names |
37
+ | `jiradc issue search <jql>` | Issues matching a JQL query. `--all` collects every match (capped at 1,000); `description` and `comment` are excluded unless asked for via `--fields` |
38
+ | `jiradc issue create` | The new issue's key and id. Requires `--project`, `--type`, `--summary`; `--fields <json>` sets anything else |
39
+ | `jiradc issue update <key>` | Applies field changes. Shortcuts for `--summary`/`--description`/`--priority`/`--assignee`, list flags for `--labels`/`--components`/`--fix-versions` |
40
+ | `jiradc issue delete <key>` | Deletes the issue. `--delete-subtasks` to take its subtasks with it |
41
+ | `jiradc issue assign <key>` | Sets the assignee. `--assignee` takes a username, `me`, or `none` to unassign |
42
+ | `jiradc issue transition <key>` | Moves the issue. `--to` accepts a transition id, a transition name, or the target status name; `--fields` supplies screen fields, `--comment` a note |
43
+ | `jiradc issue transitions <key>` | The transitions available from the current status, each with the screen fields it requires |
44
+ | `jiradc issue createmeta` | The fields a new issue accepts and the values they allow. Requires `--project` and `--type` |
45
+ | `jiradc issue editmeta <key>` | The fields this issue's edit screen accepts, with their allowed values |
46
+ | `jiradc issue link` | Links two issues. Requires `--from`, `--to` and `--type` (a link type name) |
47
+ | `jiradc issue unlink <id>` | Removes one issue link by its link id |
48
+ | `jiradc issue link-types` | The link type names this instance accepts, for `issue link --type` |
49
+ | `jiradc issue link-epic <keys...>` | Links one or more issues to `--epic <epicKey>`; reports a `failed` array if any link is rejected |
50
+ | `jiradc issue changelog <key>` | The issue's change history |
51
+ | `jiradc issue batch-changelog <keys>` | Change history for several issues at once, from a comma-separated key list |
52
+ | `jiradc issue clone <key>` | Creates a duplicate. `--include-links`, `--include-attachments`, `--summary` and `--project` control what carries over |
53
+ | `jiradc issue batch-create` | Creates many issues from `--issues <json>` (an array of field objects) |
54
+ | `jiradc issue dev-status <key>` | Branches, pull requests, commits and builds linked to the issue. `--detail` for the full records |
55
+
56
+ #### What a bare `issue get` returns
57
+
58
+ With no `--fields`, one issue comes back with every dimension that hangs off it, out of the
59
+ single request the command already makes:
60
+
61
+ | Default field | What arrives |
62
+ | --------------------------------------------------------------- | -------------------------------------------------------- |
63
+ | `summary` `status` `issuetype` `priority` `labels` `components` | The issue itself |
64
+ | `assignee` `reporter` `created` `updated` | Who and when |
65
+ | `description` | In full — it is the issue, not a dimension of it |
66
+ | `issuelinks` | `{ id, type, key, summary, status, issuetype }` per link |
67
+ | `epic` | The key of the epic this issue sits under, if any |
68
+ | `parent` `subtasks` | `{ key, summary, status, issuetype }` each |
69
+ | `attachment` | `{ files: [{ id, filename, size }], use }` |
70
+ | `comment` | `{ count, use }` — the thread is named, not carried |
71
+
72
+ The rule: **the default names every dimension, and carries it only when carrying it is
73
+ complete.** A link stub is the whole link, so links arrive whole and point at no command. An
74
+ attachment's bytes and a comment's body are not something to inline, so those arrive as an
75
+ inventory plus the command that reads them — the same `use` convention `project get` uses for
76
+ components and versions. A field with nothing in it is absent altogether, pointer included:
77
+ no `attachment` key means there are no attachments, which is readable only because the field
78
+ was asked for.
79
+
80
+ `type` on a link is the phrase true **from this issue's side** — `blocks`, `is blocked by`,
81
+ `relates to` — so the direction needs no decoding and there is no nested `inwardIssue` /
82
+ `outwardIssue`. The link's own id is `linkId`, not `id`, because `id` beside `key` reads as the
83
+ linked issue's id. `--fields issuelinks` returns the same flat shape.
84
+
85
+ Every row above is a `--fields` selector except `epic`, which is an output key: the Epic Link is
86
+ a custom field, so select it by name with `--fields "Epic Link"`.
87
+
88
+ Naming `attachment` or `comment` in `--fields`, or passing `--all-fields`, returns that field
89
+ in full instead. `worklog` and `watches` are not requested at all; ask for them by name.
90
+
91
+ Nothing here costs a second request. Development activity and the change history are separate
92
+ endpoints and stay behind `issue dev-status` and `issue changelog`.
93
+
94
+ `issue search` deliberately keeps none of this — what is one link list on one issue is fifty of
95
+ them on a page of results.
55
96
 
56
97
  #### issue comment
57
98
 
package/dist/index.js CHANGED
@@ -187,6 +187,31 @@ function transformSprint(s) {
187
187
  };
188
188
  }
189
189
 
190
+ // src/utils/constants.ts
191
+ var SEARCH_DEFAULT_FIELDS = [
192
+ "summary",
193
+ "status",
194
+ "assignee",
195
+ "reporter",
196
+ "labels",
197
+ "priority",
198
+ "created",
199
+ "updated",
200
+ "issuetype",
201
+ "components"
202
+ ];
203
+ var EPIC_LINK_FIELD_ID = "customfield_10100";
204
+ var DEFAULT_FIELDS = [
205
+ ...SEARCH_DEFAULT_FIELDS,
206
+ "description",
207
+ "comment",
208
+ "issuelinks",
209
+ EPIC_LINK_FIELD_ID,
210
+ "parent",
211
+ "subtasks",
212
+ "attachment"
213
+ ];
214
+
190
215
  // src/utils/transformers/issue.ts
191
216
  function issueBrowseUrl(key) {
192
217
  return `${jiraBaseUrl()}/browse/${key}`;
@@ -248,28 +273,24 @@ function transformAttachment(a) {
248
273
  ...author ? { author: transformUser(author) } : {}
249
274
  };
250
275
  }
251
- function transformIssueBasic(b) {
252
- const { self: _self, fields, ...rest } = b;
276
+ function transformIssueStub(b) {
277
+ const fields = b.fields;
253
278
  return {
254
- ...rest,
255
- url: issueBrowseUrl(b.key),
256
- ...fields ? {
257
- fields: {
258
- ...fields.summary !== void 0 ? { summary: fields.summary } : {},
259
- ...fields.status ? { status: transformStatus(fields.status) } : {},
260
- ...fields.priority ? { priority: transformPriority(fields.priority) } : {},
261
- ...fields.issuetype ? { issuetype: transformIssueType(fields.issuetype) } : {}
262
- }
263
- } : {}
279
+ key: b.key,
280
+ ...fields?.summary !== void 0 ? { summary: fields.summary } : {},
281
+ ...fields?.status ? { status: fields.status.name } : {},
282
+ ...fields?.issuetype ? { issuetype: fields.issuetype.name } : {}
264
283
  };
265
284
  }
266
285
  function transformIssueLink(l) {
267
- const { self: _self, type, inwardIssue, outwardIssue, ...rest } = l;
286
+ const other = l.outwardIssue ?? l.inwardIssue;
268
287
  return {
269
- ...rest,
270
- type: transformIssueLinkType(type),
271
- ...inwardIssue ? { inwardIssue: transformIssueBasic(inwardIssue) } : {},
272
- ...outwardIssue ? { outwardIssue: transformIssueBasic(outwardIssue) } : {}
288
+ linkId: l.id,
289
+ // A link with an issue on neither end is not something Jira sends. If it
290
+ // ever does, the type's name is the honest answer — a direction phrase with
291
+ // nothing on the far end of it would not be.
292
+ type: other ? l.outwardIssue ? l.type.outward : l.type.inward : l.type.name,
293
+ ...other ? transformIssueStub(other) : {}
273
294
  };
274
295
  }
275
296
  function customFieldNames(names, fields) {
@@ -279,9 +300,9 @@ function customFieldNames(names, fields) {
279
300
  );
280
301
  return kept.length > 0 ? Object.fromEntries(kept) : void 0;
281
302
  }
282
- function transformIssue(issue) {
303
+ function transformIssue(issue, opts = {}) {
283
304
  const { self: _self, expand: _expand, names, schema: _schema, fields, ...rest } = issue;
284
- const transformed = transformIssueFields(fields, issue.key);
305
+ const transformed = transformIssueFields(fields, issue.key, opts);
285
306
  const fieldNames = customFieldNames(names, transformed);
286
307
  return {
287
308
  ...rest,
@@ -323,6 +344,12 @@ function pruneSentinels(fields) {
323
344
  if (watches && watches.watchCount === 0) delete out.watches;
324
345
  return out;
325
346
  }
347
+ function nameEpicLink(fields) {
348
+ const value = fields[EPIC_LINK_FIELD_ID];
349
+ if (typeof value !== "string" || !ISSUE_KEY_PATTERN.test(value)) return fields;
350
+ const { [EPIC_LINK_FIELD_ID]: _epicLink, ...rest } = fields;
351
+ return { ...rest, epic: value };
352
+ }
326
353
  var DEV_STATUS_DUMP = /com\.atlassian\.jira\.plugin\.devstatus/;
327
354
  function collapseDevStatus(fields, key) {
328
355
  const out = { ...fields };
@@ -360,7 +387,7 @@ function collapseIssueRefs(fields) {
360
387
  }
361
388
  return out;
362
389
  }
363
- function transformIssueFields(fields, key) {
390
+ function transformIssueFields(fields, key, opts) {
364
391
  if (!fields) return {};
365
392
  const {
366
393
  // shaped sub-entities (recursed individually)
@@ -387,7 +414,7 @@ function transformIssueFields(fields, key) {
387
414
  // everything else: optional scalars, customfield_*, etc. — gets compacted
388
415
  ...rest
389
416
  } = fields;
390
- const compacted = collapseDevStatus(collapseIssueRefs(pruneSentinels(compactRecord(rest))), key);
417
+ const compacted = collapseDevStatus(collapseIssueRefs(nameEpicLink(pruneSentinels(compactRecord(rest)))), key);
391
418
  const components = compacted.components;
392
419
  return {
393
420
  ...compacted,
@@ -406,13 +433,21 @@ function transformIssueFields(fields, key) {
406
433
  ...fixVersions && fixVersions.length > 0 ? { fixVersions: fixVersions.map(transformVersion) } : {},
407
434
  ...versions2 && versions2.length > 0 ? { versions: versions2.map(transformVersion) } : {},
408
435
  ...issuelinks && issuelinks.length > 0 ? { issuelinks: issuelinks.map(transformIssueLink) } : {},
409
- ...subtasks && subtasks.length > 0 ? { subtasks: subtasks.map(transformIssueBasic) } : {},
410
- ...parent ? { parent: transformIssueBasic(parent) } : {},
436
+ ...subtasks && subtasks.length > 0 ? { subtasks: subtasks.map(transformIssueStub) } : {},
437
+ ...parent ? { parent: transformIssueStub(parent) } : {},
411
438
  // Drop empty comment / worklog containers entirely. The default Jira
412
439
  // search response includes both wrappers on every issue regardless of
413
440
  // count; on a 25-issue page that's 25 × 2 empty objects of pure noise.
414
- ...comment && comment.comments.length > 0 ? {
415
- comment: {
441
+ // The thread is named, not carried — see `JiraCommentSummaryOutput`.
442
+ //
443
+ // The compact branch gates on `total`, not on the array, because `total` is
444
+ // the only thing it reads. A deployment that caps embedded comments answers
445
+ // with a populated total and an empty `comments`; gating on the array there
446
+ // would drop the key, and a missing key is documented to mean "there are
447
+ // none" — reporting a long thread as no thread, in the one direction the
448
+ // caller was told to trust.
449
+ ...comment && (opts.compact ? comment.total > 0 : comment.comments.length > 0) ? {
450
+ comment: opts.compact ? { count: comment.total, use: `jiradc issue comment list ${key}` } : {
416
451
  comments: comment.comments.map(transformComment),
417
452
  maxResults: comment.maxResults,
418
453
  total: comment.total,
@@ -427,7 +462,19 @@ function transformIssueFields(fields, key) {
427
462
  startAt: worklog.startAt
428
463
  }
429
464
  } : {},
430
- ...attachment && attachment.length > 0 ? { attachment: attachment.map(transformAttachment) } : {}
465
+ // `attachment` is in the default field set to say that files are attached —
466
+ // not to carry their records. An empty list yields no key at all rather
467
+ // than a pointer to nothing: the absence is the answer, and it is readable
468
+ // as one only because the field was asked for.
469
+ ...attachment && attachment.length > 0 ? {
470
+ attachment: opts.compact ? {
471
+ files: attachment.map(({ id, filename, size }) => ({ id, filename, size })),
472
+ // Both commands spelled in full. Abbreviating the second to
473
+ // `download …` saved 24 characters and invited a caller to run
474
+ // exactly that, which is not a command.
475
+ use: `jiradc issue attachment download-all ${key} --output <dir> (one file: jiradc issue attachment download ${key} --attachment-id <id> --output <path>)`
476
+ } : attachment.map(transformAttachment)
477
+ } : {}
431
478
  };
432
479
  }
433
480
 
@@ -1437,21 +1484,6 @@ function editmeta(parent) {
1437
1484
  });
1438
1485
  }
1439
1486
 
1440
- // src/utils/constants.ts
1441
- var SEARCH_DEFAULT_FIELDS = [
1442
- "summary",
1443
- "status",
1444
- "assignee",
1445
- "reporter",
1446
- "labels",
1447
- "priority",
1448
- "created",
1449
- "updated",
1450
- "issuetype",
1451
- "components"
1452
- ];
1453
- var DEFAULT_FIELDS = [...SEARCH_DEFAULT_FIELDS, "description", "comment"];
1454
-
1455
1487
  // src/utils/field-selectors.ts
1456
1488
  var WILDCARD = "*";
1457
1489
  var NEGATION = "-";
@@ -1555,10 +1587,13 @@ async function loadFieldNameIndex(client) {
1555
1587
  }
1556
1588
  async function selectFields(client, opts, defaults) {
1557
1589
  if (opts.allFields === true) return void 0;
1558
- if (opts.fields === void 0) return defaults;
1559
- const tokens = parseFieldSelectors(opts.fields);
1560
- if (tokens.length === 0) return defaults;
1561
- return resolveFieldSelectors(tokens, await loadFieldNameIndex(client));
1590
+ if (usesDefaultFields(opts)) return defaults;
1591
+ return resolveFieldSelectors(parseFieldSelectors(opts.fields), await loadFieldNameIndex(client));
1592
+ }
1593
+ function usesDefaultFields(opts) {
1594
+ if (opts.allFields === true) return false;
1595
+ if (opts.fields === void 0) return true;
1596
+ return parseFieldSelectors(opts.fields).length === 0;
1562
1597
  }
1563
1598
 
1564
1599
  // src/commands/issue/get.ts
@@ -1578,7 +1613,7 @@ function get4(parent) {
1578
1613
  fields,
1579
1614
  expand: opts.expand
1580
1615
  });
1581
- output(transformIssue(result));
1616
+ output(transformIssue(result, { compact: usesDefaultFields(opts) }));
1582
1617
  });
1583
1618
  }
1584
1619
 
@@ -1590,7 +1625,7 @@ function linkEpic(parent) {
1590
1625
  const client = getClient();
1591
1626
  const results = await Promise.allSettled(
1592
1627
  keys.map(
1593
- (key) => client.issues.update({ issueKeyOrId: key, fields: { customfield_10100: opts.epic } }).then(() => key)
1628
+ (key) => client.issues.update({ issueKeyOrId: key, fields: { [EPIC_LINK_FIELD_ID]: opts.epic } }).then(() => key)
1594
1629
  )
1595
1630
  );
1596
1631
  const linked = [];
@@ -1711,7 +1746,7 @@ function search2(parent) {
1711
1746
  return;
1712
1747
  }
1713
1748
  const every = await fetchEveryPage(page, { pageSize: plan.pageSize, start: plan.start });
1714
- output({ ...every, issues: every.issues.map(transformIssue) });
1749
+ output({ ...every, issues: every.issues.map((i) => transformIssue(i)) });
1715
1750
  }
1716
1751
  );
1717
1752
  }
@@ -2757,7 +2792,7 @@ function search3(parent) {
2757
2792
  total: res.total,
2758
2793
  startAt: res.startAt,
2759
2794
  count: res.issues?.length ?? 0,
2760
- issues: (res.issues ?? []).map(transformIssue)
2795
+ issues: (res.issues ?? []).map((i) => transformIssue(i))
2761
2796
  });
2762
2797
  }
2763
2798
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiradc-cli",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "publish": true,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,9 +23,9 @@
23
23
  "tsx": "^4.19.2",
24
24
  "typescript": "^5.7.2",
25
25
  "vitest": "^4.0.16",
26
- "cli-utils": "1.0.0",
27
26
  "config-eslint": "0.0.0",
28
- "config-typescript": "0.0.0"
27
+ "config-typescript": "0.0.0",
28
+ "cli-utils": "1.0.0"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=22.0.0"