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