jiradc-cli 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +65 -59
  2. package/dist/index.js +349 -122
  3. package/package.json +6 -4
package/README.md CHANGED
@@ -17,6 +17,11 @@ export JIRA_TOKEN="your-personal-access-token" # HTTP Access Token from Jira
17
17
 
18
18
  ## Breaking changes in this release
19
19
 
20
+ - **`field options` is removed.** It called a Jira 9 endpoint and returned 404 for every field on Jira 8.x. Jira scopes a custom field's options to a context, so there is no context-free replacement — use `issue createmeta --project <key> --type <name>` (create screen), `issue editmeta <key>` (edit screen), or `issue transitions <key>` (transition screen).
21
+ - **`issue transitions` no longer reports `required`** on screen fields. Jira reports field-configuration requiredness there, which cannot see the workflow validator that actually gates the transition, so it returned `false` for fields that then rejected the transition. Supply everything in a transition's `fields` block. (`required` from `createmeta`/`editmeta` reflects field configuration and is reliable.)
22
+ - **`issue get` / `issue search` return issue-picker custom fields as a bare key string** (or an array of keys) instead of the whole nested issue. `fields.customfield_NNNNN.key` becomes `fields.customfield_NNNNN`.
23
+ - **`issue create --custom-fields` is now `--fields`**, matching `issue update`. The old name still works as a deprecated alias.
24
+ - **Argument mistakes now exit 2/3/6** (`usage`/`not_found`/`auth`) instead of 1.
20
25
  - `issue transitions` now returns a bare JSON array of transitions (previously wrapped in `{ transitions: [...] }`). Each transition is reduced to `{ id, name, to, fields? }` — verbose fields like `to.iconUrl`, `to.statusCategory`, `isGlobal`, `isInitial`, `isConditional`, `hasScreen` are stripped.
21
26
  - `component list` now requires `--project <key>` instead of a positional `<projectKey>` argument.
22
27
  - `issue link-epic` is now variadic on the issue argument: `link-epic <issueKey...> --epic <epicKey>`. Single-issue calls keep working.
@@ -27,88 +32,89 @@ All commands output JSON. Add `--pretty` to pretty-print.
27
32
 
28
33
  ### issue
29
34
 
30
- | Command | Description |
31
- |---------|-------------|
32
- | `jiradc issue get <key>` | Get issue details (`--fields` to select, `--expand` for changelog/transitions) |
33
- | `jiradc issue search <jql>` | Search issues with JQL |
34
- | `jiradc issue create` | Create an issue (`--project`, `--type`, `--summary`, `--description`, `--custom-fields`) |
35
- | `jiradc issue update <key>` | Update an issue (`--summary`, `--description`, `--priority`, `--assignee`, `--labels`, `--components`, `--fix-versions`, or `--fields` JSON) |
36
- | `jiradc issue delete <key>` | Delete an issue |
37
- | `jiradc issue assign <key> <user>` | Assign issue (user can be a username, `me`, or `none` to unassign) |
38
- | `jiradc issue transition <key>` | Transition issue to a new status (`--to` accepts ID or status name, `--comment` to add a note) |
39
- | `jiradc issue transitions <key>` | List available transitions |
40
- | `jiradc issue comment add <key>` | Add a comment (`--body`) |
41
- | `jiradc issue comment edit <key>` | Edit a comment (`--id`, `--body`) |
42
- | `jiradc issue comment delete <key>` | Delete a comment (`--id`) |
43
- | `jiradc issue link <key> <targetKey>` | Link two issues (`--type` link type name) |
44
- | `jiradc issue unlink <linkId>` | Remove a link |
45
- | `jiradc issue link-types` | List available link types |
46
- | `jiradc issue link-epic <keys...>` | Link one or more issues to an epic (`--epic <epicKey>`) |
47
- | `jiradc issue worklog add <key>` | Add a work log entry (`--time`, `--comment`, `--started`) |
48
- | `jiradc issue worklog list <key>` | Get work log entries |
49
- | `jiradc issue worklog edit <key>` | Update a work log entry (`--id`, `--time`, `--comment`, `--started`, `--adjust-estimate`, `--new-estimate`) |
50
- | `jiradc issue worklog delete <key>` | Delete a work log entry (`--id`, `--adjust-estimate`, `--new-estimate`, `--increase-by`) |
51
- | `jiradc issue changelog <key>` | Get issue changelog |
52
- | `jiradc issue batch-changelog` | Get changelog for multiple issues (`--keys`) |
53
- | `jiradc issue clone <key>` | Clone an issue with subtasks |
54
- | `jiradc issue batch-create` | Create multiple issues from JSON |
55
- | `jiradc issue dev-status <key>` | Get development status (branches, PRs, commits) |
35
+ | Command | Description |
36
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `jiradc issue get <key>` | Get issue details (`--fields` to select, `--expand` for changelog/transitions) |
38
+ | `jiradc issue search <jql>` | Search issues with JQL |
39
+ | `jiradc issue create` | Create an issue (`--project`, `--type`, `--summary`, `--description`, `--custom-fields`) |
40
+ | `jiradc issue update <key>` | Update an issue (`--summary`, `--description`, `--priority`, `--assignee`, `--labels`, `--components`, `--fix-versions`, or `--fields` JSON) |
41
+ | `jiradc issue delete <key>` | Delete an issue |
42
+ | `jiradc issue assign <key> <user>` | Assign issue (user can be a username, `me`, or `none` to unassign) |
43
+ | `jiradc issue transition <key>` | Transition issue to a new status (`--to` accepts ID or status name, `--comment` to add a note) |
44
+ | `jiradc issue transitions <key>` | List available transitions (with any fields their screens require) |
45
+ | `jiradc issue createmeta` | Fields a new issue accepts, with allowed values (`--project`, `--type`) |
46
+ | `jiradc issue editmeta <key>` | Fields an existing issue's edit screen accepts, with allowed values |
47
+ | `jiradc issue comment add <key>` | Add a comment (`--body`) |
48
+ | `jiradc issue comment edit <key>` | Edit a comment (`--id`, `--body`) |
49
+ | `jiradc issue comment delete <key>` | Delete a comment (`--id`) |
50
+ | `jiradc issue link <key> <targetKey>` | Link two issues (`--type` link type name) |
51
+ | `jiradc issue unlink <linkId>` | Remove a link |
52
+ | `jiradc issue link-types` | List available link types |
53
+ | `jiradc issue link-epic <keys...>` | Link one or more issues to an epic (`--epic <epicKey>`) |
54
+ | `jiradc issue worklog add <key>` | Add a work log entry (`--time`, `--comment`, `--started`) |
55
+ | `jiradc issue worklog list <key>` | Get work log entries |
56
+ | `jiradc issue worklog edit <key>` | Update a work log entry (`--id`, `--time`, `--comment`, `--started`, `--adjust-estimate`, `--new-estimate`) |
57
+ | `jiradc issue worklog delete <key>` | Delete a work log entry (`--id`, `--adjust-estimate`, `--new-estimate`, `--increase-by`) |
58
+ | `jiradc issue changelog <key>` | Get issue changelog |
59
+ | `jiradc issue batch-changelog` | Get changelog for multiple issues (`--keys`) |
60
+ | `jiradc issue clone <key>` | Clone an issue with subtasks |
61
+ | `jiradc issue batch-create` | Create multiple issues from JSON |
62
+ | `jiradc issue dev-status <key>` | Get development status (branches, PRs, commits) |
56
63
 
57
64
  #### issue attachment
58
65
 
59
- | Command | Description |
60
- |---------|-------------|
61
- | `jiradc issue attachment list <key>` | List attachments |
66
+ | Command | Description |
67
+ | ------------------------------------------------------- | ------------------------------ |
68
+ | `jiradc issue attachment list <key>` | List attachments |
62
69
  | `jiradc issue attachment download <key> <attachmentId>` | Download a specific attachment |
63
- | `jiradc issue attachment download-all <key>` | Download all attachments |
64
- | `jiradc issue attachment upload <key> <files...>` | Upload files |
65
- | `jiradc issue attachment delete <attachmentId>` | Delete an attachment |
70
+ | `jiradc issue attachment download-all <key>` | Download all attachments |
71
+ | `jiradc issue attachment upload <key> <files...>` | Upload files |
72
+ | `jiradc issue attachment delete <attachmentId>` | Delete an attachment |
66
73
 
67
74
  ### project
68
75
 
69
- | Command | Description |
70
- |---------|-------------|
71
- | `jiradc project list` | List projects |
76
+ | Command | Description |
77
+ | ------------------------------- | --------------------------- |
78
+ | `jiradc project list` | List projects |
72
79
  | `jiradc project versions <key>` | List versions for a project |
73
80
 
74
81
  ### component
75
82
 
76
- | Command | Description |
77
- |---------|-------------|
78
- | `jiradc component list` | List components for a project (`--project <key>`) |
79
- | `jiradc component get <id>` | Get a component by ID |
80
- | `jiradc component create` | Create a component (`--project`, `--name`, `--description`, `--lead`) |
81
- | `jiradc component update <id>` | Update a component |
82
- | `jiradc component issue-count <id>` | Get the number of issues using a component |
83
+ | Command | Description |
84
+ | ----------------------------------- | --------------------------------------------------------------------- |
85
+ | `jiradc component list` | List components for a project (`--project <key>`) |
86
+ | `jiradc component get <id>` | Get a component by ID |
87
+ | `jiradc component create` | Create a component (`--project`, `--name`, `--description`, `--lead`) |
88
+ | `jiradc component update <id>` | Update a component |
89
+ | `jiradc component issue-count <id>` | Get the number of issues using a component |
83
90
 
84
91
  ### board
85
92
 
86
- | Command | Description |
87
- |---------|-------------|
88
- | `jiradc board list` | List boards (`--project`, `--type`: scrum/kanban/simple, `--name`) |
89
- | `jiradc board issues <boardId>` | Get issues on a board |
93
+ | Command | Description |
94
+ | ------------------------------- | ------------------------------------------------------------------ |
95
+ | `jiradc board list` | List boards (`--project`, `--type`: scrum/kanban/simple, `--name`) |
96
+ | `jiradc board issues <boardId>` | Get issues on a board |
90
97
 
91
98
  ### sprint
92
99
 
93
- | Command | Description |
94
- |---------|-------------|
95
- | `jiradc sprint list <boardId>` | List sprints (`--state`: future/active/closed) |
96
- | `jiradc sprint issues <boardId> <sprintId>` | Get issues in a sprint |
97
- | `jiradc sprint create <boardId>` | Create a sprint |
98
- | `jiradc sprint update <sprintId>` | Update a sprint |
99
- | `jiradc sprint delete <sprintId>` | Delete a sprint (returns its issues to the backlog) |
100
+ | Command | Description |
101
+ | ------------------------------------------- | --------------------------------------------------- |
102
+ | `jiradc sprint list <boardId>` | List sprints (`--state`: future/active/closed) |
103
+ | `jiradc sprint issues <boardId> <sprintId>` | Get issues in a sprint |
104
+ | `jiradc sprint create <boardId>` | Create a sprint |
105
+ | `jiradc sprint update <sprintId>` | Update a sprint |
106
+ | `jiradc sprint delete <sprintId>` | Delete a sprint (returns its issues to the backlog) |
100
107
 
101
108
  ### field
102
109
 
103
- | Command | Description |
104
- |---------|-------------|
105
- | `jiradc field search` | Search for fields (`--query`, `--type`: custom/system) |
106
- | `jiradc field options <fieldKey>` | Get allowed values for a field (`--project`, `--issue-type`) |
110
+ | Command | Description |
111
+ | ------------------------------- | ----------------------------------------------------------------------------- |
112
+ | `jiradc field search <keyword>` | Search for fields by name or ID (returns name + type, **not** allowed values) |
107
113
 
108
114
  ### user
109
115
 
110
- | Command | Description |
111
- |---------|-------------|
116
+ | Command | Description |
117
+ | ---------------- | --------------------- |
112
118
  | `jiradc user me` | Get current user info |
113
119
 
114
120
  ## Pagination
package/dist/index.js CHANGED
@@ -239,6 +239,7 @@ var CliAuthError = class extends Error {
239
239
  };
240
240
  var NOT_FOUND_RECOVERY = "Verify the id/key, then check your access.";
241
241
  var NOT_FOUND_MESSAGE = "Not found: it may not exist, or you may not have permission to see it";
242
+ var USAGE_RECOVERY = "Check the command syntax and flags; run the command with --help.";
242
243
  var CliNotFoundError = class extends Error {
243
244
  recovery;
244
245
  constructor(message, recovery = NOT_FOUND_RECOVERY) {
@@ -247,6 +248,16 @@ var CliNotFoundError = class extends Error {
247
248
  this.recovery = recovery;
248
249
  }
249
250
  };
251
+ var CliUsageError = class extends Error {
252
+ recovery;
253
+ detail;
254
+ constructor(message, recovery = USAGE_RECOVERY, detail) {
255
+ super(message);
256
+ this.name = "CliUsageError";
257
+ this.recovery = recovery;
258
+ this.detail = detail;
259
+ }
260
+ };
250
261
  var SubcommandRequiredError = class extends Error {
251
262
  subcommands;
252
263
  constructor(commandPath3, subcommands) {
@@ -263,10 +274,55 @@ var TYPE_EXIT = {
263
274
  auth: EXIT.AUTH,
264
275
  rate_limited: EXIT.GENERIC,
265
276
  server: EXIT.GENERIC,
277
+ timeout: EXIT.GENERIC,
266
278
  network: EXIT.GENERIC,
267
279
  unknown: EXIT.GENERIC
268
280
  };
269
- var NETWORK_CODES = ["ENOTFOUND", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"];
281
+ var TRANSPORT_CODES = {
282
+ /** Our own timeout fired: connected (or tried to), no response in time. */
283
+ readTimeout: ["ECONNABORTED"],
284
+ /** The OS gave up establishing the connection. */
285
+ connectTimeout: ["ETIMEDOUT"],
286
+ /** No such host, or DNS itself is unavailable. */
287
+ unresolved: ["ENOTFOUND", "EAI_AGAIN"],
288
+ /** Host is there, nothing is listening on that port. */
289
+ refused: ["ECONNREFUSED"],
290
+ /** Established, then died mid-flight. */
291
+ dropped: ["ECONNRESET", "EPIPE"]
292
+ };
293
+ function humanMs(ms) {
294
+ return ms % 1e3 === 0 ? `${ms / 1e3}s` : `${ms}ms`;
295
+ }
296
+ function targetHost(err) {
297
+ const config = err?.config;
298
+ const raw = config?.baseURL ?? config?.url;
299
+ if (raw === void 0 || raw === "")
300
+ return void 0;
301
+ try {
302
+ return new URL(raw).host;
303
+ } catch {
304
+ return void 0;
305
+ }
306
+ }
307
+ function configuredTimeoutMs(err) {
308
+ const t = err?.config?.timeout;
309
+ return typeof t === "number" && t > 0 ? t : void 0;
310
+ }
311
+ function retryAfterSeconds(err) {
312
+ const headers = err?.response?.headers;
313
+ const raw = headers?.["retry-after"];
314
+ if (raw === void 0 || raw === "")
315
+ return void 0;
316
+ const seconds = Number(raw);
317
+ if (Number.isFinite(seconds))
318
+ return Math.max(0, Math.round(seconds));
319
+ if (typeof raw !== "string")
320
+ return void 0;
321
+ const at = Date.parse(raw);
322
+ if (Number.isNaN(at))
323
+ return void 0;
324
+ return Math.max(0, Math.round((at - Date.now()) / 1e3));
325
+ }
270
326
  function httpStatus(err) {
271
327
  const e = err;
272
328
  return e?.response?.status ?? e?.statusCode;
@@ -318,6 +374,15 @@ function normalize(err, opts) {
318
374
  if (err instanceof CliNotFoundError) {
319
375
  return { type: "not_found", message, recovery: err.recovery, retryable: false };
320
376
  }
377
+ if (err instanceof CliUsageError) {
378
+ return {
379
+ type: "usage",
380
+ message,
381
+ recovery: err.recovery,
382
+ retryable: false,
383
+ ...err.detail !== void 0 ? { detail: err.detail } : {}
384
+ };
385
+ }
321
386
  const status = httpStatus(err);
322
387
  const detail = responseDetail(err);
323
388
  if (status !== void 0) {
@@ -366,21 +431,24 @@ function normalize(err, opts) {
366
431
  retryable: false,
367
432
  detail
368
433
  };
369
- case 429:
434
+ case 429: {
435
+ const wait = retryAfterSeconds(err);
370
436
  return {
371
437
  type: "rate_limited",
372
438
  status,
373
- message: "Rate limited",
374
- recovery: "Wait and retry the request.",
375
- retryable: true
439
+ message: `${opts.service} is rate limiting these requests`,
440
+ recovery: wait === void 0 ? `${opts.service} did not say how long to wait. Pause a few seconds before retrying, and make fewer requests at once.` : `${opts.service} asked for a ${wait}s pause before the next request. Wait at least that long, then retry.`,
441
+ retryable: true,
442
+ ...wait === void 0 ? {} : { detail: { retryAfterSeconds: wait } }
376
443
  };
444
+ }
377
445
  }
378
446
  if (status >= 500) {
379
447
  return {
380
448
  type: "server",
381
449
  status,
382
- message: `Server error (HTTP ${status})`,
383
- recovery: `${opts.service} returned an internal error; retry shortly.`,
450
+ message: status === 503 ? `${opts.service} returned 503 (service unavailable)` : `${opts.service} returned ${status}`,
451
+ recovery: status === 503 ? `${opts.service} is temporarily refusing work. Retry shortly.` : `${opts.service} failed to complete the request. Usually temporary, so retry. If it persists, the request itself may be at fault.`,
384
452
  retryable: true,
385
453
  detail
386
454
  };
@@ -395,15 +463,59 @@ function normalize(err, opts) {
395
463
  };
396
464
  }
397
465
  const code = errorCode(err);
398
- if (code && NETWORK_CODES.includes(code) || NETWORK_CODES.some((c) => message.includes(c))) {
466
+ const transport = classifyTransport(err, code, message, opts);
467
+ if (transport)
468
+ return transport;
469
+ return { type: "unknown", message, recovery: "Unexpected error; inspect the message.", retryable: false };
470
+ }
471
+ function classifyTransport(err, code, message, opts) {
472
+ const matches = (codes) => code !== void 0 && codes.includes(code) || codes.some((c) => message.includes(c));
473
+ const host = targetHost(err);
474
+ const where = host ?? opts.service;
475
+ const urlVar = opts.urlEnvVar ?? "the base-URL environment variable";
476
+ if (matches(TRANSPORT_CODES.readTimeout)) {
477
+ const limit = configuredTimeoutMs(err);
478
+ return {
479
+ type: "timeout",
480
+ message: `${opts.service} did not respond${limit === void 0 ? " in time" : ` within ${humanMs(limit)}`}`,
481
+ recovery: `The request reached ${opts.service} but no response came back in time. Usually the server is busy or the query is too large. Retry, or ask for less: fewer results per page, fewer fields, a narrower query.`,
482
+ retryable: true
483
+ };
484
+ }
485
+ if (matches(TRANSPORT_CODES.connectTimeout)) {
486
+ const limit = configuredTimeoutMs(err);
487
+ return {
488
+ type: "timeout",
489
+ message: `Could not open a connection to ${where}${limit === void 0 ? "" : ` within ${humanMs(limit)}`}`,
490
+ recovery: `The host did not accept a connection in time. If it is reachable at all it is likely overloaded, so retry. If this repeats, confirm ${urlVar} points at a host this machine can reach.`,
491
+ retryable: true
492
+ };
493
+ }
494
+ if (matches(TRANSPORT_CODES.unresolved)) {
495
+ return {
496
+ type: "network",
497
+ message: `The host ${where} does not resolve`,
498
+ recovery: `${urlVar} points at a hostname that cannot be looked up from this machine. Check it for a typo. Retrying will not help until it changes.`,
499
+ retryable: false
500
+ };
501
+ }
502
+ if (matches(TRANSPORT_CODES.refused)) {
399
503
  return {
400
504
  type: "network",
401
- message: `Cannot connect to ${opts.service}: ${message}`,
402
- recovery: opts.networkRecovery ?? "Verify the *_URL is correct, the server is reachable, and you are on the VPN if required.",
505
+ message: `Nothing accepted a connection at ${where}`,
506
+ recovery: `The host resolved but refused the connection, usually a wrong port or a service that is not running. Retrying will not help until that changes.`,
507
+ retryable: false
508
+ };
509
+ }
510
+ if (matches(TRANSPORT_CODES.dropped)) {
511
+ return {
512
+ type: "network",
513
+ message: `The connection to ${opts.service} closed before a response arrived`,
514
+ recovery: `The connection dropped mid-request. This is usually a blip, so retry.`,
403
515
  retryable: true
404
516
  };
405
517
  }
406
- return { type: "unknown", message, recovery: "Unexpected error; inspect the message.", retryable: false };
518
+ return void 0;
407
519
  }
408
520
  function classifyError(err, opts) {
409
521
  const base = normalize(err, opts);
@@ -476,7 +588,7 @@ async function runCli(program, opts) {
476
588
 
477
589
  // src/program.ts
478
590
  import { styleText } from "util";
479
- import { Command as Command16 } from "commander";
591
+ import { Command as Command17 } from "commander";
480
592
 
481
593
  // src/commands/board/issues.ts
482
594
  import { Argument } from "commander";
@@ -531,9 +643,28 @@ function transformUser(user) {
531
643
  function transformField(field) {
532
644
  return field;
533
645
  }
534
- function transformCustomFieldOption(option) {
535
- const { self: _self, ...rest } = option;
536
- return rest;
646
+
647
+ // src/utils/transformers/field-meta.ts
648
+ function slimAllowedValue(raw) {
649
+ const out = {};
650
+ if (typeof raw.id === "string") out.id = raw.id;
651
+ if (typeof raw.name === "string") out.name = raw.name;
652
+ if (typeof raw.value === "string") out.value = raw.value;
653
+ if (Array.isArray(raw.children) && raw.children.length > 0) out.children = raw.children.map(slimAllowedValue);
654
+ return out;
655
+ }
656
+ function transformFieldMetaEntry(id, meta) {
657
+ const out = { id, required: meta.required === true };
658
+ if (typeof meta.name === "string") out.name = meta.name;
659
+ if (typeof meta.schema?.type === "string") out.type = meta.schema.type;
660
+ if (Array.isArray(meta.allowedValues) && meta.allowedValues.length > 0) {
661
+ out.allowedValues = meta.allowedValues.map(slimAllowedValue);
662
+ }
663
+ return out;
664
+ }
665
+ function transformFieldMeta(fields) {
666
+ const entries = Object.entries(fields).map(([id, meta]) => transformFieldMetaEntry(id, meta));
667
+ return [...entries.filter((f) => f.required), ...entries.filter((f) => !f.required)];
537
668
  }
538
669
 
539
670
  // src/utils/transformers/project.ts
@@ -715,6 +846,29 @@ function pruneSentinels(fields) {
715
846
  if (watches && watches.watchCount === 0) delete out.watches;
716
847
  return out;
717
848
  }
849
+ var ISSUE_KEY_PATTERN = /^[A-Z][A-Z0-9_]*-\d+$/;
850
+ function asIssueRefKey(value) {
851
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
852
+ const o = value;
853
+ if (typeof o.key !== "string" || !ISSUE_KEY_PATTERN.test(o.key)) return void 0;
854
+ if (typeof o.self !== "string" || !o.self.includes("/issue/")) return void 0;
855
+ return o.key;
856
+ }
857
+ function collapseIssueRefs(fields) {
858
+ const out = { ...fields };
859
+ for (const [key, value] of Object.entries(out)) {
860
+ const single = asIssueRefKey(value);
861
+ if (single !== void 0) {
862
+ out[key] = single;
863
+ continue;
864
+ }
865
+ if (Array.isArray(value) && value.length > 0) {
866
+ const keys = value.map(asIssueRefKey);
867
+ if (keys.every((k) => k !== void 0)) out[key] = keys;
868
+ }
869
+ }
870
+ return out;
871
+ }
718
872
  function transformIssueFields(fields) {
719
873
  const {
720
874
  // shaped sub-entities (recursed individually)
@@ -741,7 +895,7 @@ function transformIssueFields(fields) {
741
895
  // everything else: optional scalars, customfield_*, etc. — gets compacted
742
896
  ...rest
743
897
  } = fields;
744
- const compacted = pruneSentinels(compactRecord(rest));
898
+ const compacted = collapseIssueRefs(pruneSentinels(compactRecord(rest)));
745
899
  return {
746
900
  ...compacted,
747
901
  summary,
@@ -784,23 +938,23 @@ function transformIssueFields(fields) {
784
938
  }
785
939
 
786
940
  // src/utils/transformers/transition.ts
787
- function slimAllowedValue(raw) {
941
+ function slimAllowedValue2(raw) {
788
942
  if (!raw || typeof raw !== "object") return {};
789
943
  const av = raw;
790
944
  const slim = {};
791
945
  if (typeof av.id === "string") slim.id = av.id;
792
946
  if (typeof av.name === "string") slim.name = av.name;
793
947
  if (typeof av.value === "string") slim.value = av.value;
948
+ if (Array.isArray(av.children) && av.children.length > 0) slim.children = av.children.map(slimAllowedValue2);
794
949
  return slim;
795
950
  }
796
951
  function slimField(raw) {
797
952
  if (!raw || typeof raw !== "object") return {};
798
953
  const field = raw;
799
954
  const out = {};
800
- if (typeof field.required === "boolean") out.required = field.required;
801
955
  if (typeof field.name === "string") out.name = field.name;
802
956
  if (Array.isArray(field.allowedValues)) {
803
- out.allowedValues = field.allowedValues.map(slimAllowedValue);
957
+ out.allowedValues = field.allowedValues.map(slimAllowedValue2);
804
958
  }
805
959
  return out;
806
960
  }
@@ -1098,7 +1252,7 @@ function update(parent) {
1098
1252
  async (id, opts) => {
1099
1253
  const description = resolveTextOrFile(opts, "description", { required: false });
1100
1254
  if (opts.name === void 0 && description === void 0 && opts.lead === void 0 && opts.assigneeType === void 0) {
1101
- throw new Error("Provide at least one of --name, --description, --lead, --assignee-type");
1255
+ throw new CliUsageError("Provide at least one of --name, --description, --lead, --assignee-type");
1102
1256
  }
1103
1257
  const client = getClient();
1104
1258
  const result = await client.components.update({
@@ -1132,22 +1286,6 @@ function registerComponentCommands(program) {
1132
1286
  issueCount(component);
1133
1287
  }
1134
1288
 
1135
- // src/commands/field/options.ts
1136
- function options(parent) {
1137
- const cmd = parent.command("options").description("Get available options for a custom field").argument("<id>", "Field ID", text).option("--query <text>", "Filter options by text", text).option("--limit <number>", "Max results to return (1-1000)", intInRange(1, 1e3), 25).option("--start <number>", "Page number (1-indexed)", positiveInt);
1138
- examples(cmd, ["10001", '10001 --query "High"', "10001 --limit 20 --start 2"]);
1139
- cmd.action(async (id, opts) => {
1140
- const client = getClient();
1141
- const result = await client.fields.getFieldOptions({
1142
- fieldId: id,
1143
- query: opts.query,
1144
- maxResults: opts.limit,
1145
- page: opts.start
1146
- });
1147
- output(transformPaged({ ...result, startAt: result.startAt ?? 0 }, transformCustomFieldOption));
1148
- });
1149
- }
1150
-
1151
1289
  // src/commands/field/search.ts
1152
1290
  function search(parent) {
1153
1291
  const cmd = parent.command("search").description("Search for fields by name or ID").argument("<keyword>", "Search keyword", text).option("--limit <number>", "Maximum number of results (1-1000)", intInRange(1, 1e3), 25);
@@ -1162,9 +1300,8 @@ function search(parent) {
1162
1300
  // src/commands/field/index.ts
1163
1301
  function registerFieldCommands(program) {
1164
1302
  const field = program.command("field").description("Field operations");
1165
- examples(field, ['search "epic"', 'search "priority"', "options 10120"]);
1303
+ examples(field, ['search "epic"', 'search "priority"']);
1166
1304
  search(field);
1167
- options(field);
1168
1305
  }
1169
1306
 
1170
1307
  // src/utils/resolve-user.ts
@@ -1575,13 +1712,16 @@ function registerCommentCommands(parent) {
1575
1712
  }
1576
1713
 
1577
1714
  // src/commands/issue/create.ts
1715
+ import { Option as Option5 } from "commander";
1578
1716
  import { z as z2 } from "zod";
1579
- var customFieldsSchema = z2.record(z2.unknown());
1717
+ var fieldsSchema = z2.record(z2.unknown());
1580
1718
  function create3(parent) {
1581
1719
  const cmd = parent.command("create").description("Create a new issue").requiredOption("--project <key>", "Project key or ID", text).requiredOption("--type <name>", "Issue type name (e.g., Task, Bug, Story)", text).requiredOption("--summary <text>", "Issue summary/title", text).option("--assignee <user>", 'Assignee. Username, "me", or "none" to leave unassigned.', text).option("--reporter <user>", 'Reporter. Username or "me".', text).option("--priority <name>", "Priority name (e.g., High, Medium, Low)", text).option("--labels <labels>", "Comma-separated labels", text).option("--components <names>", "Comma-separated component names", text).option("--fix-versions <versions>", "Comma-separated fix version names", text).option("--due-date <date>", "Due date in YYYY-MM-DD format", date).option("--parent <key>", "Parent issue key (for subtasks)", issueKey).option(
1582
- "--custom-fields <json>",
1583
- `Additional custom fields as JSON (e.g., '{"customfield_10100": "EPIC-1"}')`,
1584
- jsonShape(customFieldsSchema)
1720
+ "--fields <json>",
1721
+ `Additional fields as JSON (e.g., '{"customfield_10100": "EPIC-1"}')`,
1722
+ jsonShape(fieldsSchema)
1723
+ ).addOption(
1724
+ new Option5("--custom-fields <json>", "Deprecated alias for --fields").argParser(jsonShape(fieldsSchema)).hideHelp()
1585
1725
  );
1586
1726
  textOrFileOption(cmd, "description", { description: "Issue description in wiki markup" });
1587
1727
  examples(cmd, [
@@ -1592,6 +1732,13 @@ function create3(parent) {
1592
1732
  cmd.action(
1593
1733
  async (opts) => {
1594
1734
  const description = resolveTextOrFile(opts, "description", { required: false });
1735
+ if (opts.fields && opts.customFields) {
1736
+ throw new CliUsageError(
1737
+ "Pass either --fields or --custom-fields, not both",
1738
+ "--custom-fields is a deprecated alias for --fields. Merge the two JSON objects into a single --fields value."
1739
+ );
1740
+ }
1741
+ const fields = opts.fields ?? opts.customFields;
1595
1742
  const assignee = opts.assignee !== void 0 ? await resolveUserToken(opts.assignee) : void 0;
1596
1743
  const reporter = opts.reporter !== void 0 ? await resolveUserToken(opts.reporter) : void 0;
1597
1744
  const client = getClient();
@@ -1608,13 +1755,31 @@ function create3(parent) {
1608
1755
  fixVersions: opts.fixVersions?.split(",").map((v) => v.trim()),
1609
1756
  dueDate: opts.dueDate,
1610
1757
  parent: opts.parent,
1611
- customFields: opts.customFields
1758
+ customFields: fields
1612
1759
  });
1613
1760
  output(transformCreatedIssue(result));
1614
1761
  }
1615
1762
  );
1616
1763
  }
1617
1764
 
1765
+ // src/commands/issue/createmeta.ts
1766
+ function createmeta(parent) {
1767
+ const cmd = parent.command("createmeta").description("Show which fields a new issue of a given type accepts, and the values they allow").requiredOption("--project <key>", "Project key", text).requiredOption("--type <name>", "Issue type name (e.g., Task, Bug, Story)", text);
1768
+ examples(cmd, ["--project PROJ --type Task", "--project PROJ --type Bug"]);
1769
+ cmd.action(async (opts) => {
1770
+ const client = getClient();
1771
+ const meta = await client.issues.getCreateMeta({ projectKey: opts.project, issueTypeName: opts.type });
1772
+ const issueType = meta.projects?.[0]?.issuetypes?.[0];
1773
+ if (!issueType?.fields) {
1774
+ throw new CliNotFoundError(
1775
+ `No create metadata for issue type "${opts.type}" in project "${opts.project}"`,
1776
+ "Verify the project key and issue type name, then check you may create that type there."
1777
+ );
1778
+ }
1779
+ output(transformFieldMeta(issueType.fields));
1780
+ });
1781
+ }
1782
+
1618
1783
  // src/commands/issue/delete.ts
1619
1784
  function deleteIssue(parent) {
1620
1785
  const cmd = parent.command("delete").description("Delete an issue").argument("<key>", "Issue key", issueKey).option("--delete-subtasks", "Also delete subtasks (default: false)");
@@ -1704,6 +1869,17 @@ function devStatus(parent) {
1704
1869
  });
1705
1870
  }
1706
1871
 
1872
+ // src/commands/issue/editmeta.ts
1873
+ function editmeta(parent) {
1874
+ const cmd = parent.command("editmeta").description("Show which fields this issue's edit screen accepts, and the values they allow").argument("<key>", "Issue key", issueKey);
1875
+ examples(cmd, ["PROJ-123"]);
1876
+ cmd.action(async (key) => {
1877
+ const client = getClient();
1878
+ const meta = await client.issues.getEditMeta({ issueKeyOrId: key });
1879
+ output(transformFieldMeta(meta.fields ?? {}));
1880
+ });
1881
+ }
1882
+
1707
1883
  // src/utils/constants.ts
1708
1884
  var DEFAULT_FIELDS = [
1709
1885
  "summary",
@@ -1832,41 +2008,60 @@ function search2(parent) {
1832
2008
  }
1833
2009
 
1834
2010
  // src/commands/issue/transition.ts
2011
+ import { z as z3 } from "zod";
2012
+ var fieldsSchema2 = z3.record(z3.unknown());
2013
+ function resolveTransitionId(to, transitions2) {
2014
+ const target = to.toLowerCase();
2015
+ const byName = transitions2.filter((t) => t.name.toLowerCase() === target);
2016
+ const matches = byName.length > 0 ? byName : transitions2.filter((t) => t.to.toLowerCase() === target);
2017
+ if (matches.length === 1) return matches[0].id;
2018
+ const available = transitions2.map((t) => ({ id: t.id, name: t.name, to: t.to }));
2019
+ if (matches.length === 0) {
2020
+ throw new CliUsageError(
2021
+ `Transition "${to}" not available from current status`,
2022
+ "Re-run --to with one of the listed transitions (by name, id, or target status).",
2023
+ { available }
2024
+ );
2025
+ }
2026
+ const ids = matches.map((t) => t.id).join(", ");
2027
+ throw new CliUsageError(
2028
+ `"${to}" matches more than one available transition`,
2029
+ `Re-run --to with one of these ids: ${ids}.`,
2030
+ { available }
2031
+ );
2032
+ }
1835
2033
  function transition(parent) {
1836
- const cmd = parent.command("transition").description("Transition issue to a new status").argument("<key>", "Issue key", issueKey).requiredOption("--to <idOrName>", "Transition ID, or status name (case-insensitive)", text);
2034
+ const cmd = parent.command("transition").description("Transition issue to a new status").argument("<key>", "Issue key", issueKey).requiredOption("--to <idOrName>", "Transition ID, transition name, or target status name (case-insensitive)", text).option(
2035
+ "--fields <json>",
2036
+ `Fields required by the transition screen, as JSON (e.g. '{"customfield_10100": {"value": "Approved"}}')`,
2037
+ jsonShape(fieldsSchema2)
2038
+ );
1837
2039
  commentOption(cmd, { description: "Comment to add during transition" });
1838
2040
  examples(cmd, [
1839
2041
  "PROJ-123 --to 31",
2042
+ 'PROJ-123 --to "Start Review"',
1840
2043
  'PROJ-123 --to "In Review"',
1841
- 'PROJ-123 --to Done --comment "Verified in staging"'
2044
+ 'PROJ-123 --to Done --comment "Verified in staging"',
2045
+ `PROJ-123 --to Approve --fields '{"customfield_10100": {"value": "Approved"}}'`
1842
2046
  ]);
1843
- cmd.action(async (key, opts) => {
1844
- const comment = resolveTextOrFile(opts, "comment", { required: false });
1845
- const client = getClient();
1846
- let transitionId;
1847
- if (/^\d+$/.test(opts.to)) {
1848
- transitionId = opts.to;
1849
- } else {
1850
- const raw = await client.issues.getTransitions({
1851
- issueKeyOrId: key,
1852
- expand: "transitions.fields"
1853
- });
1854
- const transitions2 = transformTransitions(raw);
1855
- const target = opts.to.toLowerCase();
1856
- const matches = transitions2.filter((t) => t.name.toLowerCase() === target);
1857
- if (matches.length === 0) {
1858
- const names = transitions2.map((t) => `"${t.name}"`).join(", ");
1859
- throw new Error(`Transition "${opts.to}" not available from current status. Available: ${names || "(none)"}`);
1860
- }
1861
- if (matches.length > 1) {
1862
- const ids = matches.map((t) => t.id).join(", ");
1863
- throw new Error(`Multiple transitions named "${opts.to}". Use --to <id> with one of: ${ids}`);
2047
+ cmd.action(
2048
+ async (key, opts) => {
2049
+ const comment = resolveTextOrFile(opts, "comment", { required: false });
2050
+ const client = getClient();
2051
+ let transitionId;
2052
+ if (/^\d+$/.test(opts.to)) {
2053
+ transitionId = opts.to;
2054
+ } else {
2055
+ const raw = await client.issues.getTransitions({
2056
+ issueKeyOrId: key,
2057
+ expand: "transitions.fields"
2058
+ });
2059
+ transitionId = resolveTransitionId(opts.to, transformTransitions(raw));
1864
2060
  }
1865
- transitionId = matches[0].id;
2061
+ await client.issues.transition({ issueKeyOrId: key, transitionId, fields: opts.fields, comment });
2062
+ output({ transitioned: true, issue: transformIssueRef(key) });
1866
2063
  }
1867
- await client.issues.transition({ issueKeyOrId: key, transitionId, comment });
1868
- output({ transitioned: true, issue: transformIssueRef(key) });
1869
- });
2064
+ );
1870
2065
  }
1871
2066
 
1872
2067
  // src/commands/issue/transitions.ts
@@ -1895,7 +2090,7 @@ function unlink2(parent) {
1895
2090
  }
1896
2091
 
1897
2092
  // src/commands/issue/update.ts
1898
- import { z as z3 } from "zod";
2093
+ import { z as z4 } from "zod";
1899
2094
 
1900
2095
  // src/utils/multi-value.ts
1901
2096
  import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
@@ -1924,7 +2119,7 @@ function parseMultiValue(flagName, raw) {
1924
2119
  }
1925
2120
 
1926
2121
  // src/commands/issue/update.ts
1927
- var fieldsSchema = z3.record(z3.unknown());
2122
+ var fieldsSchema3 = z4.record(z4.unknown());
1928
2123
  function buildUpdateOps(parsed, wrap) {
1929
2124
  if (parsed.mode !== "mutate") return void 0;
1930
2125
  return [...parsed.adds.map((v) => ({ add: wrap(v) })), ...parsed.removes.map((v) => ({ remove: wrap(v) }))];
@@ -1937,7 +2132,7 @@ function update3(parent) {
1937
2132
  const cmd = parent.command("update").description("Update issue fields").argument("<key>", "Issue key", issueKey).option(
1938
2133
  "--fields <json>",
1939
2134
  "JSON string of fields to update (advanced; merges with shortcuts, wins on conflict)",
1940
- jsonShape(fieldsSchema)
2135
+ jsonShape(fieldsSchema3)
1941
2136
  ).option("--no-notify-users", "Suppress notification emails (default: notify)").option("--attachments <paths>", "Comma-separated local file paths to attach", text).option("--summary <text>", "Set the issue summary", text).option("--priority <name>", "Set the priority by name (e.g. High)", text).option("--assignee <user>", 'Set the assignee. Username, "me", or "none" to unassign.', text).option("--labels <list>", 'Set labels ("a,b,c") or mutate ("+add,-remove")', text).option("--components <list>", 'Set components ("a,b") or mutate ("+add,-remove")', text).option("--fix-versions <list>", 'Set fix versions ("1.0,2.0") or mutate ("+1.0,-0.9")', text);
1942
2137
  textOrFileOption(cmd, "description", { description: "Set the issue description (wiki markup)" });
1943
2138
  examples(cmd, [
@@ -1952,7 +2147,7 @@ function update3(parent) {
1952
2147
  const description = resolveTextOrFile(opts, "description", { required: false });
1953
2148
  const hasShortcut = opts.summary !== void 0 || description !== void 0 || opts.priority !== void 0 || opts.assignee !== void 0 || opts.labels !== void 0 || opts.components !== void 0 || opts.fixVersions !== void 0;
1954
2149
  if (!opts.fields && !opts.attachments && !hasShortcut) {
1955
- throw new Error(
2150
+ throw new CliUsageError(
1956
2151
  "Provide at least one of --fields, --attachments, or a shortcut flag (--summary, --priority, --labels, ...)"
1957
2152
  );
1958
2153
  }
@@ -2040,12 +2235,12 @@ function create4(parent) {
2040
2235
  }
2041
2236
 
2042
2237
  // src/commands/issue/worklog/delete.ts
2043
- import { Option as Option5 } from "commander";
2238
+ import { Option as Option6 } from "commander";
2044
2239
  var ADJUST_ESTIMATE = ["new", "leave", "manual", "auto"];
2045
2240
  function deleteWorklog(parent) {
2046
2241
  const cmd = parent.command("delete").description("Delete a worklog entry").argument("<key>", "Issue key", issueKey);
2047
2242
  subEntityOption(cmd, "worklog", { mandatory: true });
2048
- cmd.addOption(new Option5("--adjust-estimate <mode>", "How to adjust the remaining estimate").choices(ADJUST_ESTIMATE)).option("--new-estimate <estimate>", 'New remaining estimate; required when --adjust-estimate is "new"', text).option(
2243
+ cmd.addOption(new Option6("--adjust-estimate <mode>", "How to adjust the remaining estimate").choices(ADJUST_ESTIMATE)).option("--new-estimate <estimate>", 'New remaining estimate; required when --adjust-estimate is "new"', text).option(
2049
2244
  "--increase-by <amount>",
2050
2245
  'Amount to increase the estimate by; required when --adjust-estimate is "manual"',
2051
2246
  text
@@ -2086,12 +2281,12 @@ function list4(parent) {
2086
2281
  }
2087
2282
 
2088
2283
  // src/commands/issue/worklog/update.ts
2089
- import { Option as Option6 } from "commander";
2284
+ import { Option as Option7 } from "commander";
2090
2285
  var ADJUST_ESTIMATE2 = ["new", "leave", "auto"];
2091
2286
  function update4(parent) {
2092
2287
  const cmd = parent.command("update").description("Update an existing worklog entry").argument("<key>", "Issue key", issueKey);
2093
2288
  subEntityOption(cmd, "worklog", { mandatory: true });
2094
- cmd.option("--time <timeSpent>", "Time spent (e.g., '2h', '30m', '1d 4h')", text).option("--started <datetime>", "Start time in ISO 8601 format", dateTime).addOption(new Option6("--adjust-estimate <mode>", "How to adjust the remaining estimate").choices(ADJUST_ESTIMATE2)).option("--new-estimate <estimate>", 'New remaining estimate; required when --adjust-estimate is "new"', text);
2289
+ cmd.option("--time <timeSpent>", "Time spent (e.g., '2h', '30m', '1d 4h')", text).option("--started <datetime>", "Start time in ISO 8601 format", dateTime).addOption(new Option7("--adjust-estimate <mode>", "How to adjust the remaining estimate").choices(ADJUST_ESTIMATE2)).option("--new-estimate <estimate>", 'New remaining estimate; required when --adjust-estimate is "new"', text);
2095
2290
  commentOption(cmd, { description: "Worklog comment" });
2096
2291
  examples(cmd, [
2097
2292
  'PROJ-123 --worklog-id 12345 --time "1h 30m"',
@@ -2149,6 +2344,8 @@ function registerIssueCommands(program) {
2149
2344
  deleteIssue(issue);
2150
2345
  transition(issue);
2151
2346
  transitions(issue);
2347
+ createmeta(issue);
2348
+ editmeta(issue);
2152
2349
  assign(issue);
2153
2350
  registerCommentCommands(issue);
2154
2351
  registerWorklogCommands(issue);
@@ -2250,10 +2447,10 @@ function issues2(parent) {
2250
2447
  }
2251
2448
 
2252
2449
  // src/commands/sprint/list.ts
2253
- import { Option as Option7 } from "commander";
2450
+ import { Option as Option8 } from "commander";
2254
2451
  var SPRINT_STATES = ["future", "active", "closed"];
2255
2452
  function list6(parent) {
2256
- const cmd = parent.command("list").description("List sprints for a board").requiredOption("--board <id>", "Board ID", positiveInt).addOption(new Option7("--state <state>", "Filter by sprint state").choices(SPRINT_STATES));
2453
+ const cmd = parent.command("list").description("List sprints for a board").requiredOption("--board <id>", "Board ID", positiveInt).addOption(new Option8("--state <state>", "Filter by sprint state").choices(SPRINT_STATES));
2257
2454
  examples(cmd, ["--board 42", "--board 42 --state active"]);
2258
2455
  cmd.action(async (opts) => {
2259
2456
  const client = getClient();
@@ -2266,10 +2463,10 @@ function list6(parent) {
2266
2463
  }
2267
2464
 
2268
2465
  // src/commands/sprint/update.ts
2269
- import { Argument as Argument4, Option as Option8 } from "commander";
2466
+ import { Argument as Argument4, Option as Option9 } from "commander";
2270
2467
  var SPRINT_STATES2 = ["future", "active", "closed"];
2271
2468
  function update5(parent) {
2272
- const cmd = parent.command("update").description("Update an existing sprint").addArgument(new Argument4("<id>", "Sprint ID").argParser(positiveInt)).option("--name <name>", "New sprint name", text).addOption(new Option8("--state <state>", "New sprint state").choices(SPRINT_STATES2)).option("--start-date <date>", "New start date in ISO 8601 format", text).option("--end-date <date>", "New end date in ISO 8601 format", text).option("--goal <goal>", "New sprint goal", text);
2469
+ const cmd = parent.command("update").description("Update an existing sprint").addArgument(new Argument4("<id>", "Sprint ID").argParser(positiveInt)).option("--name <name>", "New sprint name", text).addOption(new Option9("--state <state>", "New sprint state").choices(SPRINT_STATES2)).option("--start-date <date>", "New start date in ISO 8601 format", text).option("--end-date <date>", "New end date in ISO 8601 format", text).option("--goal <goal>", "New sprint goal", text);
2273
2470
  examples(cmd, [
2274
2471
  '100 --name "Sprint 10 - Extended"',
2275
2472
  "100 --state active",
@@ -2309,10 +2506,10 @@ function registerSprintCommands(program) {
2309
2506
 
2310
2507
  // src/commands/token/client.ts
2311
2508
  import { JiraClient as JiraClient2 } from "jira-data-center-client";
2312
- function getTokenClient(options2 = {}) {
2509
+ function getTokenClient(options = {}) {
2313
2510
  const baseUrl = process.env.JIRA_URL;
2314
- const username = options2.basicUsername ?? process.env.JIRA_BASIC_USERNAME;
2315
- const password = options2.basicPassword ?? process.env.JIRA_BASIC_PASSWORD;
2511
+ const username = options.basicUsername ?? process.env.JIRA_BASIC_USERNAME;
2512
+ const password = options.basicPassword ?? process.env.JIRA_BASIC_PASSWORD;
2316
2513
  if (!baseUrl || !username || !password) {
2317
2514
  const missing = [
2318
2515
  ...!baseUrl ? ["JIRA_URL"] : [],
@@ -2453,12 +2650,12 @@ function registerUserCommands(program) {
2453
2650
  function assertExactlyOne(opts, keys, label) {
2454
2651
  const present = keys.filter((k) => opts[k] !== void 0);
2455
2652
  if (present.length !== 1)
2456
- throw new Error(`Provide exactly one of ${keys.map((k) => `--${k}`).join(", ")} (${label}).`);
2653
+ throw new CliUsageError(`Provide exactly one of ${keys.map((k) => `--${k}`).join(", ")} (${label}).`);
2457
2654
  return present[0];
2458
2655
  }
2459
2656
  function assertAtLeastOne(opts, keys) {
2460
2657
  if (!keys.some((k) => opts[k] !== void 0))
2461
- throw new Error(`Provide at least one of ${keys.map((k) => `--${k}`).join(", ")}.`);
2658
+ throw new CliUsageError(`Provide at least one of ${keys.map((k) => `--${k}`).join(", ")}.`);
2462
2659
  }
2463
2660
 
2464
2661
  // src/commands/xray/execution/add.ts
@@ -2489,7 +2686,9 @@ function add(parent) {
2489
2686
  }
2490
2687
  const allKeys = [...keys];
2491
2688
  if (allKeys.length === 0) {
2492
- throw new Error("No tests to add \u2014 the given --set(s) contain no tests and no --test keys were provided.");
2689
+ throw new CliUsageError(
2690
+ "No tests to add \u2014 the given --set(s) contain no tests and no --test keys were provided."
2691
+ );
2493
2692
  }
2494
2693
  const result = await client.testExecutions.addTests({ execKey, keys: allKeys });
2495
2694
  output(result ?? { added: allKeys });
@@ -2733,7 +2932,7 @@ function update7(parent) {
2733
2932
  ]);
2734
2933
  cmd.action(async (folderId, opts) => {
2735
2934
  if (opts.name === void 0 && opts.rank === void 0) {
2736
- throw new Error("Provide at least one of --name or --rank.");
2935
+ throw new CliUsageError("Provide at least one of --name or --rank.");
2737
2936
  }
2738
2937
  const client = getClient();
2739
2938
  await client.testRepository.updateFolder({
@@ -2763,12 +2962,12 @@ function registerFolderCommands(xray) {
2763
2962
  // src/commands/xray/import/execution.ts
2764
2963
  import { readFileSync as readFileSync4 } from "fs";
2765
2964
  import { basename as basename2 } from "path";
2766
- import { Option as Option9 } from "commander";
2965
+ import { Option as Option10 } from "commander";
2767
2966
  var FORMATS = ["xray", "junit", "testng", "nunit", "xunit", "robot", "cucumber", "behave"];
2768
2967
  var NEEDS_SCOPE = /* @__PURE__ */ new Set(["junit", "testng", "nunit", "xunit", "robot"]);
2769
2968
  function execution(parent) {
2770
2969
  const cmd = parent.command("execution").description("Import test execution results into Xray").requiredOption("--file <path>", "Path to the result file", filePath).addOption(
2771
- new Option9(
2970
+ new Option10(
2772
2971
  "--format <format>",
2773
2972
  "Result format. xray/cucumber/behave: JSON body, no --project/--execution required. junit/testng/nunit/xunit/robot: XML body, requires --project OR --execution."
2774
2973
  ).choices(FORMATS).makeOptionMandatory()
@@ -2789,7 +2988,7 @@ function execution(parent) {
2789
2988
  cmd.action(
2790
2989
  async (opts) => {
2791
2990
  if (NEEDS_SCOPE.has(opts.format) && !opts.project && !opts.execution) {
2792
- throw new Error(
2991
+ throw new CliUsageError(
2793
2992
  `--project or --execution is required for ${opts.format} format. Provide --project <key> to create a new execution, or --execution <key> to import into an existing one.`
2794
2993
  );
2795
2994
  }
@@ -2966,7 +3165,7 @@ function add4(parent) {
2966
3165
  }
2967
3166
 
2968
3167
  // src/commands/xray/precondition/create.ts
2969
- import { Option as Option10 } from "commander";
3168
+ import { Option as Option11 } from "commander";
2970
3169
 
2971
3170
  // src/utils/xray/fields.ts
2972
3171
  var CONCEPT_SUFFIX = {
@@ -2997,18 +3196,22 @@ function resolveTestTypeValue(allowedValues, type) {
2997
3196
  if (type === "manual") {
2998
3197
  const match2 = allowedValues.find((v) => /manual/i.test(v));
2999
3198
  if (!match2)
3000
- throw new Error(`No 'manual' Test Type value found on this instance. Available: ${allowedValues.join(", ")}`);
3199
+ throw new CliNotFoundError(
3200
+ `No 'manual' Test Type value found on this instance. Available: ${allowedValues.join(", ")}`
3201
+ );
3001
3202
  return match2;
3002
3203
  }
3003
3204
  if (type === "generic") {
3004
3205
  const match2 = allowedValues.find((v) => /generic/i.test(v));
3005
3206
  if (!match2)
3006
- throw new Error(`No 'generic' Test Type value found on this instance. Available: ${allowedValues.join(", ")}`);
3207
+ throw new CliNotFoundError(
3208
+ `No 'generic' Test Type value found on this instance. Available: ${allowedValues.join(", ")}`
3209
+ );
3007
3210
  return match2;
3008
3211
  }
3009
3212
  const match = allowedValues.find((v) => !/manual/i.test(v) && !/generic/i.test(v));
3010
3213
  if (!match)
3011
- throw new Error(
3214
+ throw new CliNotFoundError(
3012
3215
  `No 'cucumber' (gherkin/automation) Test Type value found on this instance. Available: ${allowedValues.join(", ")}`
3013
3216
  );
3014
3217
  return match;
@@ -3029,7 +3232,7 @@ async function resolveTypeValue(client, params) {
3029
3232
  }
3030
3233
  function requireField(map, concept, label) {
3031
3234
  const id = map[concept];
3032
- if (!id) throw new Error(`Xray field for '${label}' not found on this instance.`);
3235
+ if (!id) throw new CliNotFoundError(`Xray field for '${label}' not found on this instance.`);
3033
3236
  return id;
3034
3237
  }
3035
3238
  function buildTestCustomFields(map, opts) {
@@ -3067,7 +3270,7 @@ function create10(parent) {
3067
3270
  const cmd = parent.command("create").description(
3068
3271
  "Create a new Xray Pre-Condition issue. Note: the Pre-Condition issue type is only available in projects that have it configured (e.g. BD); it is not on your project scheme."
3069
3272
  ).requiredOption("--project <key>", "Project key (e.g. BD)", text).requiredOption("--summary <text>", "Pre-Condition summary (title)", text).addOption(
3070
- new Option10("--type <type>", "Pre-Condition type").choices(["manual", "generic", "cucumber"]).default("manual")
3273
+ new Option11("--type <type>", "Pre-Condition type").choices(["manual", "generic", "cucumber"]).default("manual")
3071
3274
  );
3072
3275
  textOrFileOption(cmd, "condition", { description: "Pre-condition body / definition text" });
3073
3276
  textOrFileOption(cmd, "description", { description: "Pre-Condition issue description" });
@@ -3138,9 +3341,9 @@ function remove4(parent) {
3138
3341
  }
3139
3342
 
3140
3343
  // src/commands/xray/precondition/update.ts
3141
- import { Option as Option11 } from "commander";
3344
+ import { Option as Option12 } from "commander";
3142
3345
  function update9(parent) {
3143
- const cmd = parent.command("update").description("Update an Xray Pre-Condition issue").argument("<preKey>", "Pre-Condition issue key (e.g. PROJ-50)", issueKey).option("--summary <text>", "New summary (title)", text).addOption(new Option11("--type <type>", "New Pre-Condition type").choices(["manual", "generic", "cucumber"]));
3346
+ const cmd = parent.command("update").description("Update an Xray Pre-Condition issue").argument("<preKey>", "Pre-Condition issue key (e.g. PROJ-50)", issueKey).option("--summary <text>", "New summary (title)", text).addOption(new Option12("--type <type>", "New Pre-Condition type").choices(["manual", "generic", "cucumber"]));
3144
3347
  textOrFileOption(cmd, "condition", { description: "New pre-condition body / definition text" });
3145
3348
  textOrFileOption(cmd, "description", { description: "New Pre-Condition issue description" });
3146
3349
  examples(cmd, ['PROJ-50 --summary "Updated precondition"', 'PROJ-50 --type generic --condition "apiKey != null"']);
@@ -3163,12 +3366,12 @@ function update9(parent) {
3163
3366
  fieldMap: map
3164
3367
  });
3165
3368
  const typeId = map["precondition-type"];
3166
- if (!typeId) throw new Error("Xray field for 'Pre-Condition Type' not found on this instance.");
3369
+ if (!typeId) throw new CliNotFoundError("Xray field for 'Pre-Condition Type' not found on this instance.");
3167
3370
  customFields[typeId] = { value: typeValue };
3168
3371
  }
3169
3372
  if (condition !== void 0) {
3170
3373
  const bodyId = map["precondition-body"];
3171
- if (!bodyId) throw new Error("Xray field for 'Conditions' not found on this instance.");
3374
+ if (!bodyId) throw new CliNotFoundError("Xray field for 'Conditions' not found on this instance.");
3172
3375
  customFields[bodyId] = condition;
3173
3376
  }
3174
3377
  }
@@ -3199,9 +3402,10 @@ function registerPreconditionCommands(xray) {
3199
3402
  // src/utils/xray/run-id.ts
3200
3403
  async function resolveRunId(client, o) {
3201
3404
  if (o.runId !== void 0) return o.runId;
3202
- if (!o.execution || !o.test) throw new Error("Provide a run id, or both --execution and --test to resolve the run.");
3405
+ if (!o.execution || !o.test)
3406
+ throw new CliUsageError("Provide a run id, or both --execution and --test to resolve the run.");
3203
3407
  const run = await client.testRuns.resolve({ execKey: o.execution, testKey: o.test });
3204
- if (!run?.id) throw new Error(`No test run found for ${o.test} in ${o.execution}.`);
3408
+ if (!run?.id) throw new CliNotFoundError(`No test run found for ${o.test} in ${o.execution}.`);
3205
3409
  return Number(run.id);
3206
3410
  }
3207
3411
 
@@ -3573,7 +3777,7 @@ function update12(parent) {
3573
3777
  const data = resolveTextOrFile(opts, "data", { required: false });
3574
3778
  const result = resolveTextOrFile(opts, "result", { required: false });
3575
3779
  if (action === void 0 && data === void 0 && result === void 0) {
3576
- throw new Error(
3780
+ throw new CliUsageError(
3577
3781
  "Provide at least one of --action (or --action-file), --data (or --data-file), --result (or --result-file)."
3578
3782
  );
3579
3783
  }
@@ -3597,16 +3801,16 @@ function registerStepCommands(xray) {
3597
3801
  }
3598
3802
 
3599
3803
  // src/commands/xray/test/create.ts
3600
- import { Option as Option12 } from "commander";
3601
- import { z as z4 } from "zod";
3602
- var stepsSchema = z4.array(
3603
- z4.object({ action: z4.string(), data: z4.string().optional(), result: z4.string().optional() })
3804
+ import { Option as Option13 } from "commander";
3805
+ import { z as z5 } from "zod";
3806
+ var stepsSchema = z5.array(
3807
+ z5.object({ action: z5.string(), data: z5.string().optional(), result: z5.string().optional() })
3604
3808
  );
3605
3809
  function create11(parent) {
3606
3810
  const cmd = parent.command("create").description("Create a new Xray Test issue").requiredOption("--project <key>", "Project key (e.g. AI)", text).requiredOption("--summary <text>", "Test summary (title)", text).addOption(
3607
- new Option12("--type <type>", "Test type (when cucumber: also pass --gherkin; when generic: --definition)").choices(["manual", "cucumber", "generic"]).default("manual")
3811
+ new Option13("--type <type>", "Test type (when cucumber: also pass --gherkin; when generic: --definition)").choices(["manual", "cucumber", "generic"]).default("manual")
3608
3812
  ).addOption(
3609
- new Option12("--cucumber-type <cucumberType>", "Cucumber scenario type (required when --type cucumber)").choices([
3813
+ new Option13("--cucumber-type <cucumberType>", "Cucumber scenario type (required when --type cucumber)").choices([
3610
3814
  "scenario",
3611
3815
  "scenario-outline"
3612
3816
  ])
@@ -3634,10 +3838,10 @@ function create11(parent) {
3634
3838
  const definition = resolveTextOrFile(opts, "definition", { required: false });
3635
3839
  const description = resolveTextOrFile(opts, "description", { required: false });
3636
3840
  if (opts.type === "cucumber" && !gherkin) {
3637
- throw new Error("--gherkin (or --gherkin-file) is required when --type is cucumber.");
3841
+ throw new CliUsageError("--gherkin (or --gherkin-file) is required when --type is cucumber.");
3638
3842
  }
3639
3843
  if (opts.type === "generic" && !definition) {
3640
- throw new Error("--definition (or --definition-file) is required when --type is generic.");
3844
+ throw new CliUsageError("--definition (or --definition-file) is required when --type is generic.");
3641
3845
  }
3642
3846
  const cucumberType = opts.cucumberType === "scenario" ? "Scenario" : opts.cucumberType === "scenario-outline" ? "Scenario Outline" : void 0;
3643
3847
  const client = getClient();
@@ -3712,13 +3916,13 @@ function list18(parent) {
3712
3916
  const client = getClient();
3713
3917
  if (opts.folder !== void 0) {
3714
3918
  if (!opts.project) {
3715
- throw new Error("--project is required when --folder is specified.");
3919
+ throw new CliUsageError("--project is required when --folder is specified.");
3716
3920
  }
3717
3921
  const otherScopes = ["set", "plan", "execution", "precondition"].filter(
3718
3922
  (k) => opts[k] !== void 0
3719
3923
  );
3720
3924
  if (otherScopes.length > 0) {
3721
- throw new Error(
3925
+ throw new CliUsageError(
3722
3926
  `--folder cannot be combined with ${otherScopes.map((k) => `--${k}`).join(", ")}. Use --folder with --project only.`
3723
3927
  );
3724
3928
  }
@@ -3776,10 +3980,10 @@ function list18(parent) {
3776
3980
  }
3777
3981
 
3778
3982
  // src/commands/xray/test/update.ts
3779
- import { Option as Option13 } from "commander";
3983
+ import { Option as Option14 } from "commander";
3780
3984
  function update13(parent) {
3781
- const cmd = parent.command("update").description("Update an Xray Test issue").argument("<testKey>", "Test issue key (e.g. PROJ-584)", issueKey).option("--summary <text>", "New summary (title)", text).addOption(new Option13("--type <type>", "Test type").choices(["manual", "cucumber", "generic"])).addOption(
3782
- new Option13("--cucumber-type <cucumberType>", "Cucumber scenario type (required when --type cucumber)").choices([
3985
+ const cmd = parent.command("update").description("Update an Xray Test issue").argument("<testKey>", "Test issue key (e.g. PROJ-584)", issueKey).option("--summary <text>", "New summary (title)", text).addOption(new Option14("--type <type>", "Test type").choices(["manual", "cucumber", "generic"])).addOption(
3986
+ new Option14("--cucumber-type <cucumberType>", "Cucumber scenario type (required when --type cucumber)").choices([
3783
3987
  "scenario",
3784
3988
  "scenario-outline"
3785
3989
  ])
@@ -3810,10 +4014,10 @@ function update13(parent) {
3810
4014
  if (opts.reporter !== void 0) fields.reporter = { name: opts.reporter };
3811
4015
  if (opts.type !== void 0) {
3812
4016
  if (opts.type === "cucumber" && !gherkin) {
3813
- throw new Error("--gherkin (or --gherkin-file) is required when --type is cucumber.");
4017
+ throw new CliUsageError("--gherkin (or --gherkin-file) is required when --type is cucumber.");
3814
4018
  }
3815
4019
  if (opts.type === "generic" && !definition) {
3816
- throw new Error("--definition (or --definition-file) is required when --type is generic.");
4020
+ throw new CliUsageError("--definition (or --definition-file) is required when --type is generic.");
3817
4021
  }
3818
4022
  const cucumberType = opts.cucumberType === "scenario" ? "Scenario" : opts.cucumberType === "scenario-outline" ? "Scenario Outline" : void 0;
3819
4023
  const projectKey = testKey.split("-")[0];
@@ -3967,7 +4171,7 @@ function registerXrayCommands(program) {
3967
4171
  var DIM = "\x1B[2m";
3968
4172
  var RESET = "\x1B[0m";
3969
4173
  function buildProgram() {
3970
- const program = new Command16();
4174
+ const program = new Command17();
3971
4175
  program.name("jiradc").description("Jira Data Center CLI").version(readPackageVersion(import.meta.url)).configureHelp({
3972
4176
  styleTitle: (str) => styleText("bold", str),
3973
4177
  styleUsage: (str) => styleText("dim", str),
@@ -4028,10 +4232,33 @@ function getCredentialInfo() {
4028
4232
  };
4029
4233
  }
4030
4234
 
4235
+ // src/utils/field-error-recovery.ts
4236
+ var CUSTOM_FIELD_KEY = /^customfield_(\d+)$/;
4237
+ function customFieldIds(detail) {
4238
+ if (!detail || typeof detail !== "object") return [];
4239
+ const errors = detail.errors;
4240
+ if (!errors || typeof errors !== "object") return [];
4241
+ return Object.keys(errors).filter((k) => CUSTOM_FIELD_KEY.test(k));
4242
+ }
4243
+ function adaptFieldErrors(normalized) {
4244
+ const ids = customFieldIds(normalized.detail);
4245
+ if (ids.length === 0) return normalized;
4246
+ const numbers = ids.map((id) => CUSTOM_FIELD_KEY.exec(id)?.[1] ?? id);
4247
+ const lookup = numbers.map((n) => `jiradc field search ${n}`).join(", ");
4248
+ const plural = ids.length > 1 ? "s" : "";
4249
+ return {
4250
+ ...normalized,
4251
+ recovery: `Jira rejected field${plural} ${ids.join(", ")}. Run ${lookup} to see the field's name and type before retrying. For the values a field accepts, use issue transitions (transition screens), issue createmeta (create) or issue editmeta (edit).`
4252
+ };
4253
+ }
4254
+
4031
4255
  // src/index.ts
4032
4256
  await runCli(buildProgram(), {
4033
4257
  credentialInfo: getCredentialInfo,
4034
4258
  service: "Jira",
4035
4259
  authRecovery: "Set the JIRA_URL and JIRA_TOKEN environment variables.",
4036
- networkRecovery: "Verify JIRA_URL is correct, the server is reachable, and you are on the VPN if required."
4260
+ urlEnvVar: "JIRA_URL",
4261
+ // Jira names rejected fields by custom-field id. The generic recovery leaves
4262
+ // an agent holding an opaque id, so point it at the command that explains one.
4263
+ adapt: adaptFieldErrors
4037
4264
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiradc-cli",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "publish": true,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "commander": "^13.1.0",
15
15
  "zod": "^3.25.67",
16
- "jira-data-center-client": "1.0.41"
16
+ "jira-data-center-client": "1.0.44"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "24.10.4",
@@ -23,15 +23,17 @@
23
23
  "tsx": "^4.19.2",
24
24
  "typescript": "^5.7.2",
25
25
  "vitest": "^4.0.16",
26
- "cli-utils": "1.0.0",
26
+ "config-typescript": "0.0.0",
27
27
  "config-eslint": "0.0.0",
28
- "config-typescript": "0.0.0"
28
+ "cli-utils": "1.0.0"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=22.0.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsup",
35
+ "check-types": "tsc --noEmit -p tsconfig.json",
36
+ "clean": "rm -rf dist",
35
37
  "dev": "tsx src/index.ts",
36
38
  "lint": "eslint src tests",
37
39
  "lint:fix": "eslint src tests --fix",