jirallm 0.1.1 → 0.3.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 (266) hide show
  1. package/README.md +75 -0
  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/components.d.ts +7 -0
  35. package/dist/cli/commands/components.d.ts.map +1 -0
  36. package/dist/cli/commands/components.js +23 -0
  37. package/dist/cli/commands/components.js.map +1 -0
  38. package/dist/cli/commands/components.test.d.ts +2 -0
  39. package/dist/cli/commands/components.test.d.ts.map +1 -0
  40. package/dist/cli/commands/components.test.js +62 -0
  41. package/dist/cli/commands/components.test.js.map +1 -0
  42. package/dist/cli/commands/create.d.ts +18 -0
  43. package/dist/cli/commands/create.d.ts.map +1 -0
  44. package/dist/cli/commands/create.js +49 -0
  45. package/dist/cli/commands/create.js.map +1 -0
  46. package/dist/cli/commands/create.test.d.ts +2 -0
  47. package/dist/cli/commands/create.test.d.ts.map +1 -0
  48. package/dist/cli/commands/create.test.js +133 -0
  49. package/dist/cli/commands/create.test.js.map +1 -0
  50. package/dist/cli/commands/edit.d.ts +17 -0
  51. package/dist/cli/commands/edit.d.ts.map +1 -0
  52. package/dist/cli/commands/edit.js +52 -0
  53. package/dist/cli/commands/edit.js.map +1 -0
  54. package/dist/cli/commands/edit.test.d.ts +2 -0
  55. package/dist/cli/commands/edit.test.d.ts.map +1 -0
  56. package/dist/cli/commands/edit.test.js +96 -0
  57. package/dist/cli/commands/edit.test.js.map +1 -0
  58. package/dist/cli/commands/fetch.d.ts +7 -0
  59. package/dist/cli/commands/fetch.d.ts.map +1 -0
  60. package/dist/cli/commands/fetch.js +32 -0
  61. package/dist/cli/commands/fetch.js.map +1 -0
  62. package/dist/cli/commands/fetch.test.d.ts +2 -0
  63. package/dist/cli/commands/fetch.test.d.ts.map +1 -0
  64. package/dist/cli/commands/fetch.test.js +93 -0
  65. package/dist/cli/commands/fetch.test.js.map +1 -0
  66. package/dist/cli/commands/fields.d.ts +8 -0
  67. package/dist/cli/commands/fields.d.ts.map +1 -0
  68. package/dist/cli/commands/fields.js +47 -0
  69. package/dist/cli/commands/fields.js.map +1 -0
  70. package/dist/cli/commands/fields.test.d.ts +2 -0
  71. package/dist/cli/commands/fields.test.d.ts.map +1 -0
  72. package/dist/cli/commands/fields.test.js +78 -0
  73. package/dist/cli/commands/fields.test.js.map +1 -0
  74. package/dist/cli/commands/issuetypes.d.ts +7 -0
  75. package/dist/cli/commands/issuetypes.d.ts.map +1 -0
  76. package/dist/cli/commands/issuetypes.js +23 -0
  77. package/dist/cli/commands/issuetypes.js.map +1 -0
  78. package/dist/cli/commands/issuetypes.test.d.ts +2 -0
  79. package/dist/cli/commands/issuetypes.test.d.ts.map +1 -0
  80. package/dist/cli/commands/issuetypes.test.js +62 -0
  81. package/dist/cli/commands/issuetypes.test.js.map +1 -0
  82. package/dist/cli/commands/link.d.ts +18 -0
  83. package/dist/cli/commands/link.d.ts.map +1 -0
  84. package/dist/cli/commands/link.js +51 -0
  85. package/dist/cli/commands/link.js.map +1 -0
  86. package/dist/cli/commands/link.test.d.ts +2 -0
  87. package/dist/cli/commands/link.test.d.ts.map +1 -0
  88. package/dist/cli/commands/link.test.js +96 -0
  89. package/dist/cli/commands/link.test.js.map +1 -0
  90. package/dist/cli/commands/linktypes.d.ts +6 -0
  91. package/dist/cli/commands/linktypes.d.ts.map +1 -0
  92. package/dist/cli/commands/linktypes.js +21 -0
  93. package/dist/cli/commands/linktypes.js.map +1 -0
  94. package/dist/cli/commands/linktypes.test.d.ts +2 -0
  95. package/dist/cli/commands/linktypes.test.d.ts.map +1 -0
  96. package/dist/cli/commands/linktypes.test.js +55 -0
  97. package/dist/cli/commands/linktypes.test.js.map +1 -0
  98. package/dist/cli/commands/me.d.ts +6 -0
  99. package/dist/cli/commands/me.d.ts.map +1 -0
  100. package/dist/cli/commands/me.js +17 -0
  101. package/dist/cli/commands/me.js.map +1 -0
  102. package/dist/cli/commands/me.test.d.ts +2 -0
  103. package/dist/cli/commands/me.test.d.ts.map +1 -0
  104. package/dist/cli/commands/me.test.js +58 -0
  105. package/dist/cli/commands/me.test.js.map +1 -0
  106. package/dist/cli/commands/projects.d.ts +9 -0
  107. package/dist/cli/commands/projects.d.ts.map +1 -0
  108. package/dist/cli/commands/projects.js +25 -0
  109. package/dist/cli/commands/projects.js.map +1 -0
  110. package/dist/cli/commands/projects.test.d.ts +2 -0
  111. package/dist/cli/commands/projects.test.d.ts.map +1 -0
  112. package/dist/cli/commands/projects.test.js +73 -0
  113. package/dist/cli/commands/projects.test.js.map +1 -0
  114. package/dist/cli/commands/search.d.ts +11 -0
  115. package/dist/cli/commands/search.d.ts.map +1 -0
  116. package/dist/cli/commands/search.js +41 -0
  117. package/dist/cli/commands/search.js.map +1 -0
  118. package/dist/cli/commands/search.test.d.ts +2 -0
  119. package/dist/cli/commands/search.test.d.ts.map +1 -0
  120. package/dist/cli/commands/search.test.js +106 -0
  121. package/dist/cli/commands/search.test.js.map +1 -0
  122. package/dist/cli/commands/setup.d.ts.map +1 -1
  123. package/dist/cli/commands/setup.js +5 -13
  124. package/dist/cli/commands/setup.js.map +1 -1
  125. package/dist/cli/commands/sprints.d.ts +10 -0
  126. package/dist/cli/commands/sprints.d.ts.map +1 -0
  127. package/dist/cli/commands/sprints.js +28 -0
  128. package/dist/cli/commands/sprints.js.map +1 -0
  129. package/dist/cli/commands/sprints.test.d.ts +2 -0
  130. package/dist/cli/commands/sprints.test.d.ts.map +1 -0
  131. package/dist/cli/commands/sprints.test.js +68 -0
  132. package/dist/cli/commands/sprints.test.js.map +1 -0
  133. package/dist/cli/commands/transition.d.ts +8 -0
  134. package/dist/cli/commands/transition.d.ts.map +1 -0
  135. package/dist/cli/commands/transition.js +46 -0
  136. package/dist/cli/commands/transition.js.map +1 -0
  137. package/dist/cli/commands/upgrade.d.ts +16 -0
  138. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  139. package/dist/cli/commands/upgrade.js +83 -0
  140. package/dist/cli/commands/upgrade.js.map +1 -0
  141. package/dist/cli/commands/upgrade.test.d.ts +2 -0
  142. package/dist/cli/commands/upgrade.test.d.ts.map +1 -0
  143. package/dist/cli/commands/upgrade.test.js +146 -0
  144. package/dist/cli/commands/upgrade.test.js.map +1 -0
  145. package/dist/cli/commands/watchers.d.ts +10 -0
  146. package/dist/cli/commands/watchers.d.ts.map +1 -0
  147. package/dist/cli/commands/watchers.js +43 -0
  148. package/dist/cli/commands/watchers.js.map +1 -0
  149. package/dist/cli/commands/watchers.test.d.ts +2 -0
  150. package/dist/cli/commands/watchers.test.d.ts.map +1 -0
  151. package/dist/cli/commands/watchers.test.js +90 -0
  152. package/dist/cli/commands/watchers.test.js.map +1 -0
  153. package/dist/cli/commands/worklog.d.ts +8 -0
  154. package/dist/cli/commands/worklog.d.ts.map +1 -0
  155. package/dist/cli/commands/worklog.js +100 -0
  156. package/dist/cli/commands/worklog.js.map +1 -0
  157. package/dist/cli/commands/worklog.test.d.ts +2 -0
  158. package/dist/cli/commands/worklog.test.d.ts.map +1 -0
  159. package/dist/cli/commands/worklog.test.js +195 -0
  160. package/dist/cli/commands/worklog.test.js.map +1 -0
  161. package/dist/cli/index.js +712 -2
  162. package/dist/cli/index.js.map +1 -1
  163. package/dist/cli/issueKey.d.ts.map +1 -1
  164. package/dist/cli/issueKey.js +6 -2
  165. package/dist/cli/issueKey.js.map +1 -1
  166. package/dist/cli/issueKey.test.js +35 -0
  167. package/dist/cli/issueKey.test.js.map +1 -1
  168. package/dist/cli/jsonOutput.d.ts +6 -0
  169. package/dist/cli/jsonOutput.d.ts.map +1 -0
  170. package/dist/cli/jsonOutput.js +7 -0
  171. package/dist/cli/jsonOutput.js.map +1 -0
  172. package/dist/cli/jsonOutput.test.d.ts +2 -0
  173. package/dist/cli/jsonOutput.test.d.ts.map +1 -0
  174. package/dist/cli/jsonOutput.test.js +44 -0
  175. package/dist/cli/jsonOutput.test.js.map +1 -0
  176. package/dist/cli/resolveOrg.d.ts +2 -0
  177. package/dist/cli/resolveOrg.d.ts.map +1 -0
  178. package/dist/cli/resolveOrg.js +15 -0
  179. package/dist/cli/resolveOrg.js.map +1 -0
  180. package/dist/index.d.ts +5 -1
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +3 -0
  183. package/dist/index.js.map +1 -1
  184. package/dist/lib/chunkMarkdown.d.ts +6 -0
  185. package/dist/lib/chunkMarkdown.d.ts.map +1 -0
  186. package/dist/lib/chunkMarkdown.js +73 -0
  187. package/dist/lib/chunkMarkdown.js.map +1 -0
  188. package/dist/lib/chunkMarkdown.test.d.ts +2 -0
  189. package/dist/lib/chunkMarkdown.test.d.ts.map +1 -0
  190. package/dist/lib/chunkMarkdown.test.js +171 -0
  191. package/dist/lib/chunkMarkdown.test.js.map +1 -0
  192. package/dist/lib/config.d.ts +6 -0
  193. package/dist/lib/config.d.ts.map +1 -1
  194. package/dist/lib/config.js +25 -0
  195. package/dist/lib/config.js.map +1 -1
  196. package/dist/lib/config.test.js +67 -0
  197. package/dist/lib/config.test.js.map +1 -1
  198. package/dist/lib/configSchema.d.ts +44 -0
  199. package/dist/lib/configSchema.d.ts.map +1 -1
  200. package/dist/lib/configSchema.js +20 -0
  201. package/dist/lib/configSchema.js.map +1 -1
  202. package/dist/lib/customFieldWrite.d.ts +21 -0
  203. package/dist/lib/customFieldWrite.d.ts.map +1 -0
  204. package/dist/lib/customFieldWrite.js +83 -0
  205. package/dist/lib/customFieldWrite.js.map +1 -0
  206. package/dist/lib/customFieldWrite.test.d.ts +2 -0
  207. package/dist/lib/customFieldWrite.test.d.ts.map +1 -0
  208. package/dist/lib/customFieldWrite.test.js +87 -0
  209. package/dist/lib/customFieldWrite.test.js.map +1 -0
  210. package/dist/lib/exportFields.d.ts +30 -0
  211. package/dist/lib/exportFields.d.ts.map +1 -0
  212. package/dist/lib/exportFields.js +169 -0
  213. package/dist/lib/exportFields.js.map +1 -0
  214. package/dist/lib/exportFields.test.d.ts +2 -0
  215. package/dist/lib/exportFields.test.d.ts.map +1 -0
  216. package/dist/lib/exportFields.test.js +145 -0
  217. package/dist/lib/exportFields.test.js.map +1 -0
  218. package/dist/lib/exporter.d.ts +25 -3
  219. package/dist/lib/exporter.d.ts.map +1 -1
  220. package/dist/lib/exporter.js +195 -18
  221. package/dist/lib/exporter.js.map +1 -1
  222. package/dist/lib/exporter.test.d.ts +2 -0
  223. package/dist/lib/exporter.test.d.ts.map +1 -0
  224. package/dist/lib/exporter.test.js +227 -0
  225. package/dist/lib/exporter.test.js.map +1 -0
  226. package/dist/lib/jiraClient.d.ts +272 -1
  227. package/dist/lib/jiraClient.d.ts.map +1 -1
  228. package/dist/lib/jiraClient.discovery.test.d.ts +2 -0
  229. package/dist/lib/jiraClient.discovery.test.d.ts.map +1 -0
  230. package/dist/lib/jiraClient.discovery.test.js +209 -0
  231. package/dist/lib/jiraClient.discovery.test.js.map +1 -0
  232. package/dist/lib/jiraClient.js +656 -16
  233. package/dist/lib/jiraClient.js.map +1 -1
  234. package/dist/lib/jiraClient.test.d.ts +2 -0
  235. package/dist/lib/jiraClient.test.d.ts.map +1 -0
  236. package/dist/lib/jiraClient.test.js +383 -0
  237. package/dist/lib/jiraClient.test.js.map +1 -0
  238. package/dist/lib/jiraClient.write.test.d.ts +2 -0
  239. package/dist/lib/jiraClient.write.test.d.ts.map +1 -0
  240. package/dist/lib/jiraClient.write.test.js +358 -0
  241. package/dist/lib/jiraClient.write.test.js.map +1 -0
  242. package/dist/lib/markdownToWiki.d.ts +2 -0
  243. package/dist/lib/markdownToWiki.d.ts.map +1 -0
  244. package/dist/lib/markdownToWiki.js +110 -0
  245. package/dist/lib/markdownToWiki.js.map +1 -0
  246. package/dist/lib/markdownToWiki.test.d.ts +2 -0
  247. package/dist/lib/markdownToWiki.test.d.ts.map +1 -0
  248. package/dist/lib/markdownToWiki.test.js +122 -0
  249. package/dist/lib/markdownToWiki.test.js.map +1 -0
  250. package/dist/lib/platform.d.ts +2 -0
  251. package/dist/lib/platform.d.ts.map +1 -1
  252. package/dist/lib/platform.js +8 -0
  253. package/dist/lib/platform.js.map +1 -1
  254. package/dist/lib/runCommand.d.ts +1 -0
  255. package/dist/lib/runCommand.d.ts.map +1 -1
  256. package/dist/lib/runCommand.js +8 -1
  257. package/dist/lib/runCommand.js.map +1 -1
  258. package/dist/lib/worklog.d.ts +36 -0
  259. package/dist/lib/worklog.d.ts.map +1 -0
  260. package/dist/lib/worklog.js +188 -0
  261. package/dist/lib/worklog.js.map +1 -0
  262. package/dist/lib/worklog.test.d.ts +2 -0
  263. package/dist/lib/worklog.test.d.ts.map +1 -0
  264. package/dist/lib/worklog.test.js +118 -0
  265. package/dist/lib/worklog.test.js.map +1 -0
  266. package/package.json +3 -1
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { Command } from 'commander';
4
+ import updateNotifier from 'update-notifier';
4
5
  import { select, isCancel, cancel } from '@clack/prompts';
5
6
  import { JiraExporter } from '../lib/exporter.js';
6
7
  import { findOrgsByProjectKey, loadProfile, readConfig } from '../lib/config.js';
@@ -10,6 +11,28 @@ import { runAuthSet, runAuthRm, runAuthList, runAuthStatus } from './commands/au
10
11
  import { runOrgsList, runOrgsRemove, runProjectRemove } from './commands/orgs.js';
11
12
  import { runDoctor } from './commands/doctor.js';
12
13
  import { runSetup } from './commands/setup.js';
14
+ import { runComment, runDeleteComment } from './commands/comment.js';
15
+ import { runBoardIssues } from './commands/board.js';
16
+ import { runTransition } from './commands/transition.js';
17
+ import { runWorklog } from './commands/worklog.js';
18
+ import { runSearch } from './commands/search.js';
19
+ import { runProjects } from './commands/projects.js';
20
+ import { runBoards } from './commands/boards.js';
21
+ import { runSprints } from './commands/sprints.js';
22
+ import { runIssueTypes } from './commands/issuetypes.js';
23
+ import { runComponents } from './commands/components.js';
24
+ import { runFields } from './commands/fields.js';
25
+ import { runLinkTypes } from './commands/linktypes.js';
26
+ import { runMe } from './commands/me.js';
27
+ import { runFetch } from './commands/fetch.js';
28
+ import { runCreate } from './commands/create.js';
29
+ import { runEdit } from './commands/edit.js';
30
+ import { runAssign } from './commands/assign.js';
31
+ import { runLink, runLinkRemove } from './commands/link.js';
32
+ import { runAttach, runAttachRemove } from './commands/attach.js';
33
+ import { runWatchers } from './commands/watchers.js';
34
+ import { runUpgrade } from './commands/upgrade.js';
35
+ import { parseFieldsFlag, resolveFieldSet } from '../lib/exportFields.js';
13
36
  async function pickOrgInteractively(candidates, projectKey) {
14
37
  const raw = readConfig();
15
38
  const choice = await select({
@@ -64,6 +87,8 @@ async function runExport(rawArgs, flags) {
64
87
  let videoEnabled = flags.videoFrames;
65
88
  let fps = flags.fps ? parseInt(flags.fps, 10) : 5;
66
89
  let maxFrames = flags.maxFrames ? parseInt(flags.maxFrames, 10) : 10;
90
+ let fieldSelector = flags.fields ? parseFieldsFlag(flags.fields) : undefined;
91
+ let customFieldDefs;
67
92
  let keys;
68
93
  let org = flags.org;
69
94
  try {
@@ -89,6 +114,14 @@ async function runExport(rawArgs, flags) {
89
114
  if (flags.includeSubtasks === undefined && resolved.org.includeSubtasks !== undefined) {
90
115
  includeSubtasks = resolved.org.includeSubtasks;
91
116
  }
117
+ if (resolved.org.export) {
118
+ if (!fieldSelector && resolved.org.export.fieldSelector) {
119
+ fieldSelector = resolved.org.export.fieldSelector;
120
+ }
121
+ if (resolved.org.export.customFieldDefs) {
122
+ customFieldDefs = resolved.org.export.customFieldDefs;
123
+ }
124
+ }
92
125
  if (resolved.org.videoFrames) {
93
126
  if (resolved.org.videoFrames.enabled === false && flags.videoFrames !== false) {
94
127
  videoEnabled = false;
@@ -106,19 +139,51 @@ async function runExport(rawArgs, flags) {
106
139
  process.exit(1);
107
140
  }
108
141
  }
142
+ if (flags.dryRun) {
143
+ const resolved = resolveFieldSet(fieldSelector, customFieldDefs ?? {});
144
+ console.log('Dry run — no Jira calls or file writes will be performed.');
145
+ console.log(`Org: ${org}`);
146
+ console.log(`Project: ${projectKey}`);
147
+ console.log(`Base URL: ${baseUrl}`);
148
+ console.log(`Output dir: ${outputDir}`);
149
+ console.log(`Issues (${keys.length}): ${keys.join(', ')}`);
150
+ console.log(`Field preset: ${fieldSelector?.preset ?? '(default)'}`);
151
+ if (fieldSelector?.include?.length)
152
+ console.log(` include: ${fieldSelector.include.join(', ')}`);
153
+ if (fieldSelector?.exclude?.length)
154
+ console.log(` exclude: ${fieldSelector.exclude.join(', ')}`);
155
+ console.log(`Frontmatter fields: ${resolved.friendlyKeys.join(', ')}`);
156
+ console.log(`Jira fields requested: ${resolved.jiraFieldIds.join(', ')}`);
157
+ if (customFieldDefs && Object.keys(customFieldDefs).length > 0) {
158
+ console.log(`Custom fields: ${Object.keys(customFieldDefs).join(', ')}`);
159
+ }
160
+ return;
161
+ }
109
162
  const exporter = new JiraExporter({ baseUrl: baseUrl, projectKey: projectKey, userEmail: userEmail }, apiToken);
110
163
  console.log(`Exporting ${keys.length} issue(s) to ${outputDir}...`);
111
164
  const result = await exporter.exportIssues(keys, {
112
165
  outputDir,
113
166
  includeSubtasks,
167
+ fieldSelector,
168
+ customFieldDefs,
114
169
  videoFrames: { enabled: videoEnabled, fps, maxFrames },
115
170
  });
171
+ const printItem = (item) => {
172
+ console.log(` - ${item.key}: ${item.path}`);
173
+ if (item.attachmentCount > 0) {
174
+ console.log(` attachments: ${item.attachmentCount}`);
175
+ }
176
+ };
116
177
  console.log('\nExport summary:');
117
178
  if (result.imported.length > 0) {
118
- console.log(` Imported (${result.imported.length}): ${result.imported.join(', ')}`);
179
+ console.log(` Imported (${result.imported.length}):`);
180
+ for (const item of result.imported)
181
+ printItem(item);
119
182
  }
120
183
  if (result.updated.length > 0) {
121
- console.log(` Updated (${result.updated.length}): ${result.updated.join(', ')}`);
184
+ console.log(` Updated (${result.updated.length}):`);
185
+ for (const item of result.updated)
186
+ printItem(item);
122
187
  }
123
188
  if (result.failed.length > 0) {
124
189
  console.log(` Failed (${result.failed.length}):`);
@@ -131,6 +196,14 @@ const program = new Command();
131
196
  program.enablePositionalOptions();
132
197
  const pkg = createRequire(import.meta.url)('../../package.json');
133
198
  program.name(pkg.name).description(pkg.description).version(pkg.version);
199
+ const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
200
+ if (!process.argv.includes('--json')) {
201
+ notifier.notify({
202
+ defer: true,
203
+ isGlobal: true,
204
+ message: `Update available {currentVersion} → {latestVersion}\nRun \`${pkg.name} upgrade\` to update.`,
205
+ });
206
+ }
134
207
  program
135
208
  .argument('[issue-keys...]', 'Jira issue keys, e.g. PROJ-123 or acme/PROJ-123')
136
209
  .option('-o, --org <name>', 'Organization name from config (auto-resolved from issue prefix if unique)')
@@ -143,6 +216,8 @@ program
143
216
  .option('--fps <n>', 'Frame extraction FPS')
144
217
  .option('--max-frames <n>', 'Max frames kept per video')
145
218
  .option('--include-subtasks', 'Fetch and include subtask metadata')
219
+ .option('--fields <list>', 'Comma-separated friendly field names to include in frontmatter. Use +name/-name to add/remove, or "all" | "default" | "minimal".')
220
+ .option('--dry-run', 'Resolve config & print plan without calling Jira or writing files')
146
221
  .action(async (keys, flags) => {
147
222
  await runExport(keys, flags);
148
223
  })
@@ -259,6 +334,641 @@ Examples:
259
334
  $ jirallm setup --yes
260
335
  $ jirallm setup --bundled
261
336
  `);
337
+ program
338
+ .command('upgrade')
339
+ .description('Upgrade jirallm to the latest version (auto-detects npm/pnpm/yarn/Homebrew)')
340
+ .option('-y, --yes', 'Skip confirmation prompt')
341
+ .option('--check', 'Print whether an update is available without installing (exits non-zero if outdated)')
342
+ .action(async (opts) => {
343
+ try {
344
+ await runUpgrade(opts);
345
+ }
346
+ catch (err) {
347
+ console.error(err.message);
348
+ process.exit(1);
349
+ }
350
+ })
351
+ .addHelpText('after', `
352
+ Examples:
353
+ $ jirallm upgrade
354
+ $ jirallm upgrade --yes
355
+ $ jirallm upgrade --check
356
+ `);
357
+ program
358
+ .command('comment <issue-key>')
359
+ .description('Post a (possibly multi-part) comment on a Jira issue. Markdown converted to Jira wiki by default.')
360
+ .option('-f, --file <path>', 'Read comment body from a file')
361
+ .option('-t, --text <text>', 'Inline comment text (alternative to --file / stdin)')
362
+ .option('-o, --org <name>', 'Organization name override')
363
+ .option('--max-chars <n>', 'Max chars per comment chunk (default 25000)')
364
+ .option('--no-wiki', 'Skip markdown→wiki conversion (post body as-is)')
365
+ .option('--reply-to <commentId>', 'Post as reply to an existing comment (threaded)')
366
+ .option('--no-thread', 'When posting multiple chunks, do not chain them as replies')
367
+ .option('--dry-run', 'Show what would be posted without calling Jira')
368
+ .action(async (issueKey, opts) => {
369
+ try {
370
+ await runComment(issueKey, {
371
+ file: opts.file,
372
+ text: opts.text,
373
+ org: opts.org,
374
+ maxChars: opts.maxChars,
375
+ noWiki: opts.wiki === false,
376
+ noThread: opts.thread === false,
377
+ replyTo: opts.replyTo,
378
+ dryRun: opts.dryRun,
379
+ });
380
+ }
381
+ catch (err) {
382
+ console.error(err.message);
383
+ process.exit(1);
384
+ }
385
+ })
386
+ .addHelpText('after', `
387
+ Examples:
388
+ $ jirallm comment CN-2505 --file ./summary.md
389
+ $ jirallm comment CN-2505 -t "Quick note"
390
+ $ cat summary.md | jirallm comment CN-2505
391
+ $ jirallm comment CN-2505 --file ./summary.md --dry-run
392
+ $ jirallm comment CN-2505 --reply-to 26215 -t "follow-up"
393
+ `);
394
+ program
395
+ .command('board:issues')
396
+ .description('List issues in a board column. Output JSON with --json for piping into other tools.')
397
+ .requiredOption('-b, --board <name>', 'Board name (exact match)')
398
+ .requiredOption('-c, --column <name>', 'Column name (exact match)')
399
+ .requiredOption('-o, --org <name>', 'Organization name from config')
400
+ .option('-P, --project <key>', 'Project key override (defaults to the org\'s configured project)')
401
+ .option('-a, --assignee <accountIdOrMe>', 'Filter by assignee. Use "me" for the current user.')
402
+ .option('--json', 'Output JSON instead of human-readable')
403
+ .action(async (opts) => {
404
+ try {
405
+ await runBoardIssues(opts);
406
+ }
407
+ catch (err) {
408
+ console.error(err.message);
409
+ process.exit(1);
410
+ }
411
+ })
412
+ .addHelpText('after', `
413
+ Examples:
414
+ $ jirallm board:issues -o MyOrg -b "My Board" -c "In Review" -a me --json
415
+ $ jirallm board:issues -o MyOrg -b "My Board" -c "Done"
416
+ `);
417
+ program
418
+ .command('transition <issue-key>')
419
+ .description('Transition a Jira issue to a target status (matches transition.to.name).')
420
+ .option('-t, --to <status>', 'Target status name (e.g. "In Review")')
421
+ .option('-o, --org <name>', 'Organization name override')
422
+ .option('-l, --list', 'List available transitions for the issue instead of performing one')
423
+ .option('--json', 'Output JSON instead of human-readable')
424
+ .action(async (issueKey, opts) => {
425
+ try {
426
+ if (!opts.list && !opts.to) {
427
+ throw new Error('Either --to <status> or --list is required.');
428
+ }
429
+ await runTransition(issueKey, { to: opts.to ?? '', org: opts.org, list: opts.list, json: opts.json });
430
+ }
431
+ catch (err) {
432
+ console.error(err.message);
433
+ process.exit(1);
434
+ }
435
+ })
436
+ .addHelpText('after', `
437
+ Examples:
438
+ $ jirallm transition PROJ-123 --to "In Review"
439
+ $ jirallm transition PROJ-123 --list
440
+ `);
441
+ program
442
+ .command('worklog')
443
+ .description('Batch log work to Jira from a JSON array (stdin or --file).')
444
+ .option('-f, --file <path>', 'Read JSON array from a file (default: stdin)')
445
+ .option('-o, --org <name>', 'Default org for entries without an org/ prefix or "org" field')
446
+ .option('--no-wiki', 'Skip markdown→wiki conversion of description')
447
+ .option('--dry-run', 'Validate and print what would be posted, without calling Jira')
448
+ .action(async (opts) => {
449
+ try {
450
+ await runWorklog({
451
+ file: opts.file,
452
+ org: opts.org,
453
+ noWiki: opts.wiki === false,
454
+ dryRun: opts.dryRun,
455
+ });
456
+ }
457
+ catch (err) {
458
+ console.error(err.message);
459
+ process.exit(1);
460
+ }
461
+ })
462
+ .addHelpText('after', `
463
+ JSON entry shape (any 2 of startTime/endTime/duration required):
464
+ {
465
+ "issueKey": "PROJ-123", // required; supports "org/PROJ-123"
466
+ "startTime": "2026-05-23T09:00:00+02:00",
467
+ "endTime": "2026-05-23T10:30:00+02:00",
468
+ "duration": "1h 30m", // seconds | "1h 30m" | "PT1H30M"
469
+ "description": "**markdown** ok",
470
+ "org": "acme", // optional per-entry override
471
+ "visibility": { "type": "role", "value": "Developers" }
472
+ }
473
+
474
+ Examples:
475
+ $ jirallm worklog -f ./worklogs.json
476
+ $ cat worklogs.json | jirallm worklog --dry-run
477
+ $ echo '[{"issueKey":"PROJ-1","startTime":"2026-05-23T09:00:00+02:00","duration":"1h"}]' | jirallm worklog
478
+ `);
479
+ program
480
+ .command('comment:rm <issue-key> <comment-id>')
481
+ .description('Delete a comment from a Jira issue')
482
+ .option('-o, --org <name>', 'Organization name override')
483
+ .action(async (issueKey, commentId, opts) => {
484
+ try {
485
+ await runDeleteComment(issueKey, commentId, opts);
486
+ }
487
+ catch (err) {
488
+ console.error(err.message);
489
+ process.exit(1);
490
+ }
491
+ })
492
+ .addHelpText('after', `
493
+ Examples:
494
+ $ jirallm comment:rm PROJ-123 26215
495
+ $ jirallm comment:rm acme/PROJ-123 26215
496
+ `);
497
+ function exitOnError(err) {
498
+ console.error(err.message);
499
+ process.exit(1);
500
+ }
501
+ /** Commander collector for repeatable options (e.g. --field a=1 --field b=2). */
502
+ function collect(value, previous = []) {
503
+ return [...previous, value];
504
+ }
505
+ program
506
+ .command('search <jql>')
507
+ .description('Search issues by JQL (single page; pass --cursor for next page).')
508
+ .option('-o, --org <name>', 'Organization name')
509
+ .option('-P, --project <key>', 'Project key override')
510
+ .option('--limit <n>', 'Page size (default 50)')
511
+ .option('--cursor <token>', 'Next page token from prior search')
512
+ .option('--fields <list>', 'Comma-separated Jira field IDs to include')
513
+ .option('--json', 'Output JSON instead of human-readable')
514
+ .action(async (jql, opts) => {
515
+ try {
516
+ await runSearch({ jql, ...opts });
517
+ }
518
+ catch (err) {
519
+ exitOnError(err);
520
+ }
521
+ })
522
+ .addHelpText('after', `
523
+ JQL must be quoted in your shell (it usually contains spaces and shell metacharacters).
524
+ Output JSON includes "nextPageToken" — pass it back via --cursor for the next page.
525
+
526
+ Examples:
527
+ $ jirallm search 'assignee = currentUser() AND statusCategory != Done' -o acme --json
528
+ $ jirallm search 'project = PROJ AND sprint in openSprints()' -o acme --limit 25
529
+ $ jirallm search 'project = PROJ' -o acme --cursor eyJsYXN0SXNzdWVLZXkiOi4uLn0= --json
530
+ $ jirallm search 'project = PROJ' -o acme --fields summary,status,assignee --json
531
+ `);
532
+ program
533
+ .command('projects')
534
+ .description('List projects accessible in an org.')
535
+ .requiredOption('-o, --org <name>', 'Organization name')
536
+ .option('--query <text>', 'Filter by name/key substring')
537
+ .option('--limit <n>', 'Page size')
538
+ .option('--start-at <n>', 'Pagination offset')
539
+ .option('--json', 'Output JSON instead of human-readable')
540
+ .action(async (opts) => {
541
+ try {
542
+ await runProjects(opts);
543
+ }
544
+ catch (err) {
545
+ exitOnError(err);
546
+ }
547
+ })
548
+ .addHelpText('after', `
549
+ Examples:
550
+ $ jirallm projects -o acme
551
+ $ jirallm projects -o acme --query docs --json
552
+ $ jirallm projects -o acme --limit 50 --start-at 100 --json
553
+ `);
554
+ program
555
+ .command('boards')
556
+ .description('List agile boards in an org.')
557
+ .requiredOption('-o, --org <name>', 'Organization name')
558
+ .option('-P, --project <key>', 'Filter by project key')
559
+ .option('-t, --type <type>', 'scrum | kanban | simple')
560
+ .option('-n, --name <name>', 'Name substring filter')
561
+ .option('--limit <n>', 'Page size')
562
+ .option('--start-at <n>', 'Pagination offset')
563
+ .option('--json', 'Output JSON instead of human-readable')
564
+ .action(async (opts) => {
565
+ try {
566
+ await runBoards(opts);
567
+ }
568
+ catch (err) {
569
+ exitOnError(err);
570
+ }
571
+ })
572
+ .addHelpText('after', `
573
+ When --project is omitted, falls back to the org's default project.
574
+ The board id printed here is what you pass to \`jirallm sprints <id>\`.
575
+
576
+ Examples:
577
+ $ jirallm boards -o acme
578
+ $ jirallm boards -o acme -P PROJ -t scrum --json
579
+ $ jirallm boards -o acme -n "Team Alpha" --json
580
+ `);
581
+ program
582
+ .command('sprints <board-id>')
583
+ .description('List sprints on a board.')
584
+ .requiredOption('-o, --org <name>', 'Organization name')
585
+ .option('-s, --state <state>', 'active | future | closed')
586
+ .option('--limit <n>', 'Page size')
587
+ .option('--start-at <n>', 'Pagination offset')
588
+ .option('--json', 'Output JSON instead of human-readable')
589
+ .action(async (boardId, opts) => {
590
+ try {
591
+ await runSprints({ boardId, ...opts });
592
+ }
593
+ catch (err) {
594
+ exitOnError(err);
595
+ }
596
+ })
597
+ .addHelpText('after', `
598
+ Get board ids from \`jirallm boards --org <name>\`.
599
+
600
+ Examples:
601
+ $ jirallm sprints 42 -o acme --state active --json
602
+ $ jirallm sprints 42 -o acme --state closed --limit 20
603
+ `);
604
+ program
605
+ .command('issuetypes')
606
+ .description('List issue types (project-scoped if --project provided).')
607
+ .requiredOption('-o, --org <name>', 'Organization name')
608
+ .option('-P, --project <key>', 'Project key (defaults to org default)')
609
+ .option('--json', 'Output JSON instead of human-readable')
610
+ .action(async (opts) => {
611
+ try {
612
+ await runIssueTypes(opts);
613
+ }
614
+ catch (err) {
615
+ exitOnError(err);
616
+ }
617
+ })
618
+ .addHelpText('after', `
619
+ Use this before \`jirallm create\` to discover the valid --type values for a project.
620
+
621
+ Examples:
622
+ $ jirallm issuetypes -o acme --json
623
+ $ jirallm issuetypes -o acme -P PROJ --json
624
+ `);
625
+ program
626
+ .command('components')
627
+ .description('List components defined on a project.')
628
+ .requiredOption('-o, --org <name>', 'Organization name')
629
+ .option('-P, --project <key>', 'Project key (defaults to org default)')
630
+ .option('--json', 'Output JSON instead of human-readable')
631
+ .action(async (opts) => {
632
+ try {
633
+ await runComponents(opts);
634
+ }
635
+ catch (err) {
636
+ exitOnError(err);
637
+ }
638
+ })
639
+ .addHelpText('after', `
640
+ The "name" values are what \`jirallm create/edit --components\` expects.
641
+
642
+ Examples:
643
+ $ jirallm components -o acme -P PROJ
644
+ $ jirallm components -o acme -P PROJ --json
645
+ `);
646
+ program
647
+ .command('fields')
648
+ .description('List custom fields (and select options with --type) for use with --field.')
649
+ .requiredOption('-o, --org <name>', 'Organization name')
650
+ .option('-P, --project <key>', 'Project key (defaults to org default)')
651
+ .option('-t, --type <issueType>', 'Show create-screen fields + allowed values for this issue type')
652
+ .option('--json', 'Output JSON instead of human-readable')
653
+ .action(async (opts) => {
654
+ try {
655
+ await runFields(opts);
656
+ }
657
+ catch (err) {
658
+ exitOnError(err);
659
+ }
660
+ })
661
+ .addHelpText('after', `
662
+ The [customfield_NNNNN] ids are what \`jirallm create/edit --field\` expects.
663
+ Add --type Bug to see valid select options (e.g. for Severity, Environment).
664
+
665
+ Examples:
666
+ $ jirallm fields -o acme -P PROJ
667
+ $ jirallm fields -o acme -P PROJ --type Bug
668
+ $ jirallm fields -o acme -P PROJ --type Bug --json
669
+ `);
670
+ program
671
+ .command('linktypes')
672
+ .description('List issue link types available in an org.')
673
+ .requiredOption('-o, --org <name>', 'Organization name')
674
+ .option('--json', 'Output JSON instead of human-readable')
675
+ .action(async (opts) => {
676
+ try {
677
+ await runLinkTypes(opts);
678
+ }
679
+ catch (err) {
680
+ exitOnError(err);
681
+ }
682
+ })
683
+ .addHelpText('after', `
684
+ The "name" column is what \`jirallm link\` expects as <type> (e.g. "Blocks", "Relates").
685
+
686
+ Examples:
687
+ $ jirallm linktypes -o acme
688
+ $ jirallm linktypes -o acme --json
689
+ `);
690
+ program
691
+ .command('me')
692
+ .description('Show the currently authenticated Jira user.')
693
+ .requiredOption('-o, --org <name>', 'Organization name')
694
+ .option('--json', 'Output JSON instead of human-readable')
695
+ .action(async (opts) => {
696
+ try {
697
+ await runMe(opts);
698
+ }
699
+ catch (err) {
700
+ exitOnError(err);
701
+ }
702
+ })
703
+ .addHelpText('after', `
704
+ Returns the accountId — use it as --assignee for \`create\` / \`edit\` / \`assign\`,
705
+ or pass "me" as a shorthand in commands that resolve it (\`assign\`, \`watchers\`).
706
+
707
+ Examples:
708
+ $ jirallm me -o acme
709
+ $ jirallm me -o acme --json | jq -r .accountId
710
+ `);
711
+ program
712
+ .command('fetch <issue-key>')
713
+ .description('Fetch a single issue as JSON or pretty text (no file output).')
714
+ .option('-o, --org <name>', 'Organization name override')
715
+ .option('--json', 'Output JSON instead of human-readable')
716
+ .action(async (issueKey, opts) => {
717
+ try {
718
+ await runFetch({ issueKey, ...opts });
719
+ }
720
+ catch (err) {
721
+ exitOnError(err);
722
+ }
723
+ })
724
+ .addHelpText('after', `
725
+ For the full export bundle (attachments, video frames, on-disk folder),
726
+ use the default \`jirallm <key>\` command instead.
727
+
728
+ Examples:
729
+ $ jirallm fetch PROJ-123 --json
730
+ $ jirallm fetch acme/PROJ-123
731
+ $ jirallm fetch PROJ-123 --json | jq .status
732
+ `);
733
+ program
734
+ .command('create')
735
+ .description('Create a new Jira issue.')
736
+ .requiredOption('-o, --org <name>', 'Organization name')
737
+ .option('-P, --project <key>', 'Project key (defaults to org default)')
738
+ .requiredOption('-t, --type <type>', 'Issue type name (e.g. Task, Bug, Story)')
739
+ .requiredOption('-s, --summary <text>', 'Issue summary')
740
+ .option('-d, --description <text>', 'Description (markdown)')
741
+ .option('--description-file <path>', 'Read description (markdown) from a file')
742
+ .option('-a, --assignee <accountId>', 'Assignee accountId')
743
+ .option('-l, --labels <list>', 'Comma-separated labels')
744
+ .option('--priority <name>', 'Priority name (e.g. High)')
745
+ .option('--parent <key>', 'Parent issue key (for subtasks / epic children)')
746
+ .option('--components <names>', 'Comma-separated component names')
747
+ .option('-F, --field <pair>', 'Set a custom field: friendlyName=value or customfield_NNNNN[:type]=value (repeatable)', collect)
748
+ .option('--dry-run', 'Show what would be created without calling Jira')
749
+ .option('--json', 'Output JSON instead of human-readable')
750
+ .action(async (opts) => {
751
+ try {
752
+ await runCreate({
753
+ org: opts.org,
754
+ projectKey: opts.project,
755
+ type: opts.type,
756
+ summary: opts.summary,
757
+ description: opts.description,
758
+ descriptionFile: opts.descriptionFile,
759
+ assignee: opts.assignee,
760
+ labels: opts.labels,
761
+ priority: opts.priority,
762
+ parent: opts.parent,
763
+ components: opts.components,
764
+ field: opts.field,
765
+ dryRun: opts.dryRun,
766
+ json: opts.json,
767
+ });
768
+ }
769
+ catch (err) {
770
+ exitOnError(err);
771
+ }
772
+ })
773
+ .addHelpText('after', `
774
+ Run \`jirallm issuetypes -o <org>\` to discover valid --type values.
775
+ Run \`jirallm components -o <org> -P <project>\` to discover valid --components names.
776
+ Run \`jirallm fields -o <org> -P <project> --type Bug\` to discover custom field ids and select options.
777
+ Get a user's accountId via \`jirallm me -o <org>\` (for the current user).
778
+ --description is markdown; it gets converted to Jira wiki on the way in.
779
+
780
+ Custom fields:
781
+ Friendly names defined under [orgs.<org>.export.custom_fields] resolve automatically
782
+ (e.g. --field severity=High shapes to the right payload from the configured type).
783
+ For ad-hoc fields use the raw id with an optional type: --field customfield_10050:select=High.
784
+
785
+ Examples:
786
+ $ jirallm create -o acme -t Task -s "Investigate flaky test"
787
+ $ jirallm create -o acme -P PROJ -t Bug -s "Crash on save" --description-file ./repro.md
788
+ $ jirallm create -o acme -t Story -s "Spike X" -l backend,p1 --priority High
789
+ $ jirallm create -o acme -t Sub-task -s "Subtask of PROJ-1" --parent PROJ-1
790
+ $ jirallm create -o acme -t Bug -s "Crash" --components Web,API --field severity=High --field environment=PROD
791
+ $ jirallm create -o acme -t Bug -s "test" --dry-run --json
792
+ `);
793
+ program
794
+ .command('edit <issue-key>')
795
+ .description('Edit fields on an existing Jira issue.')
796
+ .option('-o, --org <name>', 'Organization name override')
797
+ .option('-s, --summary <text>', 'New summary')
798
+ .option('-d, --description <text>', 'New description (markdown)')
799
+ .option('--description-file <path>', 'Read description (markdown) from a file')
800
+ .option('-a, --assignee <accountId>', 'Assignee accountId')
801
+ .option('--unassign', 'Unassign the issue (clears assignee)')
802
+ .option('-l, --labels <list>', 'Comma-separated labels (replaces existing)')
803
+ .option('--priority <name>', 'Priority name')
804
+ .option('--components <names>', 'Comma-separated component names (replaces existing)')
805
+ .option('-F, --field <pair>', 'Set a custom field: friendlyName=value or customfield_NNNNN[:type]=value (repeatable)', collect)
806
+ .option('--dry-run', 'Show what would change without calling Jira')
807
+ .option('--json', 'Output JSON instead of human-readable')
808
+ .action(async (issueKey, opts) => {
809
+ try {
810
+ await runEdit({ issueKey, ...opts });
811
+ }
812
+ catch (err) {
813
+ exitOnError(err);
814
+ }
815
+ })
816
+ .addHelpText('after', `
817
+ --labels and --components REPLACE the existing sets; there is no add/remove syntax here.
818
+ --unassign and --assignee are mutually exclusive (use one).
819
+ Custom fields work like \`create\`: --field friendlyName=value or --field customfield_NNNNN[:type]=value.
820
+
821
+ Examples:
822
+ $ jirallm edit PROJ-123 --summary "New title"
823
+ $ jirallm edit PROJ-123 --description-file ./updated.md
824
+ $ jirallm edit PROJ-123 --labels backend,p1 --priority High
825
+ $ jirallm edit PROJ-123 --components Web,API --field reproductionRate=Always
826
+ $ jirallm edit PROJ-123 --assignee 5ac1234567890abcdef
827
+ $ jirallm edit PROJ-123 --unassign --dry-run --json
828
+ `);
829
+ program
830
+ .command('assign <issue-key> <assignee>')
831
+ .description('Assign an issue. Use "me" for the current user, "none" to unassign.')
832
+ .option('-o, --org <name>', 'Organization name override')
833
+ .option('--dry-run', 'Show what would change without calling Jira')
834
+ .option('--json', 'Output JSON instead of human-readable')
835
+ .action(async (issueKey, assignee, opts) => {
836
+ try {
837
+ await runAssign({ issueKey, assignee, ...opts });
838
+ }
839
+ catch (err) {
840
+ exitOnError(err);
841
+ }
842
+ })
843
+ .addHelpText('after', `
844
+ <assignee> shortcuts: "me" (current user) or "none"/"-" (unassign).
845
+ Any other value is treated as a Jira accountId.
846
+
847
+ Examples:
848
+ $ jirallm assign PROJ-123 me
849
+ $ jirallm assign PROJ-123 none
850
+ $ jirallm assign PROJ-123 5ac1234567890abcdef
851
+ $ jirallm assign PROJ-123 me --dry-run --json
852
+ `);
853
+ program
854
+ .command('link <inward-key> <type> <outward-key>')
855
+ .description('Create an issue link (e.g. `link FOO-1 "blocks" FOO-2`).')
856
+ .option('-c, --comment <text>', 'Add a comment when linking')
857
+ .option('-o, --org <name>', 'Organization name override')
858
+ .option('--dry-run', 'Show what would be created without calling Jira')
859
+ .option('--json', 'Output JSON instead of human-readable')
860
+ .action(async (inwardKey, type, outwardKey, opts) => {
861
+ try {
862
+ await runLink({ inwardKey, type, outwardKey, ...opts });
863
+ }
864
+ catch (err) {
865
+ exitOnError(err);
866
+ }
867
+ })
868
+ .addHelpText('after', `
869
+ <type> is the link-type NAME (e.g. "Blocks", "Relates", "Duplicate"), not the inward/outward label.
870
+ Discover the available types with: \`jirallm linktypes -o <org>\`.
871
+ --comment is markdown; it gets converted to Jira wiki.
872
+
873
+ Examples:
874
+ $ jirallm link PROJ-1 "Blocks" PROJ-2
875
+ $ jirallm link PROJ-1 "Relates" PROJ-2 --comment "see also **infra rewrite**"
876
+ $ jirallm link PROJ-1 "Blocks" PROJ-2 --dry-run --json
877
+ `);
878
+ program
879
+ .command('link:rm <link-id>')
880
+ .description('Remove an issue link by ID.')
881
+ .requiredOption('-o, --org <name>', 'Organization name')
882
+ .option('--dry-run', 'Show what would be deleted without calling Jira')
883
+ .option('--json', 'Output JSON instead of human-readable')
884
+ .action(async (linkId, opts) => {
885
+ try {
886
+ await runLinkRemove({ linkId, ...opts });
887
+ }
888
+ catch (err) {
889
+ exitOnError(err);
890
+ }
891
+ })
892
+ .addHelpText('after', `
893
+ Get link IDs from the issueLinks array in \`jirallm fetch <key> --json\`.
894
+
895
+ Examples:
896
+ $ jirallm link:rm 10042 -o acme
897
+ $ jirallm link:rm 10042 -o acme --dry-run --json
898
+ `);
899
+ program
900
+ .command('attach <issue-key> [files...]')
901
+ .description('Upload one or more file attachments to an issue. At least one file is required.')
902
+ .option('-o, --org <name>', 'Organization name override')
903
+ .option('--dry-run', 'Show what would be uploaded without calling Jira')
904
+ .option('--json', 'Output JSON instead of human-readable')
905
+ .action(async (issueKey, files, opts) => {
906
+ try {
907
+ if (!files || files.length === 0)
908
+ throw new Error('At least one file is required.');
909
+ await runAttach({ issueKey, files, ...opts });
910
+ }
911
+ catch (err) {
912
+ exitOnError(err);
913
+ }
914
+ })
915
+ .addHelpText('after', `
916
+ Files are uploaded sequentially; --json returns the aggregated attachments array.
917
+ The id from the response is what \`jirallm attach:rm\` takes.
918
+
919
+ Examples:
920
+ $ jirallm attach PROJ-123 ./screenshot.png
921
+ $ jirallm attach PROJ-123 ./a.png ./b.png ./recording.mp4 --json
922
+ $ jirallm attach PROJ-123 ./screenshot.png --dry-run
923
+ `);
924
+ program
925
+ .command('attach:rm <attachment-id>')
926
+ .description('Delete an attachment by ID.')
927
+ .requiredOption('-o, --org <name>', 'Organization name')
928
+ .option('--dry-run', 'Show what would be deleted without calling Jira')
929
+ .option('--json', 'Output JSON instead of human-readable')
930
+ .action(async (attachmentId, opts) => {
931
+ try {
932
+ await runAttachRemove({ attachmentId, ...opts });
933
+ }
934
+ catch (err) {
935
+ exitOnError(err);
936
+ }
937
+ })
938
+ .addHelpText('after', `
939
+ Get attachment IDs from the attachments array in \`jirallm fetch <key> --json\`.
940
+
941
+ Examples:
942
+ $ jirallm attach:rm 99021 -o acme
943
+ $ jirallm attach:rm 99021 -o acme --dry-run --json
944
+ `);
945
+ program
946
+ .command('watchers <issue-key>')
947
+ .description('List, add, or remove watchers on an issue.')
948
+ .option('-o, --org <name>', 'Organization name override')
949
+ .option('--add <accountId>', 'Add watcher by accountId (or "me")')
950
+ .option('--rm <accountId>', 'Remove watcher by accountId (or "me")')
951
+ .option('--dry-run', 'Show what would change without calling Jira')
952
+ .option('--json', 'Output JSON instead of human-readable')
953
+ .action(async (issueKey, opts) => {
954
+ try {
955
+ await runWatchers({ issueKey, ...opts });
956
+ }
957
+ catch (err) {
958
+ exitOnError(err);
959
+ }
960
+ })
961
+ .addHelpText('after', `
962
+ With no --add/--rm, just lists current watchers.
963
+ You can pass both --add and --rm in one call; both run before the final listing.
964
+ Pass "me" to add/remove yourself (resolved via \`/myself\`).
965
+
966
+ Examples:
967
+ $ jirallm watchers PROJ-123 --json
968
+ $ jirallm watchers PROJ-123 --add me
969
+ $ jirallm watchers PROJ-123 --rm 5ac1234567890abcdef
970
+ $ jirallm watchers PROJ-123 --add me --rm 5acOldUser --json
971
+ `);
262
972
  program.parseAsync(process.argv).catch((error) => {
263
973
  console.error('Unexpected error:', error);
264
974
  process.exit(1);