jirallm 0.1.0 → 0.2.0

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 (255) hide show
  1. package/README.md +87 -9
  2. package/dist/cli/commands/assign.d.ts +9 -0
  3. package/dist/cli/commands/assign.d.ts.map +1 -0
  4. package/dist/cli/commands/assign.js +37 -0
  5. package/dist/cli/commands/assign.js.map +1 -0
  6. package/dist/cli/commands/assign.test.d.ts +2 -0
  7. package/dist/cli/commands/assign.test.d.ts.map +1 -0
  8. package/dist/cli/commands/assign.test.js +69 -0
  9. package/dist/cli/commands/assign.test.js.map +1 -0
  10. package/dist/cli/commands/attach.d.ts +16 -0
  11. package/dist/cli/commands/attach.d.ts.map +1 -0
  12. package/dist/cli/commands/attach.js +52 -0
  13. package/dist/cli/commands/attach.js.map +1 -0
  14. package/dist/cli/commands/attach.test.d.ts +2 -0
  15. package/dist/cli/commands/attach.test.d.ts.map +1 -0
  16. package/dist/cli/commands/attach.test.js +92 -0
  17. package/dist/cli/commands/attach.test.js.map +1 -0
  18. package/dist/cli/commands/board.d.ts +10 -0
  19. package/dist/cli/commands/board.d.ts.map +1 -0
  20. package/dist/cli/commands/board.js +54 -0
  21. package/dist/cli/commands/board.js.map +1 -0
  22. package/dist/cli/commands/boards.d.ts +11 -0
  23. package/dist/cli/commands/boards.d.ts.map +1 -0
  24. package/dist/cli/commands/boards.js +27 -0
  25. package/dist/cli/commands/boards.js.map +1 -0
  26. package/dist/cli/commands/boards.test.d.ts +2 -0
  27. package/dist/cli/commands/boards.test.d.ts.map +1 -0
  28. package/dist/cli/commands/boards.test.js +80 -0
  29. package/dist/cli/commands/boards.test.js.map +1 -0
  30. package/dist/cli/commands/comment.d.ts +15 -0
  31. package/dist/cli/commands/comment.d.ts.map +1 -0
  32. package/dist/cli/commands/comment.js +61 -0
  33. package/dist/cli/commands/comment.js.map +1 -0
  34. package/dist/cli/commands/create.d.ts +16 -0
  35. package/dist/cli/commands/create.d.ts.map +1 -0
  36. package/dist/cli/commands/create.js +44 -0
  37. package/dist/cli/commands/create.js.map +1 -0
  38. package/dist/cli/commands/create.test.d.ts +2 -0
  39. package/dist/cli/commands/create.test.d.ts.map +1 -0
  40. package/dist/cli/commands/create.test.js +98 -0
  41. package/dist/cli/commands/create.test.js.map +1 -0
  42. package/dist/cli/commands/doctor.js +8 -8
  43. package/dist/cli/commands/doctor.js.map +1 -1
  44. package/dist/cli/commands/edit.d.ts +15 -0
  45. package/dist/cli/commands/edit.d.ts.map +1 -0
  46. package/dist/cli/commands/edit.js +47 -0
  47. package/dist/cli/commands/edit.js.map +1 -0
  48. package/dist/cli/commands/edit.test.d.ts +2 -0
  49. package/dist/cli/commands/edit.test.d.ts.map +1 -0
  50. package/dist/cli/commands/edit.test.js +77 -0
  51. package/dist/cli/commands/edit.test.js.map +1 -0
  52. package/dist/cli/commands/fetch.d.ts +7 -0
  53. package/dist/cli/commands/fetch.d.ts.map +1 -0
  54. package/dist/cli/commands/fetch.js +32 -0
  55. package/dist/cli/commands/fetch.js.map +1 -0
  56. package/dist/cli/commands/fetch.test.d.ts +2 -0
  57. package/dist/cli/commands/fetch.test.d.ts.map +1 -0
  58. package/dist/cli/commands/fetch.test.js +93 -0
  59. package/dist/cli/commands/fetch.test.js.map +1 -0
  60. package/dist/cli/commands/init.js +3 -3
  61. package/dist/cli/commands/init.js.map +1 -1
  62. package/dist/cli/commands/issuetypes.d.ts +7 -0
  63. package/dist/cli/commands/issuetypes.d.ts.map +1 -0
  64. package/dist/cli/commands/issuetypes.js +23 -0
  65. package/dist/cli/commands/issuetypes.js.map +1 -0
  66. package/dist/cli/commands/issuetypes.test.d.ts +2 -0
  67. package/dist/cli/commands/issuetypes.test.d.ts.map +1 -0
  68. package/dist/cli/commands/issuetypes.test.js +62 -0
  69. package/dist/cli/commands/issuetypes.test.js.map +1 -0
  70. package/dist/cli/commands/link.d.ts +18 -0
  71. package/dist/cli/commands/link.d.ts.map +1 -0
  72. package/dist/cli/commands/link.js +51 -0
  73. package/dist/cli/commands/link.js.map +1 -0
  74. package/dist/cli/commands/link.test.d.ts +2 -0
  75. package/dist/cli/commands/link.test.d.ts.map +1 -0
  76. package/dist/cli/commands/link.test.js +96 -0
  77. package/dist/cli/commands/link.test.js.map +1 -0
  78. package/dist/cli/commands/linktypes.d.ts +6 -0
  79. package/dist/cli/commands/linktypes.d.ts.map +1 -0
  80. package/dist/cli/commands/linktypes.js +21 -0
  81. package/dist/cli/commands/linktypes.js.map +1 -0
  82. package/dist/cli/commands/linktypes.test.d.ts +2 -0
  83. package/dist/cli/commands/linktypes.test.d.ts.map +1 -0
  84. package/dist/cli/commands/linktypes.test.js +55 -0
  85. package/dist/cli/commands/linktypes.test.js.map +1 -0
  86. package/dist/cli/commands/me.d.ts +6 -0
  87. package/dist/cli/commands/me.d.ts.map +1 -0
  88. package/dist/cli/commands/me.js +17 -0
  89. package/dist/cli/commands/me.js.map +1 -0
  90. package/dist/cli/commands/me.test.d.ts +2 -0
  91. package/dist/cli/commands/me.test.d.ts.map +1 -0
  92. package/dist/cli/commands/me.test.js +58 -0
  93. package/dist/cli/commands/me.test.js.map +1 -0
  94. package/dist/cli/commands/projects.d.ts +9 -0
  95. package/dist/cli/commands/projects.d.ts.map +1 -0
  96. package/dist/cli/commands/projects.js +25 -0
  97. package/dist/cli/commands/projects.js.map +1 -0
  98. package/dist/cli/commands/projects.test.d.ts +2 -0
  99. package/dist/cli/commands/projects.test.d.ts.map +1 -0
  100. package/dist/cli/commands/projects.test.js +73 -0
  101. package/dist/cli/commands/projects.test.js.map +1 -0
  102. package/dist/cli/commands/search.d.ts +11 -0
  103. package/dist/cli/commands/search.d.ts.map +1 -0
  104. package/dist/cli/commands/search.js +41 -0
  105. package/dist/cli/commands/search.js.map +1 -0
  106. package/dist/cli/commands/search.test.d.ts +2 -0
  107. package/dist/cli/commands/search.test.d.ts.map +1 -0
  108. package/dist/cli/commands/search.test.js +106 -0
  109. package/dist/cli/commands/search.test.js.map +1 -0
  110. package/dist/cli/commands/setup.d.ts.map +1 -1
  111. package/dist/cli/commands/setup.js +5 -13
  112. package/dist/cli/commands/setup.js.map +1 -1
  113. package/dist/cli/commands/sprints.d.ts +10 -0
  114. package/dist/cli/commands/sprints.d.ts.map +1 -0
  115. package/dist/cli/commands/sprints.js +28 -0
  116. package/dist/cli/commands/sprints.js.map +1 -0
  117. package/dist/cli/commands/sprints.test.d.ts +2 -0
  118. package/dist/cli/commands/sprints.test.d.ts.map +1 -0
  119. package/dist/cli/commands/sprints.test.js +68 -0
  120. package/dist/cli/commands/sprints.test.js.map +1 -0
  121. package/dist/cli/commands/transition.d.ts +8 -0
  122. package/dist/cli/commands/transition.d.ts.map +1 -0
  123. package/dist/cli/commands/transition.js +46 -0
  124. package/dist/cli/commands/transition.js.map +1 -0
  125. package/dist/cli/commands/upgrade.d.ts +16 -0
  126. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  127. package/dist/cli/commands/upgrade.js +83 -0
  128. package/dist/cli/commands/upgrade.js.map +1 -0
  129. package/dist/cli/commands/upgrade.test.d.ts +2 -0
  130. package/dist/cli/commands/upgrade.test.d.ts.map +1 -0
  131. package/dist/cli/commands/upgrade.test.js +146 -0
  132. package/dist/cli/commands/upgrade.test.js.map +1 -0
  133. package/dist/cli/commands/watchers.d.ts +10 -0
  134. package/dist/cli/commands/watchers.d.ts.map +1 -0
  135. package/dist/cli/commands/watchers.js +43 -0
  136. package/dist/cli/commands/watchers.js.map +1 -0
  137. package/dist/cli/commands/watchers.test.d.ts +2 -0
  138. package/dist/cli/commands/watchers.test.d.ts.map +1 -0
  139. package/dist/cli/commands/watchers.test.js +90 -0
  140. package/dist/cli/commands/watchers.test.js.map +1 -0
  141. package/dist/cli/commands/worklog.d.ts +8 -0
  142. package/dist/cli/commands/worklog.d.ts.map +1 -0
  143. package/dist/cli/commands/worklog.js +100 -0
  144. package/dist/cli/commands/worklog.js.map +1 -0
  145. package/dist/cli/commands/worklog.test.d.ts +2 -0
  146. package/dist/cli/commands/worklog.test.d.ts.map +1 -0
  147. package/dist/cli/commands/worklog.test.js +195 -0
  148. package/dist/cli/commands/worklog.test.js.map +1 -0
  149. package/dist/cli/index.js +648 -6
  150. package/dist/cli/index.js.map +1 -1
  151. package/dist/cli/issueKey.d.ts.map +1 -1
  152. package/dist/cli/issueKey.js +6 -2
  153. package/dist/cli/issueKey.js.map +1 -1
  154. package/dist/cli/issueKey.test.js +36 -1
  155. package/dist/cli/issueKey.test.js.map +1 -1
  156. package/dist/cli/jsonOutput.d.ts +6 -0
  157. package/dist/cli/jsonOutput.d.ts.map +1 -0
  158. package/dist/cli/jsonOutput.js +7 -0
  159. package/dist/cli/jsonOutput.js.map +1 -0
  160. package/dist/cli/jsonOutput.test.d.ts +2 -0
  161. package/dist/cli/jsonOutput.test.d.ts.map +1 -0
  162. package/dist/cli/jsonOutput.test.js +44 -0
  163. package/dist/cli/jsonOutput.test.js.map +1 -0
  164. package/dist/cli/resolveOrg.d.ts +2 -0
  165. package/dist/cli/resolveOrg.d.ts.map +1 -0
  166. package/dist/cli/resolveOrg.js +15 -0
  167. package/dist/cli/resolveOrg.js.map +1 -0
  168. package/dist/index.d.ts +3 -1
  169. package/dist/index.d.ts.map +1 -1
  170. package/dist/index.js +2 -0
  171. package/dist/index.js.map +1 -1
  172. package/dist/lib/chunkMarkdown.d.ts +6 -0
  173. package/dist/lib/chunkMarkdown.d.ts.map +1 -0
  174. package/dist/lib/chunkMarkdown.js +73 -0
  175. package/dist/lib/chunkMarkdown.js.map +1 -0
  176. package/dist/lib/chunkMarkdown.test.d.ts +2 -0
  177. package/dist/lib/chunkMarkdown.test.d.ts.map +1 -0
  178. package/dist/lib/chunkMarkdown.test.js +171 -0
  179. package/dist/lib/chunkMarkdown.test.js.map +1 -0
  180. package/dist/lib/config.d.ts +6 -0
  181. package/dist/lib/config.d.ts.map +1 -1
  182. package/dist/lib/config.js +25 -0
  183. package/dist/lib/config.js.map +1 -1
  184. package/dist/lib/config.test.js +73 -6
  185. package/dist/lib/config.test.js.map +1 -1
  186. package/dist/lib/configSchema.d.ts +44 -0
  187. package/dist/lib/configSchema.d.ts.map +1 -1
  188. package/dist/lib/configSchema.js +20 -0
  189. package/dist/lib/configSchema.js.map +1 -1
  190. package/dist/lib/credentials.d.ts.map +1 -1
  191. package/dist/lib/credentials.js +32 -31
  192. package/dist/lib/credentials.js.map +1 -1
  193. package/dist/lib/credentials.test.js +18 -11
  194. package/dist/lib/credentials.test.js.map +1 -1
  195. package/dist/lib/exportFields.d.ts +30 -0
  196. package/dist/lib/exportFields.d.ts.map +1 -0
  197. package/dist/lib/exportFields.js +169 -0
  198. package/dist/lib/exportFields.js.map +1 -0
  199. package/dist/lib/exportFields.test.d.ts +2 -0
  200. package/dist/lib/exportFields.test.d.ts.map +1 -0
  201. package/dist/lib/exportFields.test.js +145 -0
  202. package/dist/lib/exportFields.test.js.map +1 -0
  203. package/dist/lib/exporter.d.ts +25 -3
  204. package/dist/lib/exporter.d.ts.map +1 -1
  205. package/dist/lib/exporter.js +195 -18
  206. package/dist/lib/exporter.js.map +1 -1
  207. package/dist/lib/exporter.test.d.ts +2 -0
  208. package/dist/lib/exporter.test.d.ts.map +1 -0
  209. package/dist/lib/exporter.test.js +227 -0
  210. package/dist/lib/exporter.test.js.map +1 -0
  211. package/dist/lib/jiraClient.d.ts +248 -1
  212. package/dist/lib/jiraClient.d.ts.map +1 -1
  213. package/dist/lib/jiraClient.discovery.test.d.ts +2 -0
  214. package/dist/lib/jiraClient.discovery.test.d.ts.map +1 -0
  215. package/dist/lib/jiraClient.discovery.test.js +209 -0
  216. package/dist/lib/jiraClient.discovery.test.js.map +1 -0
  217. package/dist/lib/jiraClient.js +624 -17
  218. package/dist/lib/jiraClient.js.map +1 -1
  219. package/dist/lib/jiraClient.test.d.ts +2 -0
  220. package/dist/lib/jiraClient.test.d.ts.map +1 -0
  221. package/dist/lib/jiraClient.test.js +383 -0
  222. package/dist/lib/jiraClient.test.js.map +1 -0
  223. package/dist/lib/jiraClient.write.test.d.ts +2 -0
  224. package/dist/lib/jiraClient.write.test.d.ts.map +1 -0
  225. package/dist/lib/jiraClient.write.test.js +268 -0
  226. package/dist/lib/jiraClient.write.test.js.map +1 -0
  227. package/dist/lib/markdownToWiki.d.ts +2 -0
  228. package/dist/lib/markdownToWiki.d.ts.map +1 -0
  229. package/dist/lib/markdownToWiki.js +110 -0
  230. package/dist/lib/markdownToWiki.js.map +1 -0
  231. package/dist/lib/markdownToWiki.test.d.ts +2 -0
  232. package/dist/lib/markdownToWiki.test.d.ts.map +1 -0
  233. package/dist/lib/markdownToWiki.test.js +122 -0
  234. package/dist/lib/markdownToWiki.test.js.map +1 -0
  235. package/dist/lib/platform.d.ts +2 -0
  236. package/dist/lib/platform.d.ts.map +1 -1
  237. package/dist/lib/platform.js +8 -0
  238. package/dist/lib/platform.js.map +1 -1
  239. package/dist/lib/runCommand.d.ts +1 -0
  240. package/dist/lib/runCommand.d.ts.map +1 -1
  241. package/dist/lib/runCommand.js +8 -1
  242. package/dist/lib/runCommand.js.map +1 -1
  243. package/dist/lib/worklog.d.ts +36 -0
  244. package/dist/lib/worklog.d.ts.map +1 -0
  245. package/dist/lib/worklog.js +188 -0
  246. package/dist/lib/worklog.js.map +1 -0
  247. package/dist/lib/worklog.test.d.ts +2 -0
  248. package/dist/lib/worklog.test.d.ts.map +1 -0
  249. package/dist/lib/worklog.test.js +118 -0
  250. package/dist/lib/worklog.test.js.map +1 -0
  251. package/package.json +42 -12
  252. package/dist/cli/commands/profiles.d.ts +0 -2
  253. package/dist/cli/commands/profiles.d.ts.map +0 -1
  254. package/dist/cli/commands/profiles.js +0 -20
  255. package/dist/cli/commands/profiles.js.map +0 -1
@@ -1,7 +1,72 @@
1
1
  import { createWriteStream } from 'fs';
2
- import { mkdir } from 'fs/promises';
3
- import { dirname } from 'path';
2
+ import { mkdir, readFile, stat } from 'fs/promises';
3
+ import { basename, dirname } from 'path';
4
4
  import { pipeline } from 'stream/promises';
5
+ import { markdownToWiki } from './markdownToWiki.js';
6
+ function extractActiveSprintName(raw) {
7
+ if (!raw)
8
+ return undefined;
9
+ const arr = Array.isArray(raw) ? raw : [raw];
10
+ const items = arr
11
+ .map((entry) => {
12
+ if (entry && typeof entry === 'object')
13
+ return entry;
14
+ if (typeof entry === 'string') {
15
+ const nameMatch = entry.match(/name=([^,\]]+)/);
16
+ const stateMatch = entry.match(/state=([^,\]]+)/);
17
+ return {
18
+ name: nameMatch?.[1],
19
+ state: stateMatch?.[1],
20
+ };
21
+ }
22
+ return undefined;
23
+ })
24
+ .filter((x) => Boolean(x));
25
+ const active = items.find((s) => s.state?.toLowerCase() === 'active');
26
+ return (active ?? items[items.length - 1])?.name;
27
+ }
28
+ function extractCustomFieldValue(raw, type) {
29
+ if (raw === null || raw === undefined)
30
+ return undefined;
31
+ switch (type) {
32
+ case 'scalar':
33
+ return typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean'
34
+ ? raw
35
+ : undefined;
36
+ case 'number':
37
+ return typeof raw === 'number' ? raw : undefined;
38
+ case 'select':
39
+ if (typeof raw === 'object' && raw !== null && 'value' in raw) {
40
+ return raw.value;
41
+ }
42
+ return undefined;
43
+ case 'user':
44
+ if (typeof raw === 'object' && raw !== null && 'displayName' in raw) {
45
+ return raw.displayName;
46
+ }
47
+ return undefined;
48
+ case 'sprint':
49
+ return extractActiveSprintName(raw);
50
+ case 'array':
51
+ if (!Array.isArray(raw))
52
+ return undefined;
53
+ return raw
54
+ .map((item) => {
55
+ if (typeof item === 'string')
56
+ return item;
57
+ if (item && typeof item === 'object' && 'value' in item) {
58
+ return item.value;
59
+ }
60
+ if (item && typeof item === 'object' && 'name' in item) {
61
+ return item.name;
62
+ }
63
+ return undefined;
64
+ })
65
+ .filter((x) => x !== undefined);
66
+ default:
67
+ return undefined;
68
+ }
69
+ }
5
70
  const COMMON_EPIC_FIELDS = [
6
71
  'customfield_10014',
7
72
  'customfield_10008',
@@ -11,10 +76,50 @@ const COMMON_EPIC_FIELDS = [
11
76
  export class JiraClient {
12
77
  config;
13
78
  authHeader;
79
+ fieldsCache;
80
+ sprintFieldIdCache;
81
+ storyPointsFieldIdCache;
14
82
  constructor(config, apiToken) {
15
83
  this.config = config;
16
84
  this.authHeader = `Basic ${Buffer.from(`${config.userEmail}:${apiToken}`).toString('base64')}`;
17
85
  }
86
+ async listFields() {
87
+ if (this.fieldsCache)
88
+ return this.fieldsCache;
89
+ const all = await this.makeRequest('/field');
90
+ this.fieldsCache = all;
91
+ return all;
92
+ }
93
+ async detectSprintFieldId() {
94
+ if (this.sprintFieldIdCache !== undefined)
95
+ return this.sprintFieldIdCache ?? undefined;
96
+ try {
97
+ const fields = await this.listFields();
98
+ const match = fields.find((f) => f.schema?.custom === 'com.pyxis.greenhopper.jira:gh-sprint' ||
99
+ f.name?.toLowerCase() === 'sprint');
100
+ this.sprintFieldIdCache = match?.id ?? null;
101
+ return match?.id;
102
+ }
103
+ catch {
104
+ this.sprintFieldIdCache = null;
105
+ return undefined;
106
+ }
107
+ }
108
+ async detectStoryPointsFieldId() {
109
+ if (this.storyPointsFieldIdCache !== undefined)
110
+ return this.storyPointsFieldIdCache ?? undefined;
111
+ try {
112
+ const fields = await this.listFields();
113
+ const match = fields.find((f) => f.schema?.custom === 'com.atlassian.jira.plugin.system.customfieldtypes:float' &&
114
+ /story\s*points?/i.test(f.name ?? '')) ?? fields.find((f) => /^story\s*points?$/i.test(f.name ?? ''));
115
+ this.storyPointsFieldIdCache = match?.id ?? null;
116
+ return match?.id;
117
+ }
118
+ catch {
119
+ this.storyPointsFieldIdCache = null;
120
+ return undefined;
121
+ }
122
+ }
18
123
  extractEpicFromFields(fields) {
19
124
  for (const fieldId of COMMON_EPIC_FIELDS) {
20
125
  const epicField = fields[fieldId];
@@ -213,8 +318,8 @@ export class JiraClient {
213
318
  async getCurrentUser() {
214
319
  return this.makeRequest('/myself');
215
320
  }
216
- async fetchIssueDetails(issueKey) {
217
- const fields = [
321
+ async fetchIssueDetails(issueKey, options = {}) {
322
+ const baseFields = [
218
323
  'summary',
219
324
  'description',
220
325
  'status',
@@ -222,7 +327,16 @@ export class JiraClient {
222
327
  'attachment',
223
328
  'issuetype',
224
329
  ...COMMON_EPIC_FIELDS,
225
- ].join(',');
330
+ ];
331
+ const sprintFieldId = options.customFieldDefs?.sprint?.id ?? (await this.detectSprintFieldId());
332
+ const storyPointsFieldId = options.customFieldDefs?.storyPoints?.id ?? (await this.detectStoryPointsFieldId());
333
+ const extraFields = options.jiraFieldIds ?? [];
334
+ const fieldSet = new Set([...baseFields, ...extraFields]);
335
+ if (sprintFieldId)
336
+ fieldSet.add(sprintFieldId);
337
+ if (storyPointsFieldId)
338
+ fieldSet.add(storyPointsFieldId);
339
+ const fields = [...fieldSet].join(',');
226
340
  const response = await this.makeRequest(`/issue/${issueKey}?fields=${fields}`);
227
341
  const comments = await this.fetchIssueComments(issueKey);
228
342
  const changelog = await this.fetchIssueChangelog(issueKey);
@@ -232,21 +346,29 @@ export class JiraClient {
232
346
  url: att.content,
233
347
  })) || [];
234
348
  const history = this.mergeHistory(comments, changelog, attachmentMetadata);
235
- return {
349
+ return this.buildTaskData(response, attachmentMetadata, history, {
350
+ sprintFieldId,
351
+ storyPointsFieldId,
352
+ customFieldDefs: options.customFieldDefs ?? {},
353
+ });
354
+ }
355
+ buildTaskData(response, attachmentMetadata, history, ctx) {
356
+ const f = response.fields;
357
+ const data = {
236
358
  key: response.key,
237
- title: response.fields.summary,
238
- status: response.fields.status?.name || 'Unknown',
239
- description: this.convertADFToMarkdown(response.fields.description, attachmentMetadata),
240
- issueType: response.fields.issuetype?.name,
241
- parent: response.fields.parent
359
+ title: f.summary,
360
+ status: f.status?.name || 'Unknown',
361
+ description: this.convertADFToMarkdown(f.description, attachmentMetadata),
362
+ issueType: f.issuetype?.name,
363
+ parent: f.parent
242
364
  ? {
243
- key: response.fields.parent.key,
244
- title: response.fields.parent.fields.summary,
245
- status: response.fields.parent.fields.status?.name,
365
+ key: f.parent.key,
366
+ title: f.parent.fields.summary,
367
+ status: f.parent.fields.status?.name,
246
368
  }
247
369
  : undefined,
248
- epic: this.extractEpicFromFields(response.fields),
249
- attachments: response.fields.attachment?.map((att) => ({
370
+ epic: this.extractEpicFromFields(f),
371
+ attachments: f.attachment?.map((att) => ({
250
372
  id: att.id,
251
373
  filename: att.filename,
252
374
  url: att.content,
@@ -254,12 +376,106 @@ export class JiraClient {
254
376
  })) || [],
255
377
  history,
256
378
  };
379
+ const priority = f.priority;
380
+ if (priority?.name)
381
+ data.priority = priority.name;
382
+ const resolution = f.resolution;
383
+ if (resolution?.name)
384
+ data.resolution = resolution.name;
385
+ const assignee = f.assignee;
386
+ if (assignee?.displayName)
387
+ data.assignee = assignee.displayName;
388
+ const reporter = f.reporter;
389
+ if (reporter?.displayName)
390
+ data.reporter = reporter.displayName;
391
+ const creator = f.creator;
392
+ if (creator?.displayName)
393
+ data.creator = creator.displayName;
394
+ if (typeof f.created === 'string')
395
+ data.createdAt = f.created;
396
+ if (typeof f.updated === 'string')
397
+ data.updatedAt = f.updated;
398
+ if (typeof f.duedate === 'string' && f.duedate)
399
+ data.dueDate = f.duedate;
400
+ if (typeof f.resolutiondate === 'string' && f.resolutiondate) {
401
+ data.resolutionDate = f.resolutiondate;
402
+ }
403
+ const components = f.components;
404
+ if (components?.length)
405
+ data.components = components.map((c) => c.name);
406
+ const labels = f.labels;
407
+ if (labels?.length)
408
+ data.labels = labels;
409
+ const fixVersions = f.fixVersions;
410
+ if (fixVersions?.length)
411
+ data.fixVersions = fixVersions.map((v) => v.name);
412
+ const versions = f.versions;
413
+ if (versions?.length)
414
+ data.versions = versions.map((v) => v.name);
415
+ if (ctx.sprintFieldId) {
416
+ const sprintField = f[ctx.sprintFieldId];
417
+ const sprintName = extractActiveSprintName(sprintField);
418
+ if (sprintName)
419
+ data.sprint = sprintName;
420
+ }
421
+ if (ctx.storyPointsFieldId) {
422
+ const sp = f[ctx.storyPointsFieldId];
423
+ if (typeof sp === 'number')
424
+ data.storyPoints = sp;
425
+ }
426
+ const tt = f.timetracking;
427
+ if (tt && (tt.originalEstimate || tt.remainingEstimate || tt.timeSpent)) {
428
+ data.timetracking = {
429
+ originalEstimate: tt.originalEstimate,
430
+ remainingEstimate: tt.remainingEstimate,
431
+ timeSpent: tt.timeSpent,
432
+ };
433
+ }
434
+ const issueLinks = f.issuelinks;
435
+ if (issueLinks?.length) {
436
+ const mapped = [];
437
+ for (const link of issueLinks) {
438
+ if (link.inwardIssue) {
439
+ mapped.push({
440
+ type: link.type.inward ?? link.type.name ?? 'relates to',
441
+ key: link.inwardIssue.key,
442
+ title: link.inwardIssue.fields.summary,
443
+ status: link.inwardIssue.fields.status?.name,
444
+ });
445
+ }
446
+ else if (link.outwardIssue) {
447
+ mapped.push({
448
+ type: link.type.outward ?? link.type.name ?? 'relates to',
449
+ key: link.outwardIssue.key,
450
+ title: link.outwardIssue.fields.summary,
451
+ status: link.outwardIssue.fields.status?.name,
452
+ });
453
+ }
454
+ }
455
+ if (mapped.length)
456
+ data.issueLinks = mapped;
457
+ }
458
+ const customFieldDefs = ctx.customFieldDefs;
459
+ if (Object.keys(customFieldDefs).length > 0) {
460
+ const cf = {};
461
+ for (const [friendly, def] of Object.entries(customFieldDefs)) {
462
+ if (friendly === 'sprint' || friendly === 'storyPoints')
463
+ continue;
464
+ const raw = f[def.id];
465
+ const value = extractCustomFieldValue(raw, def.type);
466
+ if (value !== undefined && value !== null && value !== '')
467
+ cf[friendly] = value;
468
+ }
469
+ if (Object.keys(cf).length > 0)
470
+ data.customFields = cf;
471
+ }
472
+ return data;
257
473
  }
258
474
  async fetchIssueComments(issueKey) {
259
475
  const allComments = [];
260
476
  const maxResults = 100;
261
477
  let startAt = 0;
262
- let total = 0;
478
+ let total;
263
479
  do {
264
480
  const response = await this.makeRequest(`/issue/${issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}`);
265
481
  allComments.push(...response.comments);
@@ -291,6 +507,397 @@ export class JiraClient {
291
507
  return [];
292
508
  }
293
509
  }
510
+ async addComment(issueKey, wikiBody, parentCommentId) {
511
+ const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}/comment`;
512
+ const payload = { body: wikiBody };
513
+ if (parentCommentId)
514
+ payload.parentId = parentCommentId;
515
+ const response = await fetch(url, {
516
+ method: 'POST',
517
+ headers: {
518
+ Authorization: this.authHeader,
519
+ Accept: 'application/json',
520
+ 'Content-Type': 'application/json',
521
+ },
522
+ body: JSON.stringify(payload),
523
+ });
524
+ if (!response.ok) {
525
+ const errorText = await response.text();
526
+ throw new Error(`Jira addComment failed: ${response.status} ${response.statusText}\n${errorText}`);
527
+ }
528
+ const json = (await response.json());
529
+ return { id: json.id };
530
+ }
531
+ async addWorklog(issueKey, payload) {
532
+ const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}/worklog`;
533
+ const response = await fetch(url, {
534
+ method: 'POST',
535
+ headers: {
536
+ Authorization: this.authHeader,
537
+ Accept: 'application/json',
538
+ 'Content-Type': 'application/json',
539
+ },
540
+ body: JSON.stringify(payload),
541
+ });
542
+ if (!response.ok) {
543
+ const errorText = await response.text();
544
+ throw new Error(`Jira addWorklog failed: ${response.status} ${response.statusText}\n${errorText}`);
545
+ }
546
+ const json = (await response.json());
547
+ return { id: json.id, issueId: json.issueId };
548
+ }
549
+ async deleteComment(issueKey, commentId) {
550
+ const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}/comment/${commentId}`;
551
+ const response = await fetch(url, {
552
+ method: 'DELETE',
553
+ headers: { Authorization: this.authHeader, Accept: 'application/json' },
554
+ });
555
+ if (!response.ok && response.status !== 204) {
556
+ const errorText = await response.text();
557
+ throw new Error(`Jira deleteComment failed: ${response.status} ${response.statusText}\n${errorText}`);
558
+ }
559
+ }
560
+ async makeAgileRequest(endpoint, options = {}) {
561
+ const url = `${this.config.baseUrl}/rest/agile/1.0${endpoint}`;
562
+ const response = await fetch(url, {
563
+ ...options,
564
+ headers: {
565
+ Authorization: this.authHeader,
566
+ Accept: 'application/json',
567
+ 'Content-Type': 'application/json',
568
+ ...options.headers,
569
+ },
570
+ });
571
+ if (!response.ok) {
572
+ const errorText = await response.text();
573
+ throw new Error(`Jira Agile API request failed: ${response.status} ${response.statusText}\n${errorText}`);
574
+ }
575
+ return response.json();
576
+ }
577
+ async findBoardByName(name) {
578
+ const response = await this.makeAgileRequest(`/board?name=${encodeURIComponent(name)}`);
579
+ const exact = response.values.find((b) => b.name === name);
580
+ if (exact)
581
+ return exact;
582
+ if (response.values.length === 1)
583
+ return response.values[0];
584
+ if (response.values.length === 0) {
585
+ throw new Error(`No board found matching "${name}".`);
586
+ }
587
+ throw new Error(`Multiple boards matched "${name}": ${response.values.map((b) => b.name).join(', ')}. Use the exact name.`);
588
+ }
589
+ async getBoardConfiguration(boardId) {
590
+ return this.makeAgileRequest(`/board/${boardId}/configuration`);
591
+ }
592
+ async getBoardColumnStatusIds(boardName, columnName) {
593
+ const board = await this.findBoardByName(boardName);
594
+ const config = await this.getBoardConfiguration(board.id);
595
+ const column = config.columnConfig.columns.find((c) => c.name === columnName);
596
+ if (!column) {
597
+ const available = config.columnConfig.columns.map((c) => c.name).join(', ');
598
+ throw new Error(`Column "${columnName}" not found on board "${boardName}". Available: ${available}`);
599
+ }
600
+ return column.statuses.map((s) => s.id);
601
+ }
602
+ async searchByJql(jql, fields = ['summary', 'status', 'assignee', 'issuetype']) {
603
+ const all = [];
604
+ let nextPageToken;
605
+ do {
606
+ const body = { jql, fields, maxResults: 100 };
607
+ if (nextPageToken)
608
+ body.nextPageToken = nextPageToken;
609
+ const response = await this.makeRequest('/search/jql', { method: 'POST', body: JSON.stringify(body) });
610
+ all.push(...response.issues);
611
+ nextPageToken = response.isLast === false ? response.nextPageToken : undefined;
612
+ } while (nextPageToken);
613
+ return all;
614
+ }
615
+ async searchIssues(jql, opts = {}) {
616
+ const body = {
617
+ jql,
618
+ fields: opts.fields ?? ['summary', 'status', 'assignee', 'issuetype'],
619
+ maxResults: opts.limit ?? 50,
620
+ };
621
+ if (opts.nextPageToken)
622
+ body.nextPageToken = opts.nextPageToken;
623
+ const response = await this.makeRequest('/search/jql', { method: 'POST', body: JSON.stringify(body) });
624
+ return {
625
+ issues: response.issues,
626
+ nextPageToken: response.nextPageToken,
627
+ isLast: response.isLast !== false,
628
+ };
629
+ }
630
+ async listProjects(opts = {}) {
631
+ const params = new URLSearchParams();
632
+ if (opts.query)
633
+ params.set('query', opts.query);
634
+ if (opts.startAt !== undefined)
635
+ params.set('startAt', String(opts.startAt));
636
+ if (opts.limit !== undefined)
637
+ params.set('maxResults', String(opts.limit));
638
+ const qs = params.toString();
639
+ return this.makeRequest(`/project/search${qs ? `?${qs}` : ''}`);
640
+ }
641
+ async listBoards(opts = {}) {
642
+ const params = new URLSearchParams();
643
+ if (opts.projectKey)
644
+ params.set('projectKeyOrId', opts.projectKey);
645
+ if (opts.type)
646
+ params.set('type', opts.type);
647
+ if (opts.name)
648
+ params.set('name', opts.name);
649
+ if (opts.startAt !== undefined)
650
+ params.set('startAt', String(opts.startAt));
651
+ if (opts.limit !== undefined)
652
+ params.set('maxResults', String(opts.limit));
653
+ const qs = params.toString();
654
+ return this.makeAgileRequest(`/board${qs ? `?${qs}` : ''}`);
655
+ }
656
+ async listSprints(boardId, opts = {}) {
657
+ const params = new URLSearchParams();
658
+ if (opts.state)
659
+ params.set('state', opts.state);
660
+ if (opts.startAt !== undefined)
661
+ params.set('startAt', String(opts.startAt));
662
+ if (opts.limit !== undefined)
663
+ params.set('maxResults', String(opts.limit));
664
+ const qs = params.toString();
665
+ return this.makeAgileRequest(`/board/${boardId}/sprint${qs ? `?${qs}` : ''}`);
666
+ }
667
+ async listIssueTypes(projectKey) {
668
+ if (!projectKey)
669
+ return this.makeRequest('/issuetype');
670
+ const project = await this.makeRequest(`/project/${projectKey}`);
671
+ return this.makeRequest(`/issuetype/project?projectId=${project.id}`);
672
+ }
673
+ async listLinkTypes() {
674
+ const response = await this.makeRequest('/issueLinkType');
675
+ return response.issueLinkTypes;
676
+ }
677
+ async listPriorities() {
678
+ return this.makeRequest('/priority');
679
+ }
680
+ async listStatuses(projectKey) {
681
+ if (!projectKey)
682
+ return this.makeRequest('/status');
683
+ const statuses = await this.makeRequest(`/project/${projectKey}/statuses`);
684
+ const seen = new Map();
685
+ for (const entry of statuses) {
686
+ for (const s of entry.statuses)
687
+ seen.set(s.id, s);
688
+ }
689
+ return [...seen.values()];
690
+ }
691
+ async getIssueTransitions(issueKey) {
692
+ const response = await this.makeRequest(`/issue/${issueKey}/transitions`);
693
+ return response.transitions;
694
+ }
695
+ async transitionIssue(issueKey, targetStatus) {
696
+ const transitions = await this.getIssueTransitions(issueKey);
697
+ const match = transitions.find((t) => t.to.name === targetStatus) ??
698
+ transitions.find((t) => t.name === targetStatus);
699
+ if (!match) {
700
+ const available = transitions.map((t) => `"${t.name}" → "${t.to.name}"`).join(', ');
701
+ throw new Error(`No transition to "${targetStatus}" available on ${issueKey}. Available: ${available}`);
702
+ }
703
+ const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/transitions`;
704
+ const response = await fetch(url, {
705
+ method: 'POST',
706
+ headers: {
707
+ Authorization: this.authHeader,
708
+ Accept: 'application/json',
709
+ 'Content-Type': 'application/json',
710
+ },
711
+ body: JSON.stringify({ transition: { id: match.id } }),
712
+ });
713
+ if (!response.ok && response.status !== 204) {
714
+ const errorText = await response.text();
715
+ throw new Error(`Jira transition failed: ${response.status} ${response.statusText}\n${errorText}`);
716
+ }
717
+ return { name: match.name, id: match.id };
718
+ }
719
+ async createIssue(input) {
720
+ const fields = {
721
+ project: { key: input.projectKey },
722
+ issuetype: { name: input.issueType },
723
+ summary: input.summary,
724
+ };
725
+ if (input.descriptionMarkdown !== undefined) {
726
+ fields.description = markdownToWiki(input.descriptionMarkdown);
727
+ }
728
+ if (input.assigneeAccountId)
729
+ fields.assignee = { accountId: input.assigneeAccountId };
730
+ if (input.labels)
731
+ fields.labels = input.labels;
732
+ if (input.priority)
733
+ fields.priority = { name: input.priority };
734
+ if (input.parentKey)
735
+ fields.parent = { key: input.parentKey };
736
+ const url = `${this.config.baseUrl}/rest/api/2/issue`;
737
+ const response = await fetch(url, {
738
+ method: 'POST',
739
+ headers: {
740
+ Authorization: this.authHeader,
741
+ Accept: 'application/json',
742
+ 'Content-Type': 'application/json',
743
+ },
744
+ body: JSON.stringify({ fields }),
745
+ });
746
+ if (!response.ok) {
747
+ const errorText = await response.text();
748
+ throw new Error(`Jira createIssue failed: ${response.status} ${response.statusText}\n${errorText}`);
749
+ }
750
+ return response.json();
751
+ }
752
+ async editIssue(issueKey, input) {
753
+ const fields = {};
754
+ if (input.summary !== undefined)
755
+ fields.summary = input.summary;
756
+ if (input.descriptionMarkdown !== undefined) {
757
+ fields.description = markdownToWiki(input.descriptionMarkdown);
758
+ }
759
+ if (input.assigneeAccountId !== undefined) {
760
+ fields.assignee = input.assigneeAccountId === null
761
+ ? null
762
+ : { accountId: input.assigneeAccountId };
763
+ }
764
+ if (input.labels !== undefined)
765
+ fields.labels = input.labels;
766
+ if (input.priority !== undefined)
767
+ fields.priority = { name: input.priority };
768
+ if (Object.keys(fields).length === 0) {
769
+ throw new Error('editIssue called with no fields to update.');
770
+ }
771
+ const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}`;
772
+ const response = await fetch(url, {
773
+ method: 'PUT',
774
+ headers: {
775
+ Authorization: this.authHeader,
776
+ Accept: 'application/json',
777
+ 'Content-Type': 'application/json',
778
+ },
779
+ body: JSON.stringify({ fields }),
780
+ });
781
+ if (!response.ok && response.status !== 204) {
782
+ const errorText = await response.text();
783
+ throw new Error(`Jira editIssue failed: ${response.status} ${response.statusText}\n${errorText}`);
784
+ }
785
+ }
786
+ async assignIssue(issueKey, accountIdOrNull) {
787
+ const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/assignee`;
788
+ const response = await fetch(url, {
789
+ method: 'PUT',
790
+ headers: {
791
+ Authorization: this.authHeader,
792
+ Accept: 'application/json',
793
+ 'Content-Type': 'application/json',
794
+ },
795
+ body: JSON.stringify({ accountId: accountIdOrNull }),
796
+ });
797
+ if (!response.ok && response.status !== 204) {
798
+ const errorText = await response.text();
799
+ throw new Error(`Jira assignIssue failed: ${response.status} ${response.statusText}\n${errorText}`);
800
+ }
801
+ }
802
+ async linkIssues(inwardKey, outwardKey, linkTypeName, comment) {
803
+ const payload = {
804
+ type: { name: linkTypeName },
805
+ inwardIssue: { key: inwardKey },
806
+ outwardIssue: { key: outwardKey },
807
+ };
808
+ if (comment)
809
+ payload.comment = { body: markdownToWiki(comment) };
810
+ const url = `${this.config.baseUrl}/rest/api/3/issueLink`;
811
+ const response = await fetch(url, {
812
+ method: 'POST',
813
+ headers: {
814
+ Authorization: this.authHeader,
815
+ Accept: 'application/json',
816
+ 'Content-Type': 'application/json',
817
+ },
818
+ body: JSON.stringify(payload),
819
+ });
820
+ if (!response.ok && response.status !== 201) {
821
+ const errorText = await response.text();
822
+ throw new Error(`Jira linkIssues failed: ${response.status} ${response.statusText}\n${errorText}`);
823
+ }
824
+ }
825
+ async removeIssueLink(linkId) {
826
+ const url = `${this.config.baseUrl}/rest/api/3/issueLink/${linkId}`;
827
+ const response = await fetch(url, {
828
+ method: 'DELETE',
829
+ headers: { Authorization: this.authHeader, Accept: 'application/json' },
830
+ });
831
+ if (!response.ok && response.status !== 204) {
832
+ const errorText = await response.text();
833
+ throw new Error(`Jira removeIssueLink failed: ${response.status} ${response.statusText}\n${errorText}`);
834
+ }
835
+ }
836
+ async listWatchers(issueKey) {
837
+ const response = await this.makeRequest(`/issue/${issueKey}/watchers`);
838
+ return response.watchers;
839
+ }
840
+ async addWatcher(issueKey, accountId) {
841
+ const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/watchers`;
842
+ // Jira quirk: body is a JSON string (account id in quotes), not an object
843
+ const response = await fetch(url, {
844
+ method: 'POST',
845
+ headers: {
846
+ Authorization: this.authHeader,
847
+ Accept: 'application/json',
848
+ 'Content-Type': 'application/json',
849
+ },
850
+ body: JSON.stringify(accountId),
851
+ });
852
+ if (!response.ok && response.status !== 204) {
853
+ const errorText = await response.text();
854
+ throw new Error(`Jira addWatcher failed: ${response.status} ${response.statusText}\n${errorText}`);
855
+ }
856
+ }
857
+ async removeWatcher(issueKey, accountId) {
858
+ const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/watchers?accountId=${encodeURIComponent(accountId)}`;
859
+ const response = await fetch(url, {
860
+ method: 'DELETE',
861
+ headers: { Authorization: this.authHeader, Accept: 'application/json' },
862
+ });
863
+ if (!response.ok && response.status !== 204) {
864
+ const errorText = await response.text();
865
+ throw new Error(`Jira removeWatcher failed: ${response.status} ${response.statusText}\n${errorText}`);
866
+ }
867
+ }
868
+ async uploadAttachment(issueKey, filePath) {
869
+ await stat(filePath);
870
+ const buf = await readFile(filePath);
871
+ const form = new FormData();
872
+ const arr = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
873
+ form.append('file', new Blob([arr]), basename(filePath));
874
+ const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/attachments`;
875
+ const response = await fetch(url, {
876
+ method: 'POST',
877
+ headers: {
878
+ Authorization: this.authHeader,
879
+ Accept: 'application/json',
880
+ 'X-Atlassian-Token': 'no-check',
881
+ },
882
+ body: form,
883
+ });
884
+ if (!response.ok) {
885
+ const errorText = await response.text();
886
+ throw new Error(`Jira uploadAttachment failed: ${response.status} ${response.statusText}\n${errorText}`);
887
+ }
888
+ return response.json();
889
+ }
890
+ async deleteAttachment(attachmentId) {
891
+ const url = `${this.config.baseUrl}/rest/api/3/attachment/${attachmentId}`;
892
+ const response = await fetch(url, {
893
+ method: 'DELETE',
894
+ headers: { Authorization: this.authHeader, Accept: 'application/json' },
895
+ });
896
+ if (!response.ok && response.status !== 204) {
897
+ const errorText = await response.text();
898
+ throw new Error(`Jira deleteAttachment failed: ${response.status} ${response.statusText}\n${errorText}`);
899
+ }
900
+ }
294
901
  async downloadAttachment(attachmentUrl, outputPath) {
295
902
  await mkdir(dirname(outputPath), { recursive: true });
296
903
  const response = await fetch(attachmentUrl, {