atlass 1.14.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-CFyTL_x2.mjs → build-DMLlCHdF.mjs} +298 -131
- package/dist/cli.mjs +1 -1
- package/dist/confluence.mjs +1 -1
- package/dist/jira.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,17 +146,29 @@ For both, `--json` and `--copy` work as for search.
|
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
148
|
atlass jira search "safari login"
|
|
149
|
-
atlass jira search --project PROJ --assignee me --
|
|
150
|
-
atlass jira search --
|
|
151
|
-
atlass confluence search "onboarding" --space DOCS
|
|
152
|
-
atlass confluence search --cql "label = runbook ORDER BY created DESC"
|
|
149
|
+
atlass jira search --project PROJ --assignee me --open
|
|
150
|
+
atlass jira search --status "In Progress" --status "In Review" --updated 2w
|
|
151
|
+
atlass confluence search "onboarding" --space DOCS --label runbook
|
|
153
152
|
```
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`--
|
|
154
|
+
Jira filters on `--project`, `--assignee`, `--reporter`, `--status`, `--type`,
|
|
155
|
+
`--label`, `--updated` and `--open`; Confluence on `--space`, `--label`,
|
|
156
|
+
`--updated` and `--starred`. Repeating a flag matches any of its values;
|
|
157
|
+
different flags must all match. `--assignee` and `--reporter` take `me`, an
|
|
158
|
+
account id, or a name to look up. `--updated` takes `7d`, `2w`, `3m` or
|
|
159
|
+
`YYYY-MM-DD`. Results sort by most recently updated and use the same columns
|
|
160
|
+
as `jira list` and `confluence list`.
|
|
161
|
+
|
|
162
|
+
For anything the filters do not cover, write the query yourself:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
atlass jira jql "project = PROJ AND labels = regression ORDER BY created DESC"
|
|
166
|
+
atlass confluence cql "label = runbook AND creator = currentUser()"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`--limit` defaults to 25, max 100. `--json` prints machine output. Add `--copy`
|
|
170
|
+
to pick results interactively and copy each one, with `--out` as the target
|
|
171
|
+
directory. All four work on both search commands.
|
|
160
172
|
|
|
161
173
|
Discovery aids for the filters:
|
|
162
174
|
|
package/dist/bitbucket.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { Entry } from "@napi-rs/keyring";
|
|
@@ -528,6 +528,41 @@ function siteOrigin(input) {
|
|
|
528
528
|
if (!/^https?:\/\//i.test(value)) value = `https://${value}`;
|
|
529
529
|
return new URL(value).origin;
|
|
530
530
|
}
|
|
531
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
532
|
+
const RELATIVE = /^(\d+)([dwm])$/i;
|
|
533
|
+
function parseSince(value, nowMs) {
|
|
534
|
+
const raw = value.trim();
|
|
535
|
+
const iso = ISO_DATE.exec(raw);
|
|
536
|
+
if (iso) {
|
|
537
|
+
if (!isRealDate(iso)) throw invalidSince(value);
|
|
538
|
+
return raw;
|
|
539
|
+
}
|
|
540
|
+
const relative = RELATIVE.exec(raw);
|
|
541
|
+
if (!relative) throw invalidSince(value);
|
|
542
|
+
const amount = Number.parseInt(relative[1], 10);
|
|
543
|
+
const date = new Date(nowMs);
|
|
544
|
+
switch (relative[2].toLowerCase()) {
|
|
545
|
+
case "d":
|
|
546
|
+
date.setDate(date.getDate() - amount);
|
|
547
|
+
break;
|
|
548
|
+
case "w":
|
|
549
|
+
date.setDate(date.getDate() - amount * 7);
|
|
550
|
+
break;
|
|
551
|
+
default: date.setMonth(date.getMonth() - amount);
|
|
552
|
+
}
|
|
553
|
+
return localDate(date);
|
|
554
|
+
}
|
|
555
|
+
function isRealDate([, year = "", month = "", day = ""]) {
|
|
556
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
557
|
+
return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day);
|
|
558
|
+
}
|
|
559
|
+
function localDate(date) {
|
|
560
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
561
|
+
return `${date.getFullYear()}-${month}-${String(date.getDate()).padStart(2, "0")}`;
|
|
562
|
+
}
|
|
563
|
+
function invalidSince(value) {
|
|
564
|
+
return /* @__PURE__ */ new Error(`Invalid --updated "${value}". Expected 7d, 2w, 3m, or YYYY-MM-DD.`);
|
|
565
|
+
}
|
|
531
566
|
//#endregion
|
|
532
567
|
//#region src/commands/auth.ts
|
|
533
568
|
async function login({ term, profile }) {
|
|
@@ -1520,6 +1555,19 @@ function hyperlink(text, url) {
|
|
|
1520
1555
|
}
|
|
1521
1556
|
//#endregion
|
|
1522
1557
|
//#region src/commands/search-run.ts
|
|
1558
|
+
function checkedLimit(options) {
|
|
1559
|
+
if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
|
|
1560
|
+
return parseLimit(options.limit);
|
|
1561
|
+
}
|
|
1562
|
+
async function showRows(term, rows, listing, options, noun, copyOne) {
|
|
1563
|
+
await runSearch(term, rows, {
|
|
1564
|
+
json: options.json,
|
|
1565
|
+
copy: options.copy,
|
|
1566
|
+
out: options.out,
|
|
1567
|
+
empty: listing.empty,
|
|
1568
|
+
footer: listing.hasMore ? searchFooter(listing.limit) : void 0
|
|
1569
|
+
}, noun, copyOne);
|
|
1570
|
+
}
|
|
1523
1571
|
function alignedRows(items, nowMs, cells) {
|
|
1524
1572
|
const rows = items.map((item) => {
|
|
1525
1573
|
const cell = cells(item);
|
|
@@ -1829,6 +1877,103 @@ function registerBitbucket(bitbucket) {
|
|
|
1829
1877
|
return bitbucket;
|
|
1830
1878
|
}
|
|
1831
1879
|
//#endregion
|
|
1880
|
+
//#region src/cli/search-options.ts
|
|
1881
|
+
function collect(value, previous = []) {
|
|
1882
|
+
return [...previous, value];
|
|
1883
|
+
}
|
|
1884
|
+
function moved(flags, command) {
|
|
1885
|
+
const flag = flags.split(" ")[0];
|
|
1886
|
+
return new Option(flags).hideHelp().argParser(() => {
|
|
1887
|
+
throw new Error(`${flag} is now its own command: \`atlass ${command} "<query>"\`.`);
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
function outputOptions(command) {
|
|
1891
|
+
return command.option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy");
|
|
1892
|
+
}
|
|
1893
|
+
//#endregion
|
|
1894
|
+
//#region src/api/query.ts
|
|
1895
|
+
function quote$1(value) {
|
|
1896
|
+
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1897
|
+
}
|
|
1898
|
+
function values(given) {
|
|
1899
|
+
return (given ?? []).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
1900
|
+
}
|
|
1901
|
+
function inClause(field, given) {
|
|
1902
|
+
return renderClause(field, values(given).map(quote$1));
|
|
1903
|
+
}
|
|
1904
|
+
function userClause(field, given) {
|
|
1905
|
+
return renderClause(field, values(given).map((v) => v === "me" ? "currentUser()" : quote$1(v)));
|
|
1906
|
+
}
|
|
1907
|
+
function renderClause(field, rendered) {
|
|
1908
|
+
if (rendered.length === 0) return null;
|
|
1909
|
+
if (rendered.length === 1) return `${field} = ${rendered[0]}`;
|
|
1910
|
+
return `${field} in (${rendered.join(", ")})`;
|
|
1911
|
+
}
|
|
1912
|
+
function joinClauses(clauses, order) {
|
|
1913
|
+
return `${clauses.filter((c) => c !== null).join(" AND ")} ORDER BY ${order}`;
|
|
1914
|
+
}
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/util/html.ts
|
|
1917
|
+
const MAX_CODE_POINT = 1114111;
|
|
1918
|
+
const NAMED = {
|
|
1919
|
+
amp: "&",
|
|
1920
|
+
lt: "<",
|
|
1921
|
+
gt: ">",
|
|
1922
|
+
quot: "\"",
|
|
1923
|
+
apos: "'",
|
|
1924
|
+
nbsp: " "
|
|
1925
|
+
};
|
|
1926
|
+
function decodeEntities(text) {
|
|
1927
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
|
|
1928
|
+
if (body[0] === "#") {
|
|
1929
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
1930
|
+
return isCodePoint(code) ? String.fromCodePoint(code) : match;
|
|
1931
|
+
}
|
|
1932
|
+
return NAMED[body.toLowerCase()] ?? match;
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
function isCodePoint(code) {
|
|
1936
|
+
return Number.isFinite(code) && code >= 0 && code <= MAX_CODE_POINT;
|
|
1937
|
+
}
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region src/api/confluence-search.ts
|
|
1940
|
+
async function searchPages(client, site, params) {
|
|
1941
|
+
return searchPagesByCql(client, site, buildCql(params), params.limit);
|
|
1942
|
+
}
|
|
1943
|
+
async function searchPagesByCql(client, site, cql, limit) {
|
|
1944
|
+
const query = new URLSearchParams({
|
|
1945
|
+
cql,
|
|
1946
|
+
limit: String(limit),
|
|
1947
|
+
expand: "content.space"
|
|
1948
|
+
});
|
|
1949
|
+
const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
|
|
1950
|
+
const serverPageWasFull = results.length === limit;
|
|
1951
|
+
return {
|
|
1952
|
+
pages: results.filter((r) => r.content?.id).map((r) => ({
|
|
1953
|
+
id: r.content?.id ?? "",
|
|
1954
|
+
space: r.content?.space?.key ?? spaceKeyOf(r.resultGlobalContainer?.displayUrl),
|
|
1955
|
+
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
1956
|
+
updated: r.lastModified ?? "",
|
|
1957
|
+
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
1958
|
+
})),
|
|
1959
|
+
hasMore: serverPageWasFull
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
const SPACE_URL = /\/spaces\/([^/?#]+)/;
|
|
1963
|
+
function spaceKeyOf(displayUrl) {
|
|
1964
|
+
return SPACE_URL.exec(displayUrl ?? "")?.[1] ?? "";
|
|
1965
|
+
}
|
|
1966
|
+
function buildCql(params) {
|
|
1967
|
+
return joinClauses([
|
|
1968
|
+
"type = page",
|
|
1969
|
+
params.starred ? "favourite = currentUser()" : null,
|
|
1970
|
+
inClause("space", params.space),
|
|
1971
|
+
inClause("label", params.label),
|
|
1972
|
+
params.updatedSince ? `lastmodified >= ${quote$1(params.updatedSince)}` : null,
|
|
1973
|
+
params.text ? `text ~ ${quote$1(params.text)}` : null
|
|
1974
|
+
], "lastmodified DESC");
|
|
1975
|
+
}
|
|
1976
|
+
//#endregion
|
|
1832
1977
|
//#region src/adf/from-markdown.ts
|
|
1833
1978
|
function mediaNode(source, alt) {
|
|
1834
1979
|
const attrs = { ...source };
|
|
@@ -2160,62 +2305,6 @@ var UserNames = class {
|
|
|
2160
2305
|
}
|
|
2161
2306
|
};
|
|
2162
2307
|
//#endregion
|
|
2163
|
-
//#region src/util/html.ts
|
|
2164
|
-
const MAX_CODE_POINT = 1114111;
|
|
2165
|
-
const NAMED = {
|
|
2166
|
-
amp: "&",
|
|
2167
|
-
lt: "<",
|
|
2168
|
-
gt: ">",
|
|
2169
|
-
quot: "\"",
|
|
2170
|
-
apos: "'",
|
|
2171
|
-
nbsp: " "
|
|
2172
|
-
};
|
|
2173
|
-
function decodeEntities(text) {
|
|
2174
|
-
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
|
|
2175
|
-
if (body[0] === "#") {
|
|
2176
|
-
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
2177
|
-
return isCodePoint(code) ? String.fromCodePoint(code) : match;
|
|
2178
|
-
}
|
|
2179
|
-
return NAMED[body.toLowerCase()] ?? match;
|
|
2180
|
-
});
|
|
2181
|
-
}
|
|
2182
|
-
function isCodePoint(code) {
|
|
2183
|
-
return Number.isFinite(code) && code >= 0 && code <= MAX_CODE_POINT;
|
|
2184
|
-
}
|
|
2185
|
-
//#endregion
|
|
2186
|
-
//#region src/api/confluence-search.ts
|
|
2187
|
-
async function searchPages(client, site, params) {
|
|
2188
|
-
const cql = buildCql(params);
|
|
2189
|
-
const query = new URLSearchParams({
|
|
2190
|
-
cql,
|
|
2191
|
-
limit: String(params.limit),
|
|
2192
|
-
expand: "space"
|
|
2193
|
-
});
|
|
2194
|
-
const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
|
|
2195
|
-
const serverPageWasFull = results.length === params.limit;
|
|
2196
|
-
return {
|
|
2197
|
-
pages: results.filter((r) => r.content?.id).map((r) => ({
|
|
2198
|
-
id: r.content?.id ?? "",
|
|
2199
|
-
space: r.space?.key ?? r.resultGlobalContainer?.title ?? "",
|
|
2200
|
-
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
2201
|
-
updated: r.lastModified ?? "",
|
|
2202
|
-
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
2203
|
-
})),
|
|
2204
|
-
hasMore: serverPageWasFull
|
|
2205
|
-
};
|
|
2206
|
-
}
|
|
2207
|
-
function buildCql(params) {
|
|
2208
|
-
if (params.cql) return params.cql;
|
|
2209
|
-
const clauses = ["type = page"];
|
|
2210
|
-
if (params.starred) clauses.push("favourite = currentUser()");
|
|
2211
|
-
if (params.space) clauses.push(`space = ${cqlStringLiteral(params.space)}`);
|
|
2212
|
-
if (params.text) clauses.push(`text ~ ${cqlStringLiteral(params.text)}`);
|
|
2213
|
-
return `${clauses.join(" AND ")} ORDER BY lastmodified DESC`;
|
|
2214
|
-
}
|
|
2215
|
-
function cqlStringLiteral(value) {
|
|
2216
|
-
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
2217
|
-
}
|
|
2218
|
-
//#endregion
|
|
2219
2308
|
//#region src/commands/resolve-ref.ts
|
|
2220
2309
|
async function resolveRef(ask, arg, kind) {
|
|
2221
2310
|
const raw = arg ?? await ask.text({
|
|
@@ -2794,35 +2883,52 @@ async function fileSize(files, path) {
|
|
|
2794
2883
|
return {};
|
|
2795
2884
|
}
|
|
2796
2885
|
}
|
|
2886
|
+
async function copyPage(env, id, out) {
|
|
2887
|
+
env.term.err(`Fetching page ${id} ...`);
|
|
2888
|
+
await runCopy(env, planPageCopy(await fetchPage(env.session, env.session.site, id), out));
|
|
2889
|
+
}
|
|
2890
|
+
const PAGE_REF = {
|
|
2891
|
+
message: "Confluence page id or URL:",
|
|
2892
|
+
flag: "[page]",
|
|
2893
|
+
parse: parsePageId,
|
|
2894
|
+
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2895
|
+
};
|
|
2896
|
+
//#endregion
|
|
2897
|
+
//#region src/commands/confluence-search.ts
|
|
2797
2898
|
async function confluenceSearch(env, query, options) {
|
|
2798
|
-
if (options.cql && (query || options.space)) throw new Error("--cql cannot be combined with a text query or --space.");
|
|
2799
2899
|
await listPages(env, {
|
|
2800
2900
|
text: query,
|
|
2801
2901
|
space: options.space,
|
|
2802
|
-
|
|
2902
|
+
label: options.label,
|
|
2903
|
+
starred: options.starred,
|
|
2904
|
+
updatedSince: options.updated ? parseSince(options.updated, Date.now()) : void 0
|
|
2803
2905
|
}, "No matching pages.", options);
|
|
2804
2906
|
}
|
|
2805
2907
|
async function confluenceList(env, options) {
|
|
2806
2908
|
await listPages(env, {
|
|
2807
2909
|
starred: true,
|
|
2808
|
-
space: options.space
|
|
2910
|
+
space: options.space ? [options.space] : void 0
|
|
2809
2911
|
}, options.space ? `No starred pages in ${options.space}.` : "No starred pages.", options);
|
|
2810
2912
|
}
|
|
2913
|
+
async function confluenceCql(env, query, options) {
|
|
2914
|
+
const limit = checkedLimit(options);
|
|
2915
|
+
const { pages, hasMore } = await searchPagesByCql(env.session, env.session.site, query, limit);
|
|
2916
|
+
await showPages(env, pages, hasMore, limit, options);
|
|
2917
|
+
}
|
|
2811
2918
|
async function listPages(env, filter, empty, options) {
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
const limit = parseLimit(options.limit);
|
|
2815
|
-
const { pages, hasMore } = await searchPages(session, session.site, {
|
|
2919
|
+
const limit = checkedLimit(options);
|
|
2920
|
+
const { pages, hasMore } = await searchPages(env.session, env.session.site, {
|
|
2816
2921
|
...filter,
|
|
2817
2922
|
limit
|
|
2818
2923
|
});
|
|
2819
|
-
await
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2924
|
+
await showPages(env, pages, hasMore, limit, options, empty);
|
|
2925
|
+
}
|
|
2926
|
+
async function showPages(env, pages, hasMore, limit, options, empty = "No matching pages.") {
|
|
2927
|
+
await showRows(env.term, formatPageRows(pages, Date.now()), {
|
|
2823
2928
|
empty,
|
|
2824
|
-
|
|
2825
|
-
|
|
2929
|
+
hasMore,
|
|
2930
|
+
limit
|
|
2931
|
+
}, options, PAGE_NOUN, (id) => copyPage(env, id, options.out));
|
|
2826
2932
|
}
|
|
2827
2933
|
function formatPageRows(pages, nowMs) {
|
|
2828
2934
|
return alignedRows(pages, nowMs, (p) => ({
|
|
@@ -2850,16 +2956,6 @@ const PAGE_NOUN = {
|
|
|
2850
2956
|
singular: "page",
|
|
2851
2957
|
plural: "pages"
|
|
2852
2958
|
};
|
|
2853
|
-
async function copyPage(env, id, out) {
|
|
2854
|
-
env.term.err(`Fetching page ${id} ...`);
|
|
2855
|
-
await runCopy(env, planPageCopy(await fetchPage(env.session, env.session.site, id), out));
|
|
2856
|
-
}
|
|
2857
|
-
const PAGE_REF = {
|
|
2858
|
-
message: "Confluence page id or URL:",
|
|
2859
|
-
flag: "[page]",
|
|
2860
|
-
parse: parsePageId,
|
|
2861
|
-
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2862
|
-
};
|
|
2863
2959
|
//#endregion
|
|
2864
2960
|
//#region src/cli/confluence.ts
|
|
2865
2961
|
function registerConfluence(confluence) {
|
|
@@ -2867,8 +2963,9 @@ function registerConfluence(confluence) {
|
|
|
2867
2963
|
confluence.command("view [page]").description("Show a Confluence page (id or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(confluenceView));
|
|
2868
2964
|
confluence.command("copy [page]").description("Copy a Confluence page (id or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(confluenceCopy));
|
|
2869
2965
|
confluence.command("update [file]").description("Update a Confluence page from an edited Markdown file").option("--title", "also push the H1 as the page title").option("-m, --message <text>", "version message (default 'Updated via atlass')").option("-f, --force", "skip the stale-version and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(confluenceUpdate));
|
|
2870
|
-
confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space")
|
|
2871
|
-
confluence.command("search [query]").description("Search Confluence pages
|
|
2966
|
+
outputOptions(confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space")).action(jiraAction(confluenceList));
|
|
2967
|
+
outputOptions(confluence.command("search [query]").description("Search Confluence pages by text and filters").option("-s, --space <key>", "limit to a space (repeatable)", collect).option("--label <label>", "limit to a label (repeatable)", collect).option("--starred", "limit to pages you starred").option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").addOption(moved("--cql <cql>", "confluence cql"))).action(jiraAction(confluenceSearch));
|
|
2968
|
+
outputOptions(confluence.command("cql <query>").description("Search Confluence pages with a raw CQL query")).action(jiraAction(confluenceCql));
|
|
2872
2969
|
return confluence;
|
|
2873
2970
|
}
|
|
2874
2971
|
//#endregion
|
|
@@ -2937,6 +3034,24 @@ async function searchAssignableUsers(client, project, query) {
|
|
|
2937
3034
|
});
|
|
2938
3035
|
return (await client.getJson(`/rest/api/3/user/assignable/search?${params.toString()}`)).map(toJiraUser);
|
|
2939
3036
|
}
|
|
3037
|
+
const ACCOUNT_ID = /^(?:[0-9a-z]{24}|[a-z0-9]+:[0-9a-f-]{36}(?::[0-9a-f-]{36})?)$/i;
|
|
3038
|
+
async function searchUsers(client, query) {
|
|
3039
|
+
const params = new URLSearchParams({
|
|
3040
|
+
query,
|
|
3041
|
+
maxResults: "20"
|
|
3042
|
+
});
|
|
3043
|
+
return (await client.getJson(`/rest/api/3/user/search?${params.toString()}`)).map(toJiraUser);
|
|
3044
|
+
}
|
|
3045
|
+
function matchUser(users, query, noun) {
|
|
3046
|
+
const active = users.filter((u) => u.active);
|
|
3047
|
+
const needle = query.toLowerCase();
|
|
3048
|
+
const exact = active.filter((u) => u.displayName.toLowerCase() === needle || u.email.toLowerCase() === needle);
|
|
3049
|
+
const candidates = exact.length === 1 ? exact : active;
|
|
3050
|
+
if (candidates.length === 1) return candidates[0].accountId;
|
|
3051
|
+
if (candidates.length === 0) throw new Error(`no ${noun} matches "${query}".`);
|
|
3052
|
+
const names = candidates.map((u) => u.displayName).join(", ");
|
|
3053
|
+
throw new Error(`"${query}" matches ${candidates.length} users: ${names}.`);
|
|
3054
|
+
}
|
|
2940
3055
|
async function fetchMyself(client) {
|
|
2941
3056
|
return toJiraUser(await client.getJson("/rest/api/3/myself"));
|
|
2942
3057
|
}
|
|
@@ -3014,18 +3129,11 @@ function matchType(types, arg) {
|
|
|
3014
3129
|
const needle = arg.trim().toLowerCase();
|
|
3015
3130
|
return types.find((t) => t.id === arg.trim()) ?? types.find((t) => t.name.toLowerCase() === needle);
|
|
3016
3131
|
}
|
|
3017
|
-
const ACCOUNT_ID = /^(?:[0-9a-f]{24}|[a-z0-9]+:[0-9a-f-]{36}(?::[0-9a-f-]{36})?)$/i;
|
|
3018
3132
|
function userResolver(client, project) {
|
|
3019
3133
|
return async (query) => {
|
|
3020
3134
|
if (query === "me") return (await fetchMyself(client)).accountId;
|
|
3021
3135
|
if (ACCOUNT_ID.test(query)) return query;
|
|
3022
|
-
|
|
3023
|
-
const exact = users.filter((u) => u.displayName.toLowerCase() === query.toLowerCase() || u.email.toLowerCase() === query.toLowerCase());
|
|
3024
|
-
const candidates = exact.length === 1 ? exact : users;
|
|
3025
|
-
if (candidates.length === 1) return candidates[0].accountId;
|
|
3026
|
-
if (candidates.length === 0) throw new Error(`no assignable user matches "${query}".`);
|
|
3027
|
-
const names = candidates.map((u) => u.displayName).join(", ");
|
|
3028
|
-
throw new Error(`"${query}" matches ${candidates.length} users: ${names}.`);
|
|
3136
|
+
return matchUser(await searchAssignableUsers(client, project, query), query, "assignable user");
|
|
3029
3137
|
};
|
|
3030
3138
|
}
|
|
3031
3139
|
//#endregion
|
|
@@ -3639,9 +3747,13 @@ async function jiraFields({ session, term }, projectArg, typeArg, options) {
|
|
|
3639
3747
|
//#endregion
|
|
3640
3748
|
//#region src/api/jira-search.ts
|
|
3641
3749
|
const RECENT_ISSUES_CLAUSE = "updated >= -30d";
|
|
3750
|
+
const OPEN_CLAUSE = "statusCategory != Done";
|
|
3642
3751
|
const SEARCH_FIELDS = "summary,status,updated";
|
|
3643
3752
|
async function searchIssues(client, site, params) {
|
|
3644
|
-
return (
|
|
3753
|
+
return searchIssuesByJql(client, site, buildJql(params), params.limit);
|
|
3754
|
+
}
|
|
3755
|
+
async function searchIssuesByJql(client, site, jql, limit) {
|
|
3756
|
+
return ((await fetchSearchPage(client, jql, limit)).issues ?? []).map((i) => toIssueSummary(site, i));
|
|
3645
3757
|
}
|
|
3646
3758
|
async function fetchSearchPage(client, jql, maxResults, nextPageToken) {
|
|
3647
3759
|
const query = new URLSearchParams({
|
|
@@ -3662,22 +3774,28 @@ function toIssueSummary(site, i) {
|
|
|
3662
3774
|
url: browseUrl(site, i.key)
|
|
3663
3775
|
};
|
|
3664
3776
|
}
|
|
3777
|
+
const ISSUE_ORDER = "updated DESC";
|
|
3665
3778
|
function buildJql(params) {
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3779
|
+
const clauses = [
|
|
3780
|
+
inClause("project", params.project),
|
|
3781
|
+
inClause("type", params.type),
|
|
3782
|
+
inClause("status", params.status),
|
|
3783
|
+
params.open ? OPEN_CLAUSE : null,
|
|
3784
|
+
userClause("assignee", params.assignee),
|
|
3785
|
+
userClause("reporter", params.reporter),
|
|
3786
|
+
inClause("labels", params.label),
|
|
3787
|
+
params.updatedSince ? `updated >= ${quote$1(params.updatedSince)}` : null,
|
|
3788
|
+
params.text ? `text ~ ${quote$1(params.text)}` : null
|
|
3789
|
+
].filter((c) => c !== null);
|
|
3672
3790
|
if (clauses.length === 0) clauses.push(RECENT_ISSUES_CLAUSE);
|
|
3673
|
-
return
|
|
3791
|
+
return joinClauses(clauses, ISSUE_ORDER);
|
|
3674
3792
|
}
|
|
3675
|
-
const OPEN_CLAUSE = "statusCategory != Done";
|
|
3676
3793
|
function buildListJql(params) {
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3794
|
+
return joinClauses([
|
|
3795
|
+
"assignee = currentUser()",
|
|
3796
|
+
params.project ? `project = ${quote$1(params.project)}` : null,
|
|
3797
|
+
params.all ? `(${OPEN_CLAUSE} OR ${RECENT_ISSUES_CLAUSE})` : OPEN_CLAUSE
|
|
3798
|
+
], ISSUE_ORDER);
|
|
3681
3799
|
}
|
|
3682
3800
|
const LIST_PAGE_SIZE = 100;
|
|
3683
3801
|
const LIST_CAP = 500;
|
|
@@ -3712,8 +3830,59 @@ function sortByCategoryThenUpdated(issues) {
|
|
|
3712
3830
|
const updatedMs = (i) => Date.parse(i.updated) || 0;
|
|
3713
3831
|
return [...issues].sort((a, b) => rank(a) - rank(b) || updatedMs(b) - updatedMs(a));
|
|
3714
3832
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3833
|
+
//#endregion
|
|
3834
|
+
//#region src/commands/jira-filters.ts
|
|
3835
|
+
const FILTER_FLAGS = [
|
|
3836
|
+
"project",
|
|
3837
|
+
"assignee",
|
|
3838
|
+
"reporter",
|
|
3839
|
+
"status",
|
|
3840
|
+
"type",
|
|
3841
|
+
"label"
|
|
3842
|
+
];
|
|
3843
|
+
const ISSUE_KEY = /^[A-Z][A-Z0-9]+-\d+$/i;
|
|
3844
|
+
function hasFilters(filters) {
|
|
3845
|
+
return FILTER_FLAGS.some((flag) => values(filters[flag]).length > 0) || filters.updated !== void 0 || filters.open === true;
|
|
3846
|
+
}
|
|
3847
|
+
function checkQuery(query, filters) {
|
|
3848
|
+
if (query && ISSUE_KEY.test(query.trim())) throw new Error(`"${query}" looks like an issue key. Run \`jira view ${query}\` to open it.`);
|
|
3849
|
+
if (!query && !hasFilters(filters)) throw new Error("Give a text query or at least one filter. `jira list` shows your open issues.");
|
|
3850
|
+
}
|
|
3851
|
+
async function searchParams(client, query, filters, limit, nowMs) {
|
|
3852
|
+
return {
|
|
3853
|
+
text: query,
|
|
3854
|
+
project: filters.project,
|
|
3855
|
+
assignee: await resolveUsers(client, filters.assignee),
|
|
3856
|
+
reporter: await resolveUsers(client, filters.reporter),
|
|
3857
|
+
status: filters.status,
|
|
3858
|
+
type: filters.type,
|
|
3859
|
+
label: filters.label,
|
|
3860
|
+
updatedSince: filters.updated ? parseSince(filters.updated, nowMs) : void 0,
|
|
3861
|
+
open: filters.open,
|
|
3862
|
+
limit
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
async function resolveUsers(client, given) {
|
|
3866
|
+
const list = values(given);
|
|
3867
|
+
if (list.length === 0) return void 0;
|
|
3868
|
+
const resolved = [];
|
|
3869
|
+
for (const who of list) if (who === "me" || ACCOUNT_ID.test(who)) resolved.push(who);
|
|
3870
|
+
else resolved.push(matchUser(await searchUsers(client, who), who, "user"));
|
|
3871
|
+
return resolved;
|
|
3872
|
+
}
|
|
3873
|
+
const FIELD_IN_MESSAGE = /for the field '([^']+)'/;
|
|
3874
|
+
const createForm = (project) => `jira fields ${project ?? "<project>"}`;
|
|
3875
|
+
const DISCOVERY = {
|
|
3876
|
+
project: () => "jira projects",
|
|
3877
|
+
status: (p) => p ? `jira statuses --project ${p}` : "jira statuses",
|
|
3878
|
+
issuetype: createForm,
|
|
3879
|
+
type: createForm
|
|
3880
|
+
};
|
|
3881
|
+
function searchHint(err, project) {
|
|
3882
|
+
if (!(err instanceof HttpError) || err.status !== 400) return err;
|
|
3883
|
+
const field = FIELD_IN_MESSAGE.exec(err.message)?.[1]?.toLowerCase();
|
|
3884
|
+
const command = field ? DISCOVERY[field]?.(values(project)[0]) : void 0;
|
|
3885
|
+
return command ? /* @__PURE__ */ new Error(`${err.message} Run \`${command}\` to see the options.`) : err;
|
|
3717
3886
|
}
|
|
3718
3887
|
//#endregion
|
|
3719
3888
|
//#region src/api/jira-issues.ts
|
|
@@ -3900,29 +4069,29 @@ const ISSUE_REF = {
|
|
|
3900
4069
|
//#endregion
|
|
3901
4070
|
//#region src/commands/jira-search.ts
|
|
3902
4071
|
async function jiraSearch(env, query, options) {
|
|
3903
|
-
const { session
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
const
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
4072
|
+
const { session } = env;
|
|
4073
|
+
checkQuery(query, options);
|
|
4074
|
+
const limit = checkedLimit(options);
|
|
4075
|
+
const params = await searchParams(session, query, options, limit, Date.now());
|
|
4076
|
+
await showIssues(env, await searchIssues(session, session.site, params).catch((err) => {
|
|
4077
|
+
throw searchHint(err, options.project);
|
|
4078
|
+
}), limit, options);
|
|
4079
|
+
}
|
|
4080
|
+
async function jiraJql(env, query, options) {
|
|
4081
|
+
const { session } = env;
|
|
4082
|
+
const limit = checkedLimit(options);
|
|
4083
|
+
await showIssues(env, await searchIssuesByJql(session, session.site, query, limit), limit, options);
|
|
4084
|
+
}
|
|
4085
|
+
async function showIssues(env, issues, limit, options, empty = "No matching issues.") {
|
|
4086
|
+
await showRows(env.term, formatIssueRows(issues, Date.now()), {
|
|
4087
|
+
empty,
|
|
4088
|
+
hasMore: issues.length === limit,
|
|
3913
4089
|
limit
|
|
3914
|
-
});
|
|
3915
|
-
await runSearch(term, formatIssueRows(issues, Date.now()), {
|
|
3916
|
-
json: options.json,
|
|
3917
|
-
copy: options.copy,
|
|
3918
|
-
out: options.out,
|
|
3919
|
-
empty: "No matching issues.",
|
|
3920
|
-
footer: issues.length === limit ? searchFooter(limit) : void 0
|
|
3921
|
-
}, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
4090
|
+
}, options, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
3922
4091
|
}
|
|
3923
4092
|
async function jiraList(env, options) {
|
|
3924
4093
|
const { session, term } = env;
|
|
3925
|
-
|
|
4094
|
+
checkedLimit(options);
|
|
3926
4095
|
const { issues, truncated } = await listAssignedIssues(session, session.site, {
|
|
3927
4096
|
all: options.all,
|
|
3928
4097
|
project: options.project
|
|
@@ -3951,25 +4120,23 @@ const ISSUE_NOUN = {
|
|
|
3951
4120
|
};
|
|
3952
4121
|
//#endregion
|
|
3953
4122
|
//#region src/cli/jira.ts
|
|
3954
|
-
function collect(value, previous = []) {
|
|
3955
|
-
return [...previous, value];
|
|
3956
|
-
}
|
|
3957
4123
|
function registerJira(jira) {
|
|
3958
4124
|
jira.description("Jira commands");
|
|
3959
4125
|
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(jiraAction(jiraProjects));
|
|
3960
4126
|
jira.command("statuses [query]").description("List statuses (optionally filtered by name, scoped with --project)").option("-p, --project <key>", "limit to statuses used by a project").option("--json", "output results as JSON").action(jiraAction(jiraStatuses));
|
|
3961
|
-
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable
|
|
4127
|
+
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable)", collect).option("-c, --component <name>", "component (repeatable)", collect).option("--parent <key>", "parent issue key for subtasks").option("-f, --field <name=value>", "any other create-screen field (repeatable)", collect).option("--dry-run", "print the resolved payload instead of creating").option("--json", "print the created issue as JSON").action(jiraAction(jiraCreate));
|
|
3962
4128
|
jira.command("fields <project> [type]").description("Show the issue types you can create in a project, or the create form for one type").option("--json", "output results as JSON").action(jiraAction(jiraFields));
|
|
3963
4129
|
jira.command("view [issue]").description("Show a Jira issue (key or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(jiraView));
|
|
3964
4130
|
jira.command("copy [issue]").description("Copy a Jira issue (key or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(jiraCopy));
|
|
3965
4131
|
jira.command("update [file]").description("Update a Jira issue description from an edited Markdown file").option("--summary", "also push the H1 as the issue summary").option("-f, --force", "skip the stale-issue and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(jiraUpdate));
|
|
3966
|
-
jira.command("list").description("List open issues assigned to you").option("-p, --project <key>", "limit to a project").option("-a, --all", "include Done issues updated in the last 30 days")
|
|
3967
|
-
jira.command("search [query]").description("Search Jira issues
|
|
4132
|
+
outputOptions(jira.command("list").description("List open issues assigned to you").option("-p, --project <key>", "limit to a project").option("-a, --all", "include Done issues updated in the last 30 days")).action(jiraAction(jiraList));
|
|
4133
|
+
outputOptions(jira.command("search [query]").description("Search Jira issues by text and filters").option("-p, --project <key>", "limit to a project (repeatable)", collect).option("-a, --assignee <who>", "limit to an assignee: me, a name, or an account id", collect).option("--reporter <who>", "limit to a reporter: me, a name, or an account id", collect).option("-s, --status <status>", "limit to a status (repeatable)", collect).option("-t, --type <type>", "limit to an issue type (repeatable)", collect).option("--label <label>", "limit to a label (repeatable)", collect).option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").option("--open", "exclude issues in the Done category").addOption(moved("--jql <jql>", "jira jql"))).action(jiraAction(jiraSearch));
|
|
4134
|
+
outputOptions(jira.command("jql <query>").description("Search Jira issues with a raw JQL query")).action(jiraAction(jiraJql));
|
|
3968
4135
|
return jira;
|
|
3969
4136
|
}
|
|
3970
4137
|
//#endregion
|
|
3971
4138
|
//#region package.json
|
|
3972
|
-
var version = "
|
|
4139
|
+
var version = "2.0.0";
|
|
3973
4140
|
//#endregion
|
|
3974
4141
|
//#region src/cli/build.ts
|
|
3975
4142
|
const PRODUCTS = {
|
package/dist/cli.mjs
CHANGED
package/dist/confluence.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as buildStandalone, r as fail } from "./build-
|
|
2
|
+
import { n as buildStandalone, r as fail } from "./build-DMLlCHdF.mjs";
|
|
3
3
|
//#region src/confluence.ts
|
|
4
4
|
buildStandalone("confluence").parseAsync().catch(fail);
|
|
5
5
|
//#endregion
|
package/dist/jira.mjs
CHANGED