atlass 1.13.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -12
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-BblaPBOA.mjs → build-DMLlCHdF.mjs} +1535 -1001
- package/dist/cli.mjs +1 -1
- package/dist/confluence.mjs +1 -1
- package/dist/jira.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { Entry } from "@napi-rs/keyring";
|
|
@@ -7,25 +7,27 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import { stripVTControlCharacters } from "node:util";
|
|
8
8
|
import { checkbox, confirm, editor, input, password, search, select } from "@inquirer/prompts";
|
|
9
9
|
import kleur from "kleur";
|
|
10
|
+
import { common, createLowlight } from "lowlight";
|
|
10
11
|
import { marked } from "marked";
|
|
11
12
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { common, createLowlight } from "lowlight";
|
|
13
13
|
//#region src/api/http-error.ts
|
|
14
14
|
var HttpError = class extends Error {
|
|
15
15
|
status;
|
|
16
16
|
jira;
|
|
17
|
+
path;
|
|
17
18
|
constructor(status, message, jira = {
|
|
18
19
|
errorMessages: [],
|
|
19
20
|
errors: {}
|
|
20
|
-
}) {
|
|
21
|
+
}, path = "") {
|
|
21
22
|
super(message);
|
|
22
23
|
this.status = status;
|
|
23
24
|
this.jira = jira;
|
|
25
|
+
this.path = path;
|
|
24
26
|
this.name = "HttpError";
|
|
25
27
|
}
|
|
26
28
|
};
|
|
27
29
|
function httpError(status, path, body, loginHint) {
|
|
28
|
-
if (status === 401 || status === 403) return new HttpError(status, `Authentication failed (401/403). Run \`${loginHint}\` to update your token
|
|
30
|
+
if (status === 401 || status === 403) return new HttpError(status, `Authentication failed (401/403). Run \`${loginHint}\` to update your token.`, void 0, path);
|
|
29
31
|
if (status === 404) return new HttpError(status, `Not found (404): ${path}`);
|
|
30
32
|
if (status === 409) return new HttpError(status, `Conflict (409): ${extractErrorMessage(body) || "the page changed on the server"}`);
|
|
31
33
|
if (status === 413) return new HttpError(status, "Payload too large (413): the page or an attachment exceeds the size limit.");
|
|
@@ -460,6 +462,33 @@ function parsePageId(input) {
|
|
|
460
462
|
if (fromQuery) return fromQuery[1];
|
|
461
463
|
return null;
|
|
462
464
|
}
|
|
465
|
+
const PULL_REQUEST_HOSTS = ["bitbucket.org", "www.bitbucket.org"];
|
|
466
|
+
const PULL_REQUEST_PATH = /^\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)(?:\/|$)/;
|
|
467
|
+
function parsePullRequestRef(input) {
|
|
468
|
+
const trimmed = input.trim();
|
|
469
|
+
const bare = trimmed.replace(/^#/, "");
|
|
470
|
+
if (/^\d+$/.test(bare)) return { id: Number.parseInt(bare, 10) };
|
|
471
|
+
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
472
|
+
const url = parseUrl(trimmed);
|
|
473
|
+
if (!url || !PULL_REQUEST_HOSTS.includes(url.hostname.toLowerCase())) return null;
|
|
474
|
+
const match = PULL_REQUEST_PATH.exec(url.pathname);
|
|
475
|
+
if (!match) return null;
|
|
476
|
+
const [, workspace = "", repo = "", id = ""] = match;
|
|
477
|
+
return {
|
|
478
|
+
id: Number.parseInt(id, 10),
|
|
479
|
+
repo: {
|
|
480
|
+
workspace: decodeURIComponent(workspace),
|
|
481
|
+
repo: decodeURIComponent(repo)
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function parseUrl(value) {
|
|
486
|
+
try {
|
|
487
|
+
return new URL(value);
|
|
488
|
+
} catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
463
492
|
function isExternalHref(href) {
|
|
464
493
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(href);
|
|
465
494
|
}
|
|
@@ -499,6 +528,41 @@ function siteOrigin(input) {
|
|
|
499
528
|
if (!/^https?:\/\//i.test(value)) value = `https://${value}`;
|
|
500
529
|
return new URL(value).origin;
|
|
501
530
|
}
|
|
531
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
532
|
+
const RELATIVE = /^(\d+)([dwm])$/i;
|
|
533
|
+
function parseSince(value, nowMs) {
|
|
534
|
+
const raw = value.trim();
|
|
535
|
+
const iso = ISO_DATE.exec(raw);
|
|
536
|
+
if (iso) {
|
|
537
|
+
if (!isRealDate(iso)) throw invalidSince(value);
|
|
538
|
+
return raw;
|
|
539
|
+
}
|
|
540
|
+
const relative = RELATIVE.exec(raw);
|
|
541
|
+
if (!relative) throw invalidSince(value);
|
|
542
|
+
const amount = Number.parseInt(relative[1], 10);
|
|
543
|
+
const date = new Date(nowMs);
|
|
544
|
+
switch (relative[2].toLowerCase()) {
|
|
545
|
+
case "d":
|
|
546
|
+
date.setDate(date.getDate() - amount);
|
|
547
|
+
break;
|
|
548
|
+
case "w":
|
|
549
|
+
date.setDate(date.getDate() - amount * 7);
|
|
550
|
+
break;
|
|
551
|
+
default: date.setMonth(date.getMonth() - amount);
|
|
552
|
+
}
|
|
553
|
+
return localDate(date);
|
|
554
|
+
}
|
|
555
|
+
function isRealDate([, year = "", month = "", day = ""]) {
|
|
556
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
557
|
+
return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day);
|
|
558
|
+
}
|
|
559
|
+
function localDate(date) {
|
|
560
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
561
|
+
return `${date.getFullYear()}-${month}-${String(date.getDate()).padStart(2, "0")}`;
|
|
562
|
+
}
|
|
563
|
+
function invalidSince(value) {
|
|
564
|
+
return /* @__PURE__ */ new Error(`Invalid --updated "${value}". Expected 7d, 2w, 3m, or YYYY-MM-DD.`);
|
|
565
|
+
}
|
|
502
566
|
//#endregion
|
|
503
567
|
//#region src/commands/auth.ts
|
|
504
568
|
async function login({ term, profile }) {
|
|
@@ -659,13 +723,34 @@ async function* walkPages(client, firstPath) {
|
|
|
659
723
|
path = page.next ? pathAndQuery(page.next) : null;
|
|
660
724
|
}
|
|
661
725
|
}
|
|
662
|
-
async function
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
726
|
+
async function collectCapped(client, first, cap, map) {
|
|
727
|
+
const items = [];
|
|
728
|
+
let total;
|
|
729
|
+
let path = first;
|
|
730
|
+
while (path) {
|
|
731
|
+
const page = await client.getJson(path);
|
|
732
|
+
total ??= page.size;
|
|
733
|
+
for (const value of page.values ?? []) {
|
|
734
|
+
items.push(map(value));
|
|
735
|
+
if (items.length >= cap) return {
|
|
736
|
+
items,
|
|
737
|
+
total,
|
|
738
|
+
truncated: hasMore(page, total, cap)
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
path = page.next ? pathAndQuery(page.next) : null;
|
|
667
742
|
}
|
|
668
|
-
return
|
|
743
|
+
return {
|
|
744
|
+
items,
|
|
745
|
+
total: total ?? items.length,
|
|
746
|
+
truncated: false
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function hasMore(page, total, cap) {
|
|
750
|
+
return total === void 0 ? Boolean(page.next) : total > cap;
|
|
751
|
+
}
|
|
752
|
+
async function collectPages(client, first, limit, map) {
|
|
753
|
+
return (await collectCapped(client, first, limit, map)).items;
|
|
669
754
|
}
|
|
670
755
|
//#endregion
|
|
671
756
|
//#region src/api/bitbucket-pull-requests.ts
|
|
@@ -739,93 +824,115 @@ function toPullRequestSummary(ref, pr) {
|
|
|
739
824
|
async function listPullRequests(client, ref, params) {
|
|
740
825
|
return collectPages(client, `${repoPath(ref, "pullrequests")}?${pullRequestsQuery(params)}`, params.limit, (value) => toPullRequestSummary(ref, value));
|
|
741
826
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (state.name === "COMPLETED") return state.result?.name ?? "COMPLETED";
|
|
748
|
-
return state.name ?? "";
|
|
827
|
+
const PAGELEN = 100;
|
|
828
|
+
const RESOLUTION_FIELDS = "fields=%2Bvalues.resolution.*";
|
|
829
|
+
function pullRequestPath(ref, id, resource) {
|
|
830
|
+
const base = `${repoPath(ref, "pullrequests")}/${id}`;
|
|
831
|
+
return resource ? `${base}/${resource}` : base;
|
|
749
832
|
}
|
|
750
|
-
function
|
|
751
|
-
return
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
833
|
+
function toPullRequestDetail(ref, value) {
|
|
834
|
+
return {
|
|
835
|
+
id: value.id,
|
|
836
|
+
title: value.title ?? "",
|
|
837
|
+
description: value.description ?? "",
|
|
838
|
+
state: value.state ?? "",
|
|
839
|
+
draft: value.draft ?? false,
|
|
840
|
+
author: value.author?.display_name ?? "",
|
|
841
|
+
sourceBranch: value.source?.branch?.name ?? "",
|
|
842
|
+
destinationBranch: value.destination?.branch?.name ?? "",
|
|
843
|
+
closedBy: value.closed_by?.display_name ?? "",
|
|
844
|
+
createdOn: value.created_on ?? "",
|
|
845
|
+
updatedOn: value.updated_on ?? "",
|
|
846
|
+
url: pullRequestUrl(ref, value.id),
|
|
847
|
+
reviewers: toReviewers(value),
|
|
848
|
+
participants: toParticipants(value)
|
|
849
|
+
};
|
|
755
850
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
851
|
+
function toParticipants(value) {
|
|
852
|
+
const rows = /* @__PURE__ */ new Map();
|
|
853
|
+
const users = [value.author, ...(value.participants ?? []).map((entry) => entry.user)];
|
|
854
|
+
for (const user of users) {
|
|
855
|
+
if (!user?.account_id || !user.display_name) continue;
|
|
856
|
+
rows.set(user.account_id, {
|
|
857
|
+
accountId: user.account_id,
|
|
858
|
+
name: user.display_name
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return [...rows.values()];
|
|
762
862
|
}
|
|
763
|
-
function
|
|
764
|
-
|
|
863
|
+
function toReviewers(value) {
|
|
864
|
+
const rows = /* @__PURE__ */ new Map();
|
|
865
|
+
for (const requested of value.reviewers ?? []) rows.set(principalKey(requested), {
|
|
866
|
+
name: requested.display_name ?? "",
|
|
867
|
+
state: ""
|
|
868
|
+
});
|
|
869
|
+
for (const participant of value.participants ?? []) {
|
|
870
|
+
const state = participantState(participant);
|
|
871
|
+
if (participant.role !== "REVIEWER" && !state) continue;
|
|
872
|
+
rows.set(principalKey(participant.user ?? {}), {
|
|
873
|
+
name: participant.user?.display_name ?? "",
|
|
874
|
+
state
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
return [...rows.values()];
|
|
765
878
|
}
|
|
766
|
-
function
|
|
767
|
-
return
|
|
879
|
+
function participantState(participant) {
|
|
880
|
+
if (participant.state) return participant.state.toUpperCase();
|
|
881
|
+
return participant.approved ? "APPROVED" : "";
|
|
768
882
|
}
|
|
769
|
-
function
|
|
770
|
-
return
|
|
771
|
-
buildNumber: p.build_number,
|
|
772
|
-
status: pipelineStatus(p.state),
|
|
773
|
-
ref: p.target?.ref_name ?? "",
|
|
774
|
-
commit: shortHash(p.target?.commit?.hash),
|
|
775
|
-
durationSeconds: wallClockSeconds(p.created_on, p.completed_on),
|
|
776
|
-
createdOn: p.created_on ?? "",
|
|
777
|
-
creator: p.creator?.display_name ?? "",
|
|
778
|
-
uuid: p.uuid,
|
|
779
|
-
url: pipelineUrl(ref, p.build_number)
|
|
780
|
-
};
|
|
883
|
+
function principalKey(user) {
|
|
884
|
+
return user.uuid ?? user.display_name ?? "";
|
|
781
885
|
}
|
|
782
|
-
function
|
|
886
|
+
function toComment(value) {
|
|
887
|
+
if (value.deleted) return null;
|
|
888
|
+
const body = value.content?.raw ?? "";
|
|
889
|
+
if (!body.trim()) return null;
|
|
783
890
|
return {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
891
|
+
id: value.id,
|
|
892
|
+
parentId: value.parent?.id ?? null,
|
|
893
|
+
author: value.user?.display_name ?? "",
|
|
894
|
+
created: value.created_on ?? "",
|
|
895
|
+
body,
|
|
896
|
+
anchor: inlineAnchor(value),
|
|
897
|
+
resolved: toResolution(value.resolution)
|
|
787
898
|
};
|
|
788
899
|
}
|
|
789
|
-
function
|
|
900
|
+
function toResolution(value) {
|
|
901
|
+
if (!value?.created_on) return null;
|
|
790
902
|
return {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
durationSeconds: wallClockSeconds(step.started_on, step.completed_on)
|
|
903
|
+
by: value.user?.display_name ?? "",
|
|
904
|
+
at: value.created_on
|
|
794
905
|
};
|
|
795
906
|
}
|
|
796
|
-
function
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return
|
|
907
|
+
function inlineAnchor(value) {
|
|
908
|
+
const path = value.inline?.path;
|
|
909
|
+
if (!path) return "";
|
|
910
|
+
const line = value.inline?.to ?? value.inline?.from;
|
|
911
|
+
return line == null ? path : `${path}:${line}`;
|
|
801
912
|
}
|
|
802
|
-
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
return
|
|
913
|
+
function toChangedFile(value) {
|
|
914
|
+
const before = value.old?.path ?? "";
|
|
915
|
+
const after = value.new?.path ?? "";
|
|
916
|
+
return {
|
|
917
|
+
path: before && after && before !== after ? `${before} -> ${after}` : after || before,
|
|
918
|
+
status: value.status ?? "",
|
|
919
|
+
added: value.lines_added ?? 0,
|
|
920
|
+
removed: value.lines_removed ?? 0
|
|
921
|
+
};
|
|
806
922
|
}
|
|
807
|
-
async function
|
|
808
|
-
|
|
809
|
-
return await client.getJson(`${pipelinesPath(ref)}/${encodeURIComponent(String(buildNumber))}`);
|
|
810
|
-
} catch (err) {
|
|
811
|
-
if (err instanceof HttpError && (err.status === 400 || err.status === 404)) return null;
|
|
812
|
-
throw err;
|
|
813
|
-
}
|
|
923
|
+
async function getPullRequest(client, ref, id) {
|
|
924
|
+
return toPullRequestDetail(ref, await client.getJson(pullRequestPath(ref, id)));
|
|
814
925
|
}
|
|
815
|
-
async function
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
return null;
|
|
926
|
+
async function listComments(client, ref, id) {
|
|
927
|
+
const page = await collectCapped(client, `${pullRequestPath(ref, id, "comments")}?pagelen=${PAGELEN}&${RESOLUTION_FIELDS}`, 200, toComment);
|
|
928
|
+
const items = page.items.filter((comment) => comment !== null).sort((a, b) => a.created.localeCompare(b.created));
|
|
929
|
+
return {
|
|
930
|
+
...page,
|
|
931
|
+
items
|
|
932
|
+
};
|
|
823
933
|
}
|
|
824
|
-
async function
|
|
825
|
-
|
|
826
|
-
const first = `${pipelinesPath(ref)}/${encodeURIComponent(pipelineId)}/steps`;
|
|
827
|
-
for await (const step of walkPages(client, first)) out.push(toStepSummary(step));
|
|
828
|
-
return out;
|
|
934
|
+
async function listChangedFiles(client, ref, id) {
|
|
935
|
+
return collectCapped(client, `${pullRequestPath(ref, id, "diffstat")}?pagelen=${PAGELEN}`, 50, toChangedFile);
|
|
829
936
|
}
|
|
830
937
|
//#endregion
|
|
831
938
|
//#region src/util/format.ts
|
|
@@ -837,10 +944,16 @@ function formatDuration(seconds) {
|
|
|
837
944
|
if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
|
|
838
945
|
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
839
946
|
}
|
|
947
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
948
|
+
function formatDate$1(iso) {
|
|
949
|
+
const date = new Date(iso);
|
|
950
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
951
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
952
|
+
}
|
|
840
953
|
function formatDateTime(iso) {
|
|
841
954
|
const date = new Date(iso);
|
|
842
955
|
if (Number.isNaN(date.getTime())) return iso;
|
|
843
|
-
return date.
|
|
956
|
+
return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
844
957
|
}
|
|
845
958
|
const MINUTE = 60;
|
|
846
959
|
const HOUR = 3600;
|
|
@@ -859,134 +972,72 @@ function relativeTime(iso, nowMs) {
|
|
|
859
972
|
return `${Math.floor(days / 365)}y ago`;
|
|
860
973
|
}
|
|
861
974
|
//#endregion
|
|
862
|
-
//#region src/
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
age: relativeTime(cell.timestamp, nowMs),
|
|
877
|
-
json: { ...item }
|
|
878
|
-
};
|
|
879
|
-
});
|
|
880
|
-
const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
|
|
881
|
-
const idWidth = width((row) => row.id);
|
|
882
|
-
const labelWidth = width((row) => row.label);
|
|
883
|
-
const ageWidth = width((row) => row.age);
|
|
884
|
-
return rows.map((row) => ({
|
|
885
|
-
id: row.id,
|
|
886
|
-
url: row.url,
|
|
887
|
-
fixedColumns: `${row.id.padEnd(idWidth)} ${row.color(row.label.padEnd(labelWidth))} ${row.age.padEnd(ageWidth)}`,
|
|
888
|
-
freeText: row.text,
|
|
889
|
-
json: row.json
|
|
890
|
-
}));
|
|
891
|
-
}
|
|
892
|
-
function formatRows(rows, options) {
|
|
893
|
-
if (rows.length === 0) return [options.empty];
|
|
894
|
-
const lines = rows.map((row) => formatRow(row, options.width));
|
|
895
|
-
return options.footer ? [...lines, options.footer] : lines;
|
|
896
|
-
}
|
|
897
|
-
function writeRows(term, rows, options) {
|
|
898
|
-
if (options.json) term.json(rows.map((r) => r.json));
|
|
899
|
-
else term.out(formatRows(rows, {
|
|
900
|
-
...options,
|
|
901
|
-
width: term.width
|
|
902
|
-
}));
|
|
903
|
-
}
|
|
904
|
-
const COPY_CONCURRENCY = 5;
|
|
905
|
-
async function runSearch(term, rows, options, noun, copyOne) {
|
|
906
|
-
if (!(options.copy && !options.json && rows.length > 0)) {
|
|
907
|
-
writeRows(term, rows, options);
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (options.out?.endsWith(".md")) throw new Error("--out must be a directory when using --copy; a .md file path would overwrite each selection.");
|
|
911
|
-
await copySelected(term, rows, noun, copyOne);
|
|
912
|
-
}
|
|
913
|
-
function searchFooter(limit) {
|
|
914
|
-
return `
|
|
915
|
-
Showing first ${limit}; refine with flags or raise --limit.`;
|
|
916
|
-
}
|
|
917
|
-
async function copySelected(term, rows, noun, copyOne) {
|
|
918
|
-
if (!term.interactive) throw new Error("--copy requires an interactive terminal.");
|
|
919
|
-
const selected = await term.ask.pickMany({
|
|
920
|
-
message: `Select ${noun.plural} to copy:`,
|
|
921
|
-
choices: rows.map((r) => ({
|
|
922
|
-
name: formatRow(r, term.width),
|
|
923
|
-
value: r.id
|
|
924
|
-
})),
|
|
925
|
-
pageSize: 20
|
|
926
|
-
});
|
|
927
|
-
if (selected.length === 0) {
|
|
928
|
-
term.out("Nothing selected.");
|
|
929
|
-
return;
|
|
975
|
+
//#region src/commands/bitbucket-comments.ts
|
|
976
|
+
const MENTION = /@\{([^}]+)\}/g;
|
|
977
|
+
function renderedPrComments(comments, participants) {
|
|
978
|
+
const names = new Map(participants.map((person) => [person.accountId, person.name]));
|
|
979
|
+
return threadsOf(comments).map((thread) => renderThread(thread, names));
|
|
980
|
+
}
|
|
981
|
+
function threadsOf(comments) {
|
|
982
|
+
const byId = new Map(comments.map((comment) => [comment.id, comment]));
|
|
983
|
+
const threads = /* @__PURE__ */ new Map();
|
|
984
|
+
for (const comment of comments) {
|
|
985
|
+
const root = rootOf(comment, byId);
|
|
986
|
+
const thread = threads.get(root.id);
|
|
987
|
+
if (thread) thread.push(comment);
|
|
988
|
+
else threads.set(root.id, [comment]);
|
|
930
989
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
990
|
+
return [...threads.values()];
|
|
991
|
+
}
|
|
992
|
+
function rootOf(comment, byId) {
|
|
993
|
+
const seen = /* @__PURE__ */ new Set([comment.id]);
|
|
994
|
+
let current = comment;
|
|
995
|
+
while (current.parentId !== null) {
|
|
996
|
+
const parent = byId.get(current.parentId);
|
|
997
|
+
if (!parent || seen.has(parent.id)) break;
|
|
998
|
+
seen.add(parent.id);
|
|
999
|
+
current = parent;
|
|
941
1000
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const columns = hyperlink(row.id, row.url) + row.fixedColumns.slice(row.id.length);
|
|
951
|
-
return text ? `${columns} ${hyperlink(text, row.url)}` : columns;
|
|
952
|
-
}
|
|
953
|
-
function truncate(text, max) {
|
|
954
|
-
const clean = text.replace(/\s+/g, " ").trim();
|
|
955
|
-
return clean.length <= max ? clean : `${clean.slice(0, Math.max(0, max - 3))}...`;
|
|
956
|
-
}
|
|
957
|
-
//#endregion
|
|
958
|
-
//#region src/commands/bitbucket.ts
|
|
959
|
-
async function bitbucketPipelines({ session, term }, options) {
|
|
960
|
-
const ref = resolveRepo(options.repo, session);
|
|
961
|
-
const limit = parseLimit(options.limit);
|
|
962
|
-
writeRows(term, pipelineRows(await withScopeHint(PIPELINE_SCOPE, () => listPipelines(session, ref, limit)), Date.now()), {
|
|
963
|
-
json: options.json,
|
|
964
|
-
empty: "No pipelines found."
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
async function bitbucketPipeline({ session, term }, arg, options) {
|
|
968
|
-
const buildNumber = parseBuildNumber(arg);
|
|
969
|
-
const ref = resolveRepo(options.repo, session);
|
|
970
|
-
const detail = await withScopeHint(PIPELINE_SCOPE, () => getPipeline(session, ref, buildNumber));
|
|
971
|
-
const steps = await withScopeHint(PIPELINE_SCOPE, () => listSteps(session, ref, detail.uuid));
|
|
972
|
-
term.out(formatPipelineDetail(detail, steps, Date.now()));
|
|
973
|
-
}
|
|
974
|
-
function pipelineRows(pipelines, nowMs) {
|
|
975
|
-
return alignedRows(pipelines, nowMs, (p) => ({
|
|
976
|
-
id: `#${p.buildNumber}`,
|
|
977
|
-
url: p.url,
|
|
978
|
-
label: p.status || "-",
|
|
979
|
-
color: colorForBitbucketState(p.status),
|
|
980
|
-
text: `${refWithCommit(p)} · ${formatDuration(p.durationSeconds)}`,
|
|
981
|
-
timestamp: p.createdOn
|
|
1001
|
+
return current;
|
|
1002
|
+
}
|
|
1003
|
+
function renderThread(thread, names) {
|
|
1004
|
+
const rows = thread.map((comment, index) => ({
|
|
1005
|
+
author: comment.author,
|
|
1006
|
+
created: comment.created,
|
|
1007
|
+
markdown: resolveMentions(comment.body, names),
|
|
1008
|
+
...index === 0 ? { anchor: comment.anchor } : {}
|
|
982
1009
|
}));
|
|
1010
|
+
const trailer = resolvedTrailer(thread);
|
|
1011
|
+
const last = rows.at(-1);
|
|
1012
|
+
if (trailer && last) last.trailer = trailer;
|
|
1013
|
+
return rows;
|
|
1014
|
+
}
|
|
1015
|
+
function resolvedTrailer(thread) {
|
|
1016
|
+
const resolved = thread.find((comment) => comment.resolved)?.resolved;
|
|
1017
|
+
if (!resolved) return "";
|
|
1018
|
+
const by = resolved.by ? ` by ${resolved.by}` : "";
|
|
1019
|
+
return kleur.green(` ↳ resolved${by} · ${formatDateTime(resolved.at)}`);
|
|
1020
|
+
}
|
|
1021
|
+
function resolveMentions(body, names) {
|
|
1022
|
+
return body.replace(MENTION, (raw, id) => {
|
|
1023
|
+
const name = names.get(id);
|
|
1024
|
+
return name ? `@${name}` : raw;
|
|
1025
|
+
});
|
|
983
1026
|
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/commands/bitbucket-shared.ts
|
|
1029
|
+
const PIPELINE_SCOPE = "read:pipeline:bitbucket";
|
|
1030
|
+
const PULL_REQUEST_SCOPE = "read:pullrequest:bitbucket";
|
|
1031
|
+
const REPOSITORY_SCOPE = "read:repository:bitbucket";
|
|
1032
|
+
const ACCOUNT_SCOPE = "read:account";
|
|
984
1033
|
const STATE_COLORS = {
|
|
985
1034
|
SUCCESSFUL: kleur.green,
|
|
986
1035
|
MERGED: kleur.green,
|
|
1036
|
+
APPROVED: kleur.green,
|
|
987
1037
|
FAILED: kleur.red,
|
|
988
1038
|
ERROR: kleur.red,
|
|
989
1039
|
DECLINED: kleur.red,
|
|
1040
|
+
CHANGES_REQUESTED: kleur.red,
|
|
990
1041
|
IN_PROGRESS: kleur.yellow,
|
|
991
1042
|
PENDING: kleur.yellow,
|
|
992
1043
|
OPEN: kleur.yellow,
|
|
@@ -998,454 +1049,869 @@ const STATE_COLORS = {
|
|
|
998
1049
|
function colorForBitbucketState(state) {
|
|
999
1050
|
return STATE_COLORS[state.toUpperCase()] ?? kleur.white;
|
|
1000
1051
|
}
|
|
1001
|
-
function formatStepRows(steps) {
|
|
1002
|
-
const rows = steps.map((s) => ({
|
|
1003
|
-
name: s.name || "-",
|
|
1004
|
-
status: s.status || "-",
|
|
1005
|
-
dur: formatDuration(s.durationSeconds)
|
|
1006
|
-
}));
|
|
1007
|
-
const wn = Math.max(...rows.map((r) => r.name.length));
|
|
1008
|
-
const ws = Math.max(...rows.map((r) => r.status.length));
|
|
1009
|
-
return rows.map((r) => ` ${r.name.padEnd(wn)} ${r.status.padEnd(ws)} ${r.dur}`);
|
|
1010
|
-
}
|
|
1011
|
-
function formatPipelineDetail(detail, steps, nowMs) {
|
|
1012
|
-
const by = detail.creator ? ` by ${detail.creator}` : "";
|
|
1013
|
-
return [
|
|
1014
|
-
`Pipeline #${detail.buildNumber} ${detail.status || "-"}`,
|
|
1015
|
-
...detail.repo ? [`Repo: ${detail.repo}`] : [],
|
|
1016
|
-
`Ref: ${refWithCommit(detail)}`,
|
|
1017
|
-
...detail.trigger ? [`Trigger: ${detail.trigger}`] : [],
|
|
1018
|
-
`Duration: ${formatDuration(detail.durationSeconds)}`,
|
|
1019
|
-
`Created: ${relativeTime(detail.createdOn, nowMs)}${by}`,
|
|
1020
|
-
...steps.length > 0 ? [
|
|
1021
|
-
"",
|
|
1022
|
-
"Steps:",
|
|
1023
|
-
...formatStepRows(steps)
|
|
1024
|
-
] : []
|
|
1025
|
-
];
|
|
1026
|
-
}
|
|
1027
|
-
function refWithCommit(pipeline) {
|
|
1028
|
-
if (!pipeline.ref) return pipeline.commit || "-";
|
|
1029
|
-
return pipeline.commit ? `${pipeline.ref} (${pipeline.commit})` : pipeline.ref;
|
|
1030
|
-
}
|
|
1031
|
-
function parseBuildNumber(arg) {
|
|
1032
|
-
const raw = (arg ?? "").replace(/^#/, "").trim();
|
|
1033
|
-
if (!/^\d+$/.test(raw)) throw new Error(`Invalid pipeline number "${arg ?? ""}". Expected a build number, e.g. 123.`);
|
|
1034
|
-
return Number.parseInt(raw, 10);
|
|
1035
|
-
}
|
|
1036
|
-
const PIPELINE_SCOPE = "read:pipeline:bitbucket";
|
|
1037
1052
|
async function withScopeHint(scope, fn) {
|
|
1038
1053
|
try {
|
|
1039
1054
|
return await fn();
|
|
1040
1055
|
} catch (err) {
|
|
1041
|
-
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) throw new Error(`Bitbucket rejected the request (401/403). Check the token has the ${scope} scope, or run \`atlass bitbucket login\` to update it.`);
|
|
1056
|
+
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) throw new Error(`Bitbucket rejected ${err.path || "the request"} (401/403). Check the token has the ${scope} scope, or run \`atlass bitbucket login\` to update it.`);
|
|
1042
1057
|
throw err;
|
|
1043
1058
|
}
|
|
1044
1059
|
}
|
|
1045
1060
|
//#endregion
|
|
1046
|
-
//#region src/
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const limit = parseLimit(options.limit);
|
|
1054
|
-
const query = pullRequestQuery({
|
|
1055
|
-
...await resolvePrincipals(session, profile, options),
|
|
1056
|
-
query: options.query
|
|
1057
|
-
});
|
|
1058
|
-
const prs = await withScopeHint(PULL_REQUEST_SCOPE, () => listPullRequests(session, ref, {
|
|
1059
|
-
limit,
|
|
1060
|
-
states,
|
|
1061
|
-
query
|
|
1062
|
-
}));
|
|
1063
|
-
writeRows(term, pullRequestRows(prs, Date.now()), {
|
|
1064
|
-
json: options.json,
|
|
1065
|
-
empty: emptyMessage(options),
|
|
1066
|
-
footer: prs.length === limit ? searchFooter(limit) : void 0
|
|
1067
|
-
});
|
|
1061
|
+
//#region src/adf/to-markdown-inline.ts
|
|
1062
|
+
function renderMedia(node, ctx) {
|
|
1063
|
+
const attrs = node.attrs ?? {};
|
|
1064
|
+
const alt = attrs.alt ?? "";
|
|
1065
|
+
const resolved = ctx.resolveMedia?.(attrs);
|
|
1066
|
+
if (resolved) return ``;
|
|
1067
|
+
return `[embedded media: ${alt || attrs.id || "media"}]`;
|
|
1068
1068
|
}
|
|
1069
|
-
function
|
|
1070
|
-
|
|
1071
|
-
if (options.state) throw new Error("--all cannot be combined with --state.");
|
|
1072
|
-
return allPullRequestStates();
|
|
1069
|
+
function renderInline(nodes, ctx) {
|
|
1070
|
+
return nodes.map((n) => renderInlineNode(n, ctx)).join("");
|
|
1073
1071
|
}
|
|
1074
|
-
function
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
return
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1072
|
+
function renderInlineNode(node, ctx) {
|
|
1073
|
+
switch (node.type) {
|
|
1074
|
+
case "text": return applyMarks(node.text ?? "", node.marks ?? []);
|
|
1075
|
+
case "hardBreak": return " \n";
|
|
1076
|
+
case "mention": return (typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : "") || "@unknown";
|
|
1077
|
+
case "emoji": {
|
|
1078
|
+
const text = node.attrs?.["text"];
|
|
1079
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
1080
|
+
const short = node.attrs?.["shortName"];
|
|
1081
|
+
return typeof short === "string" ? short : "";
|
|
1082
|
+
}
|
|
1083
|
+
case "date": return formatDate(node.attrs?.["timestamp"]);
|
|
1084
|
+
case "status": return `\`[${typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : ""}]\``;
|
|
1085
|
+
case "inlineCard": {
|
|
1086
|
+
const url = node.attrs?.["url"];
|
|
1087
|
+
if (typeof url === "string") return `[${url}](${url})`;
|
|
1088
|
+
return "";
|
|
1089
|
+
}
|
|
1090
|
+
case "media": return renderMedia(node, ctx);
|
|
1091
|
+
default: return node.text ?? "";
|
|
1092
|
+
}
|
|
1086
1093
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1094
|
+
function applyMarks(text, marks) {
|
|
1095
|
+
if (text.length === 0) return text;
|
|
1096
|
+
let out = text;
|
|
1097
|
+
let href;
|
|
1098
|
+
for (const mark of marks) switch (mark.type) {
|
|
1099
|
+
case "code":
|
|
1100
|
+
out = `\`${out}\``;
|
|
1101
|
+
break;
|
|
1102
|
+
case "strong":
|
|
1103
|
+
out = `**${out}**`;
|
|
1104
|
+
break;
|
|
1105
|
+
case "em":
|
|
1106
|
+
out = `*${out}*`;
|
|
1107
|
+
break;
|
|
1108
|
+
case "strike":
|
|
1109
|
+
out = `~~${out}~~`;
|
|
1110
|
+
break;
|
|
1111
|
+
case "link": {
|
|
1112
|
+
const value = mark.attrs?.["href"];
|
|
1113
|
+
if (typeof value === "string") href = value;
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (href) out = `[${out}](${href})`;
|
|
1118
|
+
return out;
|
|
1093
1119
|
}
|
|
1094
|
-
function
|
|
1095
|
-
|
|
1120
|
+
function formatDate(timestamp) {
|
|
1121
|
+
const ms = typeof timestamp === "string" ? Number(timestamp) : typeof timestamp === "number" ? timestamp : NaN;
|
|
1122
|
+
if (Number.isNaN(ms)) return "";
|
|
1123
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
1096
1124
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/adf/to-markdown.ts
|
|
1127
|
+
function adfToMarkdown(doc, options = {}) {
|
|
1128
|
+
if (!doc) return "";
|
|
1129
|
+
const ctx = { resolveMedia: options.resolveMedia };
|
|
1130
|
+
return (doc.type === "doc" ? renderBlocks(doc.content ?? [], ctx, "") : renderBlock(doc, ctx, "")).trim();
|
|
1102
1131
|
}
|
|
1103
|
-
function
|
|
1104
|
-
return
|
|
1132
|
+
function renderBlocks(nodes, ctx, indent) {
|
|
1133
|
+
return nodes.map((n) => renderBlock(n, ctx, indent)).filter((s) => s.length > 0).join("\n\n");
|
|
1105
1134
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1135
|
+
function renderBlock(node, ctx, indent) {
|
|
1136
|
+
switch (node.type) {
|
|
1137
|
+
case "paragraph": return indent + renderInline(node.content ?? [], ctx);
|
|
1138
|
+
case "heading": {
|
|
1139
|
+
const level = clampLevel$1(node.attrs?.["level"]);
|
|
1140
|
+
return `${"#".repeat(level)} ${renderInline(node.content ?? [], ctx)}`;
|
|
1141
|
+
}
|
|
1142
|
+
case "bulletList": return renderList(node, ctx, indent, "bullet");
|
|
1143
|
+
case "orderedList": return renderList(node, ctx, indent, "ordered");
|
|
1144
|
+
case "taskList": return renderTaskList(node, ctx, indent);
|
|
1145
|
+
case "decisionList": return renderDecisionList(node, ctx, indent);
|
|
1146
|
+
case "codeBlock": return renderCodeBlock(node, indent);
|
|
1147
|
+
case "blockquote": return prefixLines(renderBlocks(node.content ?? [], ctx, ""), `${indent}> `);
|
|
1148
|
+
case "panel": return renderPanel(node, ctx, indent);
|
|
1149
|
+
case "rule": return `${indent}---`;
|
|
1150
|
+
case "table": return renderTable(node, ctx);
|
|
1151
|
+
case "mediaSingle":
|
|
1152
|
+
case "mediaGroup": return renderBlocks(node.content ?? [], ctx, indent);
|
|
1153
|
+
case "media": return indent + renderMedia(node, ctx);
|
|
1154
|
+
case "expand":
|
|
1155
|
+
case "nestedExpand": return renderExpand(node, ctx, indent);
|
|
1156
|
+
default:
|
|
1157
|
+
if (node.content?.length) return renderBlocks(node.content, ctx, indent);
|
|
1158
|
+
return node.text ? indent + node.text : "";
|
|
1159
|
+
}
|
|
1117
1160
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
return {
|
|
1124
|
-
type: "mediaSingle",
|
|
1125
|
-
attrs: { layout: "center" },
|
|
1126
|
-
content: [{
|
|
1127
|
-
type: "media",
|
|
1128
|
-
attrs
|
|
1129
|
-
}]
|
|
1130
|
-
};
|
|
1161
|
+
function renderList(node, ctx, indent, kind) {
|
|
1162
|
+
const start = kind === "ordered" ? toNumber(node.attrs?.["order"]) ?? 1 : 0;
|
|
1163
|
+
return (node.content ?? []).filter((n) => n.type === "listItem").map((item, i) => {
|
|
1164
|
+
return renderListItem(item, ctx, indent, kind === "ordered" ? `${start + i}. ` : "- ");
|
|
1165
|
+
}).join("\n");
|
|
1131
1166
|
}
|
|
1132
|
-
function
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
if (token.type === "image") hrefs.add(token.href);
|
|
1136
|
-
});
|
|
1137
|
-
return [...hrefs];
|
|
1167
|
+
function renderListItem(item, ctx, indent, marker) {
|
|
1168
|
+
const continuationIndent = `${indent}${" ".repeat(marker.length)}`;
|
|
1169
|
+
return (item.content ?? []).map((block, i) => i === 0 ? `${indent}${marker}${renderBlock(block, ctx, "")}` : renderBlock(block, ctx, continuationIndent)).join("\n");
|
|
1138
1170
|
}
|
|
1139
|
-
function
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
version: 1,
|
|
1144
|
-
content: blocks(marked.lexer(md), ctx)
|
|
1145
|
-
};
|
|
1171
|
+
function renderTaskList(node, ctx, indent) {
|
|
1172
|
+
return (node.content ?? []).filter((n) => n.type === "taskItem").map((item) => {
|
|
1173
|
+
return `${indent}- [${item.attrs?.["state"] === "DONE" ? "x" : " "}] ${renderInline(item.content ?? [], ctx)}`;
|
|
1174
|
+
}).join("\n");
|
|
1146
1175
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
for (const token of tokens) out.push(...block(token, ctx));
|
|
1150
|
-
return out;
|
|
1176
|
+
function renderDecisionList(node, ctx, indent) {
|
|
1177
|
+
return (node.content ?? []).filter((n) => n.type === "decisionItem").map((item) => `${indent}- (decision) ${renderInline(item.content ?? [], ctx)}`).join("\n");
|
|
1151
1178
|
}
|
|
1152
|
-
function
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
attrs: { level: clampLevel$1(token.depth) },
|
|
1158
|
-
content: inline(token.tokens ?? [], ctx)
|
|
1159
|
-
}];
|
|
1160
|
-
case "paragraph": return paragraphsSplitAtImages(token.tokens ?? [], ctx);
|
|
1161
|
-
case "text": return paragraphsSplitAtImages(token.tokens ?? [textToken(token.text)], ctx);
|
|
1162
|
-
case "list": return [list(token, ctx)];
|
|
1163
|
-
case "code": return [codeBlock(token)];
|
|
1164
|
-
case "blockquote": return [{
|
|
1165
|
-
type: "blockquote",
|
|
1166
|
-
content: blocks(token.tokens ?? [], ctx)
|
|
1167
|
-
}];
|
|
1168
|
-
case "table": return [table(token, ctx)];
|
|
1169
|
-
case "hr": return [{ type: "rule" }];
|
|
1170
|
-
default: return [];
|
|
1171
|
-
}
|
|
1179
|
+
function renderCodeBlock(node, indent) {
|
|
1180
|
+
const lang = typeof node.attrs?.["language"] === "string" ? node.attrs["language"] : "";
|
|
1181
|
+
const code = (node.content ?? []).map((n) => n.text ?? "").join("");
|
|
1182
|
+
const fence = "```";
|
|
1183
|
+
return prefixLines(`${fence}${lang}\n${code}\n${fence}`, indent);
|
|
1172
1184
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
buffer = [];
|
|
1184
|
-
};
|
|
1185
|
-
for (const token of tokens) if (token.type === "image") {
|
|
1186
|
-
flush();
|
|
1187
|
-
const image = token;
|
|
1188
|
-
const node = ctx.resolveImage(image.href, image.text ?? "");
|
|
1189
|
-
if (node) out.push(node);
|
|
1190
|
-
} else buffer.push(token);
|
|
1191
|
-
flush();
|
|
1192
|
-
return out;
|
|
1185
|
+
const PANEL_LABELS = {
|
|
1186
|
+
info: "Info",
|
|
1187
|
+
note: "Note",
|
|
1188
|
+
warning: "Warning",
|
|
1189
|
+
success: "Success",
|
|
1190
|
+
error: "Error"
|
|
1191
|
+
};
|
|
1192
|
+
function renderPanel(node, ctx, indent) {
|
|
1193
|
+
const type = typeof node.attrs?.["panelType"] === "string" ? node.attrs["panelType"] : "info";
|
|
1194
|
+
return prefixLines(`**${PANEL_LABELS[type] ?? "Note"}**\n\n${renderBlocks(node.content ?? [], ctx, "")}`, `${indent}> `);
|
|
1193
1195
|
}
|
|
1194
|
-
function
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1196
|
+
function renderExpand(node, ctx, indent) {
|
|
1197
|
+
return `${indent}<details><summary>${typeof node.attrs?.["title"] === "string" ? node.attrs["title"] : "Details"}</summary>\n\n${renderBlocks(node.content ?? [], ctx, "")}\n\n${indent}</details>`;
|
|
1198
|
+
}
|
|
1199
|
+
function renderTable(node, ctx) {
|
|
1200
|
+
const rows = (node.content ?? []).filter((n) => n.type === "tableRow");
|
|
1201
|
+
if (rows.length === 0) return "";
|
|
1202
|
+
const grid = rows.map((row) => (row.content ?? []).map((cell) => renderCellOnOneLine(cell, ctx)));
|
|
1203
|
+
const cols = Math.max(...grid.map((r) => r.length));
|
|
1204
|
+
const pad = (r) => {
|
|
1205
|
+
const copy = [...r];
|
|
1206
|
+
while (copy.length < cols) copy.push("");
|
|
1207
|
+
return copy;
|
|
1206
1208
|
};
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1209
|
+
const header = pad(grid[0] ?? []);
|
|
1210
|
+
const lines = [`| ${header.join(" | ")} |`, `| ${header.map(() => "---").join(" | ")} |`];
|
|
1211
|
+
for (const row of grid.slice(1)) lines.push(`| ${pad(row).join(" | ")} |`);
|
|
1212
|
+
return lines.join("\n");
|
|
1213
|
+
}
|
|
1214
|
+
function renderCellOnOneLine(cell, ctx) {
|
|
1215
|
+
return renderBlocks(cell.content ?? [], ctx, "").replace(/\n+/g, " ").replace(/\|/g, "\\|").trim();
|
|
1216
|
+
}
|
|
1217
|
+
function prefixLines(text, prefix) {
|
|
1218
|
+
return text.split("\n").map((line) => line.length > 0 ? prefix + line : prefix.trimEnd()).join("\n");
|
|
1219
|
+
}
|
|
1220
|
+
function clampLevel$1(value) {
|
|
1221
|
+
const n = toNumber(value) ?? 1;
|
|
1222
|
+
return Math.min(6, Math.max(1, n));
|
|
1223
|
+
}
|
|
1224
|
+
function toNumber(value) {
|
|
1225
|
+
return typeof value === "number" ? value : void 0;
|
|
1226
|
+
}
|
|
1227
|
+
//#endregion
|
|
1228
|
+
//#region src/markdown/highlight.ts
|
|
1229
|
+
const HEADING = /^#{1,6} /;
|
|
1230
|
+
const QUOTE = /^(\s*)>( |$)/;
|
|
1231
|
+
const LIST = /^(\s*)(-|\d+\.) (\[[ x]\] )?/;
|
|
1232
|
+
const RULE = /^\s*---$/;
|
|
1233
|
+
const TABLE_ROW = /^\|.*\|$/;
|
|
1234
|
+
const TABLE_SEPARATOR = /^\|( --- \|)+$/;
|
|
1235
|
+
const CELL_DIVIDER = /(?<!\\)\|/;
|
|
1236
|
+
const DETAILS_OPEN = /^(\s*)<details><summary>(.*)<\/summary>$/;
|
|
1237
|
+
const DETAILS_CLOSE = /^\s*<\/details>$/;
|
|
1238
|
+
const FENCE_OPEN = /^((?:\s|> |- |\d+\. )*)```(\S*)$/;
|
|
1239
|
+
const FENCE = "```";
|
|
1240
|
+
const INLINE = /(`[^`]+`)|\[([^\]]+)\]\(((?:[^()]|\([^()]*\))+)\)|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(?!\*)(.+?)(?<!\*)\*(?!\*)|~~(.+?)~~/g;
|
|
1241
|
+
const TOKEN_STYLES = {
|
|
1242
|
+
keyword: kleur.magenta,
|
|
1243
|
+
built_in: kleur.magenta,
|
|
1244
|
+
literal: kleur.magenta,
|
|
1245
|
+
string: kleur.green,
|
|
1246
|
+
regexp: kleur.green,
|
|
1247
|
+
addition: kleur.green,
|
|
1248
|
+
number: kleur.yellow,
|
|
1249
|
+
symbol: kleur.yellow,
|
|
1250
|
+
meta: kleur.yellow,
|
|
1251
|
+
attr: kleur.yellow,
|
|
1252
|
+
comment: kleur.dim,
|
|
1253
|
+
quote: kleur.dim,
|
|
1254
|
+
deletion: kleur.dim,
|
|
1255
|
+
title: kleur.cyan,
|
|
1256
|
+
type: kleur.cyan,
|
|
1257
|
+
section: kleur.cyan
|
|
1258
|
+
};
|
|
1259
|
+
const lowlight = createLowlight(common);
|
|
1260
|
+
function highlightMarkdown(md) {
|
|
1261
|
+
const lines = md.split("\n");
|
|
1262
|
+
const out = [];
|
|
1263
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1264
|
+
const line = lines[i] ?? "";
|
|
1265
|
+
const fence = line.match(FENCE_OPEN);
|
|
1266
|
+
if (!fence) {
|
|
1267
|
+
out.push(highlightLine(line, lines[i + 1]));
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
const [, prefix = "", lang = ""] = fence;
|
|
1271
|
+
const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
|
|
1272
|
+
out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
|
|
1273
|
+
const code = [];
|
|
1274
|
+
const prefixed = [];
|
|
1275
|
+
let j = i + 1;
|
|
1276
|
+
for (; j < lines.length; j++) {
|
|
1277
|
+
const raw = lines[j] ?? "";
|
|
1278
|
+
const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
|
|
1279
|
+
const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
|
|
1280
|
+
if (body === FENCE) break;
|
|
1281
|
+
code.push(body);
|
|
1282
|
+
prefixed.push(hasPrefix);
|
|
1283
|
+
}
|
|
1284
|
+
if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
|
|
1285
|
+
const lead = prefixed[k] ? styledPrefix : "";
|
|
1286
|
+
out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
|
|
1287
|
+
});
|
|
1288
|
+
if (j < lines.length) {
|
|
1289
|
+
const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
|
|
1290
|
+
out.push(`${lead}${kleur.dim(FENCE)}`);
|
|
1291
|
+
}
|
|
1292
|
+
i = j;
|
|
1293
|
+
}
|
|
1294
|
+
return out.join("\n");
|
|
1295
|
+
}
|
|
1296
|
+
function highlightCode(code, lang) {
|
|
1297
|
+
if (!lang || !lowlight.registered(lang)) return styleEachLine(code, kleur.yellow);
|
|
1298
|
+
return renderTree(lowlight.highlight(lang, code), (text) => text);
|
|
1299
|
+
}
|
|
1300
|
+
function renderTree(node, style) {
|
|
1301
|
+
if (node.type === "text") return styleEachLine(node.value, style);
|
|
1302
|
+
if (node.type !== "root" && node.type !== "element") return "";
|
|
1303
|
+
const own = node.type === "element" ? tokenStyle(node.properties["className"]) : void 0;
|
|
1304
|
+
const next = own ? (text) => style(own(text)) : style;
|
|
1305
|
+
return node.children.map((child) => renderTree(child, next)).join("");
|
|
1306
|
+
}
|
|
1307
|
+
function tokenStyle(className) {
|
|
1308
|
+
if (!Array.isArray(className)) return void 0;
|
|
1309
|
+
for (const name of className) {
|
|
1310
|
+
if (typeof name !== "string" || !name.startsWith("hljs-")) continue;
|
|
1311
|
+
const style = TOKEN_STYLES[name.slice(5)];
|
|
1312
|
+
if (style) return style;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function styleEachLine(text, style) {
|
|
1316
|
+
return text.split("\n").map((line) => line ? style(line) : line).join("\n");
|
|
1317
|
+
}
|
|
1318
|
+
function highlightLine(line, nextLine) {
|
|
1319
|
+
const quote = line.match(QUOTE);
|
|
1320
|
+
if (quote) {
|
|
1321
|
+
const [whole, indent = ""] = quote;
|
|
1322
|
+
const rest = line.slice(whole.length);
|
|
1323
|
+
const gap = whole.endsWith(" ") ? " " : "";
|
|
1324
|
+
return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
|
|
1325
|
+
}
|
|
1326
|
+
const list = line.match(LIST);
|
|
1327
|
+
if (list) {
|
|
1328
|
+
const [whole, indent = "", marker = "", checkbox] = list;
|
|
1329
|
+
const rest = line.slice(whole.length);
|
|
1330
|
+
const box = checkbox ? `${kleur.bold(checkbox.trim())} ` : "";
|
|
1331
|
+
return `${indent}${kleur.dim(marker)} ${box}${highlightLine(rest, void 0)}`;
|
|
1332
|
+
}
|
|
1333
|
+
if (HEADING.test(line)) return kleur.bold().cyan(line);
|
|
1334
|
+
if (RULE.test(line)) return kleur.dim(line);
|
|
1335
|
+
if (TABLE_SEPARATOR.test(line)) return kleur.dim(line);
|
|
1336
|
+
if (TABLE_ROW.test(line)) return tableRow(line, nextLine !== void 0 && TABLE_SEPARATOR.test(nextLine));
|
|
1337
|
+
const details = line.match(DETAILS_OPEN);
|
|
1338
|
+
if (details) {
|
|
1339
|
+
const [, indent = "", title = ""] = details;
|
|
1340
|
+
return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
|
|
1341
|
+
}
|
|
1342
|
+
if (DETAILS_CLOSE.test(line)) return kleur.dim(line);
|
|
1343
|
+
return highlightInline(line);
|
|
1344
|
+
}
|
|
1345
|
+
function stripPrefix(line, prefix) {
|
|
1346
|
+
return line?.startsWith(prefix) ? line.slice(prefix.length) : void 0;
|
|
1347
|
+
}
|
|
1348
|
+
function tableRow(line, header) {
|
|
1349
|
+
const styled = line.split(CELL_DIVIDER).slice(1, -1).map((cell) => header ? kleur.bold(highlightInline(cell)) : highlightInline(cell));
|
|
1350
|
+
const pipe = kleur.dim("|");
|
|
1351
|
+
return `${pipe}${styled.join(pipe)}${pipe}`;
|
|
1352
|
+
}
|
|
1353
|
+
function highlightInline(text) {
|
|
1354
|
+
return text.replace(INLINE, (match, code, linkText, url, boldItalic, bold, italic, strike) => {
|
|
1355
|
+
if (code !== void 0) return kleur.yellow(code);
|
|
1356
|
+
if (linkText !== void 0 && url !== void 0) return link(linkText, url);
|
|
1357
|
+
if (boldItalic !== void 0) return kleur.bold().italic(match);
|
|
1358
|
+
if (bold !== void 0) return kleur.bold(`**${highlightInline(bold)}**`);
|
|
1359
|
+
if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
|
|
1360
|
+
if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
|
|
1361
|
+
return match;
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function link(text, url) {
|
|
1365
|
+
const open = kleur.dim("[");
|
|
1366
|
+
const middle = kleur.dim("](");
|
|
1367
|
+
return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
|
|
1368
|
+
}
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/commands/view.ts
|
|
1371
|
+
const INDENT = " ";
|
|
1372
|
+
function fieldLines(pairs) {
|
|
1373
|
+
const kept = pairs.filter(([, value]) => value !== "");
|
|
1374
|
+
const width = Math.max(...kept.map(([label]) => label.length)) + 1;
|
|
1375
|
+
return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
|
|
1376
|
+
}
|
|
1377
|
+
function dateWithAge(iso, nowMs) {
|
|
1378
|
+
if (!iso) return "";
|
|
1379
|
+
return `${formatDate$1(iso)} (${relativeTime(iso, nowMs)})`;
|
|
1380
|
+
}
|
|
1381
|
+
function markdownBody(md) {
|
|
1382
|
+
return md ? ["", highlightMarkdown(md)] : [];
|
|
1383
|
+
}
|
|
1384
|
+
function bodyLines(body) {
|
|
1385
|
+
return markdownBody(adfToMarkdown(body));
|
|
1386
|
+
}
|
|
1387
|
+
function renderedComments(comments) {
|
|
1388
|
+
return comments.map((comment) => [{
|
|
1389
|
+
author: comment.author,
|
|
1390
|
+
created: comment.created,
|
|
1391
|
+
markdown: adfToMarkdown(comment.body)
|
|
1392
|
+
}]);
|
|
1393
|
+
}
|
|
1394
|
+
function commentSection(threads, options) {
|
|
1395
|
+
const total = threads.reduce((running, thread) => running + thread.length, 0);
|
|
1396
|
+
if (total === 0) return [];
|
|
1397
|
+
const visible = options.allComments ? threads : threads.slice(-5);
|
|
1398
|
+
const lines = ["", kleur.bold(commentHeading(total, threads.length, visible.length, options))];
|
|
1399
|
+
for (const thread of visible) thread.forEach((comment, index) => {
|
|
1400
|
+
lines.push("", commentHeader(comment, index > 0), ...commentBody(comment, index > 0), ...commentTrailer(comment));
|
|
1401
|
+
});
|
|
1402
|
+
return lines;
|
|
1403
|
+
}
|
|
1404
|
+
function commentHeading(total, threads, visible, options) {
|
|
1405
|
+
const count = options.truncated ? `${total}+` : String(total);
|
|
1406
|
+
if (visible === threads) return `Comments (${count})`;
|
|
1407
|
+
if (threads === total) return `Comments (${count}, showing last ${visible} — --all-comments for all)`;
|
|
1408
|
+
return `Comments (${count} in ${threads} threads, showing last ${visible} threads — --all-comments for all)`;
|
|
1409
|
+
}
|
|
1410
|
+
function commentHeader(comment, reply) {
|
|
1411
|
+
const author = kleur.bold(comment.author || "Unknown");
|
|
1412
|
+
const when = comment.created ? ` · ${kleur.dim(formatDateTime(comment.created))}` : "";
|
|
1413
|
+
const where = comment.anchor ? ` · ${kleur.dim(comment.anchor)}` : "";
|
|
1414
|
+
return `${reply ? `${INDENT}↳ ` : "─ "}${author}${when}${where}`;
|
|
1415
|
+
}
|
|
1416
|
+
function commentBody(comment, reply) {
|
|
1417
|
+
if (!comment.markdown) return [];
|
|
1418
|
+
const body = highlightMarkdown(comment.markdown);
|
|
1419
|
+
if (!reply) return [body];
|
|
1420
|
+
return [body.split("\n").map((line) => line ? `${INDENT}${line}` : line).join("\n")];
|
|
1421
|
+
}
|
|
1422
|
+
function commentTrailer(comment) {
|
|
1423
|
+
return comment.trailer ? ["", comment.trailer] : [];
|
|
1424
|
+
}
|
|
1425
|
+
function attachmentSection(attachments) {
|
|
1426
|
+
if (attachments.length === 0) return [];
|
|
1427
|
+
return [
|
|
1428
|
+
"",
|
|
1429
|
+
kleur.bold("Attachments"),
|
|
1430
|
+
...attachments.map((a) => `- ${a.filename}`)
|
|
1431
|
+
];
|
|
1432
|
+
}
|
|
1433
|
+
//#endregion
|
|
1434
|
+
//#region src/commands/bitbucket-pr.ts
|
|
1435
|
+
async function bitbucketPr({ session, term }, arg, options) {
|
|
1436
|
+
const ref = parseRef(arg);
|
|
1437
|
+
const repo = ref.repo ?? resolveRepo(options.repo, session);
|
|
1438
|
+
const detail = await withScopeHint(PULL_REQUEST_SCOPE, () => fetchDetail(session, repo, ref.id));
|
|
1439
|
+
const [comments, files] = await Promise.all([withScopeHint(PULL_REQUEST_SCOPE, () => listComments(session, repo, ref.id)), fetchFiles(session, repo, ref.id)]);
|
|
1440
|
+
if (options.json) {
|
|
1441
|
+
term.json({
|
|
1442
|
+
...detail,
|
|
1443
|
+
comments: comments.items,
|
|
1444
|
+
files: files?.items ?? null,
|
|
1445
|
+
truncated: {
|
|
1446
|
+
comments: comments.truncated,
|
|
1447
|
+
files: files?.truncated ?? false
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const lines = formatPullRequestView(detail, comments, files, Date.now(), options.allComments);
|
|
1453
|
+
await term.page(lines.join("\n"), { pager: options.pager });
|
|
1454
|
+
}
|
|
1455
|
+
function formatPullRequestView(detail, comments, files, nowMs, allComments = false) {
|
|
1456
|
+
const label = detail.draft ? "DRAFT" : detail.state || "-";
|
|
1457
|
+
return [
|
|
1458
|
+
kleur.bold(detail.title),
|
|
1459
|
+
...fieldLines([
|
|
1460
|
+
["State", colorForBitbucketState(label)(label)],
|
|
1461
|
+
["Author", detail.author],
|
|
1462
|
+
["Branch", branchLine(detail)],
|
|
1463
|
+
["Approvals", approvalCount(detail.reviewers)],
|
|
1464
|
+
["Created", dateWithAge(detail.createdOn, nowMs)],
|
|
1465
|
+
["Updated", dateWithAge(detail.updatedOn, nowMs)],
|
|
1466
|
+
["Closed by", detail.closedBy],
|
|
1467
|
+
["URL", detail.url]
|
|
1468
|
+
]),
|
|
1469
|
+
...markdownBody(detail.description),
|
|
1470
|
+
...reviewerSection(detail.reviewers),
|
|
1471
|
+
...fileSection(files),
|
|
1472
|
+
...commentSection(renderedPrComments(comments.items, detail.participants), {
|
|
1473
|
+
allComments,
|
|
1474
|
+
truncated: comments.truncated
|
|
1475
|
+
})
|
|
1476
|
+
];
|
|
1477
|
+
}
|
|
1478
|
+
function reviewerSection(reviewers) {
|
|
1479
|
+
const heading = ["", kleur.bold("Reviewers")];
|
|
1480
|
+
if (reviewers.length === 0) return [...heading, ` ${kleur.dim("None assigned")}`];
|
|
1481
|
+
const width = Math.max(...reviewers.map((r) => r.name.length));
|
|
1482
|
+
return [...heading, ...reviewers.map((r) => ` ${r.name.padEnd(width)} ${reviewerState(r)}`)];
|
|
1483
|
+
}
|
|
1484
|
+
function fileSection(files) {
|
|
1485
|
+
if (!files) return [
|
|
1486
|
+
"",
|
|
1487
|
+
kleur.bold("Files"),
|
|
1488
|
+
` ${kleur.dim(`Unavailable: the token needs the ${REPOSITORY_SCOPE} scope.`)}`
|
|
1489
|
+
];
|
|
1490
|
+
if (files.items.length === 0) return [];
|
|
1491
|
+
const rows = files.items.map((file) => ({
|
|
1492
|
+
added: `+${file.added}`,
|
|
1493
|
+
removed: `-${file.removed}`,
|
|
1494
|
+
path: file.path
|
|
1495
|
+
}));
|
|
1496
|
+
const wa = Math.max(...rows.map((r) => r.added.length));
|
|
1497
|
+
const wr = Math.max(...rows.map((r) => r.removed.length));
|
|
1498
|
+
const total = files.total ?? files.items.length;
|
|
1499
|
+
const added = sum(files.items, (f) => f.added);
|
|
1500
|
+
const removed = sum(files.items, (f) => f.removed);
|
|
1501
|
+
return [
|
|
1502
|
+
"",
|
|
1503
|
+
kleur.bold(`Files (${total}, +${added} -${removed})`),
|
|
1504
|
+
...rows.map((r) => ` ${kleur.green(r.added.padStart(wa))} ${kleur.red(r.removed.padStart(wr))} ${r.path}`),
|
|
1505
|
+
...moreFilesLine(files)
|
|
1506
|
+
];
|
|
1507
|
+
}
|
|
1508
|
+
function moreFilesLine(files) {
|
|
1509
|
+
if (!files.truncated) return [];
|
|
1510
|
+
const rest = files.total === void 0 ? void 0 : files.total - files.items.length;
|
|
1511
|
+
return [kleur.dim(rest === void 0 ? " ... more files not shown" : ` ... and ${rest} more`)];
|
|
1512
|
+
}
|
|
1513
|
+
function sum(files, pick) {
|
|
1514
|
+
return files.reduce((running, file) => running + pick(file), 0);
|
|
1515
|
+
}
|
|
1516
|
+
function reviewerState(reviewer) {
|
|
1517
|
+
if (!reviewer.state) return kleur.dim("—");
|
|
1518
|
+
return colorForBitbucketState(reviewer.state)(reviewer.state);
|
|
1519
|
+
}
|
|
1520
|
+
function approvalCount(reviewers) {
|
|
1521
|
+
return `${reviewers.filter((r) => r.state === "APPROVED").length}/${reviewers.length}`;
|
|
1522
|
+
}
|
|
1523
|
+
function branchLine(detail) {
|
|
1524
|
+
if (!detail.sourceBranch || !detail.destinationBranch) return detail.sourceBranch || detail.destinationBranch;
|
|
1525
|
+
return `${detail.sourceBranch} → ${detail.destinationBranch}`;
|
|
1526
|
+
}
|
|
1527
|
+
function parseRef(arg) {
|
|
1528
|
+
const ref = parsePullRequestRef(arg ?? "");
|
|
1529
|
+
if (!ref) throw new Error(`Invalid pull request "${arg ?? ""}". Expected a PR number, e.g. 42, or a Bitbucket pull request URL.`);
|
|
1530
|
+
return ref;
|
|
1531
|
+
}
|
|
1532
|
+
async function fetchFiles(session, repo, id) {
|
|
1533
|
+
try {
|
|
1534
|
+
return await listChangedFiles(session, repo, id);
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) return null;
|
|
1537
|
+
throw err;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
async function fetchDetail(session, repo, id) {
|
|
1541
|
+
try {
|
|
1542
|
+
return await getPullRequest(session, repo, id);
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`Pull request #${id} not found in ${repo.workspace}/${repo.repo}.`);
|
|
1545
|
+
throw err;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region src/util/link.ts
|
|
1550
|
+
const OSC8 = "\x1B]8;;";
|
|
1551
|
+
const BEL = "\x07";
|
|
1552
|
+
function hyperlink(text, url) {
|
|
1553
|
+
if (!kleur.enabled || !url) return text;
|
|
1554
|
+
return `${OSC8}${url}${BEL}${text}${OSC8}${BEL}`;
|
|
1555
|
+
}
|
|
1556
|
+
//#endregion
|
|
1557
|
+
//#region src/commands/search-run.ts
|
|
1558
|
+
function checkedLimit(options) {
|
|
1559
|
+
if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
|
|
1560
|
+
return parseLimit(options.limit);
|
|
1561
|
+
}
|
|
1562
|
+
async function showRows(term, rows, listing, options, noun, copyOne) {
|
|
1563
|
+
await runSearch(term, rows, {
|
|
1564
|
+
json: options.json,
|
|
1565
|
+
copy: options.copy,
|
|
1566
|
+
out: options.out,
|
|
1567
|
+
empty: listing.empty,
|
|
1568
|
+
footer: listing.hasMore ? searchFooter(listing.limit) : void 0
|
|
1569
|
+
}, noun, copyOne);
|
|
1570
|
+
}
|
|
1571
|
+
function alignedRows(items, nowMs, cells) {
|
|
1572
|
+
const rows = items.map((item) => {
|
|
1573
|
+
const cell = cells(item);
|
|
1574
|
+
return {
|
|
1575
|
+
...cell,
|
|
1576
|
+
age: relativeTime(cell.timestamp, nowMs),
|
|
1577
|
+
json: { ...item }
|
|
1578
|
+
};
|
|
1579
|
+
});
|
|
1580
|
+
const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
|
|
1581
|
+
const idWidth = width((row) => row.id);
|
|
1582
|
+
const labelWidth = width((row) => row.label);
|
|
1583
|
+
const ageWidth = width((row) => row.age);
|
|
1584
|
+
return rows.map((row) => ({
|
|
1585
|
+
id: row.id,
|
|
1586
|
+
url: row.url,
|
|
1587
|
+
fixedColumns: `${row.id.padEnd(idWidth)} ${row.color(row.label.padEnd(labelWidth))} ${row.age.padEnd(ageWidth)}`,
|
|
1588
|
+
freeText: row.text,
|
|
1589
|
+
json: row.json
|
|
1590
|
+
}));
|
|
1591
|
+
}
|
|
1592
|
+
function formatRows(rows, options) {
|
|
1593
|
+
if (rows.length === 0) return [options.empty];
|
|
1594
|
+
const lines = rows.map((row) => formatRow(row, options.width));
|
|
1595
|
+
return options.footer ? [...lines, options.footer] : lines;
|
|
1596
|
+
}
|
|
1597
|
+
function writeRows(term, rows, options) {
|
|
1598
|
+
if (options.json) term.json(rows.map((r) => r.json));
|
|
1599
|
+
else term.out(formatRows(rows, {
|
|
1600
|
+
...options,
|
|
1601
|
+
width: term.width
|
|
1602
|
+
}));
|
|
1603
|
+
}
|
|
1604
|
+
const COPY_CONCURRENCY = 5;
|
|
1605
|
+
async function runSearch(term, rows, options, noun, copyOne) {
|
|
1606
|
+
if (!(options.copy && !options.json && rows.length > 0)) {
|
|
1607
|
+
writeRows(term, rows, options);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (options.out?.endsWith(".md")) throw new Error("--out must be a directory when using --copy; a .md file path would overwrite each selection.");
|
|
1611
|
+
await copySelected(term, rows, noun, copyOne);
|
|
1612
|
+
}
|
|
1613
|
+
function searchFooter(limit) {
|
|
1614
|
+
return `
|
|
1615
|
+
Showing first ${limit}; refine with flags or raise --limit.`;
|
|
1616
|
+
}
|
|
1617
|
+
async function copySelected(term, rows, noun, copyOne) {
|
|
1618
|
+
if (!term.interactive) throw new Error("--copy requires an interactive terminal.");
|
|
1619
|
+
const selected = await term.ask.pickMany({
|
|
1620
|
+
message: `Select ${noun.plural} to copy:`,
|
|
1621
|
+
choices: rows.map((r) => ({
|
|
1622
|
+
name: formatRow(r, term.width),
|
|
1623
|
+
value: r.id
|
|
1624
|
+
})),
|
|
1625
|
+
pageSize: 20
|
|
1626
|
+
});
|
|
1627
|
+
if (selected.length === 0) {
|
|
1628
|
+
term.out("Nothing selected.");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
let copied = 0;
|
|
1632
|
+
const failures = [];
|
|
1633
|
+
const queue = [...selected];
|
|
1634
|
+
async function worker() {
|
|
1635
|
+
for (let id = queue.shift(); id !== void 0; id = queue.shift()) try {
|
|
1636
|
+
await copyOne(id);
|
|
1637
|
+
copied++;
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
failures.push(`${id} (${err instanceof Error ? err.message : String(err)})`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, selected.length) }, worker));
|
|
1643
|
+
const summary = `Copied ${copied} ${copied === 1 ? noun.singular : noun.plural}`;
|
|
1644
|
+
if (failures.length === 0) term.out(summary);
|
|
1645
|
+
else term.out(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
|
|
1646
|
+
}
|
|
1647
|
+
function formatRow(row, width) {
|
|
1648
|
+
const room = width - stripVTControlCharacters(row.fixedColumns).length - 2;
|
|
1649
|
+
const text = room > 0 ? truncate(row.freeText, room) : "";
|
|
1650
|
+
const columns = hyperlink(row.id, row.url) + row.fixedColumns.slice(row.id.length);
|
|
1651
|
+
return text ? `${columns} ${hyperlink(text, row.url)}` : columns;
|
|
1652
|
+
}
|
|
1653
|
+
function truncate(text, max) {
|
|
1654
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
1655
|
+
return clean.length <= max ? clean : `${clean.slice(0, Math.max(0, max - 3))}...`;
|
|
1656
|
+
}
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/commands/bitbucket-prs.ts
|
|
1659
|
+
async function bitbucketPrs({ session, term, profile }, options) {
|
|
1660
|
+
if (options.query && (options.author || options.reviewer)) throw new Error("--query cannot be combined with --author or --reviewer.");
|
|
1661
|
+
const states = pullRequestStates(options);
|
|
1662
|
+
const ref = resolveRepo(options.repo, session);
|
|
1663
|
+
const limit = parseLimit(options.limit);
|
|
1664
|
+
const query = pullRequestQuery({
|
|
1665
|
+
...await resolvePrincipals(session, profile, options),
|
|
1666
|
+
query: options.query
|
|
1667
|
+
});
|
|
1668
|
+
const prs = await withScopeHint(PULL_REQUEST_SCOPE, () => listPullRequests(session, ref, {
|
|
1669
|
+
limit,
|
|
1670
|
+
states,
|
|
1671
|
+
query
|
|
1672
|
+
}));
|
|
1673
|
+
writeRows(term, pullRequestRows(prs, Date.now()), {
|
|
1674
|
+
json: options.json,
|
|
1675
|
+
empty: emptyMessage(options),
|
|
1676
|
+
footer: prs.length === limit ? searchFooter(limit) : void 0
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function pullRequestStates(options) {
|
|
1680
|
+
if (!options.all) return parsePullRequestStates(options.state);
|
|
1681
|
+
if (options.state) throw new Error("--all cannot be combined with --state.");
|
|
1682
|
+
return allPullRequestStates();
|
|
1683
|
+
}
|
|
1684
|
+
function pullRequestRows(prs, nowMs) {
|
|
1685
|
+
return alignedRows(prs, nowMs, (pr) => {
|
|
1686
|
+
const label = pr.draft ? "DRAFT" : pr.state || "-";
|
|
1687
|
+
return {
|
|
1688
|
+
id: `#${pr.id}`,
|
|
1689
|
+
url: pr.url,
|
|
1690
|
+
label,
|
|
1691
|
+
color: colorForBitbucketState(label),
|
|
1692
|
+
text: pr.title,
|
|
1693
|
+
timestamp: pr.updatedOn
|
|
1694
|
+
};
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
async function resolvePrincipals(session, profile, options) {
|
|
1698
|
+
const mine = isMe(options.author) || isMe(options.reviewer) ? await currentUuid(session, profile) : void 0;
|
|
1699
|
+
return {
|
|
1700
|
+
author: isMe(options.author) ? mine : options.author,
|
|
1701
|
+
reviewer: isMe(options.reviewer) ? mine : options.reviewer
|
|
1210
1702
|
};
|
|
1211
|
-
const start = Number(token.start);
|
|
1212
|
-
if (token.ordered && Number.isFinite(start) && start !== 1) node.attrs = { order: start };
|
|
1213
|
-
return node;
|
|
1214
1703
|
}
|
|
1215
|
-
function
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1704
|
+
function isMe(value) {
|
|
1705
|
+
return value?.toLowerCase() === "me";
|
|
1706
|
+
}
|
|
1707
|
+
async function currentUuid(session, profile) {
|
|
1708
|
+
if (session.uuid) return session.uuid;
|
|
1709
|
+
const uuid = await withScopeHint(ACCOUNT_SCOPE, () => fetchCurrentUserUuid(session));
|
|
1710
|
+
await rememberBitbucketUuid(profile, uuid);
|
|
1711
|
+
return uuid;
|
|
1712
|
+
}
|
|
1713
|
+
function emptyMessage(options) {
|
|
1714
|
+
return options.author || options.reviewer || options.query || options.state || options.all ? "No matching pull requests." : "No open pull requests.";
|
|
1715
|
+
}
|
|
1716
|
+
//#endregion
|
|
1717
|
+
//#region src/api/bitbucket-pipelines.ts
|
|
1718
|
+
const BUILD_NUMBER_SCAN_LIMIT = 1e3;
|
|
1719
|
+
function pipelineStatus(state) {
|
|
1720
|
+
if (!state) return "";
|
|
1721
|
+
if (state.name === "COMPLETED") return state.result?.name ?? "COMPLETED";
|
|
1722
|
+
return state.name ?? "";
|
|
1723
|
+
}
|
|
1724
|
+
function pipelinesQuery(limit) {
|
|
1725
|
+
return new URLSearchParams({
|
|
1726
|
+
sort: "-created_on",
|
|
1727
|
+
pagelen: String(Math.min(Math.max(limit, 1), 100))
|
|
1728
|
+
}).toString();
|
|
1221
1729
|
}
|
|
1222
|
-
function
|
|
1223
|
-
return
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1730
|
+
function wallClockSeconds(startOn, endOn) {
|
|
1731
|
+
if (!startOn || !endOn) return null;
|
|
1732
|
+
const start = Date.parse(startOn);
|
|
1733
|
+
const end = Date.parse(endOn);
|
|
1734
|
+
if (Number.isNaN(start) || Number.isNaN(end)) return null;
|
|
1735
|
+
return Math.floor((end - start) / 1e3);
|
|
1227
1736
|
}
|
|
1228
|
-
function
|
|
1229
|
-
|
|
1230
|
-
if (first && "tokens" in first && first.tokens) return first.tokens;
|
|
1231
|
-
return [textToken(item.text)];
|
|
1737
|
+
function shortHash(hash) {
|
|
1738
|
+
return hash?.slice(0, 7) ?? "";
|
|
1232
1739
|
}
|
|
1233
|
-
function
|
|
1740
|
+
function pipelineUrl(ref, buildNumber) {
|
|
1741
|
+
return `${BITBUCKET_WEB_ORIGIN}/${ref.workspace}/${ref.repo}/pipelines/results/${buildNumber}`;
|
|
1742
|
+
}
|
|
1743
|
+
function toPipelineSummary(ref, p) {
|
|
1234
1744
|
return {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1745
|
+
buildNumber: p.build_number,
|
|
1746
|
+
status: pipelineStatus(p.state),
|
|
1747
|
+
ref: p.target?.ref_name ?? "",
|
|
1748
|
+
commit: shortHash(p.target?.commit?.hash),
|
|
1749
|
+
durationSeconds: wallClockSeconds(p.created_on, p.completed_on),
|
|
1750
|
+
createdOn: p.created_on ?? "",
|
|
1751
|
+
creator: p.creator?.display_name ?? "",
|
|
1752
|
+
uuid: p.uuid,
|
|
1753
|
+
url: pipelineUrl(ref, p.build_number)
|
|
1241
1754
|
};
|
|
1242
1755
|
}
|
|
1243
|
-
function
|
|
1756
|
+
function toPipelineDetail(ref, p) {
|
|
1244
1757
|
return {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
content: token.header.map((cell) => tableCell(cell, "tableHeader", ctx))
|
|
1249
|
-
}, ...token.rows.map((row) => ({
|
|
1250
|
-
type: "tableRow",
|
|
1251
|
-
content: row.map((cell) => tableCell(cell, "tableCell", ctx))
|
|
1252
|
-
}))]
|
|
1758
|
+
...toPipelineSummary(ref, p),
|
|
1759
|
+
repo: p.repository?.full_name ?? "",
|
|
1760
|
+
trigger: p.trigger?.name ?? ""
|
|
1253
1761
|
};
|
|
1254
1762
|
}
|
|
1255
|
-
function
|
|
1763
|
+
function toStepSummary(step) {
|
|
1256
1764
|
return {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
content: inline(cell.tokens, ctx)
|
|
1261
|
-
}]
|
|
1765
|
+
name: step.name ?? "",
|
|
1766
|
+
status: pipelineStatus(step.state),
|
|
1767
|
+
durationSeconds: wallClockSeconds(step.started_on, step.completed_on)
|
|
1262
1768
|
};
|
|
1263
1769
|
}
|
|
1264
|
-
function
|
|
1265
|
-
|
|
1266
|
-
for (const token of tokens) out.push(...inlineNode(token, ctx, marks));
|
|
1267
|
-
return out;
|
|
1268
|
-
}
|
|
1269
|
-
function inlineNode(token, ctx, marks) {
|
|
1270
|
-
switch (token.type) {
|
|
1271
|
-
case "text":
|
|
1272
|
-
case "escape": {
|
|
1273
|
-
const t = token;
|
|
1274
|
-
if ("tokens" in t && t.tokens?.length) return inline(t.tokens, ctx, marks);
|
|
1275
|
-
return textNode(t.text, marks);
|
|
1276
|
-
}
|
|
1277
|
-
case "strong": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strong" }));
|
|
1278
|
-
case "em": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "em" }));
|
|
1279
|
-
case "del": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strike" }));
|
|
1280
|
-
case "codespan": return textNode(token.text, withUniqueMark(marks, { type: "code" }));
|
|
1281
|
-
case "link": {
|
|
1282
|
-
const link = token;
|
|
1283
|
-
return inline(link.tokens, ctx, withUniqueMark(marks, {
|
|
1284
|
-
type: "link",
|
|
1285
|
-
attrs: { href: link.href }
|
|
1286
|
-
}));
|
|
1287
|
-
}
|
|
1288
|
-
case "br": return [{ type: "hardBreak" }];
|
|
1289
|
-
case "html": return textNode(token.text, marks);
|
|
1290
|
-
default: return "text" in token && token.text ? textNode(token.text, marks) : [];
|
|
1291
|
-
}
|
|
1770
|
+
function pipelinesPath(ref) {
|
|
1771
|
+
return repoPath(ref, "pipelines");
|
|
1292
1772
|
}
|
|
1293
|
-
function
|
|
1294
|
-
|
|
1295
|
-
const node = {
|
|
1296
|
-
type: "text",
|
|
1297
|
-
text
|
|
1298
|
-
};
|
|
1299
|
-
if (marks.length > 0) node.marks = marks;
|
|
1300
|
-
return [node];
|
|
1773
|
+
async function listPipelines(client, ref, limit) {
|
|
1774
|
+
return collectPages(client, `${pipelinesPath(ref)}?${pipelinesQuery(limit)}`, limit, (value) => toPipelineSummary(ref, value));
|
|
1301
1775
|
}
|
|
1302
|
-
function
|
|
1303
|
-
|
|
1776
|
+
async function getPipeline(client, ref, buildNumber) {
|
|
1777
|
+
const found = await fetchByBuildNumber(client, ref, buildNumber) ?? await findInRecentRuns(client, ref, buildNumber);
|
|
1778
|
+
if (!found) throw new Error(`Could not find pipeline #${buildNumber} in the ${BUILD_NUMBER_SCAN_LIMIT} most recent runs. It may be too old.`);
|
|
1779
|
+
return toPipelineDetail(ref, found);
|
|
1304
1780
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1781
|
+
async function fetchByBuildNumber(client, ref, buildNumber) {
|
|
1782
|
+
try {
|
|
1783
|
+
return await client.getJson(`${pipelinesPath(ref)}/${encodeURIComponent(String(buildNumber))}`);
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
if (err instanceof HttpError && (err.status === 400 || err.status === 404)) return null;
|
|
1786
|
+
throw err;
|
|
1787
|
+
}
|
|
1310
1788
|
}
|
|
1311
|
-
function
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1789
|
+
async function findInRecentRuns(client, ref, buildNumber) {
|
|
1790
|
+
const first = `${pipelinesPath(ref)}?${pipelinesQuery(100)}`;
|
|
1791
|
+
let scanned = 0;
|
|
1792
|
+
for await (const pipeline of walkPages(client, first)) {
|
|
1793
|
+
if (pipeline.build_number === buildNumber) return pipeline;
|
|
1794
|
+
if (++scanned >= BUILD_NUMBER_SCAN_LIMIT) break;
|
|
1795
|
+
}
|
|
1796
|
+
return null;
|
|
1318
1797
|
}
|
|
1319
|
-
function
|
|
1320
|
-
|
|
1798
|
+
async function listSteps(client, ref, pipelineId) {
|
|
1799
|
+
const out = [];
|
|
1800
|
+
const first = `${pipelinesPath(ref)}/${encodeURIComponent(pipelineId)}/steps`;
|
|
1801
|
+
for await (const step of walkPages(client, first)) out.push(toStepSummary(step));
|
|
1802
|
+
return out;
|
|
1321
1803
|
}
|
|
1322
1804
|
//#endregion
|
|
1323
|
-
//#region src/
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
})
|
|
1805
|
+
//#region src/commands/bitbucket.ts
|
|
1806
|
+
async function bitbucketPipelines({ session, term }, options) {
|
|
1807
|
+
const ref = resolveRepo(options.repo, session);
|
|
1808
|
+
const limit = parseLimit(options.limit);
|
|
1809
|
+
writeRows(term, pipelineRows(await withScopeHint(PIPELINE_SCOPE, () => listPipelines(session, ref, limit)), Date.now()), {
|
|
1810
|
+
json: options.json,
|
|
1811
|
+
empty: "No pipelines found."
|
|
1812
|
+
});
|
|
1331
1813
|
}
|
|
1332
|
-
async function
|
|
1333
|
-
|
|
1814
|
+
async function bitbucketPipeline({ session, term }, arg, options) {
|
|
1815
|
+
const buildNumber = parseBuildNumber(arg);
|
|
1816
|
+
const ref = resolveRepo(options.repo, session);
|
|
1817
|
+
const detail = await withScopeHint(PIPELINE_SCOPE, () => getPipeline(session, ref, buildNumber));
|
|
1818
|
+
const steps = await withScopeHint(PIPELINE_SCOPE, () => listSteps(session, ref, detail.uuid));
|
|
1819
|
+
term.out(formatPipelineDetail(detail, steps, Date.now()));
|
|
1334
1820
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1821
|
+
function pipelineRows(pipelines, nowMs) {
|
|
1822
|
+
return alignedRows(pipelines, nowMs, (p) => ({
|
|
1823
|
+
id: `#${p.buildNumber}`,
|
|
1824
|
+
url: p.url,
|
|
1825
|
+
label: p.status || "-",
|
|
1826
|
+
color: colorForBitbucketState(p.status),
|
|
1827
|
+
text: `${refWithCommit(p)} · ${formatDuration(p.durationSeconds)}`,
|
|
1828
|
+
timestamp: p.createdOn
|
|
1829
|
+
}));
|
|
1339
1830
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1831
|
+
function formatStepRows(steps) {
|
|
1832
|
+
const rows = steps.map((s) => ({
|
|
1833
|
+
name: s.name || "-",
|
|
1834
|
+
status: s.status || "-",
|
|
1835
|
+
dur: formatDuration(s.durationSeconds)
|
|
1345
1836
|
}));
|
|
1837
|
+
const wn = Math.max(...rows.map((r) => r.name.length));
|
|
1838
|
+
const ws = Math.max(...rows.map((r) => r.status.length));
|
|
1839
|
+
return rows.map((r) => ` ${r.name.padEnd(wn)} ${r.status.padEnd(ws)} ${r.dur}`);
|
|
1346
1840
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1841
|
+
function formatPipelineDetail(detail, steps, nowMs) {
|
|
1842
|
+
const by = detail.creator ? ` by ${detail.creator}` : "";
|
|
1843
|
+
return [
|
|
1844
|
+
`Pipeline #${detail.buildNumber} ${detail.status || "-"}`,
|
|
1845
|
+
...detail.repo ? [`Repo: ${detail.repo}`] : [],
|
|
1846
|
+
`Ref: ${refWithCommit(detail)}`,
|
|
1847
|
+
...detail.trigger ? [`Trigger: ${detail.trigger}`] : [],
|
|
1848
|
+
`Duration: ${formatDuration(detail.durationSeconds)}`,
|
|
1849
|
+
`Created: ${relativeTime(detail.createdOn, nowMs)}${by}`,
|
|
1850
|
+
...steps.length > 0 ? [
|
|
1851
|
+
"",
|
|
1852
|
+
"Steps:",
|
|
1853
|
+
...formatStepRows(steps)
|
|
1854
|
+
] : []
|
|
1855
|
+
];
|
|
1349
1856
|
}
|
|
1350
|
-
function
|
|
1351
|
-
if (
|
|
1352
|
-
return
|
|
1857
|
+
function refWithCommit(pipeline) {
|
|
1858
|
+
if (!pipeline.ref) return pipeline.commit || "-";
|
|
1859
|
+
return pipeline.commit ? `${pipeline.ref} (${pipeline.commit})` : pipeline.ref;
|
|
1353
1860
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const names = new UserNames(client);
|
|
1359
|
-
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
1360
|
-
fetchSpaceKey(client, page.spaceId),
|
|
1361
|
-
fetchAttachments(client, id),
|
|
1362
|
-
fetchComments$1(client, id, names),
|
|
1363
|
-
names.resolve(page.version?.authorId ?? page.authorId)
|
|
1364
|
-
]);
|
|
1365
|
-
const webui = page._links?.webui ?? "";
|
|
1366
|
-
return {
|
|
1367
|
-
id: page.id,
|
|
1368
|
-
title: page.title,
|
|
1369
|
-
spaceKey,
|
|
1370
|
-
version: page.version?.number ?? 0,
|
|
1371
|
-
author,
|
|
1372
|
-
createdAt: page.createdAt ?? "",
|
|
1373
|
-
updatedAt: page.version?.createdAt ?? "",
|
|
1374
|
-
url: webui ? `${site}/wiki${webui}` : "",
|
|
1375
|
-
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
1376
|
-
attachments,
|
|
1377
|
-
comments
|
|
1378
|
-
};
|
|
1861
|
+
function parseBuildNumber(arg) {
|
|
1862
|
+
const raw = (arg ?? "").replace(/^#/, "").trim();
|
|
1863
|
+
if (!/^\d+$/.test(raw)) throw new Error(`Invalid pipeline number "${arg ?? ""}". Expected a build number, e.g. 123.`);
|
|
1864
|
+
return Number.parseInt(raw, 10);
|
|
1379
1865
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1866
|
+
//#endregion
|
|
1867
|
+
//#region src/cli/bitbucket.ts
|
|
1868
|
+
function registerBitbucket(bitbucket) {
|
|
1869
|
+
bitbucket.description("Bitbucket commands");
|
|
1870
|
+
bitbucket.command("login").description("Store Bitbucket workspace and API token").action(bareAction(bitbucketLogin));
|
|
1871
|
+
bitbucket.command("logout").description("Remove stored Bitbucket credentials").action(bareAction(bitbucketLogout));
|
|
1872
|
+
bitbucket.command("status").description("Show the current Bitbucket login").action(bareAction(bitbucketStatus));
|
|
1873
|
+
bitbucket.command("pipelines").description("List recent pipeline runs for a repo").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(bitbucketAction(bitbucketPipelines));
|
|
1874
|
+
bitbucket.command("prs").description("List pull requests for a repo").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("-s, --state <state...>", "open, merged, declined, or superseded").option("--all", "include every state").option("-a, --author <who>", "limit to an author ('me', an account id, or a uuid)").option("--reviewer <who>", "limit to a reviewer ('me', an account id, or a uuid)").option("-q, --query <bbql>", "raw Bitbucket query (not for use with --author or --reviewer)").option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(bitbucketAction(bitbucketPrs));
|
|
1875
|
+
bitbucket.command("pr <id>").description("Show one pull request, its reviewers, files, and comments").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").option("--json", "output the pull request as JSON").action(bitbucketAction(bitbucketPr));
|
|
1876
|
+
bitbucket.command("pipeline <number>").description("Show one pipeline run and its steps").option("-r, --repo <repo>", "workspace/slug, or a bare slug (defaults to config)").action(bitbucketAction(bitbucketPipeline));
|
|
1877
|
+
return bitbucket;
|
|
1387
1878
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
title: params.title,
|
|
1393
|
-
body: {
|
|
1394
|
-
representation: "atlas_doc_format",
|
|
1395
|
-
value: JSON.stringify(params.body)
|
|
1396
|
-
},
|
|
1397
|
-
version: {
|
|
1398
|
-
number: params.nextVersion,
|
|
1399
|
-
message: params.message
|
|
1400
|
-
}
|
|
1401
|
-
})).version?.number ?? params.nextVersion;
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region src/cli/search-options.ts
|
|
1881
|
+
function collect(value, previous = []) {
|
|
1882
|
+
return [...previous, value];
|
|
1402
1883
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1408
|
-
return "";
|
|
1409
|
-
}
|
|
1884
|
+
function moved(flags, command) {
|
|
1885
|
+
const flag = flags.split(" ")[0];
|
|
1886
|
+
return new Option(flags).hideHelp().argParser(() => {
|
|
1887
|
+
throw new Error(`${flag} is now its own command: \`atlass ${command} "<query>"\`.`);
|
|
1888
|
+
});
|
|
1410
1889
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
return Promise.all(res.results.map(async (c) => ({
|
|
1414
|
-
author: await names.resolve(c.version?.authorId),
|
|
1415
|
-
created: c.version?.createdAt ?? "",
|
|
1416
|
-
body: parseAdfJson(c.body?.atlas_doc_format?.value)
|
|
1417
|
-
})));
|
|
1890
|
+
function outputOptions(command) {
|
|
1891
|
+
return command.option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy");
|
|
1418
1892
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1893
|
+
//#endregion
|
|
1894
|
+
//#region src/api/query.ts
|
|
1895
|
+
function quote$1(value) {
|
|
1896
|
+
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1897
|
+
}
|
|
1898
|
+
function values(given) {
|
|
1899
|
+
return (given ?? []).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
1900
|
+
}
|
|
1901
|
+
function inClause(field, given) {
|
|
1902
|
+
return renderClause(field, values(given).map(quote$1));
|
|
1903
|
+
}
|
|
1904
|
+
function userClause(field, given) {
|
|
1905
|
+
return renderClause(field, values(given).map((v) => v === "me" ? "currentUser()" : quote$1(v)));
|
|
1906
|
+
}
|
|
1907
|
+
function renderClause(field, rendered) {
|
|
1908
|
+
if (rendered.length === 0) return null;
|
|
1909
|
+
if (rendered.length === 1) return `${field} = ${rendered[0]}`;
|
|
1910
|
+
return `${field} in (${rendered.join(", ")})`;
|
|
1911
|
+
}
|
|
1912
|
+
function joinClauses(clauses, order) {
|
|
1913
|
+
return `${clauses.filter((c) => c !== null).join(" AND ")} ORDER BY ${order}`;
|
|
1426
1914
|
}
|
|
1427
|
-
var UserNames = class {
|
|
1428
|
-
client;
|
|
1429
|
-
cache = /* @__PURE__ */ new Map();
|
|
1430
|
-
constructor(client) {
|
|
1431
|
-
this.client = client;
|
|
1432
|
-
}
|
|
1433
|
-
async resolve(accountId) {
|
|
1434
|
-
if (!accountId) return "";
|
|
1435
|
-
const cached = this.cache.get(accountId);
|
|
1436
|
-
if (cached !== void 0) return cached;
|
|
1437
|
-
const name = await this.fetchDisplayName(accountId) ?? accountId;
|
|
1438
|
-
this.cache.set(accountId, name);
|
|
1439
|
-
return name;
|
|
1440
|
-
}
|
|
1441
|
-
async fetchDisplayName(accountId) {
|
|
1442
|
-
try {
|
|
1443
|
-
return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
|
|
1444
|
-
} catch {
|
|
1445
|
-
return null;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
};
|
|
1449
1915
|
//#endregion
|
|
1450
1916
|
//#region src/util/html.ts
|
|
1451
1917
|
const MAX_CODE_POINT = 1114111;
|
|
@@ -1472,18 +1938,20 @@ function isCodePoint(code) {
|
|
|
1472
1938
|
//#endregion
|
|
1473
1939
|
//#region src/api/confluence-search.ts
|
|
1474
1940
|
async function searchPages(client, site, params) {
|
|
1475
|
-
|
|
1941
|
+
return searchPagesByCql(client, site, buildCql(params), params.limit);
|
|
1942
|
+
}
|
|
1943
|
+
async function searchPagesByCql(client, site, cql, limit) {
|
|
1476
1944
|
const query = new URLSearchParams({
|
|
1477
1945
|
cql,
|
|
1478
|
-
limit: String(
|
|
1479
|
-
expand: "space"
|
|
1946
|
+
limit: String(limit),
|
|
1947
|
+
expand: "content.space"
|
|
1480
1948
|
});
|
|
1481
1949
|
const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
|
|
1482
|
-
const serverPageWasFull = results.length ===
|
|
1950
|
+
const serverPageWasFull = results.length === limit;
|
|
1483
1951
|
return {
|
|
1484
1952
|
pages: results.filter((r) => r.content?.id).map((r) => ({
|
|
1485
1953
|
id: r.content?.id ?? "",
|
|
1486
|
-
space: r.space?.key ?? r.resultGlobalContainer?.
|
|
1954
|
+
space: r.content?.space?.key ?? spaceKeyOf(r.resultGlobalContainer?.displayUrl),
|
|
1487
1955
|
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
1488
1956
|
updated: r.lastModified ?? "",
|
|
1489
1957
|
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
@@ -1491,374 +1959,362 @@ async function searchPages(client, site, params) {
|
|
|
1491
1959
|
hasMore: serverPageWasFull
|
|
1492
1960
|
};
|
|
1493
1961
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (params.starred) clauses.push("favourite = currentUser()");
|
|
1498
|
-
if (params.space) clauses.push(`space = ${cqlStringLiteral(params.space)}`);
|
|
1499
|
-
if (params.text) clauses.push(`text ~ ${cqlStringLiteral(params.text)}`);
|
|
1500
|
-
return `${clauses.join(" AND ")} ORDER BY lastmodified DESC`;
|
|
1501
|
-
}
|
|
1502
|
-
function cqlStringLiteral(value) {
|
|
1503
|
-
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1962
|
+
const SPACE_URL = /\/spaces\/([^/?#]+)/;
|
|
1963
|
+
function spaceKeyOf(displayUrl) {
|
|
1964
|
+
return SPACE_URL.exec(displayUrl ?? "")?.[1] ?? "";
|
|
1504
1965
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (!ref) throw new Error(kind.notFound(raw));
|
|
1515
|
-
return ref;
|
|
1966
|
+
function buildCql(params) {
|
|
1967
|
+
return joinClauses([
|
|
1968
|
+
"type = page",
|
|
1969
|
+
params.starred ? "favourite = currentUser()" : null,
|
|
1970
|
+
inClause("space", params.space),
|
|
1971
|
+
inClause("label", params.label),
|
|
1972
|
+
params.updatedSince ? `lastmodified >= ${quote$1(params.updatedSince)}` : null,
|
|
1973
|
+
params.text ? `text ~ ${quote$1(params.text)}` : null
|
|
1974
|
+
], "lastmodified DESC");
|
|
1516
1975
|
}
|
|
1517
1976
|
//#endregion
|
|
1518
|
-
//#region src/adf/
|
|
1519
|
-
function
|
|
1520
|
-
const attrs =
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1977
|
+
//#region src/adf/from-markdown.ts
|
|
1978
|
+
function mediaNode(source, alt) {
|
|
1979
|
+
const attrs = { ...source };
|
|
1980
|
+
if (alt) attrs["alt"] = alt;
|
|
1981
|
+
return {
|
|
1982
|
+
type: "mediaSingle",
|
|
1983
|
+
attrs: { layout: "center" },
|
|
1984
|
+
content: [{
|
|
1985
|
+
type: "media",
|
|
1986
|
+
attrs
|
|
1987
|
+
}]
|
|
1988
|
+
};
|
|
1525
1989
|
}
|
|
1526
|
-
function
|
|
1527
|
-
|
|
1990
|
+
function imageHrefs(md) {
|
|
1991
|
+
const hrefs = /* @__PURE__ */ new Set();
|
|
1992
|
+
marked.walkTokens(marked.lexer(md), (token) => {
|
|
1993
|
+
if (token.type === "image") hrefs.add(token.href);
|
|
1994
|
+
});
|
|
1995
|
+
return [...hrefs];
|
|
1528
1996
|
}
|
|
1529
|
-
function
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
if (typeof text === "string" && text.length > 0) return text;
|
|
1537
|
-
const short = node.attrs?.["shortName"];
|
|
1538
|
-
return typeof short === "string" ? short : "";
|
|
1539
|
-
}
|
|
1540
|
-
case "date": return formatDate(node.attrs?.["timestamp"]);
|
|
1541
|
-
case "status": return `\`[${typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : ""}]\``;
|
|
1542
|
-
case "inlineCard": {
|
|
1543
|
-
const url = node.attrs?.["url"];
|
|
1544
|
-
if (typeof url === "string") return `[${url}](${url})`;
|
|
1545
|
-
return "";
|
|
1546
|
-
}
|
|
1547
|
-
case "media": return renderMedia(node, ctx);
|
|
1548
|
-
default: return node.text ?? "";
|
|
1549
|
-
}
|
|
1997
|
+
function markdownToAdf(md, options = {}) {
|
|
1998
|
+
const ctx = { resolveImage: options.resolveImage ?? externalMedia };
|
|
1999
|
+
return {
|
|
2000
|
+
type: "doc",
|
|
2001
|
+
version: 1,
|
|
2002
|
+
content: blocks(marked.lexer(md), ctx)
|
|
2003
|
+
};
|
|
1550
2004
|
}
|
|
1551
|
-
function
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
let href;
|
|
1555
|
-
for (const mark of marks) switch (mark.type) {
|
|
1556
|
-
case "code":
|
|
1557
|
-
out = `\`${out}\``;
|
|
1558
|
-
break;
|
|
1559
|
-
case "strong":
|
|
1560
|
-
out = `**${out}**`;
|
|
1561
|
-
break;
|
|
1562
|
-
case "em":
|
|
1563
|
-
out = `*${out}*`;
|
|
1564
|
-
break;
|
|
1565
|
-
case "strike":
|
|
1566
|
-
out = `~~${out}~~`;
|
|
1567
|
-
break;
|
|
1568
|
-
case "link": {
|
|
1569
|
-
const value = mark.attrs?.["href"];
|
|
1570
|
-
if (typeof value === "string") href = value;
|
|
1571
|
-
break;
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
if (href) out = `[${out}](${href})`;
|
|
2005
|
+
function blocks(tokens, ctx) {
|
|
2006
|
+
const out = [];
|
|
2007
|
+
for (const token of tokens) out.push(...block(token, ctx));
|
|
1575
2008
|
return out;
|
|
1576
2009
|
}
|
|
1577
|
-
function
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
case "
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
return `${"#".repeat(level)} ${renderInline(node.content ?? [], ctx)}`;
|
|
1598
|
-
}
|
|
1599
|
-
case "bulletList": return renderList(node, ctx, indent, "bullet");
|
|
1600
|
-
case "orderedList": return renderList(node, ctx, indent, "ordered");
|
|
1601
|
-
case "taskList": return renderTaskList(node, ctx, indent);
|
|
1602
|
-
case "decisionList": return renderDecisionList(node, ctx, indent);
|
|
1603
|
-
case "codeBlock": return renderCodeBlock(node, indent);
|
|
1604
|
-
case "blockquote": return prefixLines(renderBlocks(node.content ?? [], ctx, ""), `${indent}> `);
|
|
1605
|
-
case "panel": return renderPanel(node, ctx, indent);
|
|
1606
|
-
case "rule": return `${indent}---`;
|
|
1607
|
-
case "table": return renderTable(node, ctx);
|
|
1608
|
-
case "mediaSingle":
|
|
1609
|
-
case "mediaGroup": return renderBlocks(node.content ?? [], ctx, indent);
|
|
1610
|
-
case "media": return indent + renderMedia(node, ctx);
|
|
1611
|
-
case "expand":
|
|
1612
|
-
case "nestedExpand": return renderExpand(node, ctx, indent);
|
|
1613
|
-
default:
|
|
1614
|
-
if (node.content?.length) return renderBlocks(node.content, ctx, indent);
|
|
1615
|
-
return node.text ? indent + node.text : "";
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
function renderList(node, ctx, indent, kind) {
|
|
1619
|
-
const start = kind === "ordered" ? toNumber(node.attrs?.["order"]) ?? 1 : 0;
|
|
1620
|
-
return (node.content ?? []).filter((n) => n.type === "listItem").map((item, i) => {
|
|
1621
|
-
return renderListItem(item, ctx, indent, kind === "ordered" ? `${start + i}. ` : "- ");
|
|
1622
|
-
}).join("\n");
|
|
1623
|
-
}
|
|
1624
|
-
function renderListItem(item, ctx, indent, marker) {
|
|
1625
|
-
const continuationIndent = `${indent}${" ".repeat(marker.length)}`;
|
|
1626
|
-
return (item.content ?? []).map((block, i) => i === 0 ? `${indent}${marker}${renderBlock(block, ctx, "")}` : renderBlock(block, ctx, continuationIndent)).join("\n");
|
|
1627
|
-
}
|
|
1628
|
-
function renderTaskList(node, ctx, indent) {
|
|
1629
|
-
return (node.content ?? []).filter((n) => n.type === "taskItem").map((item) => {
|
|
1630
|
-
return `${indent}- [${item.attrs?.["state"] === "DONE" ? "x" : " "}] ${renderInline(item.content ?? [], ctx)}`;
|
|
1631
|
-
}).join("\n");
|
|
1632
|
-
}
|
|
1633
|
-
function renderDecisionList(node, ctx, indent) {
|
|
1634
|
-
return (node.content ?? []).filter((n) => n.type === "decisionItem").map((item) => `${indent}- (decision) ${renderInline(item.content ?? [], ctx)}`).join("\n");
|
|
2010
|
+
function block(token, ctx) {
|
|
2011
|
+
switch (token.type) {
|
|
2012
|
+
case "space": return [];
|
|
2013
|
+
case "heading": return [{
|
|
2014
|
+
type: "heading",
|
|
2015
|
+
attrs: { level: clampLevel(token.depth) },
|
|
2016
|
+
content: inline(token.tokens ?? [], ctx)
|
|
2017
|
+
}];
|
|
2018
|
+
case "paragraph": return paragraphsSplitAtImages(token.tokens ?? [], ctx);
|
|
2019
|
+
case "text": return paragraphsSplitAtImages(token.tokens ?? [textToken(token.text)], ctx);
|
|
2020
|
+
case "list": return [list(token, ctx)];
|
|
2021
|
+
case "code": return [codeBlock(token)];
|
|
2022
|
+
case "blockquote": return [{
|
|
2023
|
+
type: "blockquote",
|
|
2024
|
+
content: blocks(token.tokens ?? [], ctx)
|
|
2025
|
+
}];
|
|
2026
|
+
case "table": return [table(token, ctx)];
|
|
2027
|
+
case "hr": return [{ type: "rule" }];
|
|
2028
|
+
default: return [];
|
|
2029
|
+
}
|
|
1635
2030
|
}
|
|
1636
|
-
function
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
const
|
|
1640
|
-
|
|
2031
|
+
function paragraphsSplitAtImages(tokens, ctx) {
|
|
2032
|
+
const out = [];
|
|
2033
|
+
let buffer = [];
|
|
2034
|
+
const flush = () => {
|
|
2035
|
+
if (buffer.length === 0) return;
|
|
2036
|
+
const nodes = inline(buffer, ctx);
|
|
2037
|
+
if (nodes.length > 0) out.push({
|
|
2038
|
+
type: "paragraph",
|
|
2039
|
+
content: nodes
|
|
2040
|
+
});
|
|
2041
|
+
buffer = [];
|
|
2042
|
+
};
|
|
2043
|
+
for (const token of tokens) if (token.type === "image") {
|
|
2044
|
+
flush();
|
|
2045
|
+
const image = token;
|
|
2046
|
+
const node = ctx.resolveImage(image.href, image.text ?? "");
|
|
2047
|
+
if (node) out.push(node);
|
|
2048
|
+
} else buffer.push(token);
|
|
2049
|
+
flush();
|
|
2050
|
+
return out;
|
|
1641
2051
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
2052
|
+
function list(token, ctx) {
|
|
2053
|
+
if (token.items.length > 0 && token.items.every((i) => i.task)) return {
|
|
2054
|
+
type: "taskList",
|
|
2055
|
+
attrs: { localId: randomUUID() },
|
|
2056
|
+
content: token.items.map((item) => ({
|
|
2057
|
+
type: "taskItem",
|
|
2058
|
+
attrs: {
|
|
2059
|
+
localId: randomUUID(),
|
|
2060
|
+
state: item.checked ? "DONE" : "TODO"
|
|
2061
|
+
},
|
|
2062
|
+
content: inline(taskItemInline(item), ctx)
|
|
2063
|
+
}))
|
|
2064
|
+
};
|
|
2065
|
+
const node = {
|
|
2066
|
+
type: token.ordered ? "orderedList" : "bulletList",
|
|
2067
|
+
content: token.items.map((item) => listItem(item, ctx))
|
|
2068
|
+
};
|
|
2069
|
+
const start = Number(token.start);
|
|
2070
|
+
if (token.ordered && Number.isFinite(start) && start !== 1) node.attrs = { order: start };
|
|
2071
|
+
return node;
|
|
1652
2072
|
}
|
|
1653
|
-
function
|
|
1654
|
-
|
|
2073
|
+
function listItem(item, ctx) {
|
|
2074
|
+
const children = blocks(item.tokens.filter((t) => t.type !== "checkbox"), ctx);
|
|
2075
|
+
return {
|
|
2076
|
+
type: "listItem",
|
|
2077
|
+
content: children.length > 0 ? children : [emptyParagraph()]
|
|
2078
|
+
};
|
|
1655
2079
|
}
|
|
1656
|
-
function
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const cols = Math.max(...grid.map((r) => r.length));
|
|
1661
|
-
const pad = (r) => {
|
|
1662
|
-
const copy = [...r];
|
|
1663
|
-
while (copy.length < cols) copy.push("");
|
|
1664
|
-
return copy;
|
|
2080
|
+
function emptyParagraph() {
|
|
2081
|
+
return {
|
|
2082
|
+
type: "paragraph",
|
|
2083
|
+
content: []
|
|
1665
2084
|
};
|
|
1666
|
-
const header = pad(grid[0] ?? []);
|
|
1667
|
-
const lines = [`| ${header.join(" | ")} |`, `| ${header.map(() => "---").join(" | ")} |`];
|
|
1668
|
-
for (const row of grid.slice(1)) lines.push(`| ${pad(row).join(" | ")} |`);
|
|
1669
|
-
return lines.join("\n");
|
|
1670
2085
|
}
|
|
1671
|
-
function
|
|
1672
|
-
|
|
2086
|
+
function taskItemInline(item) {
|
|
2087
|
+
const first = item.tokens.find((t) => t.type === "text");
|
|
2088
|
+
if (first && "tokens" in first && first.tokens) return first.tokens;
|
|
2089
|
+
return [textToken(item.text)];
|
|
1673
2090
|
}
|
|
1674
|
-
function
|
|
1675
|
-
return
|
|
2091
|
+
function codeBlock(token) {
|
|
2092
|
+
return {
|
|
2093
|
+
type: "codeBlock",
|
|
2094
|
+
attrs: token.lang ? { language: token.lang } : {},
|
|
2095
|
+
content: token.text.length > 0 ? [{
|
|
2096
|
+
type: "text",
|
|
2097
|
+
text: token.text
|
|
2098
|
+
}] : []
|
|
2099
|
+
};
|
|
1676
2100
|
}
|
|
1677
|
-
function
|
|
1678
|
-
|
|
1679
|
-
|
|
2101
|
+
function table(token, ctx) {
|
|
2102
|
+
return {
|
|
2103
|
+
type: "table",
|
|
2104
|
+
content: [{
|
|
2105
|
+
type: "tableRow",
|
|
2106
|
+
content: token.header.map((cell) => tableCell(cell, "tableHeader", ctx))
|
|
2107
|
+
}, ...token.rows.map((row) => ({
|
|
2108
|
+
type: "tableRow",
|
|
2109
|
+
content: row.map((cell) => tableCell(cell, "tableCell", ctx))
|
|
2110
|
+
}))]
|
|
2111
|
+
};
|
|
1680
2112
|
}
|
|
1681
|
-
function
|
|
1682
|
-
return
|
|
2113
|
+
function tableCell(cell, type, ctx) {
|
|
2114
|
+
return {
|
|
2115
|
+
type,
|
|
2116
|
+
content: [{
|
|
2117
|
+
type: "paragraph",
|
|
2118
|
+
content: inline(cell.tokens, ctx)
|
|
2119
|
+
}]
|
|
2120
|
+
};
|
|
1683
2121
|
}
|
|
1684
|
-
|
|
1685
|
-
//#region src/markdown/highlight.ts
|
|
1686
|
-
const HEADING = /^#{1,6} /;
|
|
1687
|
-
const QUOTE = /^(\s*)>( |$)/;
|
|
1688
|
-
const LIST = /^(\s*)(-|\d+\.) (\[[ x]\] )?/;
|
|
1689
|
-
const RULE = /^\s*---$/;
|
|
1690
|
-
const TABLE_ROW = /^\|.*\|$/;
|
|
1691
|
-
const TABLE_SEPARATOR = /^\|( --- \|)+$/;
|
|
1692
|
-
const CELL_DIVIDER = /(?<!\\)\|/;
|
|
1693
|
-
const DETAILS_OPEN = /^(\s*)<details><summary>(.*)<\/summary>$/;
|
|
1694
|
-
const DETAILS_CLOSE = /^\s*<\/details>$/;
|
|
1695
|
-
const FENCE_OPEN = /^((?:\s|> |- |\d+\. )*)```(\S*)$/;
|
|
1696
|
-
const FENCE = "```";
|
|
1697
|
-
const INLINE = /(`[^`]+`)|\[([^\]]+)\]\(((?:[^()]|\([^()]*\))+)\)|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(?!\*)(.+?)(?<!\*)\*(?!\*)|~~(.+?)~~/g;
|
|
1698
|
-
const TOKEN_STYLES = {
|
|
1699
|
-
keyword: kleur.magenta,
|
|
1700
|
-
built_in: kleur.magenta,
|
|
1701
|
-
literal: kleur.magenta,
|
|
1702
|
-
string: kleur.green,
|
|
1703
|
-
regexp: kleur.green,
|
|
1704
|
-
addition: kleur.green,
|
|
1705
|
-
number: kleur.yellow,
|
|
1706
|
-
symbol: kleur.yellow,
|
|
1707
|
-
meta: kleur.yellow,
|
|
1708
|
-
attr: kleur.yellow,
|
|
1709
|
-
comment: kleur.dim,
|
|
1710
|
-
quote: kleur.dim,
|
|
1711
|
-
deletion: kleur.dim,
|
|
1712
|
-
title: kleur.cyan,
|
|
1713
|
-
type: kleur.cyan,
|
|
1714
|
-
section: kleur.cyan
|
|
1715
|
-
};
|
|
1716
|
-
const lowlight = createLowlight(common);
|
|
1717
|
-
function highlightMarkdown(md) {
|
|
1718
|
-
const lines = md.split("\n");
|
|
2122
|
+
function inline(tokens, ctx, marks = []) {
|
|
1719
2123
|
const out = [];
|
|
1720
|
-
for (
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
const code = [];
|
|
1731
|
-
const prefixed = [];
|
|
1732
|
-
let j = i + 1;
|
|
1733
|
-
for (; j < lines.length; j++) {
|
|
1734
|
-
const raw = lines[j] ?? "";
|
|
1735
|
-
const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
|
|
1736
|
-
const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
|
|
1737
|
-
if (body === FENCE) break;
|
|
1738
|
-
code.push(body);
|
|
1739
|
-
prefixed.push(hasPrefix);
|
|
2124
|
+
for (const token of tokens) out.push(...inlineNode(token, ctx, marks));
|
|
2125
|
+
return out;
|
|
2126
|
+
}
|
|
2127
|
+
function inlineNode(token, ctx, marks) {
|
|
2128
|
+
switch (token.type) {
|
|
2129
|
+
case "text":
|
|
2130
|
+
case "escape": {
|
|
2131
|
+
const t = token;
|
|
2132
|
+
if ("tokens" in t && t.tokens?.length) return inline(t.tokens, ctx, marks);
|
|
2133
|
+
return textNode(t.text, marks);
|
|
1740
2134
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
});
|
|
1745
|
-
|
|
1746
|
-
const
|
|
1747
|
-
|
|
2135
|
+
case "strong": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strong" }));
|
|
2136
|
+
case "em": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "em" }));
|
|
2137
|
+
case "del": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strike" }));
|
|
2138
|
+
case "codespan": return textNode(token.text, withUniqueMark(marks, { type: "code" }));
|
|
2139
|
+
case "link": {
|
|
2140
|
+
const link = token;
|
|
2141
|
+
return inline(link.tokens, ctx, withUniqueMark(marks, {
|
|
2142
|
+
type: "link",
|
|
2143
|
+
attrs: { href: link.href }
|
|
2144
|
+
}));
|
|
1748
2145
|
}
|
|
1749
|
-
|
|
2146
|
+
case "br": return [{ type: "hardBreak" }];
|
|
2147
|
+
case "html": return textNode(token.text, marks);
|
|
2148
|
+
default: return "text" in token && token.text ? textNode(token.text, marks) : [];
|
|
1750
2149
|
}
|
|
1751
|
-
return out.join("\n");
|
|
1752
2150
|
}
|
|
1753
|
-
function
|
|
1754
|
-
if (
|
|
1755
|
-
|
|
2151
|
+
function textNode(text, marks) {
|
|
2152
|
+
if (text.length === 0) return [];
|
|
2153
|
+
const node = {
|
|
2154
|
+
type: "text",
|
|
2155
|
+
text
|
|
2156
|
+
};
|
|
2157
|
+
if (marks.length > 0) node.marks = marks;
|
|
2158
|
+
return [node];
|
|
1756
2159
|
}
|
|
1757
|
-
function
|
|
1758
|
-
|
|
1759
|
-
if (node.type !== "root" && node.type !== "element") return "";
|
|
1760
|
-
const own = node.type === "element" ? tokenStyle(node.properties["className"]) : void 0;
|
|
1761
|
-
const next = own ? (text) => style(own(text)) : style;
|
|
1762
|
-
return node.children.map((child) => renderTree(child, next)).join("");
|
|
2160
|
+
function withUniqueMark(marks, mark) {
|
|
2161
|
+
return [...marks.filter((m) => m.type !== mark.type), mark];
|
|
1763
2162
|
}
|
|
1764
|
-
function
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
if (style) return style;
|
|
1770
|
-
}
|
|
2163
|
+
function externalMedia(href, alt) {
|
|
2164
|
+
return mediaNode({
|
|
2165
|
+
type: "external",
|
|
2166
|
+
url: href
|
|
2167
|
+
}, alt);
|
|
1771
2168
|
}
|
|
1772
|
-
function
|
|
1773
|
-
return
|
|
2169
|
+
function textToken(text) {
|
|
2170
|
+
return {
|
|
2171
|
+
type: "text",
|
|
2172
|
+
raw: text,
|
|
2173
|
+
text,
|
|
2174
|
+
escaped: false
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function clampLevel(value) {
|
|
2178
|
+
return Math.min(6, Math.max(1, Math.trunc(value) || 1));
|
|
1774
2179
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
if (list) {
|
|
1785
|
-
const [whole, indent = "", marker = "", checkbox] = list;
|
|
1786
|
-
const rest = line.slice(whole.length);
|
|
1787
|
-
const box = checkbox ? `${kleur.bold(checkbox.trim())} ` : "";
|
|
1788
|
-
return `${indent}${kleur.dim(marker)} ${box}${highlightLine(rest, void 0)}`;
|
|
1789
|
-
}
|
|
1790
|
-
if (HEADING.test(line)) return kleur.bold().cyan(line);
|
|
1791
|
-
if (RULE.test(line)) return kleur.dim(line);
|
|
1792
|
-
if (TABLE_SEPARATOR.test(line)) return kleur.dim(line);
|
|
1793
|
-
if (TABLE_ROW.test(line)) return tableRow(line, nextLine !== void 0 && TABLE_SEPARATOR.test(nextLine));
|
|
1794
|
-
const details = line.match(DETAILS_OPEN);
|
|
1795
|
-
if (details) {
|
|
1796
|
-
const [, indent = "", title = ""] = details;
|
|
1797
|
-
return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
|
|
1798
|
-
}
|
|
1799
|
-
if (DETAILS_CLOSE.test(line)) return kleur.dim(line);
|
|
1800
|
-
return highlightInline(line);
|
|
2180
|
+
//#endregion
|
|
2181
|
+
//#region src/api/confluence-attachments.ts
|
|
2182
|
+
const UNKNOWN_SIZE = -1;
|
|
2183
|
+
async function listAttachments(client, id) {
|
|
2184
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.map((a) => ({
|
|
2185
|
+
filename: a.title ?? a.id,
|
|
2186
|
+
fileId: mediaNodeId(a),
|
|
2187
|
+
size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
|
|
2188
|
+
}));
|
|
1801
2189
|
}
|
|
1802
|
-
function
|
|
1803
|
-
return
|
|
2190
|
+
async function uploadAttachment(client, pageId, filename, bytes) {
|
|
2191
|
+
return (await client.postMultipart(`/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment`, filename, bytes)).results?.[0]?.extensions?.fileId ?? await fileIdByListing(client, pageId, filename);
|
|
1804
2192
|
}
|
|
1805
|
-
function
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
2193
|
+
async function fileIdByListing(client, pageId, filename) {
|
|
2194
|
+
const match = (await listAttachments(client, pageId)).find((a) => a.filename === filename);
|
|
2195
|
+
if (match) return match.fileId;
|
|
2196
|
+
throw new Error(`Upload of "${filename}" did not return a fileId.`);
|
|
1809
2197
|
}
|
|
1810
|
-
function
|
|
1811
|
-
return
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
|
|
1817
|
-
if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
|
|
1818
|
-
return match;
|
|
1819
|
-
});
|
|
2198
|
+
async function fetchAttachments(client, id) {
|
|
2199
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.filter((a) => a.downloadLink).map((a) => ({
|
|
2200
|
+
mediaId: mediaNodeId(a),
|
|
2201
|
+
filename: a.title ?? a.id,
|
|
2202
|
+
url: withWikiContextPath(a.downloadLink ?? "")
|
|
2203
|
+
}));
|
|
1820
2204
|
}
|
|
1821
|
-
function
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2205
|
+
function mediaNodeId(a) {
|
|
2206
|
+
return a.fileId ?? a.id;
|
|
2207
|
+
}
|
|
2208
|
+
function withWikiContextPath(link) {
|
|
2209
|
+
if (link.startsWith("http") || link.startsWith("/wiki")) return link;
|
|
2210
|
+
return `/wiki${link}`;
|
|
1825
2211
|
}
|
|
1826
2212
|
//#endregion
|
|
1827
|
-
//#region src/
|
|
1828
|
-
function
|
|
1829
|
-
const
|
|
1830
|
-
const
|
|
1831
|
-
|
|
2213
|
+
//#region src/api/confluence-pages.ts
|
|
2214
|
+
async function fetchPage(client, site, id) {
|
|
2215
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2216
|
+
const names = new UserNames(client);
|
|
2217
|
+
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
2218
|
+
fetchSpaceKey(client, page.spaceId),
|
|
2219
|
+
fetchAttachments(client, id),
|
|
2220
|
+
fetchComments$1(client, id, names),
|
|
2221
|
+
names.resolve(page.version?.authorId ?? page.authorId)
|
|
2222
|
+
]);
|
|
2223
|
+
const webui = page._links?.webui ?? "";
|
|
2224
|
+
return {
|
|
2225
|
+
id: page.id,
|
|
2226
|
+
title: page.title,
|
|
2227
|
+
spaceKey,
|
|
2228
|
+
version: page.version?.number ?? 0,
|
|
2229
|
+
author,
|
|
2230
|
+
createdAt: page.createdAt ?? "",
|
|
2231
|
+
updatedAt: page.version?.createdAt ?? "",
|
|
2232
|
+
url: webui ? `${site}/wiki${webui}` : "",
|
|
2233
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
2234
|
+
attachments,
|
|
2235
|
+
comments
|
|
2236
|
+
};
|
|
1832
2237
|
}
|
|
1833
|
-
function
|
|
1834
|
-
|
|
1835
|
-
return
|
|
2238
|
+
async function fetchPageState(client, id) {
|
|
2239
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2240
|
+
return {
|
|
2241
|
+
version: page.version?.number ?? 0,
|
|
2242
|
+
title: page.title,
|
|
2243
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value)
|
|
2244
|
+
};
|
|
1836
2245
|
}
|
|
1837
|
-
function
|
|
1838
|
-
|
|
1839
|
-
|
|
2246
|
+
async function updatePage(client, id, params) {
|
|
2247
|
+
return (await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
|
|
2248
|
+
id,
|
|
2249
|
+
status: "current",
|
|
2250
|
+
title: params.title,
|
|
2251
|
+
body: {
|
|
2252
|
+
representation: "atlas_doc_format",
|
|
2253
|
+
value: JSON.stringify(params.body)
|
|
2254
|
+
},
|
|
2255
|
+
version: {
|
|
2256
|
+
number: params.nextVersion,
|
|
2257
|
+
message: params.message
|
|
2258
|
+
}
|
|
2259
|
+
})).version?.number ?? params.nextVersion;
|
|
1840
2260
|
}
|
|
1841
|
-
function
|
|
1842
|
-
if (
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const body = adfToMarkdown(comment.body);
|
|
1848
|
-
lines.push("", commentHeader(comment), ...body ? [highlightMarkdown(body)] : []);
|
|
2261
|
+
async function fetchSpaceKey(client, spaceId) {
|
|
2262
|
+
if (!spaceId) return "";
|
|
2263
|
+
try {
|
|
2264
|
+
return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
|
|
2265
|
+
} catch {
|
|
2266
|
+
return "";
|
|
1849
2267
|
}
|
|
1850
|
-
return lines;
|
|
1851
2268
|
}
|
|
1852
|
-
function
|
|
1853
|
-
|
|
2269
|
+
async function fetchComments$1(client, id, names) {
|
|
2270
|
+
const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
|
|
2271
|
+
return Promise.all(res.results.map(async (c) => ({
|
|
2272
|
+
author: await names.resolve(c.version?.authorId),
|
|
2273
|
+
created: c.version?.createdAt ?? "",
|
|
2274
|
+
body: parseAdfJson(c.body?.atlas_doc_format?.value)
|
|
2275
|
+
})));
|
|
1854
2276
|
}
|
|
1855
|
-
function
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2277
|
+
function parseAdfJson(value) {
|
|
2278
|
+
if (!value) return null;
|
|
2279
|
+
try {
|
|
2280
|
+
return JSON.parse(value);
|
|
2281
|
+
} catch {
|
|
2282
|
+
return null;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
var UserNames = class {
|
|
2286
|
+
client;
|
|
2287
|
+
cache = /* @__PURE__ */ new Map();
|
|
2288
|
+
constructor(client) {
|
|
2289
|
+
this.client = client;
|
|
2290
|
+
}
|
|
2291
|
+
async resolve(accountId) {
|
|
2292
|
+
if (!accountId) return "";
|
|
2293
|
+
const cached = this.cache.get(accountId);
|
|
2294
|
+
if (cached !== void 0) return cached;
|
|
2295
|
+
const name = await this.fetchDisplayName(accountId) ?? accountId;
|
|
2296
|
+
this.cache.set(accountId, name);
|
|
2297
|
+
return name;
|
|
2298
|
+
}
|
|
2299
|
+
async fetchDisplayName(accountId) {
|
|
2300
|
+
try {
|
|
2301
|
+
return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
|
|
2302
|
+
} catch {
|
|
2303
|
+
return null;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
//#endregion
|
|
2308
|
+
//#region src/commands/resolve-ref.ts
|
|
2309
|
+
async function resolveRef(ask, arg, kind) {
|
|
2310
|
+
const raw = arg ?? await ask.text({
|
|
2311
|
+
message: kind.message,
|
|
2312
|
+
flag: kind.flag,
|
|
2313
|
+
required: true
|
|
2314
|
+
});
|
|
2315
|
+
const ref = kind.parse(raw);
|
|
2316
|
+
if (!ref) throw new Error(kind.notFound(raw));
|
|
2317
|
+
return ref;
|
|
1862
2318
|
}
|
|
1863
2319
|
//#endregion
|
|
1864
2320
|
//#region src/api/attachments.ts
|
|
@@ -2376,7 +2832,7 @@ function formatPageView(page, nowMs, allComments) {
|
|
|
2376
2832
|
["URL", page.url]
|
|
2377
2833
|
]),
|
|
2378
2834
|
...bodyLines(page.body),
|
|
2379
|
-
...commentSection(page.comments, allComments),
|
|
2835
|
+
...commentSection(renderedComments(page.comments), { allComments }),
|
|
2380
2836
|
...attachmentSection(page.attachments)
|
|
2381
2837
|
];
|
|
2382
2838
|
}
|
|
@@ -2427,35 +2883,52 @@ async function fileSize(files, path) {
|
|
|
2427
2883
|
return {};
|
|
2428
2884
|
}
|
|
2429
2885
|
}
|
|
2886
|
+
async function copyPage(env, id, out) {
|
|
2887
|
+
env.term.err(`Fetching page ${id} ...`);
|
|
2888
|
+
await runCopy(env, planPageCopy(await fetchPage(env.session, env.session.site, id), out));
|
|
2889
|
+
}
|
|
2890
|
+
const PAGE_REF = {
|
|
2891
|
+
message: "Confluence page id or URL:",
|
|
2892
|
+
flag: "[page]",
|
|
2893
|
+
parse: parsePageId,
|
|
2894
|
+
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2895
|
+
};
|
|
2896
|
+
//#endregion
|
|
2897
|
+
//#region src/commands/confluence-search.ts
|
|
2430
2898
|
async function confluenceSearch(env, query, options) {
|
|
2431
|
-
if (options.cql && (query || options.space)) throw new Error("--cql cannot be combined with a text query or --space.");
|
|
2432
2899
|
await listPages(env, {
|
|
2433
2900
|
text: query,
|
|
2434
2901
|
space: options.space,
|
|
2435
|
-
|
|
2902
|
+
label: options.label,
|
|
2903
|
+
starred: options.starred,
|
|
2904
|
+
updatedSince: options.updated ? parseSince(options.updated, Date.now()) : void 0
|
|
2436
2905
|
}, "No matching pages.", options);
|
|
2437
2906
|
}
|
|
2438
2907
|
async function confluenceList(env, options) {
|
|
2439
2908
|
await listPages(env, {
|
|
2440
2909
|
starred: true,
|
|
2441
|
-
space: options.space
|
|
2910
|
+
space: options.space ? [options.space] : void 0
|
|
2442
2911
|
}, options.space ? `No starred pages in ${options.space}.` : "No starred pages.", options);
|
|
2443
2912
|
}
|
|
2913
|
+
async function confluenceCql(env, query, options) {
|
|
2914
|
+
const limit = checkedLimit(options);
|
|
2915
|
+
const { pages, hasMore } = await searchPagesByCql(env.session, env.session.site, query, limit);
|
|
2916
|
+
await showPages(env, pages, hasMore, limit, options);
|
|
2917
|
+
}
|
|
2444
2918
|
async function listPages(env, filter, empty, options) {
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
const limit = parseLimit(options.limit);
|
|
2448
|
-
const { pages, hasMore } = await searchPages(session, session.site, {
|
|
2919
|
+
const limit = checkedLimit(options);
|
|
2920
|
+
const { pages, hasMore } = await searchPages(env.session, env.session.site, {
|
|
2449
2921
|
...filter,
|
|
2450
2922
|
limit
|
|
2451
2923
|
});
|
|
2452
|
-
await
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2924
|
+
await showPages(env, pages, hasMore, limit, options, empty);
|
|
2925
|
+
}
|
|
2926
|
+
async function showPages(env, pages, hasMore, limit, options, empty = "No matching pages.") {
|
|
2927
|
+
await showRows(env.term, formatPageRows(pages, Date.now()), {
|
|
2456
2928
|
empty,
|
|
2457
|
-
|
|
2458
|
-
|
|
2929
|
+
hasMore,
|
|
2930
|
+
limit
|
|
2931
|
+
}, options, PAGE_NOUN, (id) => copyPage(env, id, options.out));
|
|
2459
2932
|
}
|
|
2460
2933
|
function formatPageRows(pages, nowMs) {
|
|
2461
2934
|
return alignedRows(pages, nowMs, (p) => ({
|
|
@@ -2483,16 +2956,6 @@ const PAGE_NOUN = {
|
|
|
2483
2956
|
singular: "page",
|
|
2484
2957
|
plural: "pages"
|
|
2485
2958
|
};
|
|
2486
|
-
async function copyPage(env, id, out) {
|
|
2487
|
-
env.term.err(`Fetching page ${id} ...`);
|
|
2488
|
-
await runCopy(env, planPageCopy(await fetchPage(env.session, env.session.site, id), out));
|
|
2489
|
-
}
|
|
2490
|
-
const PAGE_REF = {
|
|
2491
|
-
message: "Confluence page id or URL:",
|
|
2492
|
-
flag: "[page]",
|
|
2493
|
-
parse: parsePageId,
|
|
2494
|
-
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2495
|
-
};
|
|
2496
2959
|
//#endregion
|
|
2497
2960
|
//#region src/cli/confluence.ts
|
|
2498
2961
|
function registerConfluence(confluence) {
|
|
@@ -2500,8 +2963,9 @@ function registerConfluence(confluence) {
|
|
|
2500
2963
|
confluence.command("view [page]").description("Show a Confluence page (id or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(confluenceView));
|
|
2501
2964
|
confluence.command("copy [page]").description("Copy a Confluence page (id or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(confluenceCopy));
|
|
2502
2965
|
confluence.command("update [file]").description("Update a Confluence page from an edited Markdown file").option("--title", "also push the H1 as the page title").option("-m, --message <text>", "version message (default 'Updated via atlass')").option("-f, --force", "skip the stale-version and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(confluenceUpdate));
|
|
2503
|
-
confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space")
|
|
2504
|
-
confluence.command("search [query]").description("Search Confluence pages
|
|
2966
|
+
outputOptions(confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space")).action(jiraAction(confluenceList));
|
|
2967
|
+
outputOptions(confluence.command("search [query]").description("Search Confluence pages by text and filters").option("-s, --space <key>", "limit to a space (repeatable)", collect).option("--label <label>", "limit to a label (repeatable)", collect).option("--starred", "limit to pages you starred").option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").addOption(moved("--cql <cql>", "confluence cql"))).action(jiraAction(confluenceSearch));
|
|
2968
|
+
outputOptions(confluence.command("cql <query>").description("Search Confluence pages with a raw CQL query")).action(jiraAction(confluenceCql));
|
|
2505
2969
|
return confluence;
|
|
2506
2970
|
}
|
|
2507
2971
|
//#endregion
|
|
@@ -2570,6 +3034,24 @@ async function searchAssignableUsers(client, project, query) {
|
|
|
2570
3034
|
});
|
|
2571
3035
|
return (await client.getJson(`/rest/api/3/user/assignable/search?${params.toString()}`)).map(toJiraUser);
|
|
2572
3036
|
}
|
|
3037
|
+
const ACCOUNT_ID = /^(?:[0-9a-z]{24}|[a-z0-9]+:[0-9a-f-]{36}(?::[0-9a-f-]{36})?)$/i;
|
|
3038
|
+
async function searchUsers(client, query) {
|
|
3039
|
+
const params = new URLSearchParams({
|
|
3040
|
+
query,
|
|
3041
|
+
maxResults: "20"
|
|
3042
|
+
});
|
|
3043
|
+
return (await client.getJson(`/rest/api/3/user/search?${params.toString()}`)).map(toJiraUser);
|
|
3044
|
+
}
|
|
3045
|
+
function matchUser(users, query, noun) {
|
|
3046
|
+
const active = users.filter((u) => u.active);
|
|
3047
|
+
const needle = query.toLowerCase();
|
|
3048
|
+
const exact = active.filter((u) => u.displayName.toLowerCase() === needle || u.email.toLowerCase() === needle);
|
|
3049
|
+
const candidates = exact.length === 1 ? exact : active;
|
|
3050
|
+
if (candidates.length === 1) return candidates[0].accountId;
|
|
3051
|
+
if (candidates.length === 0) throw new Error(`no ${noun} matches "${query}".`);
|
|
3052
|
+
const names = candidates.map((u) => u.displayName).join(", ");
|
|
3053
|
+
throw new Error(`"${query}" matches ${candidates.length} users: ${names}.`);
|
|
3054
|
+
}
|
|
2573
3055
|
async function fetchMyself(client) {
|
|
2574
3056
|
return toJiraUser(await client.getJson("/rest/api/3/myself"));
|
|
2575
3057
|
}
|
|
@@ -2647,18 +3129,11 @@ function matchType(types, arg) {
|
|
|
2647
3129
|
const needle = arg.trim().toLowerCase();
|
|
2648
3130
|
return types.find((t) => t.id === arg.trim()) ?? types.find((t) => t.name.toLowerCase() === needle);
|
|
2649
3131
|
}
|
|
2650
|
-
const ACCOUNT_ID = /^(?:[0-9a-f]{24}|[a-z0-9]+:[0-9a-f-]{36}(?::[0-9a-f-]{36})?)$/i;
|
|
2651
3132
|
function userResolver(client, project) {
|
|
2652
3133
|
return async (query) => {
|
|
2653
3134
|
if (query === "me") return (await fetchMyself(client)).accountId;
|
|
2654
3135
|
if (ACCOUNT_ID.test(query)) return query;
|
|
2655
|
-
|
|
2656
|
-
const exact = users.filter((u) => u.displayName.toLowerCase() === query.toLowerCase() || u.email.toLowerCase() === query.toLowerCase());
|
|
2657
|
-
const candidates = exact.length === 1 ? exact : users;
|
|
2658
|
-
if (candidates.length === 1) return candidates[0].accountId;
|
|
2659
|
-
if (candidates.length === 0) throw new Error(`no assignable user matches "${query}".`);
|
|
2660
|
-
const names = candidates.map((u) => u.displayName).join(", ");
|
|
2661
|
-
throw new Error(`"${query}" matches ${candidates.length} users: ${names}.`);
|
|
3136
|
+
return matchUser(await searchAssignableUsers(client, project, query), query, "assignable user");
|
|
2662
3137
|
};
|
|
2663
3138
|
}
|
|
2664
3139
|
//#endregion
|
|
@@ -3272,9 +3747,13 @@ async function jiraFields({ session, term }, projectArg, typeArg, options) {
|
|
|
3272
3747
|
//#endregion
|
|
3273
3748
|
//#region src/api/jira-search.ts
|
|
3274
3749
|
const RECENT_ISSUES_CLAUSE = "updated >= -30d";
|
|
3750
|
+
const OPEN_CLAUSE = "statusCategory != Done";
|
|
3275
3751
|
const SEARCH_FIELDS = "summary,status,updated";
|
|
3276
3752
|
async function searchIssues(client, site, params) {
|
|
3277
|
-
return (
|
|
3753
|
+
return searchIssuesByJql(client, site, buildJql(params), params.limit);
|
|
3754
|
+
}
|
|
3755
|
+
async function searchIssuesByJql(client, site, jql, limit) {
|
|
3756
|
+
return ((await fetchSearchPage(client, jql, limit)).issues ?? []).map((i) => toIssueSummary(site, i));
|
|
3278
3757
|
}
|
|
3279
3758
|
async function fetchSearchPage(client, jql, maxResults, nextPageToken) {
|
|
3280
3759
|
const query = new URLSearchParams({
|
|
@@ -3295,22 +3774,28 @@ function toIssueSummary(site, i) {
|
|
|
3295
3774
|
url: browseUrl(site, i.key)
|
|
3296
3775
|
};
|
|
3297
3776
|
}
|
|
3777
|
+
const ISSUE_ORDER = "updated DESC";
|
|
3298
3778
|
function buildJql(params) {
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3779
|
+
const clauses = [
|
|
3780
|
+
inClause("project", params.project),
|
|
3781
|
+
inClause("type", params.type),
|
|
3782
|
+
inClause("status", params.status),
|
|
3783
|
+
params.open ? OPEN_CLAUSE : null,
|
|
3784
|
+
userClause("assignee", params.assignee),
|
|
3785
|
+
userClause("reporter", params.reporter),
|
|
3786
|
+
inClause("labels", params.label),
|
|
3787
|
+
params.updatedSince ? `updated >= ${quote$1(params.updatedSince)}` : null,
|
|
3788
|
+
params.text ? `text ~ ${quote$1(params.text)}` : null
|
|
3789
|
+
].filter((c) => c !== null);
|
|
3305
3790
|
if (clauses.length === 0) clauses.push(RECENT_ISSUES_CLAUSE);
|
|
3306
|
-
return
|
|
3791
|
+
return joinClauses(clauses, ISSUE_ORDER);
|
|
3307
3792
|
}
|
|
3308
|
-
const OPEN_CLAUSE = "statusCategory != Done";
|
|
3309
3793
|
function buildListJql(params) {
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3794
|
+
return joinClauses([
|
|
3795
|
+
"assignee = currentUser()",
|
|
3796
|
+
params.project ? `project = ${quote$1(params.project)}` : null,
|
|
3797
|
+
params.all ? `(${OPEN_CLAUSE} OR ${RECENT_ISSUES_CLAUSE})` : OPEN_CLAUSE
|
|
3798
|
+
], ISSUE_ORDER);
|
|
3314
3799
|
}
|
|
3315
3800
|
const LIST_PAGE_SIZE = 100;
|
|
3316
3801
|
const LIST_CAP = 500;
|
|
@@ -3345,8 +3830,59 @@ function sortByCategoryThenUpdated(issues) {
|
|
|
3345
3830
|
const updatedMs = (i) => Date.parse(i.updated) || 0;
|
|
3346
3831
|
return [...issues].sort((a, b) => rank(a) - rank(b) || updatedMs(b) - updatedMs(a));
|
|
3347
3832
|
}
|
|
3348
|
-
|
|
3349
|
-
|
|
3833
|
+
//#endregion
|
|
3834
|
+
//#region src/commands/jira-filters.ts
|
|
3835
|
+
const FILTER_FLAGS = [
|
|
3836
|
+
"project",
|
|
3837
|
+
"assignee",
|
|
3838
|
+
"reporter",
|
|
3839
|
+
"status",
|
|
3840
|
+
"type",
|
|
3841
|
+
"label"
|
|
3842
|
+
];
|
|
3843
|
+
const ISSUE_KEY = /^[A-Z][A-Z0-9]+-\d+$/i;
|
|
3844
|
+
function hasFilters(filters) {
|
|
3845
|
+
return FILTER_FLAGS.some((flag) => values(filters[flag]).length > 0) || filters.updated !== void 0 || filters.open === true;
|
|
3846
|
+
}
|
|
3847
|
+
function checkQuery(query, filters) {
|
|
3848
|
+
if (query && ISSUE_KEY.test(query.trim())) throw new Error(`"${query}" looks like an issue key. Run \`jira view ${query}\` to open it.`);
|
|
3849
|
+
if (!query && !hasFilters(filters)) throw new Error("Give a text query or at least one filter. `jira list` shows your open issues.");
|
|
3850
|
+
}
|
|
3851
|
+
async function searchParams(client, query, filters, limit, nowMs) {
|
|
3852
|
+
return {
|
|
3853
|
+
text: query,
|
|
3854
|
+
project: filters.project,
|
|
3855
|
+
assignee: await resolveUsers(client, filters.assignee),
|
|
3856
|
+
reporter: await resolveUsers(client, filters.reporter),
|
|
3857
|
+
status: filters.status,
|
|
3858
|
+
type: filters.type,
|
|
3859
|
+
label: filters.label,
|
|
3860
|
+
updatedSince: filters.updated ? parseSince(filters.updated, nowMs) : void 0,
|
|
3861
|
+
open: filters.open,
|
|
3862
|
+
limit
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
async function resolveUsers(client, given) {
|
|
3866
|
+
const list = values(given);
|
|
3867
|
+
if (list.length === 0) return void 0;
|
|
3868
|
+
const resolved = [];
|
|
3869
|
+
for (const who of list) if (who === "me" || ACCOUNT_ID.test(who)) resolved.push(who);
|
|
3870
|
+
else resolved.push(matchUser(await searchUsers(client, who), who, "user"));
|
|
3871
|
+
return resolved;
|
|
3872
|
+
}
|
|
3873
|
+
const FIELD_IN_MESSAGE = /for the field '([^']+)'/;
|
|
3874
|
+
const createForm = (project) => `jira fields ${project ?? "<project>"}`;
|
|
3875
|
+
const DISCOVERY = {
|
|
3876
|
+
project: () => "jira projects",
|
|
3877
|
+
status: (p) => p ? `jira statuses --project ${p}` : "jira statuses",
|
|
3878
|
+
issuetype: createForm,
|
|
3879
|
+
type: createForm
|
|
3880
|
+
};
|
|
3881
|
+
function searchHint(err, project) {
|
|
3882
|
+
if (!(err instanceof HttpError) || err.status !== 400) return err;
|
|
3883
|
+
const field = FIELD_IN_MESSAGE.exec(err.message)?.[1]?.toLowerCase();
|
|
3884
|
+
const command = field ? DISCOVERY[field]?.(values(project)[0]) : void 0;
|
|
3885
|
+
return command ? /* @__PURE__ */ new Error(`${err.message} Run \`${command}\` to see the options.`) : err;
|
|
3350
3886
|
}
|
|
3351
3887
|
//#endregion
|
|
3352
3888
|
//#region src/api/jira-issues.ts
|
|
@@ -3496,7 +4032,7 @@ function formatIssueView(issue, nowMs, allComments) {
|
|
|
3496
4032
|
["URL", issue.url]
|
|
3497
4033
|
]),
|
|
3498
4034
|
...bodyLines(issue.description),
|
|
3499
|
-
...commentSection(issue.comments, allComments),
|
|
4035
|
+
...commentSection(renderedComments(issue.comments), { allComments }),
|
|
3500
4036
|
...attachmentSection(issue.attachments)
|
|
3501
4037
|
];
|
|
3502
4038
|
}
|
|
@@ -3533,29 +4069,29 @@ const ISSUE_REF = {
|
|
|
3533
4069
|
//#endregion
|
|
3534
4070
|
//#region src/commands/jira-search.ts
|
|
3535
4071
|
async function jiraSearch(env, query, options) {
|
|
3536
|
-
const { session
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
const
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
4072
|
+
const { session } = env;
|
|
4073
|
+
checkQuery(query, options);
|
|
4074
|
+
const limit = checkedLimit(options);
|
|
4075
|
+
const params = await searchParams(session, query, options, limit, Date.now());
|
|
4076
|
+
await showIssues(env, await searchIssues(session, session.site, params).catch((err) => {
|
|
4077
|
+
throw searchHint(err, options.project);
|
|
4078
|
+
}), limit, options);
|
|
4079
|
+
}
|
|
4080
|
+
async function jiraJql(env, query, options) {
|
|
4081
|
+
const { session } = env;
|
|
4082
|
+
const limit = checkedLimit(options);
|
|
4083
|
+
await showIssues(env, await searchIssuesByJql(session, session.site, query, limit), limit, options);
|
|
4084
|
+
}
|
|
4085
|
+
async function showIssues(env, issues, limit, options, empty = "No matching issues.") {
|
|
4086
|
+
await showRows(env.term, formatIssueRows(issues, Date.now()), {
|
|
4087
|
+
empty,
|
|
4088
|
+
hasMore: issues.length === limit,
|
|
3546
4089
|
limit
|
|
3547
|
-
});
|
|
3548
|
-
await runSearch(term, formatIssueRows(issues, Date.now()), {
|
|
3549
|
-
json: options.json,
|
|
3550
|
-
copy: options.copy,
|
|
3551
|
-
out: options.out,
|
|
3552
|
-
empty: "No matching issues.",
|
|
3553
|
-
footer: issues.length === limit ? searchFooter(limit) : void 0
|
|
3554
|
-
}, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
4090
|
+
}, options, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
3555
4091
|
}
|
|
3556
4092
|
async function jiraList(env, options) {
|
|
3557
4093
|
const { session, term } = env;
|
|
3558
|
-
|
|
4094
|
+
checkedLimit(options);
|
|
3559
4095
|
const { issues, truncated } = await listAssignedIssues(session, session.site, {
|
|
3560
4096
|
all: options.all,
|
|
3561
4097
|
project: options.project
|
|
@@ -3584,25 +4120,23 @@ const ISSUE_NOUN = {
|
|
|
3584
4120
|
};
|
|
3585
4121
|
//#endregion
|
|
3586
4122
|
//#region src/cli/jira.ts
|
|
3587
|
-
function collect(value, previous = []) {
|
|
3588
|
-
return [...previous, value];
|
|
3589
|
-
}
|
|
3590
4123
|
function registerJira(jira) {
|
|
3591
4124
|
jira.description("Jira commands");
|
|
3592
4125
|
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(jiraAction(jiraProjects));
|
|
3593
4126
|
jira.command("statuses [query]").description("List statuses (optionally filtered by name, scoped with --project)").option("-p, --project <key>", "limit to statuses used by a project").option("--json", "output results as JSON").action(jiraAction(jiraStatuses));
|
|
3594
|
-
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable
|
|
4127
|
+
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable)", collect).option("-c, --component <name>", "component (repeatable)", collect).option("--parent <key>", "parent issue key for subtasks").option("-f, --field <name=value>", "any other create-screen field (repeatable)", collect).option("--dry-run", "print the resolved payload instead of creating").option("--json", "print the created issue as JSON").action(jiraAction(jiraCreate));
|
|
3595
4128
|
jira.command("fields <project> [type]").description("Show the issue types you can create in a project, or the create form for one type").option("--json", "output results as JSON").action(jiraAction(jiraFields));
|
|
3596
4129
|
jira.command("view [issue]").description("Show a Jira issue (key or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(jiraView));
|
|
3597
4130
|
jira.command("copy [issue]").description("Copy a Jira issue (key or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(jiraCopy));
|
|
3598
4131
|
jira.command("update [file]").description("Update a Jira issue description from an edited Markdown file").option("--summary", "also push the H1 as the issue summary").option("-f, --force", "skip the stale-issue and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(jiraUpdate));
|
|
3599
|
-
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")
|
|
3600
|
-
jira.command("search [query]").description("Search Jira issues
|
|
4132
|
+
outputOptions(jira.command("list").description("List open issues assigned to you").option("-p, --project <key>", "limit to a project").option("-a, --all", "include Done issues updated in the last 30 days")).action(jiraAction(jiraList));
|
|
4133
|
+
outputOptions(jira.command("search [query]").description("Search Jira issues by text and filters").option("-p, --project <key>", "limit to a project (repeatable)", collect).option("-a, --assignee <who>", "limit to an assignee: me, a name, or an account id", collect).option("--reporter <who>", "limit to a reporter: me, a name, or an account id", collect).option("-s, --status <status>", "limit to a status (repeatable)", collect).option("-t, --type <type>", "limit to an issue type (repeatable)", collect).option("--label <label>", "limit to a label (repeatable)", collect).option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").option("--open", "exclude issues in the Done category").addOption(moved("--jql <jql>", "jira jql"))).action(jiraAction(jiraSearch));
|
|
4134
|
+
outputOptions(jira.command("jql <query>").description("Search Jira issues with a raw JQL query")).action(jiraAction(jiraJql));
|
|
3601
4135
|
return jira;
|
|
3602
4136
|
}
|
|
3603
4137
|
//#endregion
|
|
3604
4138
|
//#region package.json
|
|
3605
|
-
var version = "
|
|
4139
|
+
var version = "2.0.0";
|
|
3606
4140
|
//#endregion
|
|
3607
4141
|
//#region src/cli/build.ts
|
|
3608
4142
|
const PRODUCTS = {
|