atlass 2.1.0 → 2.3.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 +72 -4
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-_4VuIcqF.mjs → build-6PhpYMjp.mjs} +1180 -698
- package/dist/cli.mjs +1 -1
- package/dist/confluence.mjs +1 -1
- package/dist/jira.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { Entry } from "@napi-rs/keyring";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
@@ -29,7 +29,7 @@ var HttpError = class extends Error {
|
|
|
29
29
|
};
|
|
30
30
|
function httpError(status, path, body, loginHint) {
|
|
31
31
|
if (status === 401 || status === 403) return new HttpError(status, `Authentication failed (401/403). Run \`${loginHint}\` to update your token.`, void 0, path);
|
|
32
|
-
if (status === 404) return new HttpError(status, `Not found (404): ${path}
|
|
32
|
+
if (status === 404) return new HttpError(status, `Not found (404): ${path}`, void 0, path);
|
|
33
33
|
if (status === 409) return new HttpError(status, `Conflict (409): ${extractErrorMessage(body) || "the page changed on the server"}`);
|
|
34
34
|
if (status === 413) return new HttpError(status, "Payload too large (413): the page or an attachment exceeds the size limit.");
|
|
35
35
|
if (status === 400) return new HttpError(status, `Bad request (400): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
@@ -126,10 +126,11 @@ var AtlassianClient = class {
|
|
|
126
126
|
headers: { Accept: "application/json" }
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
async postMultipart(path, filename, bytes) {
|
|
129
|
+
async postMultipart(path, filename, bytes, extras = {}) {
|
|
130
130
|
const form = new FormData();
|
|
131
|
-
const blob = new Blob([bytes]);
|
|
131
|
+
const blob = new Blob([bytes], { type: extras.type });
|
|
132
132
|
form.append("file", blob, filename);
|
|
133
|
+
for (const [name, value] of Object.entries(extras.fields ?? {})) form.append(name, value);
|
|
133
134
|
return (await this.request(path, {
|
|
134
135
|
method: "POST",
|
|
135
136
|
headers: {
|
|
@@ -447,14 +448,58 @@ function guarded(fn) {
|
|
|
447
448
|
};
|
|
448
449
|
}
|
|
449
450
|
function fail(err) {
|
|
450
|
-
if (isPromptInterrupt(err))
|
|
451
|
+
if (isPromptInterrupt(err)) {
|
|
452
|
+
process.exitCode = SIGINT_EXIT_CODE;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
451
455
|
ttyTerminal().err(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
452
|
-
process.
|
|
456
|
+
process.exitCode = 1;
|
|
453
457
|
}
|
|
454
458
|
function isPromptInterrupt(err) {
|
|
455
459
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
456
460
|
}
|
|
457
461
|
//#endregion
|
|
462
|
+
//#region src/util/format.ts
|
|
463
|
+
function formatDuration(seconds) {
|
|
464
|
+
if (seconds == null) return "-";
|
|
465
|
+
if (seconds < 60) return `${seconds}s`;
|
|
466
|
+
const m = Math.floor(seconds / 60);
|
|
467
|
+
const s = seconds % 60;
|
|
468
|
+
if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
|
|
469
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
470
|
+
}
|
|
471
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
472
|
+
function formatDate$1(iso) {
|
|
473
|
+
const date = new Date(iso);
|
|
474
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
475
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
476
|
+
}
|
|
477
|
+
function formatDateTime(iso) {
|
|
478
|
+
const date = new Date(iso);
|
|
479
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
480
|
+
return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
481
|
+
}
|
|
482
|
+
const MINUTE = 60;
|
|
483
|
+
const HOUR = 3600;
|
|
484
|
+
const DAY = 24 * HOUR;
|
|
485
|
+
function relativeTime(iso, nowMs) {
|
|
486
|
+
if (!iso) return "-";
|
|
487
|
+
const then = Date.parse(iso);
|
|
488
|
+
if (Number.isNaN(then)) return "-";
|
|
489
|
+
const secs = Math.floor((nowMs - then) / 1e3);
|
|
490
|
+
if (secs < MINUTE) return "just now";
|
|
491
|
+
if (secs < HOUR) return `${Math.floor(secs / MINUTE)}m ago`;
|
|
492
|
+
if (secs < DAY) return `${Math.floor(secs / HOUR)}h ago`;
|
|
493
|
+
const days = Math.floor(secs / DAY);
|
|
494
|
+
if (days < 45) return `${days}d ago`;
|
|
495
|
+
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
|
|
496
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
497
|
+
}
|
|
498
|
+
function orList(items) {
|
|
499
|
+
if (items.length < 3) return items.join(" or ");
|
|
500
|
+
return `${items.slice(0, -1).join(", ")}, or ${items.at(-1)}`;
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
458
503
|
//#region src/util/parse.ts
|
|
459
504
|
function parseIssueKey(input) {
|
|
460
505
|
const match = input.toUpperCase().match(/[A-Z][A-Z0-9]+-\d+/);
|
|
@@ -469,6 +514,13 @@ function parsePageId(input) {
|
|
|
469
514
|
if (fromQuery) return fromQuery[1];
|
|
470
515
|
return null;
|
|
471
516
|
}
|
|
517
|
+
function parseBoardId(input) {
|
|
518
|
+
const trimmed = input.trim();
|
|
519
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
520
|
+
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
521
|
+
const match = trimmed.match(/\/boards\/(\d+)(?:[/?#]|$)/) ?? trimmed.match(/[?&]rapidView=(\d+)/);
|
|
522
|
+
return match ? Number(match[1]) : null;
|
|
523
|
+
}
|
|
472
524
|
const PULL_REQUEST_HOSTS = ["bitbucket.org", "www.bitbucket.org"];
|
|
473
525
|
const PULL_REQUEST_PATH = /^\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)(?:\/|$)/;
|
|
474
526
|
function parsePullRequestRef(input) {
|
|
@@ -522,6 +574,19 @@ function resolveRepo(flag, config) {
|
|
|
522
574
|
};
|
|
523
575
|
throw new Error(`No repo given. Pass --repo workspace/slug (or a bare slug), or set a default repo at \`atlass bitbucket login\`.`);
|
|
524
576
|
}
|
|
577
|
+
function parseChoices(values, choices, flag) {
|
|
578
|
+
const picked = /* @__PURE__ */ new Set();
|
|
579
|
+
const raw = (values ?? []).flatMap((v) => v.split(",")).map((v) => v.trim());
|
|
580
|
+
for (const value of raw.filter(Boolean)) {
|
|
581
|
+
const choice = choices.find((c) => c.toLowerCase() === value.toLowerCase());
|
|
582
|
+
if (!choice) {
|
|
583
|
+
const expected = orList(choices.map((c) => c.toLowerCase()));
|
|
584
|
+
throw new Error(`Invalid ${flag} "${value}". Expected ${expected}.`);
|
|
585
|
+
}
|
|
586
|
+
picked.add(choice);
|
|
587
|
+
}
|
|
588
|
+
return choices.filter((c) => picked.has(c));
|
|
589
|
+
}
|
|
525
590
|
const DEFAULT_LIMIT = 25;
|
|
526
591
|
const MAX_LIMIT = 100;
|
|
527
592
|
function parseLimit(value) {
|
|
@@ -768,15 +833,7 @@ const PULL_REQUEST_STATES = [
|
|
|
768
833
|
"SUPERSEDED"
|
|
769
834
|
];
|
|
770
835
|
function parsePullRequestStates(values) {
|
|
771
|
-
|
|
772
|
-
const states = [];
|
|
773
|
-
for (const value of raw) {
|
|
774
|
-
const state = value.trim().toUpperCase();
|
|
775
|
-
if (!state) continue;
|
|
776
|
-
if (!PULL_REQUEST_STATES.includes(state)) throw new Error(`Invalid --state "${value.trim()}". Expected ${PULL_REQUEST_STATES.map((s) => s.toLowerCase()).join(", ").replace(/, (?=[^,]*$)/, ", or ")}.`);
|
|
777
|
-
if (!states.includes(state)) states.push(state);
|
|
778
|
-
}
|
|
779
|
-
return states;
|
|
836
|
+
return parseChoices(values, PULL_REQUEST_STATES, "--state");
|
|
780
837
|
}
|
|
781
838
|
function allPullRequestStates() {
|
|
782
839
|
return [...PULL_REQUEST_STATES];
|
|
@@ -942,43 +999,6 @@ async function listChangedFiles(client, ref, id) {
|
|
|
942
999
|
return collectCapped(client, `${pullRequestPath(ref, id, "diffstat")}?pagelen=${PAGELEN}`, 50, toChangedFile);
|
|
943
1000
|
}
|
|
944
1001
|
//#endregion
|
|
945
|
-
//#region src/util/format.ts
|
|
946
|
-
function formatDuration(seconds) {
|
|
947
|
-
if (seconds == null) return "-";
|
|
948
|
-
if (seconds < 60) return `${seconds}s`;
|
|
949
|
-
const m = Math.floor(seconds / 60);
|
|
950
|
-
const s = seconds % 60;
|
|
951
|
-
if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
|
|
952
|
-
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
953
|
-
}
|
|
954
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
955
|
-
function formatDate$1(iso) {
|
|
956
|
-
const date = new Date(iso);
|
|
957
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
958
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
959
|
-
}
|
|
960
|
-
function formatDateTime(iso) {
|
|
961
|
-
const date = new Date(iso);
|
|
962
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
963
|
-
return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
964
|
-
}
|
|
965
|
-
const MINUTE = 60;
|
|
966
|
-
const HOUR = 3600;
|
|
967
|
-
const DAY = 24 * HOUR;
|
|
968
|
-
function relativeTime(iso, nowMs) {
|
|
969
|
-
if (!iso) return "-";
|
|
970
|
-
const then = Date.parse(iso);
|
|
971
|
-
if (Number.isNaN(then)) return "-";
|
|
972
|
-
const secs = Math.floor((nowMs - then) / 1e3);
|
|
973
|
-
if (secs < MINUTE) return "just now";
|
|
974
|
-
if (secs < HOUR) return `${Math.floor(secs / MINUTE)}m ago`;
|
|
975
|
-
if (secs < DAY) return `${Math.floor(secs / HOUR)}h ago`;
|
|
976
|
-
const days = Math.floor(secs / DAY);
|
|
977
|
-
if (days < 45) return `${days}d ago`;
|
|
978
|
-
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
|
|
979
|
-
return `${Math.floor(days / 365)}y ago`;
|
|
980
|
-
}
|
|
981
|
-
//#endregion
|
|
982
1002
|
//#region src/commands/bitbucket-comments.ts
|
|
983
1003
|
const MENTION = /@\{([^}]+)\}/g;
|
|
984
1004
|
function renderedPrComments(comments, participants) {
|
|
@@ -1580,6 +1600,7 @@ function alignedRows(items, nowMs, cells) {
|
|
|
1580
1600
|
const cell = cells(item);
|
|
1581
1601
|
return {
|
|
1582
1602
|
...cell,
|
|
1603
|
+
columns: cell.columns ?? [],
|
|
1583
1604
|
age: relativeTime(cell.timestamp, nowMs),
|
|
1584
1605
|
json: { ...item }
|
|
1585
1606
|
};
|
|
@@ -1587,11 +1608,17 @@ function alignedRows(items, nowMs, cells) {
|
|
|
1587
1608
|
const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
|
|
1588
1609
|
const idWidth = width((row) => row.id);
|
|
1589
1610
|
const labelWidth = width((row) => row.label);
|
|
1611
|
+
const columnWidths = (rows[0]?.columns ?? []).map((_, i) => width((row) => row.columns[i]));
|
|
1590
1612
|
const ageWidth = width((row) => row.age);
|
|
1591
1613
|
return rows.map((row) => ({
|
|
1592
1614
|
id: row.id,
|
|
1593
1615
|
url: row.url,
|
|
1594
|
-
fixedColumns:
|
|
1616
|
+
fixedColumns: [
|
|
1617
|
+
row.id.padEnd(idWidth),
|
|
1618
|
+
row.color(row.label.padEnd(labelWidth)),
|
|
1619
|
+
...row.columns.map((column, i) => column.padEnd(columnWidths[i])),
|
|
1620
|
+
row.age.padEnd(ageWidth)
|
|
1621
|
+
].join(" "),
|
|
1595
1622
|
freeText: row.text,
|
|
1596
1623
|
json: row.json
|
|
1597
1624
|
}));
|
|
@@ -1894,29 +1921,44 @@ function moved(flags, command) {
|
|
|
1894
1921
|
throw new Error(`${flag} is now its own command: \`atlass ${command} "<query>"\`.`);
|
|
1895
1922
|
});
|
|
1896
1923
|
}
|
|
1897
|
-
function outputOptions(command) {
|
|
1898
|
-
return command.option("-l, --limit <n>",
|
|
1924
|
+
function outputOptions(command, limitHelp = "max results (default 25, max 100)") {
|
|
1925
|
+
return command.option("-l, --limit <n>", limitHelp).option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy");
|
|
1899
1926
|
}
|
|
1900
1927
|
//#endregion
|
|
1901
|
-
//#region src/
|
|
1902
|
-
|
|
1903
|
-
|
|
1928
|
+
//#region src/util/media-type.ts
|
|
1929
|
+
const BY_EXTENSION = {
|
|
1930
|
+
".png": "image/png",
|
|
1931
|
+
".jpg": "image/jpeg",
|
|
1932
|
+
".jpeg": "image/jpeg",
|
|
1933
|
+
".gif": "image/gif",
|
|
1934
|
+
".webp": "image/webp",
|
|
1935
|
+
".svg": "image/svg+xml",
|
|
1936
|
+
".pdf": "application/pdf",
|
|
1937
|
+
".txt": "text/plain",
|
|
1938
|
+
".log": "text/plain",
|
|
1939
|
+
".md": "text/markdown",
|
|
1940
|
+
".csv": "text/csv",
|
|
1941
|
+
".html": "text/html",
|
|
1942
|
+
".json": "application/json",
|
|
1943
|
+
".xml": "application/xml",
|
|
1944
|
+
".zip": "application/zip",
|
|
1945
|
+
".mp4": "video/mp4",
|
|
1946
|
+
".m4v": "video/mp4",
|
|
1947
|
+
".mov": "video/quicktime",
|
|
1948
|
+
".webm": "video/webm",
|
|
1949
|
+
".ogv": "video/ogg",
|
|
1950
|
+
".avi": "video/x-msvideo",
|
|
1951
|
+
".mkv": "video/x-matroska",
|
|
1952
|
+
".wmv": "video/x-ms-wmv",
|
|
1953
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1954
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1955
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1956
|
+
};
|
|
1957
|
+
function mediaType(filename) {
|
|
1958
|
+
return BY_EXTENSION[extname(filename).toLowerCase()];
|
|
1904
1959
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
spaceId: page.spaceId,
|
|
1908
|
-
status: "current",
|
|
1909
|
-
title: page.title,
|
|
1910
|
-
...page.parentId ? { parentId: page.parentId } : {},
|
|
1911
|
-
body: {
|
|
1912
|
-
representation: "atlas_doc_format",
|
|
1913
|
-
value: JSON.stringify(page.body)
|
|
1914
|
-
}
|
|
1915
|
-
});
|
|
1916
|
-
return {
|
|
1917
|
-
id: res.id,
|
|
1918
|
-
version: res.version?.number ?? 1
|
|
1919
|
-
};
|
|
1960
|
+
function isVideo(href) {
|
|
1961
|
+
return mediaType(href.split(/[?#]/)[0] ?? "")?.startsWith("video/") ?? false;
|
|
1920
1962
|
}
|
|
1921
1963
|
//#endregion
|
|
1922
1964
|
//#region src/api/confluence-attachments.ts
|
|
@@ -1928,8 +1970,23 @@ async function listAttachments(client, id) {
|
|
|
1928
1970
|
size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
|
|
1929
1971
|
}));
|
|
1930
1972
|
}
|
|
1973
|
+
function uploadPath(pageId) {
|
|
1974
|
+
return `/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment`;
|
|
1975
|
+
}
|
|
1931
1976
|
async function uploadAttachment(client, pageId, filename, bytes) {
|
|
1932
|
-
return (await client.postMultipart(
|
|
1977
|
+
return (await client.postMultipart(uploadPath(pageId), filename, bytes)).results?.[0]?.extensions?.fileId ?? await fileIdByListing(client, pageId, filename);
|
|
1978
|
+
}
|
|
1979
|
+
async function attachToPage(client, pageId, filename, bytes, comment) {
|
|
1980
|
+
const first = (await client.postMultipart(uploadPath(pageId), filename, bytes, {
|
|
1981
|
+
type: mediaType(filename),
|
|
1982
|
+
fields: comment ? { comment } : void 0
|
|
1983
|
+
})).results?.[0];
|
|
1984
|
+
if (!first?.id) throw new Error(`Upload of "${filename}" returned no attachment.`);
|
|
1985
|
+
return {
|
|
1986
|
+
filename: first.title ?? filename,
|
|
1987
|
+
id: first.id,
|
|
1988
|
+
size: first.extensions?.fileSize ?? bytes.byteLength
|
|
1989
|
+
};
|
|
1933
1990
|
}
|
|
1934
1991
|
async function fileIdByListing(client, pageId, filename) {
|
|
1935
1992
|
const match = (await listAttachments(client, pageId)).find((a) => a.filename === filename);
|
|
@@ -1951,154 +2008,465 @@ function withWikiContextPath(link) {
|
|
|
1951
2008
|
return `/wiki${link}`;
|
|
1952
2009
|
}
|
|
1953
2010
|
//#endregion
|
|
1954
|
-
//#region src/
|
|
1955
|
-
async function
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
fetchSpaceKey(client, page.spaceId),
|
|
1960
|
-
fetchAttachments(client, id),
|
|
1961
|
-
fetchComments$1(client, id, names),
|
|
1962
|
-
names.resolve(page.version?.authorId ?? page.authorId)
|
|
1963
|
-
]);
|
|
1964
|
-
const webui = page._links?.webui ?? "";
|
|
1965
|
-
return {
|
|
1966
|
-
id: page.id,
|
|
1967
|
-
title: page.title,
|
|
1968
|
-
spaceKey,
|
|
1969
|
-
version: page.version?.number ?? 0,
|
|
1970
|
-
author,
|
|
1971
|
-
createdAt: page.createdAt ?? "",
|
|
1972
|
-
updatedAt: page.version?.createdAt ?? "",
|
|
1973
|
-
url: webui ? `${site}/wiki${webui}` : "",
|
|
1974
|
-
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
1975
|
-
attachments,
|
|
1976
|
-
comments
|
|
1977
|
-
};
|
|
1978
|
-
}
|
|
1979
|
-
async function fetchPageState(client, id) {
|
|
1980
|
-
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
1981
|
-
return {
|
|
1982
|
-
version: page.version?.number ?? 0,
|
|
1983
|
-
title: page.title,
|
|
1984
|
-
body: parseAdfJson(page.body?.atlas_doc_format?.value)
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
async function updatePage(client, id, params) {
|
|
1988
|
-
const res = await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
|
|
1989
|
-
id,
|
|
1990
|
-
status: "current",
|
|
1991
|
-
title: params.title,
|
|
1992
|
-
body: {
|
|
1993
|
-
representation: "atlas_doc_format",
|
|
1994
|
-
value: JSON.stringify(params.body)
|
|
1995
|
-
},
|
|
1996
|
-
version: {
|
|
1997
|
-
number: params.nextVersion,
|
|
1998
|
-
message: params.message
|
|
1999
|
-
}
|
|
2000
|
-
});
|
|
2001
|
-
return {
|
|
2002
|
-
number: res.version?.number ?? params.nextVersion,
|
|
2003
|
-
createdAt: res.version?.createdAt ?? ""
|
|
2004
|
-
};
|
|
2005
|
-
}
|
|
2006
|
-
async function fetchSpaceKey(client, spaceId) {
|
|
2007
|
-
if (!spaceId) return "";
|
|
2008
|
-
try {
|
|
2009
|
-
return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
|
|
2010
|
-
} catch {
|
|
2011
|
-
return "";
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
async function fetchComments$1(client, id, names) {
|
|
2015
|
-
const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
|
|
2016
|
-
return Promise.all(res.results.map(async (c) => ({
|
|
2017
|
-
author: await names.resolve(c.version?.authorId),
|
|
2018
|
-
created: c.version?.createdAt ?? "",
|
|
2019
|
-
body: parseAdfJson(c.body?.atlas_doc_format?.value)
|
|
2011
|
+
//#region src/commands/attach.ts
|
|
2012
|
+
async function readLocal(files, paths) {
|
|
2013
|
+
const read = await Promise.all(paths.map(async (path) => ({
|
|
2014
|
+
path,
|
|
2015
|
+
bytes: await files.readBytes(path).catch(() => void 0)
|
|
2020
2016
|
})));
|
|
2017
|
+
const unreadable = read.filter((r) => !r.bytes).map((r) => r.path);
|
|
2018
|
+
if (unreadable.length > 0) throw new Error(`Cannot read ${unreadable.join(", ")}.`);
|
|
2019
|
+
return read.map((r) => ({
|
|
2020
|
+
filename: basename(r.path),
|
|
2021
|
+
bytes: r.bytes
|
|
2022
|
+
}));
|
|
2021
2023
|
}
|
|
2022
|
-
function
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
} catch {
|
|
2027
|
-
return null;
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
var UserNames = class {
|
|
2031
|
-
client;
|
|
2032
|
-
cache = /* @__PURE__ */ new Map();
|
|
2033
|
-
constructor(client) {
|
|
2034
|
-
this.client = client;
|
|
2035
|
-
}
|
|
2036
|
-
async resolve(accountId) {
|
|
2037
|
-
if (!accountId) return "";
|
|
2038
|
-
const cached = this.cache.get(accountId);
|
|
2039
|
-
if (cached !== void 0) return cached;
|
|
2040
|
-
const name = await this.fetchDisplayName(accountId) ?? accountId;
|
|
2041
|
-
this.cache.set(accountId, name);
|
|
2042
|
-
return name;
|
|
2043
|
-
}
|
|
2044
|
-
async fetchDisplayName(accountId) {
|
|
2024
|
+
async function attachAll(term, target, local, options, upload) {
|
|
2025
|
+
const attached = [];
|
|
2026
|
+
for (const file of local) {
|
|
2027
|
+
term.err(`Uploading ${file.filename} ...`);
|
|
2045
2028
|
try {
|
|
2046
|
-
|
|
2047
|
-
} catch {
|
|
2048
|
-
|
|
2029
|
+
attached.push(await upload(file));
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
if (attached.length > 0) term.err(`Already attached to ${target}: ${names(attached)}.`);
|
|
2032
|
+
throw err;
|
|
2049
2033
|
}
|
|
2050
2034
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
//#region src/adf/html.ts
|
|
2054
|
-
const COMMENT = /<!--[\s\S]*?-->/g;
|
|
2055
|
-
function isOnlyComments(html) {
|
|
2056
|
-
return html.replace(COMMENT, "").trim().length === 0;
|
|
2035
|
+
if (options.json) term.json(attached);
|
|
2036
|
+
else term.out(`Attached ${names(attached)} to ${target}.`);
|
|
2057
2037
|
}
|
|
2058
|
-
function
|
|
2059
|
-
|
|
2060
|
-
let skipSpace = false;
|
|
2061
|
-
for (const token of marked.lexer(md)) {
|
|
2062
|
-
if (skipSpace && token.type === "space") continue;
|
|
2063
|
-
skipSpace = token.type === "html" && isOnlyComments(token.text);
|
|
2064
|
-
if (skipSpace) continue;
|
|
2065
|
-
kept.push(token.type === "code" ? token.raw : token.raw.replace(COMMENT, ""));
|
|
2066
|
-
}
|
|
2067
|
-
return kept.join("").trim();
|
|
2038
|
+
function names(attached) {
|
|
2039
|
+
return attached.map((a) => a.filename).join(", ");
|
|
2068
2040
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/commands/resolve-ref.ts
|
|
2043
|
+
async function resolveRef(ask, arg, kind) {
|
|
2044
|
+
const raw = arg ?? await ask.text({
|
|
2045
|
+
message: kind.message,
|
|
2046
|
+
flag: kind.flag,
|
|
2047
|
+
required: true
|
|
2076
2048
|
});
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
2049
|
+
const ref = kind.parse(raw);
|
|
2050
|
+
if (!ref) throw new Error(kind.notFound(raw));
|
|
2051
|
+
return ref;
|
|
2081
2052
|
}
|
|
2082
|
-
|
|
2083
|
-
//#region src/adf/from-markdown.ts
|
|
2084
|
-
function mediaNode(source, alt) {
|
|
2085
|
-
const attrs = { ...source };
|
|
2086
|
-
if (alt) attrs["alt"] = alt;
|
|
2053
|
+
function layoutOf(raw) {
|
|
2087
2054
|
return {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
content: [{
|
|
2091
|
-
type: "media",
|
|
2092
|
-
attrs
|
|
2093
|
-
}]
|
|
2055
|
+
bom: raw.startsWith(""),
|
|
2056
|
+
crlf: /\r?\n/.exec(raw)?.[0] === "\r\n"
|
|
2094
2057
|
};
|
|
2095
2058
|
}
|
|
2096
|
-
function
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2059
|
+
function normalized(raw) {
|
|
2060
|
+
return (raw.startsWith("") ? raw.slice(1) : raw).replace(/\r\n/g, "\n");
|
|
2061
|
+
}
|
|
2062
|
+
function editKeepingLayout(raw, edit) {
|
|
2063
|
+
return withLayout(edit(normalized(raw)), layoutOf(raw));
|
|
2064
|
+
}
|
|
2065
|
+
function withLayout(text, layout) {
|
|
2066
|
+
const lines = layout.crlf ? text.replace(/\n/g, "\r\n") : text;
|
|
2067
|
+
return layout.bom ? `${lines}` : lines;
|
|
2068
|
+
}
|
|
2069
|
+
//#endregion
|
|
2070
|
+
//#region src/markdown/frontmatter.ts
|
|
2071
|
+
const BLOCK = /^---\n([\s\S]*?)\n---\n?/;
|
|
2072
|
+
const KEY = /^([A-Za-z0-9_]+):\s*(.*)$/;
|
|
2073
|
+
const LIST_ITEM = /^ {2}- (.*)$/;
|
|
2074
|
+
const BARE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
2075
|
+
function splitFrontmatter(text) {
|
|
2076
|
+
const match = text.match(BLOCK);
|
|
2077
|
+
if (!match) return null;
|
|
2078
|
+
return {
|
|
2079
|
+
fields: parseFields(match[1] ?? ""),
|
|
2080
|
+
rest: text.slice(match[0].length)
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function formatFrontmatter(fields) {
|
|
2084
|
+
return [
|
|
2085
|
+
"---",
|
|
2086
|
+
...Object.entries(fields).flatMap(([key, value]) => fieldLines(key, value)),
|
|
2087
|
+
"---"
|
|
2088
|
+
].join("\n");
|
|
2089
|
+
}
|
|
2090
|
+
function rewriteFields(raw, fields) {
|
|
2091
|
+
return editKeepingLayout(raw, (text) => withFields(text, fields));
|
|
2092
|
+
}
|
|
2093
|
+
function withFields(text, fields) {
|
|
2094
|
+
const match = text.match(BLOCK);
|
|
2095
|
+
if (!match) return `${formatFrontmatter(fields)}\n\n${text}`;
|
|
2096
|
+
const lines = (match[1] ?? "").split("\n");
|
|
2097
|
+
const pending = new Map(Object.entries(fields));
|
|
2098
|
+
const kept = [];
|
|
2099
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2100
|
+
const line = lines[i] ?? "";
|
|
2101
|
+
const key = line.match(KEY)?.[1];
|
|
2102
|
+
const value = key === void 0 ? void 0 : pending.get(key);
|
|
2103
|
+
if (key === void 0 || value === void 0) {
|
|
2104
|
+
kept.push(line);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
kept.push(...fieldLines(key, value));
|
|
2108
|
+
pending.delete(key);
|
|
2109
|
+
while (LIST_ITEM.test(lines[i + 1] ?? "")) i++;
|
|
2110
|
+
}
|
|
2111
|
+
for (const [key, value] of pending) kept.push(...fieldLines(key, value));
|
|
2112
|
+
const closing = match[0].endsWith("\n") ? "\n" : "";
|
|
2113
|
+
return `---\n${kept.join("\n")}\n---${closing}${text.slice(match[0].length)}`;
|
|
2114
|
+
}
|
|
2115
|
+
function fieldLines(key, value) {
|
|
2116
|
+
if (Array.isArray(value)) return value.length === 0 ? [`${key}: []`] : [`${key}:`, ...value.map((v) => ` - ${quote$1(v)}`)];
|
|
2117
|
+
return [`${key}: ${typeof value === "number" ? value : quote$1(value)}`];
|
|
2118
|
+
}
|
|
2119
|
+
function parseFields(block) {
|
|
2120
|
+
const fields = {};
|
|
2121
|
+
const lines = block.split("\n");
|
|
2122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2123
|
+
const m = lines[i]?.match(KEY);
|
|
2124
|
+
if (!m) continue;
|
|
2125
|
+
const key = m[1] ?? "";
|
|
2126
|
+
const raw = (m[2] ?? "").trim();
|
|
2127
|
+
if (raw === "[]") fields[key] = [];
|
|
2128
|
+
else if (raw === "") {
|
|
2129
|
+
const items = [];
|
|
2130
|
+
for (let item; item = lines[i + 1]?.match(LIST_ITEM); i++) items.push(unquote((item[1] ?? "").trim()));
|
|
2131
|
+
fields[key] = items;
|
|
2132
|
+
} else fields[key] = scalar(raw);
|
|
2133
|
+
}
|
|
2134
|
+
return fields;
|
|
2135
|
+
}
|
|
2136
|
+
function scalar(raw) {
|
|
2137
|
+
if (raw.startsWith("\"")) return unquote(raw);
|
|
2138
|
+
if (BARE_NUMBER.test(raw)) return Number(raw);
|
|
2139
|
+
return raw;
|
|
2140
|
+
}
|
|
2141
|
+
function quote$1(value) {
|
|
2142
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
2143
|
+
}
|
|
2144
|
+
function unquote(value) {
|
|
2145
|
+
if (!value.startsWith("\"") || !value.endsWith("\"")) return value;
|
|
2146
|
+
return value.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
2147
|
+
}
|
|
2148
|
+
//#endregion
|
|
2149
|
+
//#region src/markdown/copied-document.ts
|
|
2150
|
+
const GENERATED_MARKER = "<!-- atlass:generated -->";
|
|
2151
|
+
const COMMENTS_HEADING = "## Comments";
|
|
2152
|
+
const ATTACHMENTS_HEADING = "## Attachments";
|
|
2153
|
+
const LEGACY_SECTION_HEADING = /^## (Comments|Attachments)\s*$/;
|
|
2154
|
+
function render(doc) {
|
|
2155
|
+
return `${[
|
|
2156
|
+
formatFrontmatter(doc.fields),
|
|
2157
|
+
`# ${doc.title}`,
|
|
2158
|
+
doc.body,
|
|
2159
|
+
GENERATED_MARKER,
|
|
2160
|
+
commentsSection(doc.comments),
|
|
2161
|
+
attachmentsSection(doc.attachments)
|
|
2162
|
+
].filter((s) => s.trim().length > 0).join("\n\n")}\n`;
|
|
2163
|
+
}
|
|
2164
|
+
function parse(raw) {
|
|
2165
|
+
const split = splitFrontmatter(normalized(raw));
|
|
2166
|
+
if (!split) throw new Error("Not an atlass file: no YAML frontmatter found.");
|
|
2167
|
+
return sourceOf(split, "marker-or-legacy-heading");
|
|
2168
|
+
}
|
|
2169
|
+
function parseDraft(raw) {
|
|
2170
|
+
const text = normalized(raw);
|
|
2171
|
+
return sourceOf(splitFrontmatter(text) ?? {
|
|
2172
|
+
fields: {},
|
|
2173
|
+
rest: text
|
|
2174
|
+
}, "marker");
|
|
2175
|
+
}
|
|
2176
|
+
function withGeneratedMarker(text) {
|
|
2177
|
+
if (text.split("\n").some(isMarker)) return text;
|
|
2178
|
+
return `${text.trimEnd()}\n\n${GENERATED_MARKER}\n`;
|
|
2179
|
+
}
|
|
2180
|
+
function sourceOf({ fields, rest }, ending) {
|
|
2181
|
+
const lines = rest.split("\n");
|
|
2182
|
+
const h1 = leadingH1(lines);
|
|
2183
|
+
const start = h1 ? h1.index + 1 : 0;
|
|
2184
|
+
const end = bodyEnd(lines, start, ending);
|
|
2185
|
+
const fallbackTitle = typeof fields["title"] === "string" ? fields["title"] : "";
|
|
2186
|
+
return {
|
|
2187
|
+
fields,
|
|
2188
|
+
title: h1?.title ?? fallbackTitle,
|
|
2189
|
+
body: lines.slice(start, end).join("\n").trim()
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
function parseIssueSource(text) {
|
|
2193
|
+
const source = parse(text);
|
|
2194
|
+
const key = scalarString(source.fields["key"]);
|
|
2195
|
+
if (!key) throw new Error("Frontmatter is missing the issue `key`; re-copy the issue.");
|
|
2196
|
+
return {
|
|
2197
|
+
...source,
|
|
2198
|
+
key,
|
|
2199
|
+
updatedAtCopy: scalarString(source.fields["updated"])
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function parsePageSource(text) {
|
|
2203
|
+
const source = parse(text);
|
|
2204
|
+
const id = scalarString(source.fields["id"]);
|
|
2205
|
+
if (!id) throw new Error("Frontmatter is missing the page `id`; re-copy the page.");
|
|
2206
|
+
const rawVersion = scalarString(source.fields["version"]);
|
|
2207
|
+
const version = rawVersion === "" ? NaN : Number(rawVersion);
|
|
2208
|
+
if (!Number.isFinite(version)) throw new Error("Frontmatter is missing a numeric `version`; re-copy the page.");
|
|
2209
|
+
return {
|
|
2210
|
+
...source,
|
|
2211
|
+
id,
|
|
2212
|
+
version
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function scalarString(value) {
|
|
2216
|
+
if (typeof value === "string") return value;
|
|
2217
|
+
if (typeof value === "number") return String(value);
|
|
2218
|
+
return "";
|
|
2219
|
+
}
|
|
2220
|
+
function leadingH1(lines) {
|
|
2221
|
+
for (const [index, line] of lines.entries()) {
|
|
2222
|
+
if (line.startsWith("# ")) return {
|
|
2223
|
+
index,
|
|
2224
|
+
title: line.slice(2).trim()
|
|
2225
|
+
};
|
|
2226
|
+
if (line.trim().length > 0) return null;
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
function bodyEnd(lines, start, ending) {
|
|
2231
|
+
const after = (test) => lines.findIndex((line, i) => i >= start && test(line));
|
|
2232
|
+
const marker = after(isMarker);
|
|
2233
|
+
if (marker !== -1) return marker;
|
|
2234
|
+
const legacy = ending === "marker" ? -1 : after((line) => LEGACY_SECTION_HEADING.test(line));
|
|
2235
|
+
return legacy === -1 ? lines.length : legacy;
|
|
2236
|
+
}
|
|
2237
|
+
function isMarker(line) {
|
|
2238
|
+
return line.trimEnd() === GENERATED_MARKER;
|
|
2239
|
+
}
|
|
2240
|
+
function commentsSection(comments) {
|
|
2241
|
+
if (comments.length === 0) return "";
|
|
2242
|
+
const blocks = comments.map((c) => {
|
|
2243
|
+
const heading = `### ${c.author || "Unknown"}${c.created ? ` - ${formatDateTime(c.created)}` : ""}`;
|
|
2244
|
+
return c.body ? `${heading}\n\n${c.body}` : heading;
|
|
2245
|
+
});
|
|
2246
|
+
return [
|
|
2247
|
+
COMMENTS_HEADING,
|
|
2248
|
+
"",
|
|
2249
|
+
blocks.join("\n\n")
|
|
2250
|
+
].join("\n");
|
|
2251
|
+
}
|
|
2252
|
+
function attachmentsSection(attachments) {
|
|
2253
|
+
if (attachments.length === 0) return "";
|
|
2254
|
+
const items = attachments.map((a) => `- [${a.filename}](${a.relativePath})`);
|
|
2255
|
+
return [
|
|
2256
|
+
ATTACHMENTS_HEADING,
|
|
2257
|
+
"",
|
|
2258
|
+
...items
|
|
2259
|
+
].join("\n");
|
|
2260
|
+
}
|
|
2261
|
+
//#endregion
|
|
2262
|
+
//#region src/commands/page-ref.ts
|
|
2263
|
+
const PAGE_REF = {
|
|
2264
|
+
message: "Confluence page id or URL:",
|
|
2265
|
+
flag: "[page]",
|
|
2266
|
+
parse: parsePageId,
|
|
2267
|
+
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2268
|
+
};
|
|
2269
|
+
async function pageIdOrFile({ term, files }, arg) {
|
|
2270
|
+
if (!arg?.endsWith(".md")) return resolveRef(term.ask, arg, PAGE_REF);
|
|
2271
|
+
const id = scalarString(parse(await files.readText(arg)).fields["id"]);
|
|
2272
|
+
if (!id) throw new Error(`${arg} has no page \`id\` in its frontmatter.`);
|
|
2273
|
+
return id;
|
|
2274
|
+
}
|
|
2275
|
+
//#endregion
|
|
2276
|
+
//#region src/commands/confluence-attach.ts
|
|
2277
|
+
async function confluenceAttach({ session, term, files }, arg, paths, options) {
|
|
2278
|
+
const id = await resolveRef(term.ask, arg, PAGE_REF);
|
|
2279
|
+
const local = await readLocal(files, paths);
|
|
2280
|
+
await refuseClashes(session, id, local);
|
|
2281
|
+
await attachAll(term, `page ${id}`, local, options, (file) => attachToPage(session, id, file.filename, file.bytes, options.comment));
|
|
2282
|
+
}
|
|
2283
|
+
async function refuseClashes(client, id, local) {
|
|
2284
|
+
const names = local.map((f) => f.filename);
|
|
2285
|
+
const repeated = names.filter((name, i) => names.indexOf(name) !== i);
|
|
2286
|
+
if (repeated.length > 0) throw new Error(`More than one file is named ${unique(repeated).join(", ")}.`);
|
|
2287
|
+
const existing = new Set((await listAttachments(client, id)).map((a) => a.filename));
|
|
2288
|
+
const clashes = names.filter((name) => existing.has(name));
|
|
2289
|
+
if (clashes.length > 0) throw new Error(`${clashes.join(", ")} already attached to page ${id}.`);
|
|
2290
|
+
}
|
|
2291
|
+
function unique(names) {
|
|
2292
|
+
return [...new Set(names)];
|
|
2293
|
+
}
|
|
2294
|
+
//#endregion
|
|
2295
|
+
//#region src/api/confluence-create.ts
|
|
2296
|
+
async function findSpaceId(client, key) {
|
|
2297
|
+
return (await client.getJson(`/wiki/api/v2/spaces?keys=${encodeURIComponent(key)}`)).results[0]?.id ?? null;
|
|
2298
|
+
}
|
|
2299
|
+
async function createPage(client, page) {
|
|
2300
|
+
const res = await client.postJson("/wiki/api/v2/pages", {
|
|
2301
|
+
spaceId: page.spaceId,
|
|
2302
|
+
status: "current",
|
|
2303
|
+
title: page.title,
|
|
2304
|
+
...page.parentId ? { parentId: page.parentId } : {},
|
|
2305
|
+
body: {
|
|
2306
|
+
representation: "atlas_doc_format",
|
|
2307
|
+
value: JSON.stringify(page.body)
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
return {
|
|
2311
|
+
id: res.id,
|
|
2312
|
+
version: res.version?.number ?? 1
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
//#endregion
|
|
2316
|
+
//#region src/api/confluence-pages.ts
|
|
2317
|
+
async function fetchPage(client, site, id) {
|
|
2318
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2319
|
+
const names = new UserNames(client);
|
|
2320
|
+
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
2321
|
+
fetchSpaceKey(client, page.spaceId),
|
|
2322
|
+
fetchAttachments(client, id),
|
|
2323
|
+
fetchComments$1(client, id, names),
|
|
2324
|
+
names.resolve(page.version?.authorId ?? page.authorId)
|
|
2325
|
+
]);
|
|
2326
|
+
const webui = page._links?.webui ?? "";
|
|
2327
|
+
return {
|
|
2328
|
+
id: page.id,
|
|
2329
|
+
title: page.title,
|
|
2330
|
+
spaceKey,
|
|
2331
|
+
version: page.version?.number ?? 0,
|
|
2332
|
+
author,
|
|
2333
|
+
createdAt: page.createdAt ?? "",
|
|
2334
|
+
updatedAt: page.version?.createdAt ?? "",
|
|
2335
|
+
url: webui ? `${site}/wiki${webui}` : "",
|
|
2336
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
2337
|
+
attachments,
|
|
2338
|
+
comments
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
async function fetchPageState(client, id) {
|
|
2342
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2343
|
+
return {
|
|
2344
|
+
version: page.version?.number ?? 0,
|
|
2345
|
+
title: page.title,
|
|
2346
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value)
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
async function updatePage(client, id, params) {
|
|
2350
|
+
const res = await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
|
|
2351
|
+
id,
|
|
2352
|
+
status: "current",
|
|
2353
|
+
title: params.title,
|
|
2354
|
+
body: {
|
|
2355
|
+
representation: "atlas_doc_format",
|
|
2356
|
+
value: JSON.stringify(params.body)
|
|
2357
|
+
},
|
|
2358
|
+
version: {
|
|
2359
|
+
number: params.nextVersion,
|
|
2360
|
+
message: params.message
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
return {
|
|
2364
|
+
number: res.version?.number ?? params.nextVersion,
|
|
2365
|
+
createdAt: res.version?.createdAt ?? ""
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
async function fetchSpaceKey(client, spaceId) {
|
|
2369
|
+
if (!spaceId) return "";
|
|
2370
|
+
try {
|
|
2371
|
+
return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
|
|
2372
|
+
} catch {
|
|
2373
|
+
return "";
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
async function fetchComments$1(client, id, names) {
|
|
2377
|
+
const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
|
|
2378
|
+
return Promise.all(res.results.map(async (c) => ({
|
|
2379
|
+
author: await names.resolve(c.version?.authorId),
|
|
2380
|
+
created: c.version?.createdAt ?? "",
|
|
2381
|
+
body: parseAdfJson(c.body?.atlas_doc_format?.value)
|
|
2382
|
+
})));
|
|
2383
|
+
}
|
|
2384
|
+
function parseAdfJson(value) {
|
|
2385
|
+
if (!value) return null;
|
|
2386
|
+
try {
|
|
2387
|
+
return JSON.parse(value);
|
|
2388
|
+
} catch {
|
|
2389
|
+
return null;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
var UserNames = class {
|
|
2393
|
+
client;
|
|
2394
|
+
cache = /* @__PURE__ */ new Map();
|
|
2395
|
+
constructor(client) {
|
|
2396
|
+
this.client = client;
|
|
2397
|
+
}
|
|
2398
|
+
async resolve(accountId) {
|
|
2399
|
+
if (!accountId) return "";
|
|
2400
|
+
const cached = this.cache.get(accountId);
|
|
2401
|
+
if (cached !== void 0) return cached;
|
|
2402
|
+
const name = await this.fetchDisplayName(accountId) ?? accountId;
|
|
2403
|
+
this.cache.set(accountId, name);
|
|
2404
|
+
return name;
|
|
2405
|
+
}
|
|
2406
|
+
async fetchDisplayName(accountId) {
|
|
2407
|
+
try {
|
|
2408
|
+
return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
|
|
2409
|
+
} catch {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
//#endregion
|
|
2415
|
+
//#region src/adf/html.ts
|
|
2416
|
+
const COMMENT = /<!--[\s\S]*?-->/g;
|
|
2417
|
+
function isOnlyComments(html) {
|
|
2418
|
+
return html.replace(COMMENT, "").trim().length === 0;
|
|
2419
|
+
}
|
|
2420
|
+
function withoutComments(md) {
|
|
2421
|
+
const kept = [];
|
|
2422
|
+
let skipSpace = false;
|
|
2423
|
+
for (const token of marked.lexer(md)) {
|
|
2424
|
+
if (skipSpace && token.type === "space") continue;
|
|
2425
|
+
skipSpace = token.type === "html" && isOnlyComments(token.text);
|
|
2426
|
+
if (skipSpace) continue;
|
|
2427
|
+
kept.push(token.type === "code" ? token.raw : token.raw.replace(COMMENT, ""));
|
|
2428
|
+
}
|
|
2429
|
+
return kept.join("").trim();
|
|
2430
|
+
}
|
|
2431
|
+
function htmlWarnings(md) {
|
|
2432
|
+
let blocks = 0;
|
|
2433
|
+
let inline = 0;
|
|
2434
|
+
marked.walkTokens(marked.lexer(md), (token) => {
|
|
2435
|
+
if (token.type !== "html" || isOnlyComments(token.text)) return;
|
|
2436
|
+
if (token.block) blocks++;
|
|
2437
|
+
else inline++;
|
|
2438
|
+
});
|
|
2439
|
+
return [...blocks > 0 ? [`${counted(blocks, "HTML block")} will be dropped`] : [], ...inline > 0 ? [`${counted(inline, "inline HTML tag")} will be sent as text`] : []];
|
|
2440
|
+
}
|
|
2441
|
+
function counted(n, noun) {
|
|
2442
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
2443
|
+
}
|
|
2444
|
+
//#endregion
|
|
2445
|
+
//#region src/adf/from-markdown.ts
|
|
2446
|
+
const NATURAL_SIZE = { layout: "center" };
|
|
2447
|
+
const TEXT_WIDTH = {
|
|
2448
|
+
layout: "center",
|
|
2449
|
+
width: 100,
|
|
2450
|
+
widthType: "percentage"
|
|
2451
|
+
};
|
|
2452
|
+
function mediaNode(source, alt, href) {
|
|
2453
|
+
const attrs = { ...source };
|
|
2454
|
+
if (alt) attrs["alt"] = alt;
|
|
2455
|
+
return {
|
|
2456
|
+
type: "mediaSingle",
|
|
2457
|
+
attrs: isVideo(href) ? TEXT_WIDTH : NATURAL_SIZE,
|
|
2458
|
+
content: [{
|
|
2459
|
+
type: "media",
|
|
2460
|
+
attrs
|
|
2461
|
+
}]
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
function imageHrefs(md) {
|
|
2465
|
+
const hrefs = /* @__PURE__ */ new Set();
|
|
2466
|
+
marked.walkTokens(marked.lexer(md), (token) => {
|
|
2467
|
+
if (token.type === "image") hrefs.add(token.href);
|
|
2468
|
+
});
|
|
2469
|
+
return [...hrefs];
|
|
2102
2470
|
}
|
|
2103
2471
|
function markdownToAdf(md, options = {}) {
|
|
2104
2472
|
const ctx = { resolveImage: options.resolveImage ?? externalMedia };
|
|
@@ -2273,7 +2641,7 @@ function externalMedia(href, alt) {
|
|
|
2273
2641
|
return mediaNode({
|
|
2274
2642
|
type: "external",
|
|
2275
2643
|
url: href
|
|
2276
|
-
}, alt);
|
|
2644
|
+
}, alt, href);
|
|
2277
2645
|
}
|
|
2278
2646
|
function textToken(text) {
|
|
2279
2647
|
return {
|
|
@@ -2458,7 +2826,7 @@ function planPageBody(page, attachments, localImages) {
|
|
|
2458
2826
|
type: "file",
|
|
2459
2827
|
id: entry.kind === "reuse" ? entry.fileId : href,
|
|
2460
2828
|
collection
|
|
2461
|
-
}, alt);
|
|
2829
|
+
}, alt, href);
|
|
2462
2830
|
} }),
|
|
2463
2831
|
uploads: images.flatMap((e) => e.kind === "upload" || e.kind === "changed" ? [{
|
|
2464
2832
|
href: e.href,
|
|
@@ -2579,243 +2947,9 @@ function favouritePath(id) {
|
|
|
2579
2947
|
async function starPage(client, id) {
|
|
2580
2948
|
await client.putNoContent(favouritePath(id), void 0);
|
|
2581
2949
|
}
|
|
2582
|
-
async function unstarPage(client, id) {
|
|
2583
|
-
await client.deleteNoContent(favouritePath(id));
|
|
2584
|
-
}
|
|
2585
|
-
//#endregion
|
|
2586
|
-
//#region src/commands/resolve-ref.ts
|
|
2587
|
-
async function resolveRef(ask, arg, kind) {
|
|
2588
|
-
const raw = arg ?? await ask.text({
|
|
2589
|
-
message: kind.message,
|
|
2590
|
-
flag: kind.flag,
|
|
2591
|
-
required: true
|
|
2592
|
-
});
|
|
2593
|
-
const ref = kind.parse(raw);
|
|
2594
|
-
if (!ref) throw new Error(kind.notFound(raw));
|
|
2595
|
-
return ref;
|
|
2596
|
-
}
|
|
2597
|
-
function layoutOf(raw) {
|
|
2598
|
-
return {
|
|
2599
|
-
bom: raw.startsWith(""),
|
|
2600
|
-
crlf: /\r?\n/.exec(raw)?.[0] === "\r\n"
|
|
2601
|
-
};
|
|
2602
|
-
}
|
|
2603
|
-
function normalized(raw) {
|
|
2604
|
-
return (raw.startsWith("") ? raw.slice(1) : raw).replace(/\r\n/g, "\n");
|
|
2605
|
-
}
|
|
2606
|
-
function editKeepingLayout(raw, edit) {
|
|
2607
|
-
return withLayout(edit(normalized(raw)), layoutOf(raw));
|
|
2608
|
-
}
|
|
2609
|
-
function withLayout(text, layout) {
|
|
2610
|
-
const lines = layout.crlf ? text.replace(/\n/g, "\r\n") : text;
|
|
2611
|
-
return layout.bom ? `${lines}` : lines;
|
|
2612
|
-
}
|
|
2613
|
-
//#endregion
|
|
2614
|
-
//#region src/markdown/frontmatter.ts
|
|
2615
|
-
const BLOCK = /^---\n([\s\S]*?)\n---\n?/;
|
|
2616
|
-
const KEY = /^([A-Za-z0-9_]+):\s*(.*)$/;
|
|
2617
|
-
const LIST_ITEM = /^ {2}- (.*)$/;
|
|
2618
|
-
const BARE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
2619
|
-
function splitFrontmatter(text) {
|
|
2620
|
-
const match = text.match(BLOCK);
|
|
2621
|
-
if (!match) return null;
|
|
2622
|
-
return {
|
|
2623
|
-
fields: parseFields(match[1] ?? ""),
|
|
2624
|
-
rest: text.slice(match[0].length)
|
|
2625
|
-
};
|
|
2626
|
-
}
|
|
2627
|
-
function formatFrontmatter(fields) {
|
|
2628
|
-
return [
|
|
2629
|
-
"---",
|
|
2630
|
-
...Object.entries(fields).flatMap(([key, value]) => fieldLines(key, value)),
|
|
2631
|
-
"---"
|
|
2632
|
-
].join("\n");
|
|
2633
|
-
}
|
|
2634
|
-
function rewriteFields(raw, fields) {
|
|
2635
|
-
return editKeepingLayout(raw, (text) => withFields(text, fields));
|
|
2636
|
-
}
|
|
2637
|
-
function withFields(text, fields) {
|
|
2638
|
-
const match = text.match(BLOCK);
|
|
2639
|
-
if (!match) return `${formatFrontmatter(fields)}\n\n${text}`;
|
|
2640
|
-
const lines = (match[1] ?? "").split("\n");
|
|
2641
|
-
const pending = new Map(Object.entries(fields));
|
|
2642
|
-
const kept = [];
|
|
2643
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2644
|
-
const line = lines[i] ?? "";
|
|
2645
|
-
const key = line.match(KEY)?.[1];
|
|
2646
|
-
const value = key === void 0 ? void 0 : pending.get(key);
|
|
2647
|
-
if (key === void 0 || value === void 0) {
|
|
2648
|
-
kept.push(line);
|
|
2649
|
-
continue;
|
|
2650
|
-
}
|
|
2651
|
-
kept.push(...fieldLines(key, value));
|
|
2652
|
-
pending.delete(key);
|
|
2653
|
-
while (LIST_ITEM.test(lines[i + 1] ?? "")) i++;
|
|
2654
|
-
}
|
|
2655
|
-
for (const [key, value] of pending) kept.push(...fieldLines(key, value));
|
|
2656
|
-
const closing = match[0].endsWith("\n") ? "\n" : "";
|
|
2657
|
-
return `---\n${kept.join("\n")}\n---${closing}${text.slice(match[0].length)}`;
|
|
2658
|
-
}
|
|
2659
|
-
function fieldLines(key, value) {
|
|
2660
|
-
if (Array.isArray(value)) return value.length === 0 ? [`${key}: []`] : [`${key}:`, ...value.map((v) => ` - ${quote$1(v)}`)];
|
|
2661
|
-
return [`${key}: ${typeof value === "number" ? value : quote$1(value)}`];
|
|
2662
|
-
}
|
|
2663
|
-
function parseFields(block) {
|
|
2664
|
-
const fields = {};
|
|
2665
|
-
const lines = block.split("\n");
|
|
2666
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2667
|
-
const m = lines[i]?.match(KEY);
|
|
2668
|
-
if (!m) continue;
|
|
2669
|
-
const key = m[1] ?? "";
|
|
2670
|
-
const raw = (m[2] ?? "").trim();
|
|
2671
|
-
if (raw === "[]") fields[key] = [];
|
|
2672
|
-
else if (raw === "") {
|
|
2673
|
-
const items = [];
|
|
2674
|
-
for (let item; item = lines[i + 1]?.match(LIST_ITEM); i++) items.push(unquote((item[1] ?? "").trim()));
|
|
2675
|
-
fields[key] = items;
|
|
2676
|
-
} else fields[key] = scalar(raw);
|
|
2677
|
-
}
|
|
2678
|
-
return fields;
|
|
2679
|
-
}
|
|
2680
|
-
function scalar(raw) {
|
|
2681
|
-
if (raw.startsWith("\"")) return unquote(raw);
|
|
2682
|
-
if (BARE_NUMBER.test(raw)) return Number(raw);
|
|
2683
|
-
return raw;
|
|
2684
|
-
}
|
|
2685
|
-
function quote$1(value) {
|
|
2686
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
2687
|
-
}
|
|
2688
|
-
function unquote(value) {
|
|
2689
|
-
if (!value.startsWith("\"") || !value.endsWith("\"")) return value;
|
|
2690
|
-
return value.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
2691
|
-
}
|
|
2692
|
-
//#endregion
|
|
2693
|
-
//#region src/markdown/copied-document.ts
|
|
2694
|
-
const GENERATED_MARKER = "<!-- atlass:generated -->";
|
|
2695
|
-
const COMMENTS_HEADING = "## Comments";
|
|
2696
|
-
const ATTACHMENTS_HEADING = "## Attachments";
|
|
2697
|
-
const LEGACY_SECTION_HEADING = /^## (Comments|Attachments)\s*$/;
|
|
2698
|
-
function render(doc) {
|
|
2699
|
-
return `${[
|
|
2700
|
-
formatFrontmatter(doc.fields),
|
|
2701
|
-
`# ${doc.title}`,
|
|
2702
|
-
doc.body,
|
|
2703
|
-
GENERATED_MARKER,
|
|
2704
|
-
commentsSection(doc.comments),
|
|
2705
|
-
attachmentsSection(doc.attachments)
|
|
2706
|
-
].filter((s) => s.trim().length > 0).join("\n\n")}\n`;
|
|
2707
|
-
}
|
|
2708
|
-
function parse(raw) {
|
|
2709
|
-
const split = splitFrontmatter(normalized(raw));
|
|
2710
|
-
if (!split) throw new Error("Not an atlass file: no YAML frontmatter found.");
|
|
2711
|
-
return sourceOf(split, "marker-or-legacy-heading");
|
|
2712
|
-
}
|
|
2713
|
-
function parseDraft(raw) {
|
|
2714
|
-
const text = normalized(raw);
|
|
2715
|
-
return sourceOf(splitFrontmatter(text) ?? {
|
|
2716
|
-
fields: {},
|
|
2717
|
-
rest: text
|
|
2718
|
-
}, "marker");
|
|
2719
|
-
}
|
|
2720
|
-
function withGeneratedMarker(text) {
|
|
2721
|
-
if (text.split("\n").some(isMarker)) return text;
|
|
2722
|
-
return `${text.trimEnd()}\n\n${GENERATED_MARKER}\n`;
|
|
2723
|
-
}
|
|
2724
|
-
function sourceOf({ fields, rest }, ending) {
|
|
2725
|
-
const lines = rest.split("\n");
|
|
2726
|
-
const h1 = leadingH1(lines);
|
|
2727
|
-
const start = h1 ? h1.index + 1 : 0;
|
|
2728
|
-
const end = bodyEnd(lines, start, ending);
|
|
2729
|
-
const fallbackTitle = typeof fields["title"] === "string" ? fields["title"] : "";
|
|
2730
|
-
return {
|
|
2731
|
-
fields,
|
|
2732
|
-
title: h1?.title ?? fallbackTitle,
|
|
2733
|
-
body: lines.slice(start, end).join("\n").trim()
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
2736
|
-
function parseIssueSource(text) {
|
|
2737
|
-
const source = parse(text);
|
|
2738
|
-
const key = scalarString(source.fields["key"]);
|
|
2739
|
-
if (!key) throw new Error("Frontmatter is missing the issue `key`; re-copy the issue.");
|
|
2740
|
-
return {
|
|
2741
|
-
...source,
|
|
2742
|
-
key,
|
|
2743
|
-
updatedAtCopy: scalarString(source.fields["updated"])
|
|
2744
|
-
};
|
|
2745
|
-
}
|
|
2746
|
-
function parsePageSource(text) {
|
|
2747
|
-
const source = parse(text);
|
|
2748
|
-
const id = scalarString(source.fields["id"]);
|
|
2749
|
-
if (!id) throw new Error("Frontmatter is missing the page `id`; re-copy the page.");
|
|
2750
|
-
const rawVersion = scalarString(source.fields["version"]);
|
|
2751
|
-
const version = rawVersion === "" ? NaN : Number(rawVersion);
|
|
2752
|
-
if (!Number.isFinite(version)) throw new Error("Frontmatter is missing a numeric `version`; re-copy the page.");
|
|
2753
|
-
return {
|
|
2754
|
-
...source,
|
|
2755
|
-
id,
|
|
2756
|
-
version
|
|
2757
|
-
};
|
|
2758
|
-
}
|
|
2759
|
-
function scalarString(value) {
|
|
2760
|
-
if (typeof value === "string") return value;
|
|
2761
|
-
if (typeof value === "number") return String(value);
|
|
2762
|
-
return "";
|
|
2763
|
-
}
|
|
2764
|
-
function leadingH1(lines) {
|
|
2765
|
-
for (const [index, line] of lines.entries()) {
|
|
2766
|
-
if (line.startsWith("# ")) return {
|
|
2767
|
-
index,
|
|
2768
|
-
title: line.slice(2).trim()
|
|
2769
|
-
};
|
|
2770
|
-
if (line.trim().length > 0) return null;
|
|
2771
|
-
}
|
|
2772
|
-
return null;
|
|
2773
|
-
}
|
|
2774
|
-
function bodyEnd(lines, start, ending) {
|
|
2775
|
-
const after = (test) => lines.findIndex((line, i) => i >= start && test(line));
|
|
2776
|
-
const marker = after(isMarker);
|
|
2777
|
-
if (marker !== -1) return marker;
|
|
2778
|
-
const legacy = ending === "marker" ? -1 : after((line) => LEGACY_SECTION_HEADING.test(line));
|
|
2779
|
-
return legacy === -1 ? lines.length : legacy;
|
|
2780
|
-
}
|
|
2781
|
-
function isMarker(line) {
|
|
2782
|
-
return line.trimEnd() === GENERATED_MARKER;
|
|
2783
|
-
}
|
|
2784
|
-
function commentsSection(comments) {
|
|
2785
|
-
if (comments.length === 0) return "";
|
|
2786
|
-
const blocks = comments.map((c) => {
|
|
2787
|
-
const heading = `### ${c.author || "Unknown"}${c.created ? ` - ${formatDateTime(c.created)}` : ""}`;
|
|
2788
|
-
return c.body ? `${heading}\n\n${c.body}` : heading;
|
|
2789
|
-
});
|
|
2790
|
-
return [
|
|
2791
|
-
COMMENTS_HEADING,
|
|
2792
|
-
"",
|
|
2793
|
-
blocks.join("\n\n")
|
|
2794
|
-
].join("\n");
|
|
2795
|
-
}
|
|
2796
|
-
function attachmentsSection(attachments) {
|
|
2797
|
-
if (attachments.length === 0) return "";
|
|
2798
|
-
const items = attachments.map((a) => `- [${a.filename}](${a.relativePath})`);
|
|
2799
|
-
return [
|
|
2800
|
-
ATTACHMENTS_HEADING,
|
|
2801
|
-
"",
|
|
2802
|
-
...items
|
|
2803
|
-
].join("\n");
|
|
2804
|
-
}
|
|
2805
|
-
//#endregion
|
|
2806
|
-
//#region src/commands/page-ref.ts
|
|
2807
|
-
const PAGE_REF = {
|
|
2808
|
-
message: "Confluence page id or URL:",
|
|
2809
|
-
flag: "[page]",
|
|
2810
|
-
parse: parsePageId,
|
|
2811
|
-
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2812
|
-
};
|
|
2813
|
-
async function pageIdOrFile({ term, files }, arg) {
|
|
2814
|
-
if (!arg?.endsWith(".md")) return resolveRef(term.ask, arg, PAGE_REF);
|
|
2815
|
-
const id = scalarString(parse(await files.readText(arg)).fields["id"]);
|
|
2816
|
-
if (!id) throw new Error(`${arg} has no page \`id\` in its frontmatter.`);
|
|
2817
|
-
return id;
|
|
2818
|
-
}
|
|
2950
|
+
async function unstarPage(client, id) {
|
|
2951
|
+
await client.deleteNoContent(favouritePath(id));
|
|
2952
|
+
}
|
|
2819
2953
|
//#endregion
|
|
2820
2954
|
//#region src/commands/confluence-star.ts
|
|
2821
2955
|
async function confluenceStar(env, arg) {
|
|
@@ -3074,6 +3208,11 @@ function inClause(field, given) {
|
|
|
3074
3208
|
function userClause(field, given) {
|
|
3075
3209
|
return renderClause(field, values(given).map((v) => v === "me" ? "currentUser()" : quote(v)));
|
|
3076
3210
|
}
|
|
3211
|
+
function anyOf(terms) {
|
|
3212
|
+
if (terms.length === 0) return null;
|
|
3213
|
+
if (terms.length === 1) return terms[0];
|
|
3214
|
+
return `(${terms.join(" OR ")})`;
|
|
3215
|
+
}
|
|
3077
3216
|
function renderClause(field, rendered) {
|
|
3078
3217
|
if (rendered.length === 0) return null;
|
|
3079
3218
|
if (rendered.length === 1) return `${field} = ${rendered[0]}`;
|
|
@@ -3328,6 +3467,7 @@ function registerConfluence(confluence) {
|
|
|
3328
3467
|
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));
|
|
3329
3468
|
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));
|
|
3330
3469
|
confluence.command("create [file]").description("Create a Confluence page from a Markdown file").option("-s, --space <key>", "space to create it in (default: the frontmatter space)").option("--parent <page>", "parent page id or URL (default: the space homepage)").option("--star", "star the new page").option("--dry-run", "show what would be created without writing").action(jiraAction(confluenceCreate));
|
|
3470
|
+
confluence.command("attach <page> <files...>").description("Upload files as attachments to a Confluence page (id or URL)").option("--comment <text>", "attachment comment, applied to every file").option("--json", "print the uploaded attachments as JSON").action(jiraAction(confluenceAttach));
|
|
3331
3471
|
confluence.command("star [page]").description("Star a Confluence page (id, URL, or copied file)").action(jiraAction(confluenceStar));
|
|
3332
3472
|
confluence.command("unstar [page]").description("Unstar a Confluence page (id, URL, or copied file)").action(jiraAction(confluenceUnstar));
|
|
3333
3473
|
outputOptions(confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space")).action(jiraAction(confluenceList));
|
|
@@ -3336,9 +3476,385 @@ function registerConfluence(confluence) {
|
|
|
3336
3476
|
return confluence;
|
|
3337
3477
|
}
|
|
3338
3478
|
//#endregion
|
|
3339
|
-
//#region src/api/jira-
|
|
3340
|
-
function
|
|
3341
|
-
|
|
3479
|
+
//#region src/api/jira-attachments.ts
|
|
3480
|
+
async function attachToIssue(client, key, filename, bytes) {
|
|
3481
|
+
const [first] = await client.postMultipart(`/rest/api/3/issue/${encodeURIComponent(key)}/attachments`, filename, bytes, { type: mediaType(filename) });
|
|
3482
|
+
if (!first) throw new Error(`Upload of "${filename}" returned no attachment.`);
|
|
3483
|
+
return {
|
|
3484
|
+
filename: first.filename,
|
|
3485
|
+
id: first.id,
|
|
3486
|
+
size: first.size
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
//#endregion
|
|
3490
|
+
//#region src/api/jira-url.ts
|
|
3491
|
+
function browseUrl(site, key) {
|
|
3492
|
+
return `${site}/browse/${key}`;
|
|
3493
|
+
}
|
|
3494
|
+
function boardUrl(site, id) {
|
|
3495
|
+
return `${site}/secure/RapidBoard.jspa?rapidView=${id}`;
|
|
3496
|
+
}
|
|
3497
|
+
//#endregion
|
|
3498
|
+
//#region src/api/jira-issues.ts
|
|
3499
|
+
const FIELDS = [
|
|
3500
|
+
"summary",
|
|
3501
|
+
"description",
|
|
3502
|
+
"issuetype",
|
|
3503
|
+
"status",
|
|
3504
|
+
"assignee",
|
|
3505
|
+
"reporter",
|
|
3506
|
+
"priority",
|
|
3507
|
+
"labels",
|
|
3508
|
+
"created",
|
|
3509
|
+
"updated",
|
|
3510
|
+
"attachment"
|
|
3511
|
+
].join(",");
|
|
3512
|
+
async function fetchIssue(client, site, key) {
|
|
3513
|
+
const issue = await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${FIELDS}`);
|
|
3514
|
+
const comments = await fetchComments(client, key);
|
|
3515
|
+
const f = issue.fields;
|
|
3516
|
+
return {
|
|
3517
|
+
key: issue.key,
|
|
3518
|
+
url: browseUrl(site, issue.key),
|
|
3519
|
+
summary: f.summary ?? "",
|
|
3520
|
+
type: f.issuetype?.name ?? "",
|
|
3521
|
+
status: f.status?.name ?? "",
|
|
3522
|
+
statusCategory: f.status?.statusCategory?.key ?? "",
|
|
3523
|
+
assignee: f.assignee?.displayName ?? "Unassigned",
|
|
3524
|
+
reporter: f.reporter?.displayName ?? "",
|
|
3525
|
+
priority: f.priority?.name ?? "",
|
|
3526
|
+
labels: f.labels ?? [],
|
|
3527
|
+
created: f.created ?? "",
|
|
3528
|
+
updated: f.updated ?? "",
|
|
3529
|
+
description: f.description ?? null,
|
|
3530
|
+
comments,
|
|
3531
|
+
attachments: (f.attachment ?? []).map((a) => ({
|
|
3532
|
+
mediaId: a.id,
|
|
3533
|
+
filename: a.filename,
|
|
3534
|
+
url: a.content
|
|
3535
|
+
}))
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
async function updateIssue(client, key, update) {
|
|
3539
|
+
const fields = { description: update.description };
|
|
3540
|
+
if (update.summary !== void 0) fields["summary"] = update.summary;
|
|
3541
|
+
const path = `/rest/api/3/issue/${encodeURIComponent(key)}`;
|
|
3542
|
+
await client.putNoContent(path, { fields });
|
|
3543
|
+
return (await client.getJson(`${path}?fields=updated`)).fields.updated ?? "";
|
|
3544
|
+
}
|
|
3545
|
+
async function fetchComments(client, key) {
|
|
3546
|
+
return (await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}/comment?maxResults=100&orderBy=created`)).comments.map((c) => ({
|
|
3547
|
+
author: c.author?.displayName ?? "",
|
|
3548
|
+
created: c.created ?? "",
|
|
3549
|
+
body: c.body ?? null
|
|
3550
|
+
}));
|
|
3551
|
+
}
|
|
3552
|
+
//#endregion
|
|
3553
|
+
//#region src/api/offset-pages.ts
|
|
3554
|
+
async function allOffsetPages(client, pathAt) {
|
|
3555
|
+
const all = [];
|
|
3556
|
+
for (let startAt = 0;;) {
|
|
3557
|
+
const res = await client.getJson(pathAt(startAt));
|
|
3558
|
+
const values = res.values ?? [];
|
|
3559
|
+
all.push(...values);
|
|
3560
|
+
if (res.isLast || values.length === 0) return all;
|
|
3561
|
+
startAt += values.length;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
//#endregion
|
|
3565
|
+
//#region src/api/jira-projects.ts
|
|
3566
|
+
async function listProjects(client, site, query) {
|
|
3567
|
+
return (await allOffsetPages(client, (startAt) => `/rest/api/3/project/search?${projectSearchQuery(query, startAt)}`)).map((p) => ({
|
|
3568
|
+
key: p.key,
|
|
3569
|
+
name: p.name,
|
|
3570
|
+
id: p.id,
|
|
3571
|
+
type: p.projectTypeKey ?? "",
|
|
3572
|
+
url: browseUrl(site, p.key)
|
|
3573
|
+
}));
|
|
3574
|
+
}
|
|
3575
|
+
function projectSearchQuery(query, startAt) {
|
|
3576
|
+
const params = new URLSearchParams({
|
|
3577
|
+
orderBy: "key",
|
|
3578
|
+
maxResults: String(50),
|
|
3579
|
+
startAt: String(startAt)
|
|
3580
|
+
});
|
|
3581
|
+
if (query) params.set("query", query);
|
|
3582
|
+
return params.toString();
|
|
3583
|
+
}
|
|
3584
|
+
//#endregion
|
|
3585
|
+
//#region src/api/jira-statuses.ts
|
|
3586
|
+
async function listStatuses(client, project) {
|
|
3587
|
+
return dedupeAndSortStatuses((project ? await fetchProjectStatuses(client, project) : await fetchAllStatuses(client)).map(toStatusSummary));
|
|
3588
|
+
}
|
|
3589
|
+
async function statusesById(client) {
|
|
3590
|
+
const raw = await fetchAllStatuses(client);
|
|
3591
|
+
return new Map(raw.map((s) => [s.id, toStatusSummary(s)]));
|
|
3592
|
+
}
|
|
3593
|
+
function fetchAllStatuses(client) {
|
|
3594
|
+
return client.getJson("/rest/api/3/status");
|
|
3595
|
+
}
|
|
3596
|
+
async function fetchProjectStatuses(client, project) {
|
|
3597
|
+
let groups;
|
|
3598
|
+
try {
|
|
3599
|
+
groups = await client.getJson(`/rest/api/3/project/${encodeURIComponent(project)}/statuses`);
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`No project found with key "${project}".`);
|
|
3602
|
+
throw err;
|
|
3603
|
+
}
|
|
3604
|
+
return groups.flatMap((g) => g.statuses ?? []);
|
|
3605
|
+
}
|
|
3606
|
+
function toStatusSummary(s) {
|
|
3607
|
+
return {
|
|
3608
|
+
name: s.name,
|
|
3609
|
+
id: s.id,
|
|
3610
|
+
category: s.statusCategory?.name ?? "",
|
|
3611
|
+
categoryKey: s.statusCategory?.key ?? ""
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
const CATEGORY_ORDER = {
|
|
3615
|
+
new: 0,
|
|
3616
|
+
indeterminate: 1,
|
|
3617
|
+
done: 2
|
|
3618
|
+
};
|
|
3619
|
+
const UNKNOWN_CATEGORY_RANK = Object.keys(CATEGORY_ORDER).length;
|
|
3620
|
+
function dedupeAndSortStatuses(statuses) {
|
|
3621
|
+
const byNameCategory = /* @__PURE__ */ new Map();
|
|
3622
|
+
for (const s of statuses) {
|
|
3623
|
+
const key = `${s.name}\0${s.categoryKey}`;
|
|
3624
|
+
if (!byNameCategory.has(key)) byNameCategory.set(key, s);
|
|
3625
|
+
}
|
|
3626
|
+
return [...byNameCategory.values()].sort((a, b) => {
|
|
3627
|
+
const rank = (s) => CATEGORY_ORDER[s.categoryKey] ?? UNKNOWN_CATEGORY_RANK;
|
|
3628
|
+
return rank(a) - rank(b) || a.name.localeCompare(b.name);
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
//#endregion
|
|
3632
|
+
//#region src/commands/jira.ts
|
|
3633
|
+
async function jiraProjects({ session, term }, query, options) {
|
|
3634
|
+
const projects = await listProjects(session, session.site, query);
|
|
3635
|
+
if (options.json) term.json(projects);
|
|
3636
|
+
else if (projects.length === 0) term.out("No matching projects.");
|
|
3637
|
+
else term.out(formatProjectRows(projects));
|
|
3638
|
+
}
|
|
3639
|
+
function formatProjectRows(projects) {
|
|
3640
|
+
const width = Math.max(...projects.map((p) => p.key.length));
|
|
3641
|
+
return projects.map((p) => `${p.key.padEnd(width)} ${p.name}`);
|
|
3642
|
+
}
|
|
3643
|
+
async function jiraStatuses({ session, term }, query, options) {
|
|
3644
|
+
let statuses = await listStatuses(session, options.project);
|
|
3645
|
+
if (query) {
|
|
3646
|
+
const needle = query.toLowerCase();
|
|
3647
|
+
statuses = statuses.filter((s) => s.name.toLowerCase().includes(needle));
|
|
3648
|
+
}
|
|
3649
|
+
if (options.json) term.json(statuses);
|
|
3650
|
+
else if (statuses.length === 0) term.out("No matching statuses.");
|
|
3651
|
+
else term.out(formatStatusRows(statuses));
|
|
3652
|
+
}
|
|
3653
|
+
function formatStatusRows(statuses) {
|
|
3654
|
+
const width = Math.max(...statuses.map((s) => s.name.length));
|
|
3655
|
+
return statuses.map((s) => `${s.name.padEnd(width)} ${s.category}`);
|
|
3656
|
+
}
|
|
3657
|
+
async function jiraView({ session, term }, arg, options) {
|
|
3658
|
+
const key = await resolveRef(term.ask, arg, ISSUE_REF);
|
|
3659
|
+
const lines = formatIssueView(await fetchIssue(session, session.site, key), Date.now(), options.allComments ?? false);
|
|
3660
|
+
await term.page(lines.join("\n"), { pager: options.pager });
|
|
3661
|
+
}
|
|
3662
|
+
const CATEGORY_COLORS = {
|
|
3663
|
+
new: kleur.cyan,
|
|
3664
|
+
indeterminate: kleur.yellow,
|
|
3665
|
+
done: kleur.green
|
|
3666
|
+
};
|
|
3667
|
+
function colorForCategory(category) {
|
|
3668
|
+
return CATEGORY_COLORS[category] ?? ((text) => text);
|
|
3669
|
+
}
|
|
3670
|
+
function formatIssueView(issue, nowMs, allComments) {
|
|
3671
|
+
const colorStatus = colorForCategory(issue.statusCategory);
|
|
3672
|
+
return [
|
|
3673
|
+
`${kleur.bold(issue.key)} ${issue.summary}`,
|
|
3674
|
+
...fieldLines$1([
|
|
3675
|
+
["Type", issue.type],
|
|
3676
|
+
["Status", issue.status && colorStatus(issue.status)],
|
|
3677
|
+
["Assignee", issue.assignee],
|
|
3678
|
+
["Reporter", issue.reporter],
|
|
3679
|
+
["Priority", issue.priority],
|
|
3680
|
+
["Labels", issue.labels.join(", ")],
|
|
3681
|
+
["Created", dateWithAge(issue.created, nowMs)],
|
|
3682
|
+
["Updated", dateWithAge(issue.updated, nowMs)],
|
|
3683
|
+
["URL", issue.url]
|
|
3684
|
+
]),
|
|
3685
|
+
...bodyLines(issue.description),
|
|
3686
|
+
...commentSection(renderedComments(issue.comments), { allComments }),
|
|
3687
|
+
...attachmentSection(issue.attachments)
|
|
3688
|
+
];
|
|
3689
|
+
}
|
|
3690
|
+
async function jiraCopy(env, arg, options) {
|
|
3691
|
+
await copyIssue(env, await resolveRef(env.term.ask, arg, ISSUE_REF), options.out);
|
|
3692
|
+
}
|
|
3693
|
+
async function jiraUpdate({ session, term, files }, arg, options) {
|
|
3694
|
+
const file = arg ?? await term.ask.text({
|
|
3695
|
+
message: "Path to the issue Markdown file:",
|
|
3696
|
+
flag: "[file]",
|
|
3697
|
+
required: true
|
|
3698
|
+
});
|
|
3699
|
+
const raw = await files.readText(file);
|
|
3700
|
+
const src = parseIssueSource(raw);
|
|
3701
|
+
const plan = planIssueUpdate(src, await fetchIssue(session, session.site, src.key), options);
|
|
3702
|
+
await runPlan(term, plan, options, async () => {
|
|
3703
|
+
const { current, next } = plan.headline;
|
|
3704
|
+
const updated = await updateIssue(session, src.key, {
|
|
3705
|
+
description: plan.body,
|
|
3706
|
+
summary: next !== current ? next : void 0
|
|
3707
|
+
});
|
|
3708
|
+
term.out(`Updated ${src.key}.`);
|
|
3709
|
+
if (updated) await files.writeText(file, rewriteFields(raw, { updated }));
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
async function copyIssue(env, key, out) {
|
|
3713
|
+
env.term.err(`Fetching ${key} ...`);
|
|
3714
|
+
await runCopy(env, planIssueCopy(await fetchIssue(env.session, env.session.site, key), out));
|
|
3715
|
+
}
|
|
3716
|
+
const ISSUE_REF = {
|
|
3717
|
+
message: "Jira issue key or URL:",
|
|
3718
|
+
flag: "[issue]",
|
|
3719
|
+
parse: parseIssueKey,
|
|
3720
|
+
notFound: (raw) => `Could not find an issue key in "${raw}" (expected e.g. PROJ-123).`
|
|
3721
|
+
};
|
|
3722
|
+
//#endregion
|
|
3723
|
+
//#region src/commands/jira-attach.ts
|
|
3724
|
+
async function jiraAttach({ session, term, files }, arg, paths, options) {
|
|
3725
|
+
const key = await resolveRef(term.ask, arg, ISSUE_REF);
|
|
3726
|
+
await attachAll(term, key, await readLocal(files, paths), options, (file) => attachToIssue(session, key, file.filename, file.bytes));
|
|
3727
|
+
}
|
|
3728
|
+
//#endregion
|
|
3729
|
+
//#region src/api/jira-board.ts
|
|
3730
|
+
async function fetchBoard(client, site, id) {
|
|
3731
|
+
const [config, statuses] = await Promise.all([client.getJson(`/rest/agile/1.0/board/${id}/configuration`), statusesById(client)]);
|
|
3732
|
+
const statusFor = (statusId) => statuses.get(statusId) ?? {
|
|
3733
|
+
id: statusId,
|
|
3734
|
+
name: statusId,
|
|
3735
|
+
category: "",
|
|
3736
|
+
categoryKey: ""
|
|
3737
|
+
};
|
|
3738
|
+
return {
|
|
3739
|
+
id: config.id,
|
|
3740
|
+
name: config.name,
|
|
3741
|
+
type: config.type,
|
|
3742
|
+
url: boardUrl(site, config.id),
|
|
3743
|
+
estimation: estimationOf(config),
|
|
3744
|
+
filter: {
|
|
3745
|
+
id: config.filter.id,
|
|
3746
|
+
jql: await visibleFilterJql(client, config.filter.id),
|
|
3747
|
+
subQuery: config.subQuery?.query || null
|
|
3748
|
+
},
|
|
3749
|
+
columns: (config.columnConfig?.columns ?? []).map((c) => ({
|
|
3750
|
+
name: c.name,
|
|
3751
|
+
statuses: (c.statuses ?? []).map((s) => statusFor(s.id))
|
|
3752
|
+
}))
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
async function visibleFilterJql(client, filterId) {
|
|
3756
|
+
try {
|
|
3757
|
+
return (await client.getJson(`/rest/api/3/filter/${filterId}`)).jql ?? null;
|
|
3758
|
+
} catch (err) {
|
|
3759
|
+
if (err instanceof HttpError && err.status === 404) return null;
|
|
3760
|
+
throw err;
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
function estimationOf(config) {
|
|
3764
|
+
const field = config.estimation?.type === "field" ? config.estimation.field : void 0;
|
|
3765
|
+
return field ? {
|
|
3766
|
+
fieldId: field.fieldId,
|
|
3767
|
+
fieldName: field.displayName
|
|
3768
|
+
} : null;
|
|
3769
|
+
}
|
|
3770
|
+
//#endregion
|
|
3771
|
+
//#region src/api/jira-boards.ts
|
|
3772
|
+
async function listBoards(client, site) {
|
|
3773
|
+
return (await allOffsetPages(client, (startAt) => `/rest/agile/1.0/board?startAt=${startAt}&maxResults=50`)).map((b) => ({
|
|
3774
|
+
id: b.id,
|
|
3775
|
+
name: b.name,
|
|
3776
|
+
type: b.type,
|
|
3777
|
+
project: b.location?.projectKey ?? null,
|
|
3778
|
+
url: boardUrl(site, b.id)
|
|
3779
|
+
}));
|
|
3780
|
+
}
|
|
3781
|
+
//#endregion
|
|
3782
|
+
//#region src/commands/board-ref.ts
|
|
3783
|
+
async function withBoard(session, ref, load) {
|
|
3784
|
+
const id = await resolveBoardId(session, ref);
|
|
3785
|
+
try {
|
|
3786
|
+
return await load(id);
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
if (isMissingBoard(err, id)) throw unknownBoard(ref);
|
|
3789
|
+
throw err;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
async function resolveBoardId(session, ref) {
|
|
3793
|
+
const id = parseBoardId(ref);
|
|
3794
|
+
if (id !== null) return id;
|
|
3795
|
+
const name = ref.trim().toLowerCase();
|
|
3796
|
+
const matches = (await listBoards(session, session.site)).filter((b) => b.name.toLowerCase() === name);
|
|
3797
|
+
const [only, ...others] = matches;
|
|
3798
|
+
if (!only) throw unknownBoard(ref);
|
|
3799
|
+
if (others.length === 0) return only.id;
|
|
3800
|
+
const width = Math.max(...matches.map((b) => String(b.id).length));
|
|
3801
|
+
throw new Error([`"${ref}" matches several boards. Use an id:`, ...matches.map((b) => ` ${String(b.id).padEnd(width)} ${b.name}`)].join("\n"));
|
|
3802
|
+
}
|
|
3803
|
+
function isMissingBoard(err, id) {
|
|
3804
|
+
return err instanceof HttpError && err.status === 404 && err.path.startsWith(`/rest/agile/1.0/board/${id}/`);
|
|
3805
|
+
}
|
|
3806
|
+
function unknownBoard(ref) {
|
|
3807
|
+
return /* @__PURE__ */ new Error(`No board matches "${ref}". Run \`atlass jira boards\` to list boards.`);
|
|
3808
|
+
}
|
|
3809
|
+
//#endregion
|
|
3810
|
+
//#region src/commands/jira-board.ts
|
|
3811
|
+
async function jiraBoard({ session, term }, ref, options) {
|
|
3812
|
+
const board = await withBoard(session, ref, (id) => fetchBoard(session, session.site, id));
|
|
3813
|
+
if (options.json) term.json(board);
|
|
3814
|
+
else term.out(formatBoard(board));
|
|
3815
|
+
}
|
|
3816
|
+
function formatBoard(board) {
|
|
3817
|
+
return [
|
|
3818
|
+
board.name,
|
|
3819
|
+
...fieldLines$1([
|
|
3820
|
+
["ID", String(board.id)],
|
|
3821
|
+
["Type", board.type],
|
|
3822
|
+
["Estimation", estimationLine(board)],
|
|
3823
|
+
["Filter", board.filter.jql ?? `not visible to you (filter ${board.filter.id})`],
|
|
3824
|
+
["Sub-filter", board.filter.subQuery ?? ""],
|
|
3825
|
+
["URL", board.url]
|
|
3826
|
+
]),
|
|
3827
|
+
"",
|
|
3828
|
+
...columnLines(board)
|
|
3829
|
+
];
|
|
3830
|
+
}
|
|
3831
|
+
function estimationLine({ estimation }) {
|
|
3832
|
+
return estimation ? `${estimation.fieldName} (${estimation.fieldId})` : "none, this board does not estimate with a field";
|
|
3833
|
+
}
|
|
3834
|
+
function columnLines({ columns }) {
|
|
3835
|
+
const statuses = columns.flatMap((c) => c.statuses);
|
|
3836
|
+
const width = Math.max(0, ...statuses.map((s) => s.name.length));
|
|
3837
|
+
return columns.flatMap((c) => [c.name, ...c.statuses.map((s) => ` ${s.name.padEnd(width)} ${s.category}`.trimEnd())]);
|
|
3838
|
+
}
|
|
3839
|
+
//#endregion
|
|
3840
|
+
//#region src/commands/jira-boards.ts
|
|
3841
|
+
async function jiraBoards({ session, term }, query, options) {
|
|
3842
|
+
const boards = filterBoards(await listBoards(session, session.site), query, options.project);
|
|
3843
|
+
if (options.json) term.json(boards);
|
|
3844
|
+
else if (boards.length === 0) term.out("No matching boards.");
|
|
3845
|
+
else term.out(formatBoardRows(boards));
|
|
3846
|
+
}
|
|
3847
|
+
function filterBoards(boards, query, project) {
|
|
3848
|
+
const needle = query?.toLowerCase();
|
|
3849
|
+
const key = project?.toUpperCase();
|
|
3850
|
+
return boards.filter((b) => (!needle || b.name.toLowerCase().includes(needle)) && (!key || b.project?.toUpperCase() === key));
|
|
3851
|
+
}
|
|
3852
|
+
function formatBoardRows(boards) {
|
|
3853
|
+
const width = (pick) => Math.max(...boards.map((b) => pick(b).length));
|
|
3854
|
+
const idWidth = width((b) => String(b.id));
|
|
3855
|
+
const typeWidth = width((b) => b.type);
|
|
3856
|
+
const nameWidth = width((b) => b.name);
|
|
3857
|
+
return boards.map((b) => `${String(b.id).padEnd(idWidth)} ${b.type.padEnd(typeWidth)} ${b.name.padEnd(nameWidth)} ${b.project ?? ""}`.trimEnd());
|
|
3342
3858
|
}
|
|
3343
3859
|
//#endregion
|
|
3344
3860
|
//#region src/api/jira-createmeta.ts
|
|
@@ -3431,35 +3947,6 @@ function toJiraUser(u) {
|
|
|
3431
3947
|
};
|
|
3432
3948
|
}
|
|
3433
3949
|
//#endregion
|
|
3434
|
-
//#region src/api/jira-projects.ts
|
|
3435
|
-
const PROJECT_PAGE_SIZE = 50;
|
|
3436
|
-
async function listProjects(client, site, query) {
|
|
3437
|
-
const projects = [];
|
|
3438
|
-
for (let startAt = 0;;) {
|
|
3439
|
-
const res = await client.getJson(`/rest/api/3/project/search?${projectSearchQuery(query, startAt)}`);
|
|
3440
|
-
const values = res.values ?? [];
|
|
3441
|
-
for (const p of values) projects.push({
|
|
3442
|
-
key: p.key,
|
|
3443
|
-
name: p.name,
|
|
3444
|
-
id: p.id,
|
|
3445
|
-
type: p.projectTypeKey ?? "",
|
|
3446
|
-
url: browseUrl(site, p.key)
|
|
3447
|
-
});
|
|
3448
|
-
if (res.isLast || values.length === 0) break;
|
|
3449
|
-
startAt += values.length;
|
|
3450
|
-
}
|
|
3451
|
-
return projects;
|
|
3452
|
-
}
|
|
3453
|
-
function projectSearchQuery(query, startAt) {
|
|
3454
|
-
const params = new URLSearchParams({
|
|
3455
|
-
orderBy: "key",
|
|
3456
|
-
maxResults: String(PROJECT_PAGE_SIZE),
|
|
3457
|
-
startAt: String(startAt)
|
|
3458
|
-
});
|
|
3459
|
-
if (query) params.set("query", query);
|
|
3460
|
-
return params.toString();
|
|
3461
|
-
}
|
|
3462
|
-
//#endregion
|
|
3463
3950
|
//#region src/commands/jira-create-resolve.ts
|
|
3464
3951
|
async function resolveProject(ask, client, site, arg, strict) {
|
|
3465
3952
|
if (arg) return arg.toUpperCase();
|
|
@@ -4098,17 +4585,55 @@ function describeRejection(err, meta) {
|
|
|
4098
4585
|
...perField
|
|
4099
4586
|
].join("\n"));
|
|
4100
4587
|
}
|
|
4101
|
-
|
|
4588
|
+
//#endregion
|
|
4589
|
+
//#region src/api/jira-fields.ts
|
|
4590
|
+
async function listSiteFields(client) {
|
|
4591
|
+
return (await client.getJson("/rest/api/3/field")).map((f) => ({
|
|
4592
|
+
id: f.id,
|
|
4593
|
+
name: decodeEntities(f.name),
|
|
4594
|
+
custom: f.custom ?? false,
|
|
4595
|
+
schema: f.schema ?? null
|
|
4596
|
+
}));
|
|
4597
|
+
}
|
|
4598
|
+
//#endregion
|
|
4599
|
+
//#region src/commands/jira-fields.ts
|
|
4600
|
+
async function jiraFields(env, projectArg, typeArg, options) {
|
|
4601
|
+
if (!projectArg) return siteFields(env, options);
|
|
4102
4602
|
const project = projectArg.toUpperCase();
|
|
4603
|
+
if (options.search !== void 0) throw new Error(`--search lists site fields and takes no project. Drop ${project}, or run \`atlass jira fields ${project}\` alone.`);
|
|
4604
|
+
return createForm$1(env, project, typeArg, options);
|
|
4605
|
+
}
|
|
4606
|
+
async function siteFields({ session, term }, options) {
|
|
4607
|
+
const fields = matchingFields(await listSiteFields(session), options.search).sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
|
|
4608
|
+
if (options.json) term.json(fields);
|
|
4609
|
+
else if (fields.length === 0) term.out("No matching fields.");
|
|
4610
|
+
else term.out(formatSiteFieldRows(fields));
|
|
4611
|
+
}
|
|
4612
|
+
function matchingFields(fields, search) {
|
|
4613
|
+
const needle = search?.toLowerCase();
|
|
4614
|
+
return fields.filter((f) => !needle || f.name.toLowerCase().includes(needle) || f.id.toLowerCase().includes(needle));
|
|
4615
|
+
}
|
|
4616
|
+
function formatSiteFieldRows(fields) {
|
|
4617
|
+
const width = (pick) => Math.max(...fields.map((f) => pick(f).length));
|
|
4618
|
+
const idWidth = width((f) => f.id);
|
|
4619
|
+
const nameWidth = width((f) => f.name);
|
|
4620
|
+
return fields.map((f) => `${f.id.padEnd(idWidth)} ${f.name.padEnd(nameWidth)} ${shortFieldType(f)}`.trimEnd());
|
|
4621
|
+
}
|
|
4622
|
+
function shortFieldType(field) {
|
|
4623
|
+
const plugin = field.schema?.custom;
|
|
4624
|
+
if (plugin) return plugin.slice(plugin.lastIndexOf(":") + 1);
|
|
4625
|
+
return field.schema?.type ?? "";
|
|
4626
|
+
}
|
|
4627
|
+
async function createForm$1({ session, term }, project, typeArg, { json }) {
|
|
4103
4628
|
const types = await fetchCreateIssueTypes(session, project);
|
|
4104
4629
|
if (!typeArg) {
|
|
4105
|
-
if (
|
|
4630
|
+
if (json) term.json(types);
|
|
4106
4631
|
else if (types.length === 0) term.out(`You cannot create issues in ${project}.`);
|
|
4107
4632
|
else term.out(formatIssueTypeRows(types));
|
|
4108
4633
|
return;
|
|
4109
4634
|
}
|
|
4110
4635
|
const fields = await fetchCreateFields(session, project, (await resolveType(term.ask, project, types, typeArg, true)).id);
|
|
4111
|
-
if (
|
|
4636
|
+
if (json) term.json(fields);
|
|
4112
4637
|
else term.out(formatFieldRows(fields));
|
|
4113
4638
|
}
|
|
4114
4639
|
//#endregion
|
|
@@ -4116,23 +4641,23 @@ async function jiraFields({ session, term }, projectArg, typeArg, options) {
|
|
|
4116
4641
|
const RECENT_ISSUES_CLAUSE = "updated >= -30d";
|
|
4117
4642
|
const OPEN_CLAUSE = "statusCategory != Done";
|
|
4118
4643
|
const SEARCH_FIELDS = "summary,status,updated";
|
|
4119
|
-
async function searchIssues(client, site, params) {
|
|
4120
|
-
return searchIssuesByJql(client, site, buildJql(params), params.limit);
|
|
4644
|
+
async function searchIssues(client, site, params, fieldIds = []) {
|
|
4645
|
+
return searchIssuesByJql(client, site, buildJql(params), params.limit, fieldIds);
|
|
4121
4646
|
}
|
|
4122
|
-
async function searchIssuesByJql(client, site, jql, limit) {
|
|
4123
|
-
return ((await fetchSearchPage(client, jql, limit)).issues ?? []).map((i) => toIssueSummary(site, i));
|
|
4647
|
+
async function searchIssuesByJql(client, site, jql, limit, fieldIds = []) {
|
|
4648
|
+
return ((await fetchSearchPage(client, jql, limit, fieldIds)).issues ?? []).map((i) => toIssueSummary(site, i, fieldIds));
|
|
4124
4649
|
}
|
|
4125
|
-
async function fetchSearchPage(client, jql, maxResults, nextPageToken) {
|
|
4650
|
+
async function fetchSearchPage(client, jql, maxResults, fieldIds, nextPageToken) {
|
|
4126
4651
|
const query = new URLSearchParams({
|
|
4127
4652
|
jql,
|
|
4128
4653
|
maxResults: String(maxResults),
|
|
4129
|
-
fields: SEARCH_FIELDS
|
|
4654
|
+
fields: [SEARCH_FIELDS, ...fieldIds].join(",")
|
|
4130
4655
|
});
|
|
4131
4656
|
if (nextPageToken) query.set("nextPageToken", nextPageToken);
|
|
4132
4657
|
return client.getJson(`/rest/api/3/search/jql?${query.toString()}`);
|
|
4133
4658
|
}
|
|
4134
|
-
function toIssueSummary(site, i) {
|
|
4135
|
-
|
|
4659
|
+
function toIssueSummary(site, i, fieldIds) {
|
|
4660
|
+
const summary = {
|
|
4136
4661
|
key: i.key,
|
|
4137
4662
|
status: i.fields?.status?.name ?? "",
|
|
4138
4663
|
statusCategory: i.fields?.status?.statusCategory?.key ?? "",
|
|
@@ -4140,6 +4665,11 @@ function toIssueSummary(site, i) {
|
|
|
4140
4665
|
updated: i.fields?.updated ?? "",
|
|
4141
4666
|
url: browseUrl(site, i.key)
|
|
4142
4667
|
};
|
|
4668
|
+
if (fieldIds.length === 0) return summary;
|
|
4669
|
+
return {
|
|
4670
|
+
...summary,
|
|
4671
|
+
fields: Object.fromEntries(fieldIds.map((id) => [id, i.fields?.[id] ?? null]))
|
|
4672
|
+
};
|
|
4143
4673
|
}
|
|
4144
4674
|
const ISSUE_ORDER = "updated DESC";
|
|
4145
4675
|
function buildJql(params) {
|
|
@@ -4151,12 +4681,22 @@ function buildJql(params) {
|
|
|
4151
4681
|
userClause("assignee", params.assignee),
|
|
4152
4682
|
userClause("reporter", params.reporter),
|
|
4153
4683
|
inClause("labels", params.label),
|
|
4684
|
+
anyOf(values(params.sprint).map(sprintTerm)),
|
|
4154
4685
|
params.updatedSince ? `updated >= ${quote(params.updatedSince)}` : null,
|
|
4155
4686
|
params.text ? `text ~ ${quote(params.text)}` : null
|
|
4156
4687
|
].filter((c) => c !== null);
|
|
4157
4688
|
if (clauses.length === 0) clauses.push(RECENT_ISSUES_CLAUSE);
|
|
4158
4689
|
return joinClauses(clauses, ISSUE_ORDER);
|
|
4159
4690
|
}
|
|
4691
|
+
const SPRINT_FUNCTIONS = {
|
|
4692
|
+
active: "openSprints()",
|
|
4693
|
+
future: "futureSprints()"
|
|
4694
|
+
};
|
|
4695
|
+
function sprintTerm(sprint) {
|
|
4696
|
+
const sprintFunction = SPRINT_FUNCTIONS[sprint];
|
|
4697
|
+
if (sprintFunction) return `sprint in ${sprintFunction}`;
|
|
4698
|
+
return `sprint = ${/^\d+$/.test(sprint) ? sprint : quote(sprint)}`;
|
|
4699
|
+
}
|
|
4160
4700
|
function buildListJql(params) {
|
|
4161
4701
|
return joinClauses([
|
|
4162
4702
|
"assignee = currentUser()",
|
|
@@ -4166,15 +4706,15 @@ function buildListJql(params) {
|
|
|
4166
4706
|
}
|
|
4167
4707
|
const LIST_PAGE_SIZE = 100;
|
|
4168
4708
|
const LIST_CAP = 500;
|
|
4169
|
-
async function listAssignedIssues(client, site, params) {
|
|
4709
|
+
async function listAssignedIssues(client, site, params, fieldIds = []) {
|
|
4170
4710
|
const jql = buildListJql(params);
|
|
4171
4711
|
const issues = [];
|
|
4172
4712
|
let nextPageToken;
|
|
4173
4713
|
for (;;) {
|
|
4174
4714
|
const room = LIST_CAP - issues.length;
|
|
4175
|
-
const res = await fetchSearchPage(client, jql, Math.min(LIST_PAGE_SIZE, room), nextPageToken);
|
|
4715
|
+
const res = await fetchSearchPage(client, jql, Math.min(LIST_PAGE_SIZE, room), fieldIds, nextPageToken);
|
|
4176
4716
|
const page = res.issues ?? [];
|
|
4177
|
-
issues.push(...page.map((i) => toIssueSummary(site, i)));
|
|
4717
|
+
issues.push(...page.map((i) => toIssueSummary(site, i, fieldIds)));
|
|
4178
4718
|
if (res.isLast || !res.nextPageToken || page.length === 0) return {
|
|
4179
4719
|
issues,
|
|
4180
4720
|
truncated: false
|
|
@@ -4198,6 +4738,54 @@ function sortByCategoryThenUpdated(issues) {
|
|
|
4198
4738
|
return [...issues].sort((a, b) => rank(a) - rank(b) || updatedMs(b) - updatedMs(a));
|
|
4199
4739
|
}
|
|
4200
4740
|
//#endregion
|
|
4741
|
+
//#region src/commands/jira-field-columns.ts
|
|
4742
|
+
async function resolveFieldColumns(session, given) {
|
|
4743
|
+
const names = values(given);
|
|
4744
|
+
if (names.length === 0) return [];
|
|
4745
|
+
const fields = await listSiteFields(session);
|
|
4746
|
+
return names.map((name) => ({
|
|
4747
|
+
name,
|
|
4748
|
+
id: resolveFieldId(fields, name)
|
|
4749
|
+
}));
|
|
4750
|
+
}
|
|
4751
|
+
function resolveFieldId(fields, name) {
|
|
4752
|
+
const byId = fields.find((f) => f.id === name);
|
|
4753
|
+
if (byId) return byId.id;
|
|
4754
|
+
const lower = name.toLowerCase();
|
|
4755
|
+
const matches = fields.filter((f) => f.name.toLowerCase() === lower);
|
|
4756
|
+
const [only, ...others] = matches;
|
|
4757
|
+
if (!only) throw new Error(`No field matches "${name}". Run \`atlass jira fields --search ${shellWord(name)}\` to find it.`);
|
|
4758
|
+
if (others.length === 0) return only.id;
|
|
4759
|
+
const width = Math.max(...matches.map((f) => f.id.length));
|
|
4760
|
+
throw new Error([`"${name}" matches several fields. Use an id:`, ...matches.map((f) => ` ${f.id.padEnd(width)} ${f.name}`)].join("\n"));
|
|
4761
|
+
}
|
|
4762
|
+
function shellWord(text) {
|
|
4763
|
+
return /\s/.test(text) ? `"${text}"` : text;
|
|
4764
|
+
}
|
|
4765
|
+
function fieldIds(columns) {
|
|
4766
|
+
return [...new Set(columns.map((c) => c.id))];
|
|
4767
|
+
}
|
|
4768
|
+
function withNamedFields(issues, columns) {
|
|
4769
|
+
if (columns.length === 0) return issues;
|
|
4770
|
+
return issues.map((issue) => ({
|
|
4771
|
+
...issue,
|
|
4772
|
+
fields: Object.fromEntries(columns.map((c) => [c.name, issue.fields?.[c.id] ?? null]))
|
|
4773
|
+
}));
|
|
4774
|
+
}
|
|
4775
|
+
const EMPTY_CELL = "-";
|
|
4776
|
+
function fieldCell(value) {
|
|
4777
|
+
const text = (Array.isArray(value) ? value.map(shownValue) : [shownValue(value)]).filter((p) => p !== "").join(", ");
|
|
4778
|
+
return text === "" ? EMPTY_CELL : text;
|
|
4779
|
+
}
|
|
4780
|
+
function shownValue(value) {
|
|
4781
|
+
if (value === null || value === void 0) return "";
|
|
4782
|
+
if (typeof value === "string") return value;
|
|
4783
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4784
|
+
const named = value;
|
|
4785
|
+
const shown = named.value ?? named.displayName ?? named.name;
|
|
4786
|
+
return shown === void 0 ? JSON.stringify(value) : shownValue(shown);
|
|
4787
|
+
}
|
|
4788
|
+
//#endregion
|
|
4201
4789
|
//#region src/commands/jira-filters.ts
|
|
4202
4790
|
const FILTER_FLAGS = [
|
|
4203
4791
|
"project",
|
|
@@ -4205,7 +4793,8 @@ const FILTER_FLAGS = [
|
|
|
4205
4793
|
"reporter",
|
|
4206
4794
|
"status",
|
|
4207
4795
|
"type",
|
|
4208
|
-
"label"
|
|
4796
|
+
"label",
|
|
4797
|
+
"sprint"
|
|
4209
4798
|
];
|
|
4210
4799
|
const ISSUE_KEY = /^[A-Z][A-Z0-9]+-\d+$/i;
|
|
4211
4800
|
function hasFilters(filters) {
|
|
@@ -4224,6 +4813,7 @@ async function searchParams(client, query, filters, limit, nowMs) {
|
|
|
4224
4813
|
status: filters.status,
|
|
4225
4814
|
type: filters.type,
|
|
4226
4815
|
label: filters.label,
|
|
4816
|
+
sprint: filters.sprint,
|
|
4227
4817
|
updatedSince: filters.updated ? parseSince(filters.updated, nowMs) : void 0,
|
|
4228
4818
|
open: filters.open,
|
|
4229
4819
|
limit
|
|
@@ -4252,209 +4842,25 @@ function searchHint(err, project) {
|
|
|
4252
4842
|
return command ? /* @__PURE__ */ new Error(`${err.message} Run \`${command}\` to see the options.`) : err;
|
|
4253
4843
|
}
|
|
4254
4844
|
//#endregion
|
|
4255
|
-
//#region src/api/jira-issues.ts
|
|
4256
|
-
const FIELDS = [
|
|
4257
|
-
"summary",
|
|
4258
|
-
"description",
|
|
4259
|
-
"issuetype",
|
|
4260
|
-
"status",
|
|
4261
|
-
"assignee",
|
|
4262
|
-
"reporter",
|
|
4263
|
-
"priority",
|
|
4264
|
-
"labels",
|
|
4265
|
-
"created",
|
|
4266
|
-
"updated",
|
|
4267
|
-
"attachment"
|
|
4268
|
-
].join(",");
|
|
4269
|
-
async function fetchIssue(client, site, key) {
|
|
4270
|
-
const issue = await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${FIELDS}`);
|
|
4271
|
-
const comments = await fetchComments(client, key);
|
|
4272
|
-
const f = issue.fields;
|
|
4273
|
-
return {
|
|
4274
|
-
key: issue.key,
|
|
4275
|
-
url: browseUrl(site, issue.key),
|
|
4276
|
-
summary: f.summary ?? "",
|
|
4277
|
-
type: f.issuetype?.name ?? "",
|
|
4278
|
-
status: f.status?.name ?? "",
|
|
4279
|
-
statusCategory: f.status?.statusCategory?.key ?? "",
|
|
4280
|
-
assignee: f.assignee?.displayName ?? "Unassigned",
|
|
4281
|
-
reporter: f.reporter?.displayName ?? "",
|
|
4282
|
-
priority: f.priority?.name ?? "",
|
|
4283
|
-
labels: f.labels ?? [],
|
|
4284
|
-
created: f.created ?? "",
|
|
4285
|
-
updated: f.updated ?? "",
|
|
4286
|
-
description: f.description ?? null,
|
|
4287
|
-
comments,
|
|
4288
|
-
attachments: (f.attachment ?? []).map((a) => ({
|
|
4289
|
-
mediaId: a.id,
|
|
4290
|
-
filename: a.filename,
|
|
4291
|
-
url: a.content
|
|
4292
|
-
}))
|
|
4293
|
-
};
|
|
4294
|
-
}
|
|
4295
|
-
async function updateIssue(client, key, update) {
|
|
4296
|
-
const fields = { description: update.description };
|
|
4297
|
-
if (update.summary !== void 0) fields["summary"] = update.summary;
|
|
4298
|
-
const path = `/rest/api/3/issue/${encodeURIComponent(key)}`;
|
|
4299
|
-
await client.putNoContent(path, { fields });
|
|
4300
|
-
return (await client.getJson(`${path}?fields=updated`)).fields.updated ?? "";
|
|
4301
|
-
}
|
|
4302
|
-
async function fetchComments(client, key) {
|
|
4303
|
-
return (await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}/comment?maxResults=100&orderBy=created`)).comments.map((c) => ({
|
|
4304
|
-
author: c.author?.displayName ?? "",
|
|
4305
|
-
created: c.created ?? "",
|
|
4306
|
-
body: c.body ?? null
|
|
4307
|
-
}));
|
|
4308
|
-
}
|
|
4309
|
-
//#endregion
|
|
4310
|
-
//#region src/api/jira-statuses.ts
|
|
4311
|
-
async function listStatuses(client, project) {
|
|
4312
|
-
return dedupeAndSortStatuses((project ? await fetchProjectStatuses(client, project) : await client.getJson("/rest/api/3/status")).map(toStatusSummary));
|
|
4313
|
-
}
|
|
4314
|
-
async function fetchProjectStatuses(client, project) {
|
|
4315
|
-
let groups;
|
|
4316
|
-
try {
|
|
4317
|
-
groups = await client.getJson(`/rest/api/3/project/${encodeURIComponent(project)}/statuses`);
|
|
4318
|
-
} catch (err) {
|
|
4319
|
-
if (err instanceof HttpError && err.status === 404) throw new Error(`No project found with key "${project}".`);
|
|
4320
|
-
throw err;
|
|
4321
|
-
}
|
|
4322
|
-
return groups.flatMap((g) => g.statuses ?? []);
|
|
4323
|
-
}
|
|
4324
|
-
function toStatusSummary(s) {
|
|
4325
|
-
return {
|
|
4326
|
-
name: s.name,
|
|
4327
|
-
id: s.id,
|
|
4328
|
-
category: s.statusCategory?.name ?? "",
|
|
4329
|
-
categoryKey: s.statusCategory?.key ?? ""
|
|
4330
|
-
};
|
|
4331
|
-
}
|
|
4332
|
-
const CATEGORY_ORDER = {
|
|
4333
|
-
new: 0,
|
|
4334
|
-
indeterminate: 1,
|
|
4335
|
-
done: 2
|
|
4336
|
-
};
|
|
4337
|
-
const UNKNOWN_CATEGORY_RANK = Object.keys(CATEGORY_ORDER).length;
|
|
4338
|
-
function dedupeAndSortStatuses(statuses) {
|
|
4339
|
-
const byNameCategory = /* @__PURE__ */ new Map();
|
|
4340
|
-
for (const s of statuses) {
|
|
4341
|
-
const key = `${s.name}\0${s.categoryKey}`;
|
|
4342
|
-
if (!byNameCategory.has(key)) byNameCategory.set(key, s);
|
|
4343
|
-
}
|
|
4344
|
-
return [...byNameCategory.values()].sort((a, b) => {
|
|
4345
|
-
const rank = (s) => CATEGORY_ORDER[s.categoryKey] ?? UNKNOWN_CATEGORY_RANK;
|
|
4346
|
-
return rank(a) - rank(b) || a.name.localeCompare(b.name);
|
|
4347
|
-
});
|
|
4348
|
-
}
|
|
4349
|
-
//#endregion
|
|
4350
|
-
//#region src/commands/jira.ts
|
|
4351
|
-
async function jiraProjects({ session, term }, query, options) {
|
|
4352
|
-
const projects = await listProjects(session, session.site, query);
|
|
4353
|
-
if (options.json) term.json(projects);
|
|
4354
|
-
else if (projects.length === 0) term.out("No matching projects.");
|
|
4355
|
-
else term.out(formatProjectRows(projects));
|
|
4356
|
-
}
|
|
4357
|
-
function formatProjectRows(projects) {
|
|
4358
|
-
const width = Math.max(...projects.map((p) => p.key.length));
|
|
4359
|
-
return projects.map((p) => `${p.key.padEnd(width)} ${p.name}`);
|
|
4360
|
-
}
|
|
4361
|
-
async function jiraStatuses({ session, term }, query, options) {
|
|
4362
|
-
let statuses = await listStatuses(session, options.project);
|
|
4363
|
-
if (query) {
|
|
4364
|
-
const needle = query.toLowerCase();
|
|
4365
|
-
statuses = statuses.filter((s) => s.name.toLowerCase().includes(needle));
|
|
4366
|
-
}
|
|
4367
|
-
if (options.json) term.json(statuses);
|
|
4368
|
-
else if (statuses.length === 0) term.out("No matching statuses.");
|
|
4369
|
-
else term.out(formatStatusRows(statuses));
|
|
4370
|
-
}
|
|
4371
|
-
function formatStatusRows(statuses) {
|
|
4372
|
-
const width = Math.max(...statuses.map((s) => s.name.length));
|
|
4373
|
-
return statuses.map((s) => `${s.name.padEnd(width)} ${s.category}`);
|
|
4374
|
-
}
|
|
4375
|
-
async function jiraView({ session, term }, arg, options) {
|
|
4376
|
-
const key = await resolveRef(term.ask, arg, ISSUE_REF);
|
|
4377
|
-
const lines = formatIssueView(await fetchIssue(session, session.site, key), Date.now(), options.allComments ?? false);
|
|
4378
|
-
await term.page(lines.join("\n"), { pager: options.pager });
|
|
4379
|
-
}
|
|
4380
|
-
const CATEGORY_COLORS = {
|
|
4381
|
-
new: kleur.cyan,
|
|
4382
|
-
indeterminate: kleur.yellow,
|
|
4383
|
-
done: kleur.green
|
|
4384
|
-
};
|
|
4385
|
-
function colorForCategory(category) {
|
|
4386
|
-
return CATEGORY_COLORS[category] ?? ((text) => text);
|
|
4387
|
-
}
|
|
4388
|
-
function formatIssueView(issue, nowMs, allComments) {
|
|
4389
|
-
const colorStatus = colorForCategory(issue.statusCategory);
|
|
4390
|
-
return [
|
|
4391
|
-
`${kleur.bold(issue.key)} ${issue.summary}`,
|
|
4392
|
-
...fieldLines$1([
|
|
4393
|
-
["Type", issue.type],
|
|
4394
|
-
["Status", issue.status && colorStatus(issue.status)],
|
|
4395
|
-
["Assignee", issue.assignee],
|
|
4396
|
-
["Reporter", issue.reporter],
|
|
4397
|
-
["Priority", issue.priority],
|
|
4398
|
-
["Labels", issue.labels.join(", ")],
|
|
4399
|
-
["Created", dateWithAge(issue.created, nowMs)],
|
|
4400
|
-
["Updated", dateWithAge(issue.updated, nowMs)],
|
|
4401
|
-
["URL", issue.url]
|
|
4402
|
-
]),
|
|
4403
|
-
...bodyLines(issue.description),
|
|
4404
|
-
...commentSection(renderedComments(issue.comments), { allComments }),
|
|
4405
|
-
...attachmentSection(issue.attachments)
|
|
4406
|
-
];
|
|
4407
|
-
}
|
|
4408
|
-
async function jiraCopy(env, arg, options) {
|
|
4409
|
-
await copyIssue(env, await resolveRef(env.term.ask, arg, ISSUE_REF), options.out);
|
|
4410
|
-
}
|
|
4411
|
-
async function jiraUpdate({ session, term, files }, arg, options) {
|
|
4412
|
-
const file = arg ?? await term.ask.text({
|
|
4413
|
-
message: "Path to the issue Markdown file:",
|
|
4414
|
-
flag: "[file]",
|
|
4415
|
-
required: true
|
|
4416
|
-
});
|
|
4417
|
-
const raw = await files.readText(file);
|
|
4418
|
-
const src = parseIssueSource(raw);
|
|
4419
|
-
const plan = planIssueUpdate(src, await fetchIssue(session, session.site, src.key), options);
|
|
4420
|
-
await runPlan(term, plan, options, async () => {
|
|
4421
|
-
const { current, next } = plan.headline;
|
|
4422
|
-
const updated = await updateIssue(session, src.key, {
|
|
4423
|
-
description: plan.body,
|
|
4424
|
-
summary: next !== current ? next : void 0
|
|
4425
|
-
});
|
|
4426
|
-
term.out(`Updated ${src.key}.`);
|
|
4427
|
-
if (updated) await files.writeText(file, rewriteFields(raw, { updated }));
|
|
4428
|
-
});
|
|
4429
|
-
}
|
|
4430
|
-
async function copyIssue(env, key, out) {
|
|
4431
|
-
env.term.err(`Fetching ${key} ...`);
|
|
4432
|
-
await runCopy(env, planIssueCopy(await fetchIssue(env.session, env.session.site, key), out));
|
|
4433
|
-
}
|
|
4434
|
-
const ISSUE_REF = {
|
|
4435
|
-
message: "Jira issue key or URL:",
|
|
4436
|
-
flag: "[issue]",
|
|
4437
|
-
parse: parseIssueKey,
|
|
4438
|
-
notFound: (raw) => `Could not find an issue key in "${raw}" (expected e.g. PROJ-123).`
|
|
4439
|
-
};
|
|
4440
|
-
//#endregion
|
|
4441
4845
|
//#region src/commands/jira-search.ts
|
|
4442
4846
|
async function jiraSearch(env, query, options) {
|
|
4443
4847
|
const { session } = env;
|
|
4444
4848
|
checkQuery(query, options);
|
|
4445
4849
|
const limit = checkedLimit(options);
|
|
4850
|
+
const columns = await resolveFieldColumns(session, options.field);
|
|
4446
4851
|
const params = await searchParams(session, query, options, limit, Date.now());
|
|
4447
|
-
await showIssues(env, await searchIssues(session, session.site, params).catch((err) => {
|
|
4852
|
+
await showIssues(env, withNamedFields(await searchIssues(session, session.site, params, fieldIds(columns)).catch((err) => {
|
|
4448
4853
|
throw searchHint(err, options.project);
|
|
4449
|
-
}), limit, options);
|
|
4854
|
+
}), columns), columns, limit, options);
|
|
4450
4855
|
}
|
|
4451
4856
|
async function jiraJql(env, query, options) {
|
|
4452
4857
|
const { session } = env;
|
|
4453
4858
|
const limit = checkedLimit(options);
|
|
4454
|
-
|
|
4859
|
+
const columns = await resolveFieldColumns(session, options.field);
|
|
4860
|
+
await showIssues(env, withNamedFields(await searchIssuesByJql(session, session.site, query, limit, fieldIds(columns)), columns), columns, limit, options);
|
|
4455
4861
|
}
|
|
4456
|
-
async function showIssues(env, issues, limit, options, empty = "No matching issues.") {
|
|
4457
|
-
await showRows(env.term, formatIssueRows(issues, Date.now()), {
|
|
4862
|
+
async function showIssues(env, issues, columns, limit, options, empty = "No matching issues.") {
|
|
4863
|
+
await showRows(env.term, formatIssueRows(issues, Date.now(), columns), {
|
|
4458
4864
|
empty,
|
|
4459
4865
|
hasMore: issues.length === limit,
|
|
4460
4866
|
limit
|
|
@@ -4462,27 +4868,36 @@ async function showIssues(env, issues, limit, options, empty = "No matching issu
|
|
|
4462
4868
|
}
|
|
4463
4869
|
async function jiraList(env, options) {
|
|
4464
4870
|
const { session, term } = env;
|
|
4465
|
-
checkedLimit(options);
|
|
4871
|
+
const limit = checkedLimit(options);
|
|
4872
|
+
const columns = await resolveFieldColumns(session, options.field);
|
|
4466
4873
|
const { issues, truncated } = await listAssignedIssues(session, session.site, {
|
|
4467
4874
|
all: options.all,
|
|
4468
4875
|
project: options.project
|
|
4469
|
-
});
|
|
4470
|
-
|
|
4876
|
+
}, fieldIds(columns));
|
|
4877
|
+
const sorted = sortByCategoryThenUpdated(withNamedFields(issues, columns));
|
|
4878
|
+
const shown = options.limit === void 0 ? sorted : sorted.slice(0, limit);
|
|
4879
|
+
await runSearch(term, formatIssueRows(shown, Date.now(), columns), {
|
|
4471
4880
|
json: options.json,
|
|
4472
4881
|
copy: options.copy,
|
|
4473
4882
|
out: options.out,
|
|
4474
4883
|
empty: options.all ? "No issues assigned to you." : "No open issues assigned to you.",
|
|
4475
|
-
footer:
|
|
4884
|
+
footer: listFooter(shown.length, sorted.length, truncated, limit)
|
|
4476
4885
|
}, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
4477
4886
|
}
|
|
4478
|
-
function
|
|
4887
|
+
function listFooter(shown, fetched, truncated, limit) {
|
|
4888
|
+
if (shown < fetched) return searchFooter(limit);
|
|
4889
|
+
if (truncated) return `
|
|
4890
|
+
Showing the first ${shown}; narrow with --project.`;
|
|
4891
|
+
}
|
|
4892
|
+
function formatIssueRows(issues, nowMs, columns = []) {
|
|
4479
4893
|
return alignedRows(issues, nowMs, (i) => ({
|
|
4480
4894
|
id: i.key,
|
|
4481
4895
|
url: i.url,
|
|
4482
4896
|
label: i.status,
|
|
4483
4897
|
color: colorForCategory(i.statusCategory),
|
|
4484
4898
|
text: i.summary,
|
|
4485
|
-
timestamp: i.updated
|
|
4899
|
+
timestamp: i.updated,
|
|
4900
|
+
columns: columns.map((c) => fieldCell(i.fields?.[c.name]))
|
|
4486
4901
|
}));
|
|
4487
4902
|
}
|
|
4488
4903
|
const ISSUE_NOUN = {
|
|
@@ -4490,24 +4905,91 @@ const ISSUE_NOUN = {
|
|
|
4490
4905
|
plural: "issues"
|
|
4491
4906
|
};
|
|
4492
4907
|
//#endregion
|
|
4908
|
+
//#region src/api/jira-types.ts
|
|
4909
|
+
const SPRINT_STATES = [
|
|
4910
|
+
"active",
|
|
4911
|
+
"future",
|
|
4912
|
+
"closed"
|
|
4913
|
+
];
|
|
4914
|
+
//#endregion
|
|
4915
|
+
//#region src/api/jira-sprints.ts
|
|
4916
|
+
async function listSprints(client, boardId, states) {
|
|
4917
|
+
const state = states.join(",");
|
|
4918
|
+
return (await allOffsetPages(client, (startAt) => `/rest/agile/1.0/board/${boardId}/sprint?state=${state}&startAt=${startAt}&maxResults=50`)).toSorted(byRelevance).map((s) => ({
|
|
4919
|
+
id: s.id,
|
|
4920
|
+
state: s.state,
|
|
4921
|
+
name: s.name,
|
|
4922
|
+
start: s.startDate ?? null,
|
|
4923
|
+
end: s.endDate ?? null,
|
|
4924
|
+
boardId: s.originBoardId ?? boardId
|
|
4925
|
+
}));
|
|
4926
|
+
}
|
|
4927
|
+
function byRelevance(a, b) {
|
|
4928
|
+
const rank = SPRINT_STATES.indexOf(a.state) - SPRINT_STATES.indexOf(b.state);
|
|
4929
|
+
if (rank !== 0 || a.state !== "closed") return rank;
|
|
4930
|
+
return finishedAt(b) - finishedAt(a);
|
|
4931
|
+
}
|
|
4932
|
+
function finishedAt(sprint) {
|
|
4933
|
+
return Date.parse(sprint.completeDate ?? sprint.endDate ?? "") || 0;
|
|
4934
|
+
}
|
|
4935
|
+
//#endregion
|
|
4936
|
+
//#region src/commands/jira-sprints.ts
|
|
4937
|
+
async function jiraSprints({ session, term }, ref, options) {
|
|
4938
|
+
const chosen = parseChoices(options.state, SPRINT_STATES, "--state");
|
|
4939
|
+
const states = chosen.length > 0 ? chosen : ["active"];
|
|
4940
|
+
const limit = parseLimit(options.limit);
|
|
4941
|
+
const sprints = await withBoard(session, ref, (id) => listSprints(session, id, states).catch((err) => {
|
|
4942
|
+
throw withoutSprints(err, id);
|
|
4943
|
+
}));
|
|
4944
|
+
const shown = sprints.slice(0, limit);
|
|
4945
|
+
if (options.json) term.json(shown);
|
|
4946
|
+
else if (shown.length === 0) term.out(`No ${orList(states)} sprints.`);
|
|
4947
|
+
else if (sprints.length > limit) term.out([...formatSprintRows(shown), searchFooter(limit)]);
|
|
4948
|
+
else term.out(formatSprintRows(shown));
|
|
4949
|
+
}
|
|
4950
|
+
function withoutSprints(err, id) {
|
|
4951
|
+
if (!(err instanceof HttpError) || err.status !== 400) return err;
|
|
4952
|
+
return /* @__PURE__ */ new Error(`Board ${id} does not support sprints. Only scrum boards have them.`);
|
|
4953
|
+
}
|
|
4954
|
+
function formatSprintRows(sprints) {
|
|
4955
|
+
const cells = sprints.map((s) => [
|
|
4956
|
+
String(s.id),
|
|
4957
|
+
s.state,
|
|
4958
|
+
s.name,
|
|
4959
|
+
s.start ? formatDate$1(s.start) : "-",
|
|
4960
|
+
s.end ? formatDate$1(s.end) : "-"
|
|
4961
|
+
]);
|
|
4962
|
+
const widths = cells[0].map((_, i) => Math.max(...cells.map((row) => row[i].length)));
|
|
4963
|
+
return cells.map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd());
|
|
4964
|
+
}
|
|
4965
|
+
//#endregion
|
|
4493
4966
|
//#region src/cli/jira.ts
|
|
4967
|
+
const FIELD_OPTION = [
|
|
4968
|
+
"-f, --field <name|id>",
|
|
4969
|
+
"add a field's value to each issue (repeatable)",
|
|
4970
|
+
collect
|
|
4971
|
+
];
|
|
4494
4972
|
function registerJira(jira) {
|
|
4495
4973
|
jira.description("Jira commands");
|
|
4496
4974
|
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(jiraAction(jiraProjects));
|
|
4497
4975
|
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));
|
|
4976
|
+
jira.command("boards [query]").description("List boards (optionally filtered by name, scoped with --project)").option("-p, --project <key>", "limit to boards located in a project").option("--json", "output results as JSON").action(jiraAction(jiraBoards));
|
|
4977
|
+
jira.command("board <ref>").description("Show a board's estimation field, filter JQL and columns (id, URL or exact name)").option("--json", "output results as JSON").action(jiraAction(jiraBoard));
|
|
4978
|
+
jira.command("sprints <ref>").description("List a board's sprints, active by default (id, URL or exact name)").option("-s, --state <state>", "active, future, or closed (repeatable)", collect).option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(jiraAction(jiraSprints));
|
|
4498
4979
|
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));
|
|
4499
|
-
jira.command("fields
|
|
4980
|
+
jira.command("fields [project] [type]").description("List site fields, or the issue types a project offers, or the create form for one type").option("--search <text>", "site fields only: filter by name or id").option("--json", "output results as JSON").action(jiraAction(jiraFields));
|
|
4500
4981
|
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));
|
|
4501
4982
|
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));
|
|
4502
4983
|
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));
|
|
4503
|
-
|
|
4504
|
-
outputOptions(jira.command("
|
|
4505
|
-
outputOptions(jira.command("
|
|
4984
|
+
jira.command("attach <issue> <files...>").description("Upload files as attachments to a Jira issue (key or URL)").option("--json", "print the uploaded attachments as JSON").action(jiraAction(jiraAttach));
|
|
4985
|
+
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").option(...FIELD_OPTION), "max results (default all, max 100)").action(jiraAction(jiraList));
|
|
4986
|
+
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("--sprint <sprint>", "limit to a sprint: active, future, an id, or a name (repeatable)", collect).option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").option("--open", "exclude issues in the Done category").option(...FIELD_OPTION).addOption(moved("--jql <jql>", "jira jql"))).action(jiraAction(jiraSearch));
|
|
4987
|
+
outputOptions(jira.command("jql <query>").description("Search Jira issues with a raw JQL query").option(...FIELD_OPTION)).action(jiraAction(jiraJql));
|
|
4506
4988
|
return jira;
|
|
4507
4989
|
}
|
|
4508
4990
|
//#endregion
|
|
4509
4991
|
//#region package.json
|
|
4510
|
-
var version = "2.
|
|
4992
|
+
var version = "2.3.0";
|
|
4511
4993
|
//#endregion
|
|
4512
4994
|
//#region src/cli/build.ts
|
|
4513
4995
|
const PRODUCTS = {
|