jirallm 0.5.0 → 0.6.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 (224) hide show
  1. package/dist/cli/commands/assign.d.ts.map +1 -1
  2. package/dist/cli/commands/assign.js +23 -21
  3. package/dist/cli/commands/assign.js.map +1 -1
  4. package/dist/cli/commands/assign.test.js +78 -13
  5. package/dist/cli/commands/assign.test.js.map +1 -1
  6. package/dist/cli/commands/attach.d.ts +3 -2
  7. package/dist/cli/commands/attach.d.ts.map +1 -1
  8. package/dist/cli/commands/attach.js +110 -14
  9. package/dist/cli/commands/attach.js.map +1 -1
  10. package/dist/cli/commands/attach.test.js +135 -10
  11. package/dist/cli/commands/attach.test.js.map +1 -1
  12. package/dist/cli/commands/auth.d.ts.map +1 -1
  13. package/dist/cli/commands/auth.js +5 -3
  14. package/dist/cli/commands/auth.js.map +1 -1
  15. package/dist/cli/commands/auth.test.d.ts +2 -0
  16. package/dist/cli/commands/auth.test.d.ts.map +1 -0
  17. package/dist/cli/commands/auth.test.js +191 -0
  18. package/dist/cli/commands/auth.test.js.map +1 -0
  19. package/dist/cli/commands/board.d.ts +1 -1
  20. package/dist/cli/commands/board.d.ts.map +1 -1
  21. package/dist/cli/commands/board.js +23 -7
  22. package/dist/cli/commands/board.js.map +1 -1
  23. package/dist/cli/commands/board.test.d.ts +2 -0
  24. package/dist/cli/commands/board.test.d.ts.map +1 -0
  25. package/dist/cli/commands/board.test.js +106 -0
  26. package/dist/cli/commands/board.test.js.map +1 -0
  27. package/dist/cli/commands/boards.d.ts +1 -1
  28. package/dist/cli/commands/boards.d.ts.map +1 -1
  29. package/dist/cli/commands/boards.js +19 -3
  30. package/dist/cli/commands/boards.js.map +1 -1
  31. package/dist/cli/commands/boards.test.js +78 -5
  32. package/dist/cli/commands/boards.test.js.map +1 -1
  33. package/dist/cli/commands/comment.d.ts +13 -3
  34. package/dist/cli/commands/comment.d.ts.map +1 -1
  35. package/dist/cli/commands/comment.js +97 -16
  36. package/dist/cli/commands/comment.js.map +1 -1
  37. package/dist/cli/commands/comment.test.d.ts +2 -0
  38. package/dist/cli/commands/comment.test.d.ts.map +1 -0
  39. package/dist/cli/commands/comment.test.js +170 -0
  40. package/dist/cli/commands/comment.test.js.map +1 -0
  41. package/dist/cli/commands/components.d.ts +1 -1
  42. package/dist/cli/commands/components.d.ts.map +1 -1
  43. package/dist/cli/commands/components.js +27 -15
  44. package/dist/cli/commands/components.js.map +1 -1
  45. package/dist/cli/commands/components.test.js +40 -5
  46. package/dist/cli/commands/components.test.js.map +1 -1
  47. package/dist/cli/commands/create.d.ts +1 -1
  48. package/dist/cli/commands/create.d.ts.map +1 -1
  49. package/dist/cli/commands/create.js +20 -7
  50. package/dist/cli/commands/create.js.map +1 -1
  51. package/dist/cli/commands/create.test.js +92 -13
  52. package/dist/cli/commands/create.test.js.map +1 -1
  53. package/dist/cli/commands/doctor.d.ts +2 -0
  54. package/dist/cli/commands/doctor.d.ts.map +1 -1
  55. package/dist/cli/commands/doctor.js +39 -11
  56. package/dist/cli/commands/doctor.js.map +1 -1
  57. package/dist/cli/commands/doctor.test.d.ts +2 -0
  58. package/dist/cli/commands/doctor.test.d.ts.map +1 -0
  59. package/dist/cli/commands/doctor.test.js +271 -0
  60. package/dist/cli/commands/doctor.test.js.map +1 -0
  61. package/dist/cli/commands/edit.d.ts.map +1 -1
  62. package/dist/cli/commands/edit.js +19 -6
  63. package/dist/cli/commands/edit.js.map +1 -1
  64. package/dist/cli/commands/edit.test.js +76 -3
  65. package/dist/cli/commands/edit.test.js.map +1 -1
  66. package/dist/cli/commands/fields.d.ts +1 -1
  67. package/dist/cli/commands/fields.d.ts.map +1 -1
  68. package/dist/cli/commands/fields.js +9 -3
  69. package/dist/cli/commands/fields.js.map +1 -1
  70. package/dist/cli/commands/fields.test.js +72 -7
  71. package/dist/cli/commands/fields.test.js.map +1 -1
  72. package/dist/cli/commands/init.d.ts +9 -1
  73. package/dist/cli/commands/init.d.ts.map +1 -1
  74. package/dist/cli/commands/init.js +107 -4
  75. package/dist/cli/commands/init.js.map +1 -1
  76. package/dist/cli/commands/init.test.d.ts +2 -0
  77. package/dist/cli/commands/init.test.d.ts.map +1 -0
  78. package/dist/cli/commands/init.test.js +272 -0
  79. package/dist/cli/commands/init.test.js.map +1 -0
  80. package/dist/cli/commands/issuetypes.d.ts +1 -1
  81. package/dist/cli/commands/issuetypes.d.ts.map +1 -1
  82. package/dist/cli/commands/issuetypes.js +5 -3
  83. package/dist/cli/commands/issuetypes.js.map +1 -1
  84. package/dist/cli/commands/issuetypes.test.js +39 -7
  85. package/dist/cli/commands/issuetypes.test.js.map +1 -1
  86. package/dist/cli/commands/link.d.ts +2 -1
  87. package/dist/cli/commands/link.d.ts.map +1 -1
  88. package/dist/cli/commands/link.js +116 -14
  89. package/dist/cli/commands/link.js.map +1 -1
  90. package/dist/cli/commands/link.test.js +178 -4
  91. package/dist/cli/commands/link.test.js.map +1 -1
  92. package/dist/cli/commands/linktypes.js +2 -2
  93. package/dist/cli/commands/linktypes.js.map +1 -1
  94. package/dist/cli/commands/linktypes.test.js +15 -4
  95. package/dist/cli/commands/linktypes.test.js.map +1 -1
  96. package/dist/cli/commands/me.js +2 -2
  97. package/dist/cli/commands/me.js.map +1 -1
  98. package/dist/cli/commands/me.test.js +20 -3
  99. package/dist/cli/commands/me.test.js.map +1 -1
  100. package/dist/cli/commands/orgs.d.ts +13 -5
  101. package/dist/cli/commands/orgs.d.ts.map +1 -1
  102. package/dist/cli/commands/orgs.js +68 -37
  103. package/dist/cli/commands/orgs.js.map +1 -1
  104. package/dist/cli/commands/orgs.test.d.ts +2 -0
  105. package/dist/cli/commands/orgs.test.d.ts.map +1 -0
  106. package/dist/cli/commands/orgs.test.js +230 -0
  107. package/dist/cli/commands/orgs.test.js.map +1 -0
  108. package/dist/cli/commands/projects.js +2 -2
  109. package/dist/cli/commands/projects.js.map +1 -1
  110. package/dist/cli/commands/projects.test.js +24 -3
  111. package/dist/cli/commands/projects.test.js.map +1 -1
  112. package/dist/cli/commands/search.d.ts +1 -1
  113. package/dist/cli/commands/search.d.ts.map +1 -1
  114. package/dist/cli/commands/search.js +4 -4
  115. package/dist/cli/commands/search.js.map +1 -1
  116. package/dist/cli/commands/search.test.js +28 -4
  117. package/dist/cli/commands/search.test.js.map +1 -1
  118. package/dist/cli/commands/setup.d.ts +2 -0
  119. package/dist/cli/commands/setup.d.ts.map +1 -1
  120. package/dist/cli/commands/setup.js +50 -4
  121. package/dist/cli/commands/setup.js.map +1 -1
  122. package/dist/cli/commands/setup.test.d.ts +2 -0
  123. package/dist/cli/commands/setup.test.d.ts.map +1 -0
  124. package/dist/cli/commands/setup.test.js +274 -0
  125. package/dist/cli/commands/setup.test.js.map +1 -0
  126. package/dist/cli/commands/sprints.js +2 -2
  127. package/dist/cli/commands/sprints.js.map +1 -1
  128. package/dist/cli/commands/sprints.test.js +17 -5
  129. package/dist/cli/commands/sprints.test.js.map +1 -1
  130. package/dist/cli/commands/transition.d.ts +1 -0
  131. package/dist/cli/commands/transition.d.ts.map +1 -1
  132. package/dist/cli/commands/transition.js +13 -16
  133. package/dist/cli/commands/transition.js.map +1 -1
  134. package/dist/cli/commands/transition.test.d.ts +2 -0
  135. package/dist/cli/commands/transition.test.d.ts.map +1 -0
  136. package/dist/cli/commands/transition.test.js +100 -0
  137. package/dist/cli/commands/transition.test.js.map +1 -0
  138. package/dist/cli/commands/upgrade.d.ts +1 -0
  139. package/dist/cli/commands/upgrade.d.ts.map +1 -1
  140. package/dist/cli/commands/upgrade.js +10 -5
  141. package/dist/cli/commands/upgrade.js.map +1 -1
  142. package/dist/cli/commands/upgrade.test.js +22 -0
  143. package/dist/cli/commands/upgrade.test.js.map +1 -1
  144. package/dist/cli/commands/watchers.d.ts.map +1 -1
  145. package/dist/cli/commands/watchers.js +17 -10
  146. package/dist/cli/commands/watchers.js.map +1 -1
  147. package/dist/cli/commands/watchers.test.js +69 -9
  148. package/dist/cli/commands/watchers.test.js.map +1 -1
  149. package/dist/cli/commands/worklog.d.ts +3 -0
  150. package/dist/cli/commands/worklog.d.ts.map +1 -1
  151. package/dist/cli/commands/worklog.js +63 -21
  152. package/dist/cli/commands/worklog.js.map +1 -1
  153. package/dist/cli/commands/worklog.test.js +54 -0
  154. package/dist/cli/commands/worklog.test.js.map +1 -1
  155. package/dist/cli/confirm.d.ts +7 -0
  156. package/dist/cli/confirm.d.ts.map +1 -0
  157. package/dist/cli/confirm.js +27 -0
  158. package/dist/cli/confirm.js.map +1 -0
  159. package/dist/cli/confirm.test.d.ts +2 -0
  160. package/dist/cli/confirm.test.d.ts.map +1 -0
  161. package/dist/cli/confirm.test.js +67 -0
  162. package/dist/cli/confirm.test.js.map +1 -0
  163. package/dist/cli/index.d.ts +2 -1
  164. package/dist/cli/index.d.ts.map +1 -1
  165. package/dist/cli/index.js +701 -624
  166. package/dist/cli/index.js.map +1 -1
  167. package/dist/cli/index.test.d.ts +2 -0
  168. package/dist/cli/index.test.d.ts.map +1 -0
  169. package/dist/cli/index.test.js +490 -0
  170. package/dist/cli/index.test.js.map +1 -0
  171. package/dist/cli/resolveOrg.d.ts +1 -0
  172. package/dist/cli/resolveOrg.d.ts.map +1 -1
  173. package/dist/cli/resolveOrg.js +36 -1
  174. package/dist/cli/resolveOrg.js.map +1 -1
  175. package/dist/cli/resolveOrg.test.d.ts +2 -0
  176. package/dist/cli/resolveOrg.test.d.ts.map +1 -0
  177. package/dist/cli/resolveOrg.test.js +76 -0
  178. package/dist/cli/resolveOrg.test.js.map +1 -0
  179. package/dist/cli/resolveUser.d.ts +28 -0
  180. package/dist/cli/resolveUser.d.ts.map +1 -0
  181. package/dist/cli/resolveUser.js +60 -0
  182. package/dist/cli/resolveUser.js.map +1 -0
  183. package/dist/cli/resolveUser.test.d.ts +2 -0
  184. package/dist/cli/resolveUser.test.d.ts.map +1 -0
  185. package/dist/cli/resolveUser.test.js +91 -0
  186. package/dist/cli/resolveUser.test.js.map +1 -0
  187. package/dist/lib/config.d.ts +10 -0
  188. package/dist/lib/config.d.ts.map +1 -1
  189. package/dist/lib/config.js +28 -4
  190. package/dist/lib/config.js.map +1 -1
  191. package/dist/lib/config.test.js +114 -3
  192. package/dist/lib/config.test.js.map +1 -1
  193. package/dist/lib/credentials.d.ts +1 -0
  194. package/dist/lib/credentials.d.ts.map +1 -1
  195. package/dist/lib/credentials.js +22 -1
  196. package/dist/lib/credentials.js.map +1 -1
  197. package/dist/lib/credentials.test.js +52 -1
  198. package/dist/lib/credentials.test.js.map +1 -1
  199. package/dist/lib/exporter.test.js +58 -3
  200. package/dist/lib/exporter.test.js.map +1 -1
  201. package/dist/lib/jiraClient.d.ts +35 -3
  202. package/dist/lib/jiraClient.d.ts.map +1 -1
  203. package/dist/lib/jiraClient.discovery.test.js +169 -0
  204. package/dist/lib/jiraClient.discovery.test.js.map +1 -1
  205. package/dist/lib/jiraClient.js +50 -5
  206. package/dist/lib/jiraClient.js.map +1 -1
  207. package/dist/lib/jiraClient.test.js +322 -2
  208. package/dist/lib/jiraClient.test.js.map +1 -1
  209. package/dist/lib/jiraClient.write.test.js +284 -1
  210. package/dist/lib/jiraClient.write.test.js.map +1 -1
  211. package/dist/lib/platform.test.d.ts +2 -0
  212. package/dist/lib/platform.test.d.ts.map +1 -0
  213. package/dist/lib/platform.test.js +161 -0
  214. package/dist/lib/platform.test.js.map +1 -0
  215. package/dist/lib/runCommand.test.d.ts +2 -0
  216. package/dist/lib/runCommand.test.d.ts.map +1 -0
  217. package/dist/lib/runCommand.test.js +95 -0
  218. package/dist/lib/runCommand.test.js.map +1 -0
  219. package/dist/lib/worklog.d.ts.map +1 -1
  220. package/dist/lib/worklog.js +3 -2
  221. package/dist/lib/worklog.js.map +1 -1
  222. package/dist/lib/worklog.test.js +11 -0
  223. package/dist/lib/worklog.test.js.map +1 -1
  224. package/package.json +3 -1
package/dist/cli/index.js CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { Command } from 'commander';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { Command, Option } from 'commander';
4
5
  import updateNotifier from 'update-notifier';
5
- import { select, isCancel, cancel } from '@clack/prompts';
6
6
  import { JiraExporter } from '../lib/exporter.js';
7
- import { findOrgsByProjectKey, loadProfile, readConfig } from '../lib/config.js';
7
+ import { listOrgs, loadProfile } from '../lib/config.js';
8
+ import { resolveOrgInteractive } from './resolveOrg.js';
8
9
  import { parseIssueKeyArgs } from './issueKey.js';
9
10
  import { runInit } from './commands/init.js';
10
11
  import { runAuthSet, runAuthRm, runAuthList, runAuthStatus } from './commands/auth.js';
11
12
  import { runOrgsList, runOrgsRemove, runProjectRemove } from './commands/orgs.js';
12
13
  import { runDoctor } from './commands/doctor.js';
13
14
  import { runSetup } from './commands/setup.js';
14
- import { runComment, runDeleteComment } from './commands/comment.js';
15
+ import { runComment, runCommentList, runDeleteComment } from './commands/comment.js';
15
16
  import { runBoardIssues } from './commands/board.js';
16
17
  import { runTransition } from './commands/transition.js';
17
18
  import { runWorklog } from './commands/worklog.js';
@@ -33,22 +34,6 @@ import { runAttach, runAttachRemove } from './commands/attach.js';
33
34
  import { runWatchers } from './commands/watchers.js';
34
35
  import { runUpgrade } from './commands/upgrade.js';
35
36
  import { parseFieldsFlag, resolveFieldSet } from '../lib/exportFields.js';
36
- async function pickOrgInteractively(candidates, projectKey) {
37
- const raw = readConfig();
38
- const choice = await select({
39
- message: `Multiple orgs have a "${projectKey}" project. Which one?`,
40
- options: candidates.map((name) => ({
41
- value: name,
42
- label: name,
43
- hint: raw.orgs?.[name]?.base_url,
44
- })),
45
- });
46
- if (isCancel(choice)) {
47
- cancel('Cancelled.');
48
- process.exit(0);
49
- }
50
- return choice;
51
- }
52
37
  async function resolveOrgAndKeys(rawArgs, flags) {
53
38
  const parsed = parseIssueKeyArgs(rawArgs);
54
39
  const keys = parsed.keys;
@@ -56,25 +41,15 @@ async function resolveOrgAndKeys(rawArgs, flags) {
56
41
  throw new Error(`--org "${flags.org}" conflicts with the org/ prefix "${parsed.org}/" in the issue keys.`);
57
42
  }
58
43
  const projectKey = flags.project ?? parsed.projectKey;
59
- const explicitOrg = flags.org ?? parsed.org;
60
- if (explicitOrg)
61
- return { org: explicitOrg, projectKey, keys };
62
- const matches = findOrgsByProjectKey(parsed.projectKey);
63
- if (matches.length === 0) {
64
- throw new Error(`Project "${parsed.projectKey}" not found in any configured org. ` +
65
- 'Run `jirallm init` to add it, or pass --org explicitly.');
66
- }
67
- if (matches.length === 1)
68
- return { org: matches[0], projectKey, keys };
69
- if (!process.stdin.isTTY) {
70
- throw new Error(`Project "${parsed.projectKey}" exists in multiple orgs (${matches.join(', ')}). ` +
71
- `Pass --org or use the org/${parsed.projectKey}-N syntax.`);
72
- }
73
- const org = await pickOrgInteractively(matches, parsed.projectKey);
44
+ const org = await resolveOrgInteractive(parsed.org, flags.org, parsed.projectKey);
74
45
  return { org, projectKey, keys };
75
46
  }
76
- async function runExport(rawArgs, flags) {
47
+ async function runExport(rawArgs, flags, program) {
77
48
  if (rawArgs.length === 0) {
49
+ if (listOrgs().length > 0) {
50
+ program.outputHelp();
51
+ process.exit(0);
52
+ }
78
53
  await runInit();
79
54
  process.exit(0);
80
55
  }
@@ -146,6 +121,10 @@ async function runExport(rawArgs, flags) {
146
121
  console.log(`Project: ${projectKey}`);
147
122
  console.log(`Base URL: ${baseUrl}`);
148
123
  console.log(`Output dir: ${outputDir}`);
124
+ console.log(`Include subtasks: ${includeSubtasks}`);
125
+ console.log(`With history: ${flags.withHistory ?? false}`);
126
+ console.log(`With worklog: ${flags.withWorklog ?? false}`);
127
+ console.log(`Video frames: ${videoEnabled ? `enabled (fps=${fps}, maxFrames=${maxFrames})` : 'disabled'}`);
149
128
  console.log(`Issues (${keys.length}): ${keys.join(', ')}`);
150
129
  console.log(`Field preset: ${fieldSelector?.preset ?? '(default)'}`);
151
130
  if (fieldSelector?.include?.length)
@@ -194,49 +173,53 @@ async function runExport(rawArgs, flags) {
194
173
  process.exit(1);
195
174
  }
196
175
  }
197
- const program = new Command();
198
- program.enablePositionalOptions();
199
- const pkg = createRequire(import.meta.url)('../../package.json');
200
- program.name(pkg.name).description(pkg.description).version(pkg.version);
201
- const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
202
- if (!process.argv.includes('--json')) {
203
- notifier.notify({
204
- defer: true,
205
- isGlobal: true,
206
- message: `Update available {currentVersion} → {latestVersion}\nRun \`${pkg.name} upgrade\` to update.`,
207
- });
208
- }
209
- function addExportOptions(cmd) {
210
- return cmd
211
- .option('-o, --org <name>', 'Organization name from config (auto-resolved from issue prefix if unique)')
212
- .option('-P, --project <key>', 'Project key override')
213
- .option('--output-dir <path>', 'Output directory (default: ./jira-export)')
214
- .option('--base-url <url>', 'Jira base URL (overrides config)')
215
- .option('--user-email <email>', 'Jira user email (overrides config)')
216
- .option('--api-token <token>', 'Jira API token (overrides keychain)')
217
- .option('--no-video-frames', 'Disable video frame extraction')
218
- .option('--fps <n>', 'Frame extraction FPS')
219
- .option('--max-frames <n>', 'Max frames kept per video')
220
- .option('--include-subtasks', 'Fetch and include subtask metadata')
221
- .option('--fields <list>', 'Comma-separated friendly field names to include in frontmatter. Use +name/-name to add/remove, or "all" | "default" | "minimal".')
222
- .option('--with-history', 'Include full field-change history (not just status changes)')
223
- .option('--with-worklog', 'Include worklogs')
224
- .option('--dry-run', 'Resolve config & print plan without calling Jira or writing files');
225
- }
226
- addExportOptions(program.argument('[issue-keys...]', 'Jira issue keys, e.g. PROJ-123 or acme/PROJ-123'))
227
- .action(async (keys, flags) => {
228
- await runExport(keys, flags);
229
- })
230
- .addHelpText('after', `
176
+ export function buildProgram() {
177
+ const program = new Command();
178
+ program.enablePositionalOptions();
179
+ const pkg = createRequire(import.meta.url)('../../package.json');
180
+ program.name(pkg.name).description(pkg.description).version(pkg.version);
181
+ const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
182
+ if (!process.argv.includes('--json')) {
183
+ notifier.notify({
184
+ defer: true,
185
+ isGlobal: true,
186
+ message: `Update available {currentVersion} → {latestVersion}\nRun \`${pkg.name} upgrade\` to update.`,
187
+ });
188
+ }
189
+ function addExportOptions(cmd) {
190
+ return cmd
191
+ .option('-o, --org <name>', 'Organization name from config (auto-resolved from issue prefix if unique)')
192
+ .option('-P, --project <key>', 'Project key override')
193
+ .option('--output-dir <path>', 'Output directory (default: ./jira-export)')
194
+ .option('--base-url <url>', 'Jira base URL (overrides config)')
195
+ .option('--user-email <email>', 'Jira user email (overrides config)')
196
+ .option('--api-token <token>', 'Jira API token (overrides keychain)')
197
+ .option('--no-video-frames', 'Disable video frame extraction')
198
+ .option('--fps <n>', 'Frame extraction FPS')
199
+ .option('--max-frames <n>', 'Max frames kept per video')
200
+ .option('--include-subtasks', 'Fetch subtask metadata (may already be enabled via org config).')
201
+ .option('--fields <list>', 'Comma-separated friendly field names to include in frontmatter. Use +name/-name to add/remove, or "all" | "default" | "minimal".')
202
+ .option('--with-history', 'Include full field-change history (not just status changes)')
203
+ .option('--with-worklog', 'Include worklogs')
204
+ .option('--dry-run', 'Resolve config & print plan without calling Jira or writing files');
205
+ }
206
+ addExportOptions(program.argument('[issue-keys...]', 'Jira issue keys, e.g. PROJ-123 or acme/PROJ-123'))
207
+ .action(async (keys, flags) => {
208
+ await runExport(keys, flags, program);
209
+ })
210
+ .addHelpText('after', `
211
+ Read-command output auto-switches to JSON when piped (stdout is not a TTY);
212
+ pass --json to force it explicitly.
213
+
231
214
  Examples:
232
215
  $ jirallm init
233
216
  $ jirallm PROJ-123 # auto-resolves org from PROJ prefix
234
217
  $ jirallm acme/PROJ-123 # disambiguate when multiple orgs have PROJ
218
+ $ jirallm --org acme PROJ-123 # or pass --org when multiple orgs have PROJ
235
219
  $ jirallm PROJ-123 PROJ-124 --output-dir ./context
236
220
  $ jirallm PROJ-123 --no-video-frames
237
221
  $ jirallm PROJ-123 --fps 2 --max-frames 6
238
222
  $ jirallm PROJ-123 --include-subtasks
239
- $ jirallm --org acme PROJ-123
240
223
  $ jirallm export PROJ-123
241
224
  $ jirallm fetch PROJ-123 --with-comments --with-history
242
225
  $ jirallm fetch PROJ-123 --full
@@ -246,235 +229,277 @@ Examples:
246
229
 
247
230
  Run \`jirallm <command> --help\` for command-specific options.
248
231
  `);
249
- addExportOptions(program.command('export [issue-keys...]'))
250
- .description('Export issues as LLM-ready context bundles (same as the default command).')
251
- .action(async (keys, flags) => {
252
- await runExport(keys, flags);
253
- });
254
- program
255
- .command('init')
256
- .description('Interactive setup wizard — creates an org or adds a project, and stores the API token in your OS keychain')
257
- .action(async () => {
258
- await runInit();
259
- })
260
- .addHelpText('after', '\nExample:\n $ jirallm init\n');
261
- const auth = program.command('auth').description('Manage stored API tokens (per organization)');
262
- auth
263
- .command('set')
264
- .description('Store/replace the API token for an organization')
265
- .requiredOption('-o, --org <name>', 'Organization name')
266
- .action(async (opts) => {
267
- await runAuthSet(opts.org);
268
- });
269
- auth
270
- .command('rm')
271
- .description('Remove the stored API token for an organization')
272
- .requiredOption('-o, --org <name>', 'Organization name')
273
- .option('-y, --yes', 'Skip confirmation prompt')
274
- .action(async (opts) => {
275
- await runAuthRm(opts.org, { yes: opts.yes });
276
- });
277
- auth
278
- .command('list')
279
- .description('List all orgs and the resolved state of their API tokens')
280
- .action(async () => {
281
- await runAuthList();
282
- });
283
- auth
284
- .command('status')
285
- .description('Show whether a token is resolvable for an org (exits non-zero if missing)')
286
- .requiredOption('-o, --org <name>', 'Organization name')
287
- .action(async (opts) => {
288
- await runAuthStatus(opts.org);
289
- });
290
- auth.addHelpText('after', `
232
+ addExportOptions(program.command('export [issue-keys...]'))
233
+ .description('Export issues as LLM-ready context bundles (same as the default command).')
234
+ .action(async (keys, flags) => {
235
+ await runExport(keys, flags, program);
236
+ });
237
+ program
238
+ .command('init')
239
+ .description('Interactive setup wizard — creates an org or adds a project, and stores the API token in your OS keychain')
240
+ .option('--org <name>', 'Organization name (non-interactive)')
241
+ .option('--base-url <url>', 'Jira base URL (non-interactive)')
242
+ .option('--email <email>', 'Jira user email (non-interactive)')
243
+ .option('--project <key>', 'Project key (non-interactive)')
244
+ .option('--token-stdin', 'Read the API token from stdin (non-interactive)')
245
+ .option('-y, --yes', 'Skip confirmation prompts (non-interactive)')
246
+ .action(async (opts) => {
247
+ await runInit(opts);
248
+ })
249
+ .addHelpText('after', `
250
+ A token stored in your OS keychain is preferred. As a fallback, the API token may be
251
+ supplied via the JIRALLM_API_TOKEN environment variable (or JIRALLM_API_TOKEN_<ORG>
252
+ for a specific org) when no keychain token is stored.
253
+
254
+ Examples:
255
+ $ jirallm init
256
+ $ jirallm init --org acme --base-url https://acme.atlassian.net --email me@acme.com --project PROJ --token-stdin --yes
257
+ `);
258
+ const auth = program.command('auth').description('Manage stored API tokens (per organization)');
259
+ auth
260
+ .command('set')
261
+ .description('Store/replace the API token for an organization')
262
+ .requiredOption('-o, --org <name>', 'Organization name')
263
+ .action(async (opts) => {
264
+ await runAuthSet(opts.org);
265
+ });
266
+ auth
267
+ .command('rm')
268
+ .description('Remove the stored API token for an organization')
269
+ .requiredOption('-o, --org <name>', 'Organization name')
270
+ .option('-y, --yes', 'Skip confirmation prompt')
271
+ .action(async (opts) => {
272
+ await runAuthRm(opts.org, { yes: opts.yes });
273
+ });
274
+ auth
275
+ .command('list')
276
+ .description('List all orgs and the resolved state of their API tokens')
277
+ .action(async () => {
278
+ await runAuthList();
279
+ });
280
+ auth
281
+ .command('status')
282
+ .description('Show whether a token is resolvable for an org (exits non-zero if missing)')
283
+ .requiredOption('-o, --org <name>', 'Organization name')
284
+ .action(async (opts) => {
285
+ await runAuthStatus(opts.org);
286
+ });
287
+ auth.addHelpText('after', `
288
+ Tokens are stored in your OS keychain. As a fallback, an API token may be supplied via
289
+ the JIRALLM_API_TOKEN environment variable (or JIRALLM_API_TOKEN_<ORG> for a specific
290
+ org) when no keychain token is stored; \`auth status\`/\`auth list\` report which source is used.
291
+
291
292
  Examples:
292
293
  $ jirallm auth list
293
294
  $ jirallm auth status --org acme
294
295
  $ jirallm auth set --org acme
295
296
  $ jirallm auth rm --org acme --yes
296
297
  `);
297
- const orgs = program.command('orgs').description('Inspect or modify organizations and their projects');
298
- orgs
299
- .command('list')
300
- .description('List all configured organizations and projects')
301
- .action(async () => {
302
- await runOrgsList();
303
- });
304
- orgs
305
- .command('rm')
306
- .description('Remove an organization (and its keychain token) along with all its projects')
307
- .requiredOption('-o, --org <name>', 'Organization name')
308
- .option('-y, --yes', 'Skip confirmation prompt')
309
- .action(async (opts) => {
310
- await runOrgsRemove(opts.org, { yes: opts.yes });
311
- });
312
- const orgProject = orgs.command('project').description('Manage projects within an organization');
313
- orgProject
314
- .command('rm')
315
- .description('Remove a project from an organization')
316
- .requiredOption('-o, --org <name>', 'Organization name')
317
- .requiredOption('-k, --key <key>', 'Project key')
318
- .option('-y, --yes', 'Skip confirmation prompt')
319
- .action(async (opts) => {
320
- await runProjectRemove(opts.org, opts.key, { yes: opts.yes });
321
- });
322
- orgs.addHelpText('after', `
298
+ const orgs = program.command('orgs').description('Inspect or modify organizations and their projects');
299
+ orgs
300
+ .command('list')
301
+ .description('List all configured organizations and projects')
302
+ .option('--json', 'Output JSON instead of human-readable')
303
+ .action(async (opts) => {
304
+ await runOrgsList(opts);
305
+ });
306
+ orgs
307
+ .command('rm [org]')
308
+ .description('Remove an organization (and its keychain token) along with all its projects')
309
+ .option('-o, --org <name>', 'Organization name (alias for the positional)')
310
+ .option('--dry-run', 'Show what would be removed without changing config')
311
+ .option('-y, --yes', 'Skip confirmation prompt')
312
+ .action(async (orgArg, opts) => {
313
+ const org = orgArg ?? opts.org;
314
+ if (!org)
315
+ throw new Error('Organization name is required (positional or --org).');
316
+ await runOrgsRemove(org, { yes: opts.yes, dryRun: opts.dryRun });
317
+ });
318
+ const orgProject = orgs.command('project').description('Manage projects within an organization');
319
+ orgProject
320
+ .command('rm')
321
+ .description('Remove a project from an organization')
322
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the project key if unique)')
323
+ .option('-P, --project <key>', 'Project key')
324
+ .addOption(new Option('-k, --key <key>', 'Project key (alias for --project)').hideHelp())
325
+ .option('-y, --yes', 'Skip confirmation prompt')
326
+ .action(async (opts) => {
327
+ const projectKey = opts.project ?? opts.key;
328
+ if (!projectKey)
329
+ throw new Error('Project key is required (-P/--project).');
330
+ await runProjectRemove(opts.org, projectKey, { yes: opts.yes });
331
+ });
332
+ orgs.addHelpText('after', `
323
333
  Examples:
324
334
  $ jirallm orgs list
325
- $ jirallm orgs rm --org acme
326
- $ jirallm orgs project rm --org acme --key PROJ
335
+ $ jirallm orgs rm acme
336
+ $ jirallm orgs rm --org acme --dry-run
337
+ $ jirallm orgs project rm -P PROJ
338
+ $ jirallm orgs project rm --org acme -P PROJ
327
339
  `);
328
- program
329
- .command('doctor')
330
- .description('Check that everything jirallm needs is installed and reachable')
331
- .option('-o, --org <name>', 'Organization to use for the Jira reachability check')
332
- .option('-P, --project <key>', 'Project key to use for the Jira reachability check')
333
- .action(async (opts) => {
334
- await runDoctor({ org: opts.org, project: opts.project });
335
- })
336
- .addHelpText('after', '\nExample:\n $ jirallm doctor --org acme\n');
337
- program
338
- .command('setup')
339
- .description('Install missing system dependencies (currently: ffmpeg) with cascading consent')
340
- .option('--bundled', 'Install ffmpeg-static globally instead of system ffmpeg')
341
- .option('-y, --yes', 'Auto-confirm package-manager install (does NOT auto-confirm Homebrew install)')
342
- .action(async (opts) => {
343
- await runSetup({ bundled: opts.bundled, yes: opts.yes });
344
- })
345
- .addHelpText('after', `
340
+ program
341
+ .command('doctor')
342
+ .description('Check that everything jirallm needs is installed and reachable')
343
+ .option('-o, --org <name>', 'Organization to use for the Jira reachability check')
344
+ .option('-P, --project <key>', 'Project key to use for the Jira reachability check')
345
+ .option('--strict', 'Exit non-zero on warnings as well as failures')
346
+ .option('--json', 'Output JSON instead of human-readable')
347
+ .action(async (opts) => {
348
+ await runDoctor({ org: opts.org, project: opts.project, strict: opts.strict, json: opts.json });
349
+ })
350
+ .addHelpText('after', '\nExamples:\n $ jirallm doctor --org acme\n $ jirallm doctor --org acme --strict --json\n');
351
+ program
352
+ .command('setup')
353
+ .description('Install missing system dependencies (currently: ffmpeg) with cascading consent')
354
+ .option('--bundled', 'Install ffmpeg-static globally instead of system ffmpeg')
355
+ .option('-y, --yes', 'Auto-confirm package-manager install (does NOT auto-confirm Homebrew install)')
356
+ .option('--allow-brew', 'Also auto-confirm the Homebrew install step')
357
+ .option('--dry-run', 'Show what would be installed without running any installer')
358
+ .action(async (opts) => {
359
+ await runSetup({ bundled: opts.bundled, yes: opts.yes, allowBrew: opts.allowBrew, dryRun: opts.dryRun });
360
+ })
361
+ .addHelpText('after', `
346
362
  Examples:
347
363
  $ jirallm setup
348
364
  $ jirallm setup --yes
349
365
  $ jirallm setup --bundled
366
+ $ jirallm setup --yes --allow-brew
367
+ $ jirallm setup --dry-run
350
368
  `);
351
- program
352
- .command('upgrade')
353
- .description('Upgrade jirallm to the latest version (auto-detects npm/pnpm/yarn/Homebrew)')
354
- .option('-y, --yes', 'Skip confirmation prompt')
355
- .option('--check', 'Print whether an update is available without installing (exits non-zero if outdated)')
356
- .action(async (opts) => {
357
- try {
358
- await runUpgrade(opts);
359
- }
360
- catch (err) {
361
- console.error(err.message);
362
- process.exit(1);
363
- }
364
- })
365
- .addHelpText('after', `
369
+ program
370
+ .command('upgrade')
371
+ .description('Upgrade jirallm to the latest version (auto-detects npm/pnpm/yarn/Homebrew)')
372
+ .option('-y, --yes', 'Skip confirmation prompt')
373
+ .option('--check', 'Print whether an update is available without installing (exits non-zero if outdated)')
374
+ .option('--json', 'Output JSON instead of human-readable')
375
+ .action(async (opts) => {
376
+ try {
377
+ await runUpgrade(opts);
378
+ }
379
+ catch (err) {
380
+ exitOnError(err);
381
+ }
382
+ })
383
+ .addHelpText('after', `
366
384
  Examples:
367
385
  $ jirallm upgrade
368
386
  $ jirallm upgrade --yes
369
387
  $ jirallm upgrade --check
370
388
  `);
371
- program
372
- .command('comment <issue-key>')
373
- .description('Post a (possibly multi-part) comment on a Jira issue. Markdown converted to Jira wiki by default.')
374
- .option('-f, --file <path>', 'Read comment body from a file')
375
- .option('-t, --text <text>', 'Inline comment text (alternative to --file / stdin)')
376
- .option('-o, --org <name>', 'Organization name override')
377
- .option('--max-chars <n>', 'Max chars per comment chunk (default 25000)')
378
- .option('--no-wiki', 'Skip markdown→wiki conversion (post body as-is)')
379
- .option('--reply-to <commentId>', 'Post as reply to an existing comment (threaded)')
380
- .option('--no-thread', 'When posting multiple chunks, do not chain them as replies')
381
- .option('--dry-run', 'Show what would be posted without calling Jira')
382
- .action(async (issueKey, opts) => {
383
- try {
384
- await runComment(issueKey, {
385
- file: opts.file,
386
- text: opts.text,
387
- org: opts.org,
388
- maxChars: opts.maxChars,
389
- noWiki: opts.wiki === false,
390
- noThread: opts.thread === false,
391
- replyTo: opts.replyTo,
392
- dryRun: opts.dryRun,
393
- });
394
- }
395
- catch (err) {
396
- console.error(err.message);
397
- process.exit(1);
398
- }
399
- })
400
- .addHelpText('after', `
389
+ program
390
+ .command('comment <issue-key>')
391
+ .description('Post a (possibly multi-part) comment on a Jira issue. Markdown converted to Jira wiki by default.')
392
+ .option('-f, --file <path>', 'Read comment body from a file')
393
+ .option('-t, --text <text>', 'Inline comment text (alternative to --file / stdin)')
394
+ .option('-o, --org <name>', 'Organization name override')
395
+ .option('--max-chars <n>', 'Max chars per comment chunk (default 25000)')
396
+ .option('--no-wiki', 'Skip markdown→wiki conversion (post body as-is)')
397
+ .option('--reply-to <commentId>', 'Post as reply to an existing comment (threaded)')
398
+ .option('--no-thread', 'When posting multiple chunks, do not chain them as replies')
399
+ .option('--dry-run', 'Show what would be posted without calling Jira')
400
+ .option('--json', 'Output JSON instead of human-readable')
401
+ .action(async (issueKey, opts) => {
402
+ try {
403
+ await runComment(issueKey, {
404
+ file: opts.file,
405
+ text: opts.text,
406
+ org: opts.org,
407
+ maxChars: opts.maxChars,
408
+ noWiki: opts.wiki === false,
409
+ noThread: opts.thread === false,
410
+ replyTo: opts.replyTo,
411
+ dryRun: opts.dryRun,
412
+ json: opts.json,
413
+ });
414
+ }
415
+ catch (err) {
416
+ exitOnError(err);
417
+ }
418
+ })
419
+ .addHelpText('after', `
401
420
  Examples:
402
- $ jirallm comment CN-2505 --file ./summary.md
403
- $ jirallm comment CN-2505 -t "Quick note"
404
- $ cat summary.md | jirallm comment CN-2505
405
- $ jirallm comment CN-2505 --file ./summary.md --dry-run
406
- $ jirallm comment CN-2505 --reply-to 26215 -t "follow-up"
421
+ $ jirallm comment PROJ-123 --file ./summary.md
422
+ $ jirallm comment PROJ-123 -t "Quick note"
423
+ $ cat summary.md | jirallm comment PROJ-123
424
+ $ jirallm comment PROJ-123 --file ./summary.md --dry-run
425
+ $ jirallm comment PROJ-123 --reply-to 10001 -t "follow-up"
407
426
  `);
408
- program
409
- .command('board:issues')
410
- .description('List issues in a board column. Output JSON with --json for piping into other tools.')
411
- .requiredOption('-b, --board <name>', 'Board name (exact match)')
412
- .requiredOption('-c, --column <name>', 'Column name (exact match)')
413
- .requiredOption('-o, --org <name>', 'Organization name from config')
414
- .option('-P, --project <key>', 'Project key override (defaults to the org\'s configured project)')
415
- .option('-a, --assignee <accountIdOrMe>', 'Filter by assignee. Use "me" for the current user.')
416
- .option('--json', 'Output JSON instead of human-readable')
417
- .action(async (opts) => {
418
- try {
419
- await runBoardIssues(opts);
420
- }
421
- catch (err) {
422
- console.error(err.message);
423
- process.exit(1);
424
- }
425
- })
426
- .addHelpText('after', `
427
+ program
428
+ .command('board:issues')
429
+ .description('List issues in a board column. Output JSON with --json for piping into other tools.')
430
+ .requiredOption('-b, --board <name>', 'Board name (case-insensitive match)')
431
+ .option('-c, --column <name>', 'Column name (case-insensitive match); lists all columns when omitted')
432
+ .requiredOption('-o, --org <name>', 'Organization name from config')
433
+ .option('-P, --project <key>', 'Project key override (auto-selected when the org has a single project)')
434
+ .option('-a, --assignee <accountIdOrMe>', 'Filter by assignee. Use "me" for the current user.')
435
+ .option('--json', 'Output JSON instead of human-readable')
436
+ .action(async (opts) => {
437
+ try {
438
+ await runBoardIssues(opts);
439
+ }
440
+ catch (err) {
441
+ exitOnError(err);
442
+ }
443
+ })
444
+ .addHelpText('after', `
427
445
  Examples:
428
446
  $ jirallm board:issues -o MyOrg -b "My Board" -c "In Review" -a me --json
429
447
  $ jirallm board:issues -o MyOrg -b "My Board" -c "Done"
430
448
  `);
431
- program
432
- .command('transition <issue-key>')
433
- .description('Transition a Jira issue to a target status (matches transition.to.name).')
434
- .option('-t, --to <status>', 'Target status name (e.g. "In Review")')
435
- .option('-o, --org <name>', 'Organization name override')
436
- .option('-l, --list', 'List available transitions for the issue instead of performing one')
437
- .option('--json', 'Output JSON instead of human-readable')
438
- .action(async (issueKey, opts) => {
439
- try {
440
- if (!opts.list && !opts.to) {
441
- throw new Error('Either --to <status> or --list is required.');
449
+ program
450
+ .command('transition <issue-key>')
451
+ .description('Transition a Jira issue to a target status (case-insensitive match on transition to.name, then name).')
452
+ .option('-t, --to <status>', 'Target status name (e.g. "In Review")')
453
+ .option('-o, --org <name>', 'Organization name override')
454
+ .option('-l, --list', 'List available transitions (matched case-insensitively by to.name then name) instead of performing one')
455
+ .option('--dry-run', 'Resolve the transition and print it without calling Jira')
456
+ .option('--json', 'Output JSON instead of human-readable')
457
+ .action(async (issueKey, opts) => {
458
+ try {
459
+ if (!opts.list && !opts.to) {
460
+ throw new Error('Either --to <status> or --list is required.');
461
+ }
462
+ await runTransition(issueKey, { to: opts.to ?? '', org: opts.org, list: opts.list, dryRun: opts.dryRun, json: opts.json });
442
463
  }
443
- await runTransition(issueKey, { to: opts.to ?? '', org: opts.org, list: opts.list, json: opts.json });
444
- }
445
- catch (err) {
446
- console.error(err.message);
447
- process.exit(1);
448
- }
449
- })
450
- .addHelpText('after', `
464
+ catch (err) {
465
+ exitOnError(err);
466
+ }
467
+ })
468
+ .addHelpText('after', `
451
469
  Examples:
452
470
  $ jirallm transition PROJ-123 --to "In Review"
471
+ $ jirallm transition PROJ-123 --to "In Review" --dry-run
453
472
  $ jirallm transition PROJ-123 --list
454
473
  `);
455
- program
456
- .command('worklog')
457
- .description('Batch log work to Jira from a JSON array (stdin or --file).')
458
- .option('-f, --file <path>', 'Read JSON array from a file (default: stdin)')
459
- .option('-o, --org <name>', 'Default org for entries without an org/ prefix or "org" field')
460
- .option('--no-wiki', 'Skip markdown→wiki conversion of description')
461
- .option('--dry-run', 'Validate and print what would be posted, without calling Jira')
462
- .action(async (opts) => {
463
- try {
464
- await runWorklog({
465
- file: opts.file,
466
- org: opts.org,
467
- noWiki: opts.wiki === false,
468
- dryRun: opts.dryRun,
469
- });
470
- }
471
- catch (err) {
472
- console.error(err.message);
473
- process.exit(1);
474
- }
475
- })
476
- .addHelpText('after', `
477
- JSON entry shape (any 2 of startTime/endTime/duration required):
474
+ program
475
+ .command('worklog [issue-key] [duration]')
476
+ .description('Log work to Jira. Quick form: `worklog PROJ-123 "1h 30m"`. Batch form: JSON array on stdin or --file.')
477
+ .option('-f, --file <path>', 'Read JSON array from a file (default: stdin)')
478
+ .option('-o, --org <name>', 'Default org for entries without an org/ prefix or "org" field')
479
+ .option('--no-wiki', 'Skip markdown→wiki conversion of description')
480
+ .option('--dry-run', 'Validate and print what would be posted, without calling Jira')
481
+ .option('--json', 'Output JSON instead of human-readable')
482
+ .action(async (issueKey, duration, opts) => {
483
+ try {
484
+ await runWorklog({
485
+ issueKey,
486
+ duration,
487
+ file: opts.file,
488
+ org: opts.org,
489
+ noWiki: opts.wiki === false,
490
+ dryRun: opts.dryRun,
491
+ json: opts.json,
492
+ });
493
+ }
494
+ catch (err) {
495
+ exitOnError(err);
496
+ }
497
+ })
498
+ .addHelpText('after', `
499
+ Quick single-entry form logs against "now": \`jirallm worklog PROJ-123 "1h 30m"\`.
500
+ For the batch form, pass a JSON array on stdin or via --file.
501
+
502
+ JSON entry shape (a duration alone is accepted — startTime defaults to now; or supply any 2 of startTime/endTime/duration):
478
503
  {
479
504
  "issueKey": "PROJ-123", // required; supports "org/PROJ-123"
480
505
  "startTime": "2026-05-23T09:00:00+02:00",
@@ -490,50 +515,82 @@ Examples:
490
515
  $ cat worklogs.json | jirallm worklog --dry-run
491
516
  $ echo '[{"issueKey":"PROJ-1","startTime":"2026-05-23T09:00:00+02:00","duration":"1h"}]' | jirallm worklog
492
517
  `);
493
- program
494
- .command('comment:rm <issue-key> <comment-id>')
495
- .description('Delete a comment from a Jira issue')
496
- .option('-o, --org <name>', 'Organization name override')
497
- .action(async (issueKey, commentId, opts) => {
498
- try {
499
- await runDeleteComment(issueKey, commentId, opts);
500
- }
501
- catch (err) {
502
- console.error(err.message);
503
- process.exit(1);
504
- }
505
- })
506
- .addHelpText('after', `
518
+ program
519
+ .command('comment:rm <issue-key> <comment-id>')
520
+ .description('Delete a comment from a Jira issue')
521
+ .option('-o, --org <name>', 'Organization name override')
522
+ .option('--dry-run', 'Show what would be deleted without calling Jira')
523
+ .option('-y, --yes', 'Skip confirmation prompt')
524
+ .action(async (issueKey, commentId, opts) => {
525
+ try {
526
+ await runDeleteComment(issueKey, commentId, { org: opts.org, dryRun: opts.dryRun, yes: opts.yes });
527
+ }
528
+ catch (err) {
529
+ exitOnError(err);
530
+ }
531
+ })
532
+ .addHelpText('after', `
507
533
  Examples:
508
534
  $ jirallm comment:rm PROJ-123 26215
509
- $ jirallm comment:rm acme/PROJ-123 26215
535
+ $ jirallm comment:rm acme/PROJ-123 26215 --dry-run
536
+ $ jirallm comment:rm PROJ-123 26215 --yes
510
537
  `);
511
- function exitOnError(err) {
512
- console.error(err.message);
513
- process.exit(1);
514
- }
515
- /** Commander collector for repeatable options (e.g. --field a=1 --field b=2). */
516
- function collect(value, previous = []) {
517
- return [...previous, value];
518
- }
519
- program
520
- .command('search <jql>')
521
- .description('Search issues by JQL (single page; pass --cursor for next page).')
522
- .option('-o, --org <name>', 'Organization name')
523
- .option('-P, --project <key>', 'Project key override')
524
- .option('--limit <n>', 'Page size (default 50)')
525
- .option('--cursor <token>', 'Next page token from prior search')
526
- .option('--fields <list>', 'Comma-separated Jira field IDs to include')
527
- .option('--json', 'Output JSON instead of human-readable')
528
- .action(async (jql, opts) => {
529
- try {
530
- await runSearch({ jql, ...opts });
538
+ program
539
+ .command('comment:ls <issue-key>')
540
+ .description('List comments on a Jira issue.')
541
+ .option('-o, --org <name>', 'Organization name override')
542
+ .option('--json', 'Output JSON instead of human-readable')
543
+ .action(async (issueKey, opts) => {
544
+ try {
545
+ await runCommentList(issueKey, { org: opts.org, json: opts.json });
546
+ }
547
+ catch (err) {
548
+ exitOnError(err);
549
+ }
550
+ })
551
+ .addHelpText('after', `
552
+ Examples:
553
+ $ jirallm comment:ls PROJ-123
554
+ $ jirallm comment:ls acme/PROJ-123 --json
555
+ `);
556
+ function jsonModeRequested() {
557
+ return process.argv.includes('--json');
531
558
  }
532
- catch (err) {
533
- exitOnError(err);
559
+ function reportError(err) {
560
+ const message = err.message ?? String(err);
561
+ if (jsonModeRequested()) {
562
+ console.error(JSON.stringify({ error: message }));
563
+ return;
564
+ }
565
+ console.error(message);
566
+ }
567
+ function exitOnError(err) {
568
+ reportError(err);
569
+ process.exit(1);
534
570
  }
535
- })
536
- .addHelpText('after', `
571
+ /** Commander collector for repeatable options (e.g. --field a=1 --field b=2). */
572
+ function collect(value, previous = []) {
573
+ return [...previous, value];
574
+ }
575
+ program
576
+ .command('search <jql>')
577
+ .description('Search issues by JQL (single page; pass --cursor for next page).')
578
+ .option('-o, --org <name>', 'Organization name')
579
+ .option('--limit <n>', 'Page size (default 50)')
580
+ .option('--cursor <token>', 'Next page token from prior search')
581
+ .option('--next-page-token <token>', 'Alias for --cursor')
582
+ .option('--fields <list>', 'Comma-separated Jira field IDs to include')
583
+ .option('--json', 'Output JSON instead of human-readable')
584
+ .action(async (jql, opts) => {
585
+ try {
586
+ const { nextPageToken, ...rest } = opts;
587
+ await runSearch({ jql, ...rest, cursor: opts.cursor ?? nextPageToken });
588
+ }
589
+ catch (err) {
590
+ exitOnError(err);
591
+ }
592
+ })
593
+ .addHelpText('after', `
537
594
  JQL must be quoted in your shell (it usually contains spaces and shell metacharacters).
538
595
  Output JSON includes "nextPageToken" — pass it back via --cursor for the next page.
539
596
 
@@ -543,48 +600,48 @@ Examples:
543
600
  $ jirallm search 'project = PROJ' -o acme --cursor eyJsYXN0SXNzdWVLZXkiOi4uLn0= --json
544
601
  $ jirallm search 'project = PROJ' -o acme --fields summary,status,assignee --json
545
602
  `);
546
- program
547
- .command('projects')
548
- .description('List projects accessible in an org.')
549
- .requiredOption('-o, --org <name>', 'Organization name')
550
- .option('--query <text>', 'Filter by name/key substring')
551
- .option('--limit <n>', 'Page size')
552
- .option('--start-at <n>', 'Pagination offset')
553
- .option('--json', 'Output JSON instead of human-readable')
554
- .action(async (opts) => {
555
- try {
556
- await runProjects(opts);
557
- }
558
- catch (err) {
559
- exitOnError(err);
560
- }
561
- })
562
- .addHelpText('after', `
603
+ program
604
+ .command('projects')
605
+ .description('List projects accessible in an org.')
606
+ .requiredOption('-o, --org <name>', 'Organization name')
607
+ .option('--query <text>', 'Filter by name/key substring')
608
+ .option('--limit <n>', 'Page size')
609
+ .option('--start-at <n>', 'Pagination offset')
610
+ .option('--json', 'Output JSON instead of human-readable')
611
+ .action(async (opts) => {
612
+ try {
613
+ await runProjects(opts);
614
+ }
615
+ catch (err) {
616
+ exitOnError(err);
617
+ }
618
+ })
619
+ .addHelpText('after', `
563
620
  Examples:
564
621
  $ jirallm projects -o acme
565
622
  $ jirallm projects -o acme --query docs --json
566
623
  $ jirallm projects -o acme --limit 50 --start-at 100 --json
567
624
  `);
568
- program
569
- .command('boards')
570
- .description('List agile boards in an org.')
571
- .requiredOption('-o, --org <name>', 'Organization name')
572
- .option('-P, --project <key>', 'Filter by project key')
573
- .option('-t, --type <type>', 'scrum | kanban | simple')
574
- .option('-n, --name <name>', 'Name substring filter')
575
- .option('--limit <n>', 'Page size')
576
- .option('--start-at <n>', 'Pagination offset')
577
- .option('--json', 'Output JSON instead of human-readable')
578
- .action(async (opts) => {
579
- try {
580
- await runBoards(opts);
581
- }
582
- catch (err) {
583
- exitOnError(err);
584
- }
585
- })
586
- .addHelpText('after', `
587
- When --project is omitted, falls back to the org's default project.
625
+ program
626
+ .command('boards')
627
+ .description('List agile boards in an org.')
628
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the -P project key when it maps to a unique org)')
629
+ .option('-P, --project <key>', 'Filter by project key')
630
+ .option('-t, --type <type>', 'scrum | kanban | simple')
631
+ .option('-n, --name <name>', 'Name substring filter')
632
+ .option('--limit <n>', 'Page size')
633
+ .option('--start-at <n>', 'Pagination offset')
634
+ .option('--json', 'Output JSON instead of human-readable')
635
+ .action(async (opts) => {
636
+ try {
637
+ await runBoards(opts);
638
+ }
639
+ catch (err) {
640
+ exitOnError(err);
641
+ }
642
+ })
643
+ .addHelpText('after', `
644
+ When --project is omitted and the org has a single project, that project is used automatically.
588
645
  The board id printed here is what you pass to \`jirallm sprints <id>\`.
589
646
 
590
647
  Examples:
@@ -592,87 +649,87 @@ Examples:
592
649
  $ jirallm boards -o acme -P PROJ -t scrum --json
593
650
  $ jirallm boards -o acme -n "Team Alpha" --json
594
651
  `);
595
- program
596
- .command('sprints <board-id>')
597
- .description('List sprints on a board.')
598
- .requiredOption('-o, --org <name>', 'Organization name')
599
- .option('-s, --state <state>', 'active | future | closed')
600
- .option('--limit <n>', 'Page size')
601
- .option('--start-at <n>', 'Pagination offset')
602
- .option('--json', 'Output JSON instead of human-readable')
603
- .action(async (boardId, opts) => {
604
- try {
605
- await runSprints({ boardId, ...opts });
606
- }
607
- catch (err) {
608
- exitOnError(err);
609
- }
610
- })
611
- .addHelpText('after', `
652
+ program
653
+ .command('sprints <board-id>')
654
+ .description('List sprints on a board.')
655
+ .requiredOption('-o, --org <name>', 'Organization name')
656
+ .option('-s, --state <state>', 'active | future | closed')
657
+ .option('--limit <n>', 'Page size')
658
+ .option('--start-at <n>', 'Pagination offset')
659
+ .option('--json', 'Output JSON instead of human-readable')
660
+ .action(async (boardId, opts) => {
661
+ try {
662
+ await runSprints({ boardId, ...opts });
663
+ }
664
+ catch (err) {
665
+ exitOnError(err);
666
+ }
667
+ })
668
+ .addHelpText('after', `
612
669
  Get board ids from \`jirallm boards --org <name>\`.
613
670
 
614
671
  Examples:
615
672
  $ jirallm sprints 42 -o acme --state active --json
616
673
  $ jirallm sprints 42 -o acme --state closed --limit 20
617
674
  `);
618
- program
619
- .command('issuetypes')
620
- .description('List issue types (project-scoped if --project provided).')
621
- .requiredOption('-o, --org <name>', 'Organization name')
622
- .option('-P, --project <key>', 'Project key (defaults to org default)')
623
- .option('--json', 'Output JSON instead of human-readable')
624
- .action(async (opts) => {
625
- try {
626
- await runIssueTypes(opts);
627
- }
628
- catch (err) {
629
- exitOnError(err);
630
- }
631
- })
632
- .addHelpText('after', `
675
+ program
676
+ .command('issuetypes')
677
+ .description('List issue types (project-scoped if --project provided).')
678
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the -P project key when it maps to a unique org)')
679
+ .option('-P, --project <key>', 'Project key (auto-selected when the org has a single project)')
680
+ .option('--json', 'Output JSON instead of human-readable')
681
+ .action(async (opts) => {
682
+ try {
683
+ await runIssueTypes(opts);
684
+ }
685
+ catch (err) {
686
+ exitOnError(err);
687
+ }
688
+ })
689
+ .addHelpText('after', `
633
690
  Use this before \`jirallm create\` to discover the valid --type values for a project.
634
691
 
635
692
  Examples:
636
693
  $ jirallm issuetypes -o acme --json
637
694
  $ jirallm issuetypes -o acme -P PROJ --json
638
695
  `);
639
- program
640
- .command('components')
641
- .description('List components defined on a project.')
642
- .requiredOption('-o, --org <name>', 'Organization name')
643
- .option('-P, --project <key>', 'Project key (defaults to org default)')
644
- .option('--json', 'Output JSON instead of human-readable')
645
- .action(async (opts) => {
646
- try {
647
- await runComponents(opts);
648
- }
649
- catch (err) {
650
- exitOnError(err);
651
- }
652
- })
653
- .addHelpText('after', `
696
+ program
697
+ .command('components')
698
+ .description('List components defined on a project.')
699
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the project key if unique)')
700
+ .option('-P, --project <key>', 'Project key (auto-selected when the org has a single project)')
701
+ .option('--json', 'Output JSON instead of human-readable')
702
+ .action(async (opts) => {
703
+ try {
704
+ await runComponents(opts);
705
+ }
706
+ catch (err) {
707
+ exitOnError(err);
708
+ }
709
+ })
710
+ .addHelpText('after', `
654
711
  The "name" values are what \`jirallm create/edit --components\` expects.
655
712
 
656
713
  Examples:
657
714
  $ jirallm components -o acme -P PROJ
658
715
  $ jirallm components -o acme -P PROJ --json
659
716
  `);
660
- program
661
- .command('fields')
662
- .description('List custom fields (and select options with --type) for use with --field.')
663
- .requiredOption('-o, --org <name>', 'Organization name')
664
- .option('-P, --project <key>', 'Project key (defaults to org default)')
665
- .option('-t, --type <issueType>', 'Show create-screen fields + allowed values for this issue type')
666
- .option('--json', 'Output JSON instead of human-readable')
667
- .action(async (opts) => {
668
- try {
669
- await runFields(opts);
670
- }
671
- catch (err) {
672
- exitOnError(err);
673
- }
674
- })
675
- .addHelpText('after', `
717
+ program
718
+ .command('fields')
719
+ .description('List custom fields (and select options with --type) for use with --field.')
720
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the -P project key when it maps to a unique org)')
721
+ .option('-P, --project <key>', 'Project key (auto-selected when the org has a single project)')
722
+ .option('-t, --type <issueType>', 'Show create-screen fields + allowed values for this issue type')
723
+ .option('--json', 'Output JSON instead of human-readable')
724
+ .action(async (opts) => {
725
+ try {
726
+ await runFields(opts);
727
+ }
728
+ catch (err) {
729
+ exitOnError(err);
730
+ }
731
+ })
732
+ .addHelpText('after', `
676
733
  The [customfield_NNNNN] ids are what \`jirallm create/edit --field\` expects.
677
734
  Add --type Bug to see valid select options (e.g. for Severity, Environment).
678
735
 
@@ -681,40 +738,40 @@ Examples:
681
738
  $ jirallm fields -o acme -P PROJ --type Bug
682
739
  $ jirallm fields -o acme -P PROJ --type Bug --json
683
740
  `);
684
- program
685
- .command('linktypes')
686
- .description('List issue link types available in an org.')
687
- .requiredOption('-o, --org <name>', 'Organization name')
688
- .option('--json', 'Output JSON instead of human-readable')
689
- .action(async (opts) => {
690
- try {
691
- await runLinkTypes(opts);
692
- }
693
- catch (err) {
694
- exitOnError(err);
695
- }
696
- })
697
- .addHelpText('after', `
741
+ program
742
+ .command('linktypes')
743
+ .description('List issue link types available in an org.')
744
+ .requiredOption('-o, --org <name>', 'Organization name')
745
+ .option('--json', 'Output JSON instead of human-readable')
746
+ .action(async (opts) => {
747
+ try {
748
+ await runLinkTypes(opts);
749
+ }
750
+ catch (err) {
751
+ exitOnError(err);
752
+ }
753
+ })
754
+ .addHelpText('after', `
698
755
  The "name" column is what \`jirallm link\` expects as <type> (e.g. "Blocks", "Relates").
699
756
 
700
757
  Examples:
701
758
  $ jirallm linktypes -o acme
702
759
  $ jirallm linktypes -o acme --json
703
760
  `);
704
- program
705
- .command('me')
706
- .description('Show the currently authenticated Jira user.')
707
- .requiredOption('-o, --org <name>', 'Organization name')
708
- .option('--json', 'Output JSON instead of human-readable')
709
- .action(async (opts) => {
710
- try {
711
- await runMe(opts);
712
- }
713
- catch (err) {
714
- exitOnError(err);
715
- }
716
- })
717
- .addHelpText('after', `
761
+ program
762
+ .command('me')
763
+ .description('Show the currently authenticated Jira user.')
764
+ .requiredOption('-o, --org <name>', 'Organization name')
765
+ .option('--json', 'Output JSON instead of human-readable')
766
+ .action(async (opts) => {
767
+ try {
768
+ await runMe(opts);
769
+ }
770
+ catch (err) {
771
+ exitOnError(err);
772
+ }
773
+ })
774
+ .addHelpText('after', `
718
775
  Returns the accountId — use it as --assignee for \`create\` / \`edit\` / \`assign\`,
719
776
  or pass "me" as a shorthand in commands that resolve it (\`assign\`, \`watchers\`).
720
777
 
@@ -722,27 +779,27 @@ Examples:
722
779
  $ jirallm me -o acme
723
780
  $ jirallm me -o acme --json | jq -r .accountId
724
781
  `);
725
- program
726
- .command('fetch <issue-key>')
727
- .description('Fetch a single issue as JSON or pretty text (no file output).')
728
- .option('-o, --org <name>', 'Organization name override')
729
- .option('--json', 'Output JSON instead of human-readable')
730
- .option('--with-comments', 'Include comments')
731
- .option('--with-history', 'Include status + field-change history')
732
- .option('--with-worklog', 'Include worklogs')
733
- .option('--with-subtasks', 'Include subtask metadata')
734
- .option('--with-links', 'Include issue links')
735
- .option('--with-attachments', 'List attachment metadata (does not download)')
736
- .option('--full', 'Include everything (comments, history, worklog, subtasks, links, attachments)')
737
- .action(async (issueKey, opts) => {
738
- try {
739
- await runFetch({ issueKey, ...opts });
740
- }
741
- catch (err) {
742
- exitOnError(err);
743
- }
744
- })
745
- .addHelpText('after', `
782
+ program
783
+ .command('fetch <issue-key>')
784
+ .description('Fetch a single issue as JSON or pretty text (no file output).')
785
+ .option('-o, --org <name>', 'Organization name override')
786
+ .option('--json', 'Output JSON instead of human-readable')
787
+ .option('--with-comments', 'Include comments')
788
+ .option('--with-history', 'Include status + field-change history')
789
+ .option('--with-worklog', 'Include worklogs')
790
+ .option('--with-subtasks', 'Include subtask metadata')
791
+ .option('--with-links', 'Include issue links')
792
+ .option('--with-attachments', 'List attachment metadata (does not download)')
793
+ .option('--full', 'Include everything (comments, history, worklog, subtasks, links, attachments)')
794
+ .action(async (issueKey, opts) => {
795
+ try {
796
+ await runFetch({ issueKey, ...opts });
797
+ }
798
+ catch (err) {
799
+ exitOnError(err);
800
+ }
801
+ })
802
+ .addHelpText('after', `
746
803
  For the full export bundle (attachments, video frames, on-disk folder),
747
804
  use the default \`jirallm <key>\` command instead.
748
805
 
@@ -753,47 +810,47 @@ Examples:
753
810
  $ jirallm fetch PROJ-123 --full
754
811
  $ jirallm fetch PROJ-123 --json | jq .status
755
812
  `);
756
- program
757
- .command('create')
758
- .description('Create a new Jira issue.')
759
- .requiredOption('-o, --org <name>', 'Organization name')
760
- .option('-P, --project <key>', 'Project key (defaults to org default)')
761
- .requiredOption('-t, --type <type>', 'Issue type name (e.g. Task, Bug, Story)')
762
- .requiredOption('-s, --summary <text>', 'Issue summary')
763
- .option('-d, --description <text>', 'Description (markdown)')
764
- .option('--description-file <path>', 'Read description (markdown) from a file')
765
- .option('-a, --assignee <accountId>', 'Assignee accountId')
766
- .option('-l, --labels <list>', 'Comma-separated labels')
767
- .option('--priority <name>', 'Priority name (e.g. High)')
768
- .option('--parent <key>', 'Parent issue key (for subtasks / epic children)')
769
- .option('--components <names>', 'Comma-separated component names')
770
- .option('-F, --field <pair>', 'Set a custom field: friendlyName=value or customfield_NNNNN[:type]=value (repeatable)', collect)
771
- .option('--dry-run', 'Show what would be created without calling Jira')
772
- .option('--json', 'Output JSON instead of human-readable')
773
- .action(async (opts) => {
774
- try {
775
- await runCreate({
776
- org: opts.org,
777
- projectKey: opts.project,
778
- type: opts.type,
779
- summary: opts.summary,
780
- description: opts.description,
781
- descriptionFile: opts.descriptionFile,
782
- assignee: opts.assignee,
783
- labels: opts.labels,
784
- priority: opts.priority,
785
- parent: opts.parent,
786
- components: opts.components,
787
- field: opts.field,
788
- dryRun: opts.dryRun,
789
- json: opts.json,
790
- });
791
- }
792
- catch (err) {
793
- exitOnError(err);
794
- }
795
- })
796
- .addHelpText('after', `
813
+ program
814
+ .command('create')
815
+ .description('Create a new Jira issue.')
816
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the project key if unique)')
817
+ .option('-P, --project <key>', 'Project key (auto-selected when the org has a single project)')
818
+ .requiredOption('-t, --type <type>', 'Issue type name (e.g. Task, Bug, Story)')
819
+ .requiredOption('-s, --summary <text>', 'Issue summary')
820
+ .option('-d, --description <text>', 'Description (markdown)')
821
+ .option('--description-file <path>', 'Read description (markdown) from a file')
822
+ .option('-a, --assignee <user>', 'Assignee: an accountId, email, display name, or "me"')
823
+ .option('-l, --labels <list>', 'Comma-separated labels')
824
+ .option('--priority <name>', 'Priority name (e.g. High)')
825
+ .option('--parent <key>', 'Parent issue key (for subtasks / epic children)')
826
+ .option('--components <names>', 'Comma-separated component names')
827
+ .option('-F, --field <pair>', 'Set a custom field: friendlyName=value or customfield_NNNNN[:type]=value (repeatable)', collect)
828
+ .option('--dry-run', 'Show what would be created without calling Jira')
829
+ .option('--json', 'Output JSON instead of human-readable')
830
+ .action(async (opts) => {
831
+ try {
832
+ await runCreate({
833
+ org: opts.org,
834
+ projectKey: opts.project,
835
+ type: opts.type,
836
+ summary: opts.summary,
837
+ description: opts.description,
838
+ descriptionFile: opts.descriptionFile,
839
+ assignee: opts.assignee,
840
+ labels: opts.labels,
841
+ priority: opts.priority,
842
+ parent: opts.parent,
843
+ components: opts.components,
844
+ field: opts.field,
845
+ dryRun: opts.dryRun,
846
+ json: opts.json,
847
+ });
848
+ }
849
+ catch (err) {
850
+ exitOnError(err);
851
+ }
852
+ })
853
+ .addHelpText('after', `
797
854
  Run \`jirallm issuetypes -o <org>\` to discover valid --type values.
798
855
  Run \`jirallm components -o <org> -P <project>\` to discover valid --components names.
799
856
  Run \`jirallm fields -o <org> -P <project> --type Bug\` to discover custom field ids and select options.
@@ -813,30 +870,30 @@ Examples:
813
870
  $ jirallm create -o acme -t Bug -s "Crash" --components Web,API --field severity=High --field environment=PROD
814
871
  $ jirallm create -o acme -t Bug -s "test" --dry-run --json
815
872
  `);
816
- program
817
- .command('edit <issue-key>')
818
- .description('Edit fields on an existing Jira issue.')
819
- .option('-o, --org <name>', 'Organization name override')
820
- .option('-s, --summary <text>', 'New summary')
821
- .option('-d, --description <text>', 'New description (markdown)')
822
- .option('--description-file <path>', 'Read description (markdown) from a file')
823
- .option('-a, --assignee <accountId>', 'Assignee accountId')
824
- .option('--unassign', 'Unassign the issue (clears assignee)')
825
- .option('-l, --labels <list>', 'Comma-separated labels (replaces existing)')
826
- .option('--priority <name>', 'Priority name')
827
- .option('--components <names>', 'Comma-separated component names (replaces existing)')
828
- .option('-F, --field <pair>', 'Set a custom field: friendlyName=value or customfield_NNNNN[:type]=value (repeatable)', collect)
829
- .option('--dry-run', 'Show what would change without calling Jira')
830
- .option('--json', 'Output JSON instead of human-readable')
831
- .action(async (issueKey, opts) => {
832
- try {
833
- await runEdit({ issueKey, ...opts });
834
- }
835
- catch (err) {
836
- exitOnError(err);
837
- }
838
- })
839
- .addHelpText('after', `
873
+ program
874
+ .command('edit <issue-key>')
875
+ .description('Edit fields on an existing Jira issue.')
876
+ .option('-o, --org <name>', 'Organization name override')
877
+ .option('-s, --summary <text>', 'New summary')
878
+ .option('-d, --description <text>', 'New description (markdown)')
879
+ .option('--description-file <path>', 'Read description (markdown) from a file')
880
+ .option('-a, --assignee <user>', 'Assignee: an accountId, email, display name, or "me"')
881
+ .option('--unassign', 'Unassign the issue (clears assignee)')
882
+ .option('-l, --labels <list>', 'Comma-separated labels (replaces existing)')
883
+ .option('--priority <name>', 'Priority name')
884
+ .option('--components <names>', 'Comma-separated component names (replaces existing)')
885
+ .option('-F, --field <pair>', 'Set a custom field: friendlyName=value or customfield_NNNNN[:type]=value (repeatable)', collect)
886
+ .option('--dry-run', 'Show what would change without calling Jira')
887
+ .option('--json', 'Output JSON instead of human-readable')
888
+ .action(async (issueKey, opts) => {
889
+ try {
890
+ await runEdit({ issueKey, ...opts });
891
+ }
892
+ catch (err) {
893
+ exitOnError(err);
894
+ }
895
+ })
896
+ .addHelpText('after', `
840
897
  --labels and --components REPLACE the existing sets; there is no add/remove syntax here.
841
898
  --unassign and --assignee are mutually exclusive (use one).
842
899
  Custom fields work like \`create\`: --field friendlyName=value or --field customfield_NNNNN[:type]=value.
@@ -849,23 +906,23 @@ Examples:
849
906
  $ jirallm edit PROJ-123 --assignee 5ac1234567890abcdef
850
907
  $ jirallm edit PROJ-123 --unassign --dry-run --json
851
908
  `);
852
- program
853
- .command('assign <issue-key> <assignee>')
854
- .description('Assign an issue. Use "me" for the current user, "none" to unassign.')
855
- .option('-o, --org <name>', 'Organization name override')
856
- .option('--dry-run', 'Show what would change without calling Jira')
857
- .option('--json', 'Output JSON instead of human-readable')
858
- .action(async (issueKey, assignee, opts) => {
859
- try {
860
- await runAssign({ issueKey, assignee, ...opts });
861
- }
862
- catch (err) {
863
- exitOnError(err);
864
- }
865
- })
866
- .addHelpText('after', `
909
+ program
910
+ .command('assign <issue-key> <assignee>')
911
+ .description('Assign an issue. Use "me" for the current user, "none" to unassign.')
912
+ .option('-o, --org <name>', 'Organization name override')
913
+ .option('--dry-run', 'Show what would change without calling Jira')
914
+ .option('--json', 'Output JSON instead of human-readable')
915
+ .action(async (issueKey, assignee, opts) => {
916
+ try {
917
+ await runAssign({ issueKey, assignee, ...opts });
918
+ }
919
+ catch (err) {
920
+ exitOnError(err);
921
+ }
922
+ })
923
+ .addHelpText('after', `
867
924
  <assignee> shortcuts: "me" (current user) or "none"/"-" (unassign).
868
- Any other value is treated as a Jira accountId.
925
+ Any other value is resolved as an email, display name, or accountId.
869
926
 
870
927
  Examples:
871
928
  $ jirallm assign PROJ-123 me
@@ -873,22 +930,22 @@ Examples:
873
930
  $ jirallm assign PROJ-123 5ac1234567890abcdef
874
931
  $ jirallm assign PROJ-123 me --dry-run --json
875
932
  `);
876
- program
877
- .command('link <inward-key> <type> <outward-key>')
878
- .description('Create an issue link (e.g. `link FOO-1 "blocks" FOO-2`).')
879
- .option('-c, --comment <text>', 'Add a comment when linking')
880
- .option('-o, --org <name>', 'Organization name override')
881
- .option('--dry-run', 'Show what would be created without calling Jira')
882
- .option('--json', 'Output JSON instead of human-readable')
883
- .action(async (inwardKey, type, outwardKey, opts) => {
884
- try {
885
- await runLink({ inwardKey, type, outwardKey, ...opts });
886
- }
887
- catch (err) {
888
- exitOnError(err);
889
- }
890
- })
891
- .addHelpText('after', `
933
+ program
934
+ .command('link <inward-key> <type> <outward-key>')
935
+ .description('Create an issue link (e.g. `link FOO-1 "blocks" FOO-2`).')
936
+ .option('-c, --comment <text>', 'Add a comment when linking')
937
+ .option('-o, --org <name>', 'Organization name override')
938
+ .option('--dry-run', 'Show what would be created without calling Jira')
939
+ .option('--json', 'Output JSON instead of human-readable')
940
+ .action(async (inwardKey, type, outwardKey, opts) => {
941
+ try {
942
+ await runLink({ inwardKey, type, outwardKey, ...opts });
943
+ }
944
+ catch (err) {
945
+ exitOnError(err);
946
+ }
947
+ })
948
+ .addHelpText('after', `
892
949
  <type> is the link-type NAME (e.g. "Blocks", "Relates", "Duplicate"), not the inward/outward label.
893
950
  Discover the available types with: \`jirallm linktypes -o <org>\`.
894
951
  --comment is markdown; it gets converted to Jira wiki.
@@ -898,44 +955,50 @@ Examples:
898
955
  $ jirallm link PROJ-1 "Relates" PROJ-2 --comment "see also **infra rewrite**"
899
956
  $ jirallm link PROJ-1 "Blocks" PROJ-2 --dry-run --json
900
957
  `);
901
- program
902
- .command('link:rm <link-id>')
903
- .description('Remove an issue link by ID.')
904
- .requiredOption('-o, --org <name>', 'Organization name')
905
- .option('--dry-run', 'Show what would be deleted without calling Jira')
906
- .option('--json', 'Output JSON instead of human-readable')
907
- .action(async (linkId, opts) => {
908
- try {
909
- await runLinkRemove({ linkId, ...opts });
910
- }
911
- catch (err) {
912
- exitOnError(err);
913
- }
914
- })
915
- .addHelpText('after', `
958
+ program
959
+ .command('link:rm [issue-key] [link-id]')
960
+ .description('Remove an issue link by ID, or by issue key + --to target.')
961
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the issue/project key if unique)')
962
+ .option('--to <key>', 'Remove the link between <issue-key> and this issue key')
963
+ .option('--dry-run', 'Show what would be deleted without calling Jira')
964
+ .option('--json', 'Output JSON instead of human-readable')
965
+ .action(async (issueKey, linkId, opts) => {
966
+ try {
967
+ const target = issueKey ?? linkId;
968
+ if (!target)
969
+ throw new Error('Provide a link id, or an issue key with --to.');
970
+ await runLinkRemove({ linkId: target, to: opts.to, org: opts.org, dryRun: opts.dryRun, json: opts.json });
971
+ }
972
+ catch (err) {
973
+ exitOnError(err);
974
+ }
975
+ })
976
+ .addHelpText('after', `
916
977
  Get link IDs from the issueLinks array in \`jirallm fetch <key> --json\`.
978
+ Alternatively, drop the link between two issues with --to.
917
979
 
918
980
  Examples:
919
981
  $ jirallm link:rm 10042 -o acme
920
982
  $ jirallm link:rm 10042 -o acme --dry-run --json
983
+ $ jirallm link:rm PROJ-1 --to PROJ-2
921
984
  `);
922
- program
923
- .command('attach <issue-key> [files...]')
924
- .description('Upload one or more file attachments to an issue. At least one file is required.')
925
- .option('-o, --org <name>', 'Organization name override')
926
- .option('--dry-run', 'Show what would be uploaded without calling Jira')
927
- .option('--json', 'Output JSON instead of human-readable')
928
- .action(async (issueKey, files, opts) => {
929
- try {
930
- if (!files || files.length === 0)
931
- throw new Error('At least one file is required.');
932
- await runAttach({ issueKey, files, ...opts });
933
- }
934
- catch (err) {
935
- exitOnError(err);
936
- }
937
- })
938
- .addHelpText('after', `
985
+ program
986
+ .command('attach <issue-key> [files...]')
987
+ .description('Upload one or more file attachments to an issue. At least one file is required.')
988
+ .option('-o, --org <name>', 'Organization name override')
989
+ .option('--dry-run', 'Show what would be uploaded without calling Jira')
990
+ .option('--json', 'Output JSON instead of human-readable')
991
+ .action(async (issueKey, files, opts) => {
992
+ try {
993
+ if (!files || files.length === 0)
994
+ throw new Error('At least one file is required.');
995
+ await runAttach({ issueKey, files, ...opts });
996
+ }
997
+ catch (err) {
998
+ exitOnError(err);
999
+ }
1000
+ })
1001
+ .addHelpText('after', `
939
1002
  Files are uploaded sequentially; --json returns the aggregated attachments array.
940
1003
  The id from the response is what \`jirallm attach:rm\` takes.
941
1004
 
@@ -944,44 +1007,47 @@ Examples:
944
1007
  $ jirallm attach PROJ-123 ./a.png ./b.png ./recording.mp4 --json
945
1008
  $ jirallm attach PROJ-123 ./screenshot.png --dry-run
946
1009
  `);
947
- program
948
- .command('attach:rm <attachment-id>')
949
- .description('Delete an attachment by ID.')
950
- .requiredOption('-o, --org <name>', 'Organization name')
951
- .option('--dry-run', 'Show what would be deleted without calling Jira')
952
- .option('--json', 'Output JSON instead of human-readable')
953
- .action(async (attachmentId, opts) => {
954
- try {
955
- await runAttachRemove({ attachmentId, ...opts });
956
- }
957
- catch (err) {
958
- exitOnError(err);
959
- }
960
- })
961
- .addHelpText('after', `
962
- Get attachment IDs from the attachments array in \`jirallm fetch <key> --json\`.
1010
+ program
1011
+ .command('attach:rm [target] [filename]')
1012
+ .description('Delete an attachment by ID, or by issue key + filename.')
1013
+ .option('-o, --org <name>', 'Organization name (auto-resolved from the issue/project key if unique)')
1014
+ .option('--dry-run', 'Show what would be deleted without calling Jira')
1015
+ .option('--json', 'Output JSON instead of human-readable')
1016
+ .action(async (target, filename, opts) => {
1017
+ try {
1018
+ await runAttachRemove({ target, filename, org: opts.org, dryRun: opts.dryRun, json: opts.json });
1019
+ }
1020
+ catch (err) {
1021
+ exitOnError(err);
1022
+ }
1023
+ })
1024
+ .addHelpText('after', `
1025
+ Delete by attachment id (get ids from the attachments array in \`jirallm fetch <key> --json\`);
1026
+ a bare numeric id needs -o (or an org/KEY) since it cannot infer the org on its own.
1027
+ Alternatively, delete by issue key + filename — the org is inferred from the key prefix.
963
1028
 
964
1029
  Examples:
965
1030
  $ jirallm attach:rm 99021 -o acme
966
1031
  $ jirallm attach:rm 99021 -o acme --dry-run --json
1032
+ $ jirallm attach:rm PROJ-123 screenshot.png
967
1033
  `);
968
- program
969
- .command('watchers <issue-key>')
970
- .description('List, add, or remove watchers on an issue.')
971
- .option('-o, --org <name>', 'Organization name override')
972
- .option('--add <accountId>', 'Add watcher by accountId (or "me")')
973
- .option('--rm <accountId>', 'Remove watcher by accountId (or "me")')
974
- .option('--dry-run', 'Show what would change without calling Jira')
975
- .option('--json', 'Output JSON instead of human-readable')
976
- .action(async (issueKey, opts) => {
977
- try {
978
- await runWatchers({ issueKey, ...opts });
979
- }
980
- catch (err) {
981
- exitOnError(err);
982
- }
983
- })
984
- .addHelpText('after', `
1034
+ program
1035
+ .command('watchers <issue-key>')
1036
+ .description('List, add, or remove watchers on an issue.')
1037
+ .option('-o, --org <name>', 'Organization name override')
1038
+ .option('--add <user>', 'Add watcher: an accountId, email, display name, or "me"')
1039
+ .option('--rm <user>', 'Remove watcher: an accountId, email, display name, or "me"')
1040
+ .option('--dry-run', 'Show what would change without calling Jira')
1041
+ .option('--json', 'Output JSON instead of human-readable')
1042
+ .action(async (issueKey, opts) => {
1043
+ try {
1044
+ await runWatchers({ issueKey, ...opts });
1045
+ }
1046
+ catch (err) {
1047
+ exitOnError(err);
1048
+ }
1049
+ })
1050
+ .addHelpText('after', `
985
1051
  With no --add/--rm, just lists current watchers.
986
1052
  You can pass both --add and --rm in one call; both run before the final listing.
987
1053
  Pass "me" to add/remove yourself (resolved via \`/myself\`).
@@ -992,8 +1058,19 @@ Examples:
992
1058
  $ jirallm watchers PROJ-123 --rm 5ac1234567890abcdef
993
1059
  $ jirallm watchers PROJ-123 --add me --rm 5acOldUser --json
994
1060
  `);
995
- program.parseAsync(process.argv).catch((error) => {
996
- console.error('Unexpected error:', error);
997
- process.exit(1);
998
- });
1061
+ return program;
1062
+ }
1063
+ const isDirectRun = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
1064
+ if (isDirectRun) {
1065
+ buildProgram()
1066
+ .parseAsync(process.argv)
1067
+ .catch((error) => {
1068
+ if (process.argv.includes('--json')) {
1069
+ console.error(JSON.stringify({ error: error.message ?? String(error) }));
1070
+ process.exit(1);
1071
+ }
1072
+ console.error('Unexpected error:', error);
1073
+ process.exit(1);
1074
+ });
1075
+ }
999
1076
  //# sourceMappingURL=index.js.map