jiradc-cli 1.0.33 → 1.0.34
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.
- package/dist/index.js +246 -24
- 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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
1998
|
+
var SEARCH_DEFAULT_FIELDS = [
|
|
1885
1999
|
"summary",
|
|
1886
|
-
"description",
|
|
1887
2000
|
"status",
|
|
1888
2001
|
"assignee",
|
|
1889
2002
|
"reporter",
|
|
@@ -1892,13 +2005,78 @@ 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 byName = /* @__PURE__ */ new Map();
|
|
2018
|
+
for (const field of fields) {
|
|
2019
|
+
ids.add(field.id);
|
|
2020
|
+
byName.set(field.name.toLowerCase(), field.id);
|
|
2021
|
+
for (const clause of field.clauseNames ?? []) byName.set(clause.toLowerCase(), field.id);
|
|
2022
|
+
}
|
|
2023
|
+
for (const field of fields) byName.set(field.id.toLowerCase(), field.id);
|
|
2024
|
+
return { ids, byName };
|
|
2025
|
+
}
|
|
2026
|
+
function resolveOne(token, index) {
|
|
2027
|
+
if (index.ids.has(token)) return token;
|
|
2028
|
+
return index.byName.get(token.toLowerCase());
|
|
2029
|
+
}
|
|
2030
|
+
function resolveFieldSelectors(tokens, index) {
|
|
2031
|
+
const resolved = [];
|
|
2032
|
+
const unresolved = [];
|
|
2033
|
+
for (const token of tokens) {
|
|
2034
|
+
if (token.startsWith(WILDCARD)) {
|
|
2035
|
+
resolved.push(token);
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const negated = token.startsWith(NEGATION);
|
|
2039
|
+
const bare = negated ? token.slice(1) : token;
|
|
2040
|
+
const id = resolveOne(bare, index);
|
|
2041
|
+
if (id === void 0) {
|
|
2042
|
+
unresolved.push(token);
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
resolved.push(negated ? `${NEGATION}${id}` : id);
|
|
2046
|
+
}
|
|
2047
|
+
if (unresolved.length > 0) {
|
|
2048
|
+
const plural = unresolved.length > 1 ? "s" : "";
|
|
2049
|
+
throw new CliUsageError(
|
|
2050
|
+
`Unknown field name${plural}: ${unresolved.join(", ")}`,
|
|
2051
|
+
`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. Left unresolved these would be dropped silently, returning an answer missing the field${plural} you asked for.`,
|
|
2052
|
+
{ unresolved }
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
return [...new Set(resolved)];
|
|
2056
|
+
}
|
|
2057
|
+
function parseFieldSelectors(raw) {
|
|
2058
|
+
return raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
2059
|
+
}
|
|
2060
|
+
var FIELD_CACHE = { name: "jiradc", ttl: 36e5 };
|
|
2061
|
+
async function loadFieldNameIndex(client) {
|
|
2062
|
+
const fields = await cacheGetOrFetch(
|
|
2063
|
+
FIELD_CACHE,
|
|
2064
|
+
"field-names",
|
|
2065
|
+
async () => (await client.fields.getAll()).map(({ id, name, clauseNames }) => ({ id, name, clauseNames }))
|
|
2066
|
+
);
|
|
2067
|
+
return buildFieldNameIndex(fields);
|
|
2068
|
+
}
|
|
2069
|
+
async function selectFields(client, opts, defaults) {
|
|
2070
|
+
if (opts.allFields === true) return void 0;
|
|
2071
|
+
if (opts.fields === void 0) return defaults;
|
|
2072
|
+
const tokens = parseFieldSelectors(opts.fields);
|
|
2073
|
+
if (tokens.length === 0) return defaults;
|
|
2074
|
+
return resolveFieldSelectors(tokens, await loadFieldNameIndex(client));
|
|
2075
|
+
}
|
|
1898
2076
|
|
|
1899
2077
|
// src/commands/issue/get.ts
|
|
1900
2078
|
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
|
|
2079
|
+
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
2080
|
examples(cmd, [
|
|
1903
2081
|
"PROJ-123",
|
|
1904
2082
|
"PROJ-123 --fields summary,status,assignee",
|
|
@@ -1907,7 +2085,7 @@ function get2(parent) {
|
|
|
1907
2085
|
]);
|
|
1908
2086
|
cmd.action(async (key, opts) => {
|
|
1909
2087
|
const client = getClient();
|
|
1910
|
-
const fields =
|
|
2088
|
+
const fields = await selectFields(client, opts, DEFAULT_FIELDS);
|
|
1911
2089
|
const result = await client.issues.get({
|
|
1912
2090
|
issueKeyOrId: key,
|
|
1913
2091
|
fields,
|
|
@@ -1986,25 +2164,69 @@ function link(parent) {
|
|
|
1986
2164
|
});
|
|
1987
2165
|
}
|
|
1988
2166
|
|
|
2167
|
+
// src/utils/paging.ts
|
|
2168
|
+
var ALL_RESULTS_CAP = 1e3;
|
|
2169
|
+
function planSearch(opts, maxPageSize) {
|
|
2170
|
+
const start = opts.start ?? 0;
|
|
2171
|
+
if (opts.all !== true) return { all: false, pageSize: opts.limit, start };
|
|
2172
|
+
if (opts.limitGiven) {
|
|
2173
|
+
throw new CliUsageError(
|
|
2174
|
+
"Pass either --all or --limit, not both",
|
|
2175
|
+
"--all collects every match; --limit caps how many come back. Drop --limit to collect everything, or drop --all and page with --limit and --start."
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
return { all: true, pageSize: maxPageSize, start };
|
|
2179
|
+
}
|
|
2180
|
+
async function fetchEveryPage(fetchPage, opts) {
|
|
2181
|
+
const first = await fetchPage(opts.start, opts.pageSize);
|
|
2182
|
+
const outstanding = first.total - opts.start;
|
|
2183
|
+
if (outstanding > ALL_RESULTS_CAP) {
|
|
2184
|
+
throw new CliUsageError(
|
|
2185
|
+
`--all would collect ${outstanding} issues, above the ${ALL_RESULTS_CAP} limit`,
|
|
2186
|
+
`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.`,
|
|
2187
|
+
{ total: first.total, cap: ALL_RESULTS_CAP }
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
const offsets = [];
|
|
2191
|
+
for (let at = opts.start + first.issues.length; at < first.total; at += opts.pageSize) offsets.push(at);
|
|
2192
|
+
const rest = await Promise.all(offsets.map(async (at) => fetchPage(at, opts.pageSize)));
|
|
2193
|
+
const issues3 = [first, ...rest].flatMap((page) => page.issues);
|
|
2194
|
+
return { startAt: opts.start, maxResults: issues3.length, total: first.total, isLast: true, issues: issues3 };
|
|
2195
|
+
}
|
|
2196
|
+
|
|
1989
2197
|
// src/commands/issue/search.ts
|
|
2198
|
+
var MAX_PAGE_SIZE = 50;
|
|
1990
2199
|
function search2(parent) {
|
|
1991
|
-
const cmd = parent.command("search").description("Search issues using JQL").argument("<jql>", "JQL query string", text).option(
|
|
2200
|
+
const cmd = parent.command("search").description("Search issues using JQL").argument("<jql>", "JQL query string", text).option(
|
|
2201
|
+
"--limit <number>",
|
|
2202
|
+
`Max results (1-${MAX_PAGE_SIZE}, Jira DC caps at ${MAX_PAGE_SIZE})`,
|
|
2203
|
+
intInRange(1, MAX_PAGE_SIZE),
|
|
2204
|
+
25
|
|
2205
|
+
).option("--start <number>", "Starting index for pagination", nonNegativeInt).option(
|
|
2206
|
+
"--fields <fields>",
|
|
2207
|
+
"Comma-separated fields to return, by name or id (defaults to a lean set without description/comment)",
|
|
2208
|
+
text
|
|
2209
|
+
).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
2210
|
examples(cmd, [
|
|
1993
2211
|
'"project = PROJ AND status = Open"',
|
|
1994
2212
|
'"assignee = currentUser()" --limit 10 --fields summary,status',
|
|
2213
|
+
'"project = PROJ AND status = Open" --all',
|
|
1995
2214
|
'"project = PROJ" --start 50 --limit 50'
|
|
1996
2215
|
]);
|
|
1997
|
-
cmd.action(
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
startAt
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2216
|
+
cmd.action(
|
|
2217
|
+
async (jql, opts) => {
|
|
2218
|
+
const client = getClient();
|
|
2219
|
+
const fields = await selectFields(client, opts, SEARCH_DEFAULT_FIELDS);
|
|
2220
|
+
const plan = planSearch({ ...opts, limitGiven: cmd.getOptionValueSource("limit") === "cli" }, MAX_PAGE_SIZE);
|
|
2221
|
+
const page = async (startAt, maxResults) => client.issues.search({ jql, startAt, maxResults, fields });
|
|
2222
|
+
if (!plan.all) {
|
|
2223
|
+
output(transformPaged(await page(plan.start, plan.pageSize), transformIssue));
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const every = await fetchEveryPage(page, { pageSize: plan.pageSize, start: plan.start });
|
|
2227
|
+
output({ ...every, issues: every.issues.map(transformIssue) });
|
|
2228
|
+
}
|
|
2229
|
+
);
|
|
2008
2230
|
}
|
|
2009
2231
|
|
|
2010
2232
|
// src/commands/issue/transition.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jiradc-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.34",
|
|
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
|
-
"
|
|
26
|
+
"cli-utils": "1.0.0",
|
|
27
27
|
"config-eslint": "0.0.0",
|
|
28
|
-
"
|
|
28
|
+
"config-typescript": "0.0.0"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=22.0.0"
|