jiradc-cli 1.0.33 → 1.0.35

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 (2) hide show
  1. package/dist/index.js +285 -24
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4,6 +4,45 @@
4
4
  import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
+ var DEFAULT_TTL = 36e5;
8
+ function getCacheDir(name) {
9
+ return join(homedir(), ".cache", name);
10
+ }
11
+ function getCachePath(name, key) {
12
+ return join(getCacheDir(name), `${key}.json`);
13
+ }
14
+ function cacheGet(options, key) {
15
+ const ttl = options.ttl ?? DEFAULT_TTL;
16
+ const path = getCachePath(options.name, key);
17
+ try {
18
+ const stat = statSync(path);
19
+ if (Date.now() - stat.mtimeMs > ttl)
20
+ return null;
21
+ const raw = readFileSync(path, "utf-8");
22
+ const entry = JSON.parse(raw);
23
+ return entry.data;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function cacheSet(options, key, data) {
29
+ const dir = getCacheDir(options.name);
30
+ const path = getCachePath(options.name, key);
31
+ const entry = { data, timestamp: Date.now() };
32
+ try {
33
+ mkdirSync(dir, { recursive: true });
34
+ writeFileSync(path, JSON.stringify(entry));
35
+ } catch {
36
+ }
37
+ }
38
+ async function cacheGetOrFetch(options, key, fetcher) {
39
+ const cached = cacheGet(options, key);
40
+ if (cached !== null)
41
+ return cached;
42
+ const data = await fetcher();
43
+ cacheSet(options, key, data);
44
+ return data;
45
+ }
7
46
 
8
47
  // ../../cli-utils/dist/bootstrap.js
9
48
  import { readFileSync as readFileSync2 } from "fs";
@@ -266,6 +305,16 @@ var SubcommandRequiredError = class extends Error {
266
305
  this.subcommands = subcommands;
267
306
  }
268
307
  };
308
+ var UnknownSubcommandError = class extends Error {
309
+ subcommands;
310
+ suggestion;
311
+ constructor(commandPath3, token, subcommands, suggestion) {
312
+ super(`'${token}' is not a subcommand of '${commandPath3}'`);
313
+ this.name = "UnknownSubcommandError";
314
+ this.subcommands = subcommands;
315
+ this.suggestion = suggestion;
316
+ }
317
+ };
269
318
  var TYPE_EXIT = {
270
319
  usage: EXIT.USAGE,
271
320
  not_found: EXIT.NOT_FOUND,
@@ -368,6 +417,16 @@ function normalize(err, opts) {
368
417
  detail: { subcommands: err.subcommands }
369
418
  };
370
419
  }
420
+ if (err instanceof UnknownSubcommandError) {
421
+ const didYouMean = err.suggestion === void 0 ? "" : `Did you mean '${err.suggestion}'? `;
422
+ return {
423
+ type: "usage",
424
+ message,
425
+ recovery: `${didYouMean}Valid subcommands: ${err.subcommands.join(", ")}.`,
426
+ retryable: false,
427
+ detail: { subcommands: err.subcommands }
428
+ };
429
+ }
371
430
  if (err instanceof CliAuthError) {
372
431
  return { type: "auth", message: message || "Missing credentials", recovery: opts.authRecovery, retryable: false };
373
432
  }
@@ -568,11 +627,39 @@ function attachSubcommandGuards(cmd) {
568
627
  const hasAction = Boolean(cmd._actionHandler);
569
628
  if (hasAction)
570
629
  return;
571
- cmd.action(() => {
630
+ cmd.allowExcessArguments(true);
631
+ cmd.action((...params) => {
632
+ const invoked = params[params.length - 1];
572
633
  const names = cmd.commands.map((c) => c.name()).filter((name) => name !== "help");
573
- throw new SubcommandRequiredError(commandPath2(cmd), names);
634
+ const [token] = invoked.args;
635
+ if (token === void 0)
636
+ throw new SubcommandRequiredError(commandPath2(cmd), names);
637
+ throw new UnknownSubcommandError(commandPath2(cmd), token, names, suggestSubcommand(token, names));
574
638
  });
575
639
  }
640
+ function editDistance(a, b) {
641
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
642
+ for (let i = 1; i <= a.length; i++) {
643
+ const row = [i];
644
+ for (let j = 1; j <= b.length; j++) {
645
+ const substitution = prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
646
+ row[j] = Math.min(row[j - 1] + 1, prev[j] + 1, substitution);
647
+ }
648
+ prev = row;
649
+ }
650
+ return prev[b.length];
651
+ }
652
+ function suggestSubcommand(token, names) {
653
+ const MAX_EDITS = 2;
654
+ let best;
655
+ for (const name of names) {
656
+ const distance = editDistance(token.toLowerCase(), name.toLowerCase());
657
+ if (distance <= MAX_EDITS && distance < token.length && (!best || distance < best.distance)) {
658
+ best = { name, distance };
659
+ }
660
+ }
661
+ return best?.name;
662
+ }
576
663
  async function runCli(program, opts) {
577
664
  routeErrors(program);
578
665
  attachSubcommandGuards(program);
@@ -805,12 +892,22 @@ function transformIssueLink(l) {
805
892
  ...outwardIssue ? { outwardIssue: transformIssueBasic(outwardIssue) } : {}
806
893
  };
807
894
  }
895
+ function customFieldNames(names, fields) {
896
+ if (!names) return void 0;
897
+ const kept = Object.entries(names).filter(
898
+ ([id]) => id.startsWith("customfield_") && fields[id] !== void 0
899
+ );
900
+ return kept.length > 0 ? Object.fromEntries(kept) : void 0;
901
+ }
808
902
  function transformIssue(issue) {
809
- const { self: _self, expand: _expand, names: _names, schema: _schema, fields, ...rest } = issue;
903
+ const { self: _self, expand: _expand, names, schema: _schema, fields, ...rest } = issue;
904
+ const transformed = transformIssueFields(fields, issue.key);
905
+ const fieldNames = customFieldNames(names, transformed);
810
906
  return {
811
907
  ...rest,
812
908
  url: issueBrowseUrl(issue.key),
813
- fields: transformIssueFields(fields)
909
+ fields: transformed,
910
+ ...fieldNames ? { names: fieldNames } : {}
814
911
  };
815
912
  }
816
913
  function isEmpty(v) {
@@ -846,6 +943,20 @@ function pruneSentinels(fields) {
846
943
  if (watches && watches.watchCount === 0) delete out.watches;
847
944
  return out;
848
945
  }
946
+ var DEV_STATUS_DUMP = /com\.atlassian\.jira\.plugin\.devstatus/;
947
+ function collapseDevStatus(fields, key) {
948
+ const out = { ...fields };
949
+ for (const [field, value] of Object.entries(out)) {
950
+ if (typeof value === "string" && DEV_STATUS_DUMP.test(value)) {
951
+ out[field] = `(use: jiradc issue dev-status ${key})`;
952
+ }
953
+ }
954
+ return out;
955
+ }
956
+ function transformInlineComponent(c) {
957
+ const { self: _self, description: _description, ...rest } = c;
958
+ return rest;
959
+ }
849
960
  var ISSUE_KEY_PATTERN = /^[A-Z][A-Z0-9_]*-\d+$/;
850
961
  function asIssueRefKey(value) {
851
962
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -869,7 +980,8 @@ function collapseIssueRefs(fields) {
869
980
  }
870
981
  return out;
871
982
  }
872
- function transformIssueFields(fields) {
983
+ function transformIssueFields(fields, key) {
984
+ if (!fields) return {};
873
985
  const {
874
986
  // shaped sub-entities (recursed individually)
875
987
  issuetype,
@@ -895,9 +1007,11 @@ function transformIssueFields(fields) {
895
1007
  // everything else: optional scalars, customfield_*, etc. — gets compacted
896
1008
  ...rest
897
1009
  } = fields;
898
- const compacted = collapseIssueRefs(pruneSentinels(compactRecord(rest)));
1010
+ const compacted = collapseDevStatus(collapseIssueRefs(pruneSentinels(compactRecord(rest))), key);
1011
+ const components = compacted.components;
899
1012
  return {
900
1013
  ...compacted,
1014
+ ...Array.isArray(components) ? { components: components.map(transformInlineComponent) } : {},
901
1015
  summary,
902
1016
  project,
903
1017
  created,
@@ -1881,9 +1995,8 @@ function editmeta(parent) {
1881
1995
  }
1882
1996
 
1883
1997
  // src/utils/constants.ts
1884
- var DEFAULT_FIELDS = [
1998
+ var SEARCH_DEFAULT_FIELDS = [
1885
1999
  "summary",
1886
- "description",
1887
2000
  "status",
1888
2001
  "assignee",
1889
2002
  "reporter",
@@ -1892,13 +2005,117 @@ var DEFAULT_FIELDS = [
1892
2005
  "created",
1893
2006
  "updated",
1894
2007
  "issuetype",
1895
- "components",
1896
- "comment"
2008
+ "components"
1897
2009
  ];
2010
+ var DEFAULT_FIELDS = [...SEARCH_DEFAULT_FIELDS, "description", "comment"];
2011
+
2012
+ // src/utils/field-selectors.ts
2013
+ var WILDCARD = "*";
2014
+ var NEGATION = "-";
2015
+ function buildFieldNameIndex(fields) {
2016
+ const ids = /* @__PURE__ */ new Set();
2017
+ const idsByLower = /* @__PURE__ */ new Map();
2018
+ const byName = /* @__PURE__ */ new Map();
2019
+ const record = (alias, id) => {
2020
+ const key = alias.toLowerCase();
2021
+ const already = byName.get(key);
2022
+ if (!already) {
2023
+ byName.set(key, [id]);
2024
+ return;
2025
+ }
2026
+ if (!already.includes(id)) already.push(id);
2027
+ };
2028
+ for (const field of fields) {
2029
+ ids.add(field.id);
2030
+ idsByLower.set(field.id.toLowerCase(), field.id);
2031
+ record(field.name, field.id);
2032
+ for (const clause of field.clauseNames ?? []) record(clause, field.id);
2033
+ }
2034
+ return { ids, idsByLower, byName };
2035
+ }
2036
+ function candidatesFor(token, index) {
2037
+ if (index.ids.has(token)) return [token];
2038
+ const byId = index.idsByLower.get(token.toLowerCase());
2039
+ if (byId !== void 0) return [byId];
2040
+ return index.byName.get(token.toLowerCase()) ?? [];
2041
+ }
2042
+ function resolveFieldSelectors(tokens, index) {
2043
+ const resolved = [];
2044
+ const unresolved = [];
2045
+ const ambiguous = [];
2046
+ for (const token of tokens) {
2047
+ if (token.startsWith(WILDCARD)) {
2048
+ resolved.push(token);
2049
+ continue;
2050
+ }
2051
+ const negated = token.startsWith(NEGATION);
2052
+ const bare = negated ? token.slice(1) : token;
2053
+ const candidates = candidatesFor(bare, index);
2054
+ if (candidates.length === 0) {
2055
+ unresolved.push(token);
2056
+ continue;
2057
+ }
2058
+ if (candidates.length > 1) {
2059
+ ambiguous.push({ name: bare, candidates });
2060
+ continue;
2061
+ }
2062
+ resolved.push(negated ? `${NEGATION}${candidates[0]}` : candidates[0]);
2063
+ }
2064
+ if (unresolved.length > 0 || ambiguous.length > 0) {
2065
+ throw new CliUsageError(describeFailure(unresolved, ambiguous), recoveryFor(unresolved, ambiguous), {
2066
+ ...unresolved.length > 0 ? { unresolved } : {},
2067
+ ...ambiguous.length > 0 ? { ambiguous } : {}
2068
+ });
2069
+ }
2070
+ return [...new Set(resolved)];
2071
+ }
2072
+ function describeFailure(unresolved, ambiguous) {
2073
+ const parts = [];
2074
+ if (unresolved.length > 0) {
2075
+ parts.push(`Unknown field name${unresolved.length > 1 ? "s" : ""}: ${unresolved.join(", ")}`);
2076
+ }
2077
+ for (const { name, candidates } of ambiguous) {
2078
+ parts.push(`'${name}' matches ${candidates.length} fields on this instance: ${candidates.join(", ")}`);
2079
+ }
2080
+ return parts.join(". ");
2081
+ }
2082
+ function recoveryFor(unresolved, ambiguous) {
2083
+ const parts = [];
2084
+ if (unresolved.length > 0) {
2085
+ parts.push(
2086
+ "Jira selects fields by id, and these matched no field id, name or JQL clause name on this instance. Run jiradc field search <keyword> to find the right name or id."
2087
+ );
2088
+ }
2089
+ if (ambiguous.length > 0) {
2090
+ parts.push(
2091
+ `Field names are not unique on a Jira instance. Re-run with the id you mean \u2014 ${ambiguous.map((a) => a.candidates.join(" or ")).join("; ")} \u2014 or run jiradc issue get <key> --all-fields to see which one this issue carries. Choosing one for you would return an answer that looks complete and names no field it dropped.`
2092
+ );
2093
+ }
2094
+ return parts.join(" ");
2095
+ }
2096
+ function parseFieldSelectors(raw) {
2097
+ return raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
2098
+ }
2099
+ var FIELD_CACHE = { name: "jiradc", ttl: 36e5 };
2100
+ async function loadFieldNameIndex(client) {
2101
+ const fields = await cacheGetOrFetch(
2102
+ FIELD_CACHE,
2103
+ "field-names",
2104
+ async () => (await client.fields.getAll()).map(({ id, name, clauseNames }) => ({ id, name, clauseNames }))
2105
+ );
2106
+ return buildFieldNameIndex(fields);
2107
+ }
2108
+ async function selectFields(client, opts, defaults) {
2109
+ if (opts.allFields === true) return void 0;
2110
+ if (opts.fields === void 0) return defaults;
2111
+ const tokens = parseFieldSelectors(opts.fields);
2112
+ if (tokens.length === 0) return defaults;
2113
+ return resolveFieldSelectors(tokens, await loadFieldNameIndex(client));
2114
+ }
1898
2115
 
1899
2116
  // src/commands/issue/get.ts
1900
2117
  function get2(parent) {
1901
- const cmd = parent.command("get").description("Get issue details").argument("<key>", "Issue key", issueKey).option("--fields <fields>", "Comma-separated fields to return (defaults to essential fields)", text).option("--all-fields", "Return all fields instead of defaults").option("--expand <expand>", 'Expand options (e.g., "transitions", "changelog")', text);
2118
+ const cmd = parent.command("get").description("Get issue details").argument("<key>", "Issue key", issueKey).option("--fields <fields>", "Comma-separated fields to return, by name or id", text).option("--all-fields", "Return all fields instead of defaults").option("--expand <expand>", 'Expand options (e.g., "transitions", "changelog")', text);
1902
2119
  examples(cmd, [
1903
2120
  "PROJ-123",
1904
2121
  "PROJ-123 --fields summary,status,assignee",
@@ -1907,7 +2124,7 @@ function get2(parent) {
1907
2124
  ]);
1908
2125
  cmd.action(async (key, opts) => {
1909
2126
  const client = getClient();
1910
- const fields = opts.allFields ? void 0 : opts.fields?.split(",").map((f) => f.trim()) ?? DEFAULT_FIELDS;
2127
+ const fields = await selectFields(client, opts, DEFAULT_FIELDS);
1911
2128
  const result = await client.issues.get({
1912
2129
  issueKeyOrId: key,
1913
2130
  fields,
@@ -1986,25 +2203,69 @@ function link(parent) {
1986
2203
  });
1987
2204
  }
1988
2205
 
2206
+ // src/utils/paging.ts
2207
+ var ALL_RESULTS_CAP = 1e3;
2208
+ function planSearch(opts, maxPageSize) {
2209
+ const start = opts.start ?? 0;
2210
+ if (opts.all !== true) return { all: false, pageSize: opts.limit, start };
2211
+ if (opts.limitGiven) {
2212
+ throw new CliUsageError(
2213
+ "Pass either --all or --limit, not both",
2214
+ "--all collects every match; --limit caps how many come back. Drop --limit to collect everything, or drop --all and page with --limit and --start."
2215
+ );
2216
+ }
2217
+ return { all: true, pageSize: maxPageSize, start };
2218
+ }
2219
+ async function fetchEveryPage(fetchPage, opts) {
2220
+ const first = await fetchPage(opts.start, opts.pageSize);
2221
+ const outstanding = first.total - opts.start;
2222
+ if (outstanding > ALL_RESULTS_CAP) {
2223
+ throw new CliUsageError(
2224
+ `--all would collect ${outstanding} issues, above the ${ALL_RESULTS_CAP} limit`,
2225
+ `Narrow the query, or page it yourself with --start and --limit. Returning the first ${ALL_RESULTS_CAP} instead would look like a complete answer and would not be one.`,
2226
+ { total: first.total, cap: ALL_RESULTS_CAP }
2227
+ );
2228
+ }
2229
+ const offsets = [];
2230
+ for (let at = opts.start + first.issues.length; at < first.total; at += opts.pageSize) offsets.push(at);
2231
+ const rest = await Promise.all(offsets.map(async (at) => fetchPage(at, opts.pageSize)));
2232
+ const issues3 = [first, ...rest].flatMap((page) => page.issues);
2233
+ return { startAt: opts.start, maxResults: issues3.length, total: first.total, isLast: true, issues: issues3 };
2234
+ }
2235
+
1989
2236
  // src/commands/issue/search.ts
2237
+ var MAX_PAGE_SIZE = 50;
1990
2238
  function search2(parent) {
1991
- const cmd = parent.command("search").description("Search issues using JQL").argument("<jql>", "JQL query string", text).option("--limit <number>", "Max results (1-50, Jira DC caps at 50)", intInRange(1, 50), 25).option("--start <number>", "Starting index for pagination", nonNegativeInt).option("--fields <fields>", "Comma-separated fields to return (defaults to essential fields)", text).option("--all-fields", "Return all fields instead of defaults");
2239
+ const cmd = parent.command("search").description("Search issues using JQL").argument("<jql>", "JQL query string", text).option(
2240
+ "--limit <number>",
2241
+ `Max results (1-${MAX_PAGE_SIZE}, Jira DC caps at ${MAX_PAGE_SIZE})`,
2242
+ intInRange(1, MAX_PAGE_SIZE),
2243
+ 25
2244
+ ).option("--start <number>", "Starting index for pagination", nonNegativeInt).option(
2245
+ "--fields <fields>",
2246
+ "Comma-separated fields to return, by name or id (defaults to a lean set without description/comment)",
2247
+ text
2248
+ ).option("--all-fields", "Return all fields instead of defaults").option("--all", `Collect every match, not just one page (up to ${ALL_RESULTS_CAP}; fails above it)`);
1992
2249
  examples(cmd, [
1993
2250
  '"project = PROJ AND status = Open"',
1994
2251
  '"assignee = currentUser()" --limit 10 --fields summary,status',
2252
+ '"project = PROJ AND status = Open" --all',
1995
2253
  '"project = PROJ" --start 50 --limit 50'
1996
2254
  ]);
1997
- cmd.action(async (jql, opts) => {
1998
- const client = getClient();
1999
- const fields = opts.allFields ? void 0 : opts.fields?.split(",").map((f) => f.trim()) ?? DEFAULT_FIELDS;
2000
- const result = await client.issues.search({
2001
- jql,
2002
- startAt: opts.start,
2003
- maxResults: opts.limit,
2004
- fields
2005
- });
2006
- output(transformPaged(result, transformIssue));
2007
- });
2255
+ cmd.action(
2256
+ async (jql, opts) => {
2257
+ const client = getClient();
2258
+ const fields = await selectFields(client, opts, SEARCH_DEFAULT_FIELDS);
2259
+ const plan = planSearch({ ...opts, limitGiven: cmd.getOptionValueSource("limit") === "cli" }, MAX_PAGE_SIZE);
2260
+ const page = async (startAt, maxResults) => client.issues.search({ jql, startAt, maxResults, fields });
2261
+ if (!plan.all) {
2262
+ output(transformPaged(await page(plan.start, plan.pageSize), transformIssue));
2263
+ return;
2264
+ }
2265
+ const every = await fetchEveryPage(page, { pageSize: plan.pageSize, start: plan.start });
2266
+ output({ ...every, issues: every.issues.map(transformIssue) });
2267
+ }
2268
+ );
2008
2269
  }
2009
2270
 
2010
2271
  // src/commands/issue/transition.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiradc-cli",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "publish": true,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,9 +23,9 @@
23
23
  "tsx": "^4.19.2",
24
24
  "typescript": "^5.7.2",
25
25
  "vitest": "^4.0.16",
26
- "config-typescript": "0.0.0",
27
26
  "config-eslint": "0.0.0",
28
- "cli-utils": "1.0.0"
27
+ "cli-utils": "1.0.0",
28
+ "config-typescript": "0.0.0"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=22.0.0"