atlass 1.13.0 → 1.14.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.
@@ -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
  }
@@ -659,13 +688,34 @@ async function* walkPages(client, firstPath) {
659
688
  path = page.next ? pathAndQuery(page.next) : null;
660
689
  }
661
690
  }
662
- async function collectPages(client, first, limit, map) {
663
- const out = [];
664
- for await (const value of walkPages(client, first)) {
665
- out.push(map(value));
666
- if (out.length >= limit) break;
691
+ async function collectCapped(client, first, cap, map) {
692
+ const items = [];
693
+ let total;
694
+ let path = first;
695
+ while (path) {
696
+ const page = await client.getJson(path);
697
+ total ??= page.size;
698
+ for (const value of page.values ?? []) {
699
+ items.push(map(value));
700
+ if (items.length >= cap) return {
701
+ items,
702
+ total,
703
+ truncated: hasMore(page, total, cap)
704
+ };
705
+ }
706
+ path = page.next ? pathAndQuery(page.next) : null;
667
707
  }
668
- return out;
708
+ return {
709
+ items,
710
+ total: total ?? items.length,
711
+ truncated: false
712
+ };
713
+ }
714
+ function hasMore(page, total, cap) {
715
+ return total === void 0 ? Boolean(page.next) : total > cap;
716
+ }
717
+ async function collectPages(client, first, limit, map) {
718
+ return (await collectCapped(client, first, limit, map)).items;
669
719
  }
670
720
  //#endregion
671
721
  //#region src/api/bitbucket-pull-requests.ts
@@ -739,93 +789,115 @@ function toPullRequestSummary(ref, pr) {
739
789
  async function listPullRequests(client, ref, params) {
740
790
  return collectPages(client, `${repoPath(ref, "pullrequests")}?${pullRequestsQuery(params)}`, params.limit, (value) => toPullRequestSummary(ref, value));
741
791
  }
742
- //#endregion
743
- //#region src/api/bitbucket-pipelines.ts
744
- const BUILD_NUMBER_SCAN_LIMIT = 1e3;
745
- function pipelineStatus(state) {
746
- if (!state) return "";
747
- if (state.name === "COMPLETED") return state.result?.name ?? "COMPLETED";
748
- return state.name ?? "";
792
+ const PAGELEN = 100;
793
+ const RESOLUTION_FIELDS = "fields=%2Bvalues.resolution.*";
794
+ function pullRequestPath(ref, id, resource) {
795
+ const base = `${repoPath(ref, "pullrequests")}/${id}`;
796
+ return resource ? `${base}/${resource}` : base;
749
797
  }
750
- function pipelinesQuery(limit) {
751
- return new URLSearchParams({
752
- sort: "-created_on",
753
- pagelen: String(Math.min(Math.max(limit, 1), 100))
754
- }).toString();
798
+ function toPullRequestDetail(ref, value) {
799
+ return {
800
+ id: value.id,
801
+ title: value.title ?? "",
802
+ description: value.description ?? "",
803
+ state: value.state ?? "",
804
+ draft: value.draft ?? false,
805
+ author: value.author?.display_name ?? "",
806
+ sourceBranch: value.source?.branch?.name ?? "",
807
+ destinationBranch: value.destination?.branch?.name ?? "",
808
+ closedBy: value.closed_by?.display_name ?? "",
809
+ createdOn: value.created_on ?? "",
810
+ updatedOn: value.updated_on ?? "",
811
+ url: pullRequestUrl(ref, value.id),
812
+ reviewers: toReviewers(value),
813
+ participants: toParticipants(value)
814
+ };
755
815
  }
756
- function wallClockSeconds(startOn, endOn) {
757
- if (!startOn || !endOn) return null;
758
- const start = Date.parse(startOn);
759
- const end = Date.parse(endOn);
760
- if (Number.isNaN(start) || Number.isNaN(end)) return null;
761
- return Math.floor((end - start) / 1e3);
816
+ function toParticipants(value) {
817
+ const rows = /* @__PURE__ */ new Map();
818
+ const users = [value.author, ...(value.participants ?? []).map((entry) => entry.user)];
819
+ for (const user of users) {
820
+ if (!user?.account_id || !user.display_name) continue;
821
+ rows.set(user.account_id, {
822
+ accountId: user.account_id,
823
+ name: user.display_name
824
+ });
825
+ }
826
+ return [...rows.values()];
762
827
  }
763
- function shortHash(hash) {
764
- return hash?.slice(0, 7) ?? "";
828
+ function toReviewers(value) {
829
+ const rows = /* @__PURE__ */ new Map();
830
+ for (const requested of value.reviewers ?? []) rows.set(principalKey(requested), {
831
+ name: requested.display_name ?? "",
832
+ state: ""
833
+ });
834
+ for (const participant of value.participants ?? []) {
835
+ const state = participantState(participant);
836
+ if (participant.role !== "REVIEWER" && !state) continue;
837
+ rows.set(principalKey(participant.user ?? {}), {
838
+ name: participant.user?.display_name ?? "",
839
+ state
840
+ });
841
+ }
842
+ return [...rows.values()];
765
843
  }
766
- function pipelineUrl(ref, buildNumber) {
767
- return `${BITBUCKET_WEB_ORIGIN}/${ref.workspace}/${ref.repo}/pipelines/results/${buildNumber}`;
844
+ function participantState(participant) {
845
+ if (participant.state) return participant.state.toUpperCase();
846
+ return participant.approved ? "APPROVED" : "";
768
847
  }
769
- function toPipelineSummary(ref, p) {
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
- };
848
+ function principalKey(user) {
849
+ return user.uuid ?? user.display_name ?? "";
781
850
  }
782
- function toPipelineDetail(ref, p) {
851
+ function toComment(value) {
852
+ if (value.deleted) return null;
853
+ const body = value.content?.raw ?? "";
854
+ if (!body.trim()) return null;
783
855
  return {
784
- ...toPipelineSummary(ref, p),
785
- repo: p.repository?.full_name ?? "",
786
- trigger: p.trigger?.name ?? ""
856
+ id: value.id,
857
+ parentId: value.parent?.id ?? null,
858
+ author: value.user?.display_name ?? "",
859
+ created: value.created_on ?? "",
860
+ body,
861
+ anchor: inlineAnchor(value),
862
+ resolved: toResolution(value.resolution)
787
863
  };
788
864
  }
789
- function toStepSummary(step) {
865
+ function toResolution(value) {
866
+ if (!value?.created_on) return null;
790
867
  return {
791
- name: step.name ?? "",
792
- status: pipelineStatus(step.state),
793
- durationSeconds: wallClockSeconds(step.started_on, step.completed_on)
868
+ by: value.user?.display_name ?? "",
869
+ at: value.created_on
794
870
  };
795
871
  }
796
- function pipelinesPath(ref) {
797
- return repoPath(ref, "pipelines");
798
- }
799
- async function listPipelines(client, ref, limit) {
800
- return collectPages(client, `${pipelinesPath(ref)}?${pipelinesQuery(limit)}`, limit, (value) => toPipelineSummary(ref, value));
872
+ function inlineAnchor(value) {
873
+ const path = value.inline?.path;
874
+ if (!path) return "";
875
+ const line = value.inline?.to ?? value.inline?.from;
876
+ return line == null ? path : `${path}:${line}`;
801
877
  }
802
- async function getPipeline(client, ref, buildNumber) {
803
- const found = await fetchByBuildNumber(client, ref, buildNumber) ?? await findInRecentRuns(client, ref, buildNumber);
804
- if (!found) throw new Error(`Could not find pipeline #${buildNumber} in the ${BUILD_NUMBER_SCAN_LIMIT} most recent runs. It may be too old.`);
805
- return toPipelineDetail(ref, found);
878
+ function toChangedFile(value) {
879
+ const before = value.old?.path ?? "";
880
+ const after = value.new?.path ?? "";
881
+ return {
882
+ path: before && after && before !== after ? `${before} -> ${after}` : after || before,
883
+ status: value.status ?? "",
884
+ added: value.lines_added ?? 0,
885
+ removed: value.lines_removed ?? 0
886
+ };
806
887
  }
807
- async function fetchByBuildNumber(client, ref, buildNumber) {
808
- try {
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
- }
888
+ async function getPullRequest(client, ref, id) {
889
+ return toPullRequestDetail(ref, await client.getJson(pullRequestPath(ref, id)));
814
890
  }
815
- async function findInRecentRuns(client, ref, buildNumber) {
816
- const first = `${pipelinesPath(ref)}?${pipelinesQuery(100)}`;
817
- let scanned = 0;
818
- for await (const pipeline of walkPages(client, first)) {
819
- if (pipeline.build_number === buildNumber) return pipeline;
820
- if (++scanned >= BUILD_NUMBER_SCAN_LIMIT) break;
821
- }
822
- return null;
891
+ async function listComments(client, ref, id) {
892
+ const page = await collectCapped(client, `${pullRequestPath(ref, id, "comments")}?pagelen=${PAGELEN}&${RESOLUTION_FIELDS}`, 200, toComment);
893
+ const items = page.items.filter((comment) => comment !== null).sort((a, b) => a.created.localeCompare(b.created));
894
+ return {
895
+ ...page,
896
+ items
897
+ };
823
898
  }
824
- async function listSteps(client, ref, pipelineId) {
825
- const out = [];
826
- const first = `${pipelinesPath(ref)}/${encodeURIComponent(pipelineId)}/steps`;
827
- for await (const step of walkPages(client, first)) out.push(toStepSummary(step));
828
- return out;
899
+ async function listChangedFiles(client, ref, id) {
900
+ return collectCapped(client, `${pullRequestPath(ref, id, "diffstat")}?pagelen=${PAGELEN}`, 50, toChangedFile);
829
901
  }
830
902
  //#endregion
831
903
  //#region src/util/format.ts
@@ -837,10 +909,16 @@ function formatDuration(seconds) {
837
909
  if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
838
910
  return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
839
911
  }
912
+ const pad = (n) => String(n).padStart(2, "0");
913
+ function formatDate$1(iso) {
914
+ const date = new Date(iso);
915
+ if (Number.isNaN(date.getTime())) return iso;
916
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
917
+ }
840
918
  function formatDateTime(iso) {
841
919
  const date = new Date(iso);
842
920
  if (Number.isNaN(date.getTime())) return iso;
843
- return date.toISOString().replace("T", " ").slice(0, 16);
921
+ return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
844
922
  }
845
923
  const MINUTE = 60;
846
924
  const HOUR = 3600;
@@ -859,134 +937,72 @@ function relativeTime(iso, nowMs) {
859
937
  return `${Math.floor(days / 365)}y ago`;
860
938
  }
861
939
  //#endregion
862
- //#region src/util/link.ts
863
- const OSC8 = "\x1B]8;;";
864
- const BEL = "\x07";
865
- function hyperlink(text, url) {
866
- if (!kleur.enabled || !url) return text;
867
- return `${OSC8}${url}${BEL}${text}${OSC8}${BEL}`;
868
- }
869
- //#endregion
870
- //#region src/commands/search-run.ts
871
- function alignedRows(items, nowMs, cells) {
872
- const rows = items.map((item) => {
873
- const cell = cells(item);
874
- return {
875
- ...cell,
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
940
+ //#region src/commands/bitbucket-comments.ts
941
+ const MENTION = /@\{([^}]+)\}/g;
942
+ function renderedPrComments(comments, participants) {
943
+ const names = new Map(participants.map((person) => [person.accountId, person.name]));
944
+ return threadsOf(comments).map((thread) => renderThread(thread, names));
945
+ }
946
+ function threadsOf(comments) {
947
+ const byId = new Map(comments.map((comment) => [comment.id, comment]));
948
+ const threads = /* @__PURE__ */ new Map();
949
+ for (const comment of comments) {
950
+ const root = rootOf(comment, byId);
951
+ const thread = threads.get(root.id);
952
+ if (thread) thread.push(comment);
953
+ else threads.set(root.id, [comment]);
954
+ }
955
+ return [...threads.values()];
956
+ }
957
+ function rootOf(comment, byId) {
958
+ const seen = /* @__PURE__ */ new Set([comment.id]);
959
+ let current = comment;
960
+ while (current.parentId !== null) {
961
+ const parent = byId.get(current.parentId);
962
+ if (!parent || seen.has(parent.id)) break;
963
+ seen.add(parent.id);
964
+ current = parent;
965
+ }
966
+ return current;
967
+ }
968
+ function renderThread(thread, names) {
969
+ const rows = thread.map((comment, index) => ({
970
+ author: comment.author,
971
+ created: comment.created,
972
+ markdown: resolveMentions(comment.body, names),
973
+ ...index === 0 ? { anchor: comment.anchor } : {}
902
974
  }));
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
975
+ const trailer = resolvedTrailer(thread);
976
+ const last = rows.at(-1);
977
+ if (trailer && last) last.trailer = trailer;
978
+ return rows;
979
+ }
980
+ function resolvedTrailer(thread) {
981
+ const resolved = thread.find((comment) => comment.resolved)?.resolved;
982
+ if (!resolved) return "";
983
+ const by = resolved.by ? ` by ${resolved.by}` : "";
984
+ return kleur.green(` ↳ resolved${by} · ${formatDateTime(resolved.at)}`);
985
+ }
986
+ function resolveMentions(body, names) {
987
+ return body.replace(MENTION, (raw, id) => {
988
+ const name = names.get(id);
989
+ return name ? `@${name}` : raw;
926
990
  });
927
- if (selected.length === 0) {
928
- term.out("Nothing selected.");
929
- return;
930
- }
931
- let copied = 0;
932
- const failures = [];
933
- const queue = [...selected];
934
- async function worker() {
935
- for (let id = queue.shift(); id !== void 0; id = queue.shift()) try {
936
- await copyOne(id);
937
- copied++;
938
- } catch (err) {
939
- failures.push(`${id} (${err instanceof Error ? err.message : String(err)})`);
940
- }
941
- }
942
- await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, selected.length) }, worker));
943
- const summary = `Copied ${copied} ${copied === 1 ? noun.singular : noun.plural}`;
944
- if (failures.length === 0) term.out(summary);
945
- else term.out(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
946
- }
947
- function formatRow(row, width) {
948
- const room = width - stripVTControlCharacters(row.fixedColumns).length - 2;
949
- const text = room > 0 ? truncate(row.freeText, room) : "";
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
991
  }
957
992
  //#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
982
- }));
983
- }
993
+ //#region src/commands/bitbucket-shared.ts
994
+ const PIPELINE_SCOPE = "read:pipeline:bitbucket";
995
+ const PULL_REQUEST_SCOPE = "read:pullrequest:bitbucket";
996
+ const REPOSITORY_SCOPE = "read:repository:bitbucket";
997
+ const ACCOUNT_SCOPE = "read:account";
984
998
  const STATE_COLORS = {
985
999
  SUCCESSFUL: kleur.green,
986
1000
  MERGED: kleur.green,
1001
+ APPROVED: kleur.green,
987
1002
  FAILED: kleur.red,
988
1003
  ERROR: kleur.red,
989
1004
  DECLINED: kleur.red,
1005
+ CHANGES_REQUESTED: kleur.red,
990
1006
  IN_PROGRESS: kleur.yellow,
991
1007
  PENDING: kleur.yellow,
992
1008
  OPEN: kleur.yellow,
@@ -998,867 +1014,1218 @@ const STATE_COLORS = {
998
1014
  function colorForBitbucketState(state) {
999
1015
  return STATE_COLORS[state.toUpperCase()] ?? kleur.white;
1000
1016
  }
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}`);
1017
+ async function withScopeHint(scope, fn) {
1018
+ try {
1019
+ return await fn();
1020
+ } catch (err) {
1021
+ 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.`);
1022
+ throw err;
1023
+ }
1010
1024
  }
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
- ];
1025
+ //#endregion
1026
+ //#region src/adf/to-markdown-inline.ts
1027
+ function renderMedia(node, ctx) {
1028
+ const attrs = node.attrs ?? {};
1029
+ const alt = attrs.alt ?? "";
1030
+ const resolved = ctx.resolveMedia?.(attrs);
1031
+ if (resolved) return `![${alt}](${resolved})`;
1032
+ return `[embedded media: ${alt || attrs.id || "media"}]`;
1026
1033
  }
1027
- function refWithCommit(pipeline) {
1028
- if (!pipeline.ref) return pipeline.commit || "-";
1029
- return pipeline.commit ? `${pipeline.ref} (${pipeline.commit})` : pipeline.ref;
1034
+ function renderInline(nodes, ctx) {
1035
+ return nodes.map((n) => renderInlineNode(n, ctx)).join("");
1030
1036
  }
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);
1037
+ function renderInlineNode(node, ctx) {
1038
+ switch (node.type) {
1039
+ case "text": return applyMarks(node.text ?? "", node.marks ?? []);
1040
+ case "hardBreak": return " \n";
1041
+ case "mention": return (typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : "") || "@unknown";
1042
+ case "emoji": {
1043
+ const text = node.attrs?.["text"];
1044
+ if (typeof text === "string" && text.length > 0) return text;
1045
+ const short = node.attrs?.["shortName"];
1046
+ return typeof short === "string" ? short : "";
1047
+ }
1048
+ case "date": return formatDate(node.attrs?.["timestamp"]);
1049
+ case "status": return `\`[${typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : ""}]\``;
1050
+ case "inlineCard": {
1051
+ const url = node.attrs?.["url"];
1052
+ if (typeof url === "string") return `[${url}](${url})`;
1053
+ return "";
1054
+ }
1055
+ case "media": return renderMedia(node, ctx);
1056
+ default: return node.text ?? "";
1057
+ }
1035
1058
  }
1036
- const PIPELINE_SCOPE = "read:pipeline:bitbucket";
1037
- async function withScopeHint(scope, fn) {
1038
- try {
1039
- return await fn();
1040
- } 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.`);
1042
- throw err;
1059
+ function applyMarks(text, marks) {
1060
+ if (text.length === 0) return text;
1061
+ let out = text;
1062
+ let href;
1063
+ for (const mark of marks) switch (mark.type) {
1064
+ case "code":
1065
+ out = `\`${out}\``;
1066
+ break;
1067
+ case "strong":
1068
+ out = `**${out}**`;
1069
+ break;
1070
+ case "em":
1071
+ out = `*${out}*`;
1072
+ break;
1073
+ case "strike":
1074
+ out = `~~${out}~~`;
1075
+ break;
1076
+ case "link": {
1077
+ const value = mark.attrs?.["href"];
1078
+ if (typeof value === "string") href = value;
1079
+ break;
1080
+ }
1043
1081
  }
1082
+ if (href) out = `[${out}](${href})`;
1083
+ return out;
1084
+ }
1085
+ function formatDate(timestamp) {
1086
+ const ms = typeof timestamp === "string" ? Number(timestamp) : typeof timestamp === "number" ? timestamp : NaN;
1087
+ if (Number.isNaN(ms)) return "";
1088
+ return new Date(ms).toISOString().slice(0, 10);
1044
1089
  }
1045
1090
  //#endregion
1046
- //#region src/commands/bitbucket-prs.ts
1047
- const PULL_REQUEST_SCOPE = "read:pullrequest:bitbucket";
1048
- const ACCOUNT_SCOPE = "read:account";
1049
- async function bitbucketPrs({ session, term, profile }, options) {
1050
- if (options.query && (options.author || options.reviewer)) throw new Error("--query cannot be combined with --author or --reviewer.");
1051
- const states = pullRequestStates(options);
1052
- const ref = resolveRepo(options.repo, session);
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
- });
1091
+ //#region src/adf/to-markdown.ts
1092
+ function adfToMarkdown(doc, options = {}) {
1093
+ if (!doc) return "";
1094
+ const ctx = { resolveMedia: options.resolveMedia };
1095
+ return (doc.type === "doc" ? renderBlocks(doc.content ?? [], ctx, "") : renderBlock(doc, ctx, "")).trim();
1068
1096
  }
1069
- function pullRequestStates(options) {
1070
- if (!options.all) return parsePullRequestStates(options.state);
1071
- if (options.state) throw new Error("--all cannot be combined with --state.");
1072
- return allPullRequestStates();
1097
+ function renderBlocks(nodes, ctx, indent) {
1098
+ return nodes.map((n) => renderBlock(n, ctx, indent)).filter((s) => s.length > 0).join("\n\n");
1073
1099
  }
1074
- function pullRequestRows(prs, nowMs) {
1075
- return alignedRows(prs, nowMs, (pr) => {
1076
- const label = pr.draft ? "DRAFT" : pr.state || "-";
1077
- return {
1078
- id: `#${pr.id}`,
1079
- url: pr.url,
1080
- label,
1081
- color: colorForBitbucketState(label),
1082
- text: pr.title,
1083
- timestamp: pr.updatedOn
1084
- };
1085
- });
1100
+ function renderBlock(node, ctx, indent) {
1101
+ switch (node.type) {
1102
+ case "paragraph": return indent + renderInline(node.content ?? [], ctx);
1103
+ case "heading": {
1104
+ const level = clampLevel$1(node.attrs?.["level"]);
1105
+ return `${"#".repeat(level)} ${renderInline(node.content ?? [], ctx)}`;
1106
+ }
1107
+ case "bulletList": return renderList(node, ctx, indent, "bullet");
1108
+ case "orderedList": return renderList(node, ctx, indent, "ordered");
1109
+ case "taskList": return renderTaskList(node, ctx, indent);
1110
+ case "decisionList": return renderDecisionList(node, ctx, indent);
1111
+ case "codeBlock": return renderCodeBlock(node, indent);
1112
+ case "blockquote": return prefixLines(renderBlocks(node.content ?? [], ctx, ""), `${indent}> `);
1113
+ case "panel": return renderPanel(node, ctx, indent);
1114
+ case "rule": return `${indent}---`;
1115
+ case "table": return renderTable(node, ctx);
1116
+ case "mediaSingle":
1117
+ case "mediaGroup": return renderBlocks(node.content ?? [], ctx, indent);
1118
+ case "media": return indent + renderMedia(node, ctx);
1119
+ case "expand":
1120
+ case "nestedExpand": return renderExpand(node, ctx, indent);
1121
+ default:
1122
+ if (node.content?.length) return renderBlocks(node.content, ctx, indent);
1123
+ return node.text ? indent + node.text : "";
1124
+ }
1086
1125
  }
1087
- async function resolvePrincipals(session, profile, options) {
1088
- const mine = isMe(options.author) || isMe(options.reviewer) ? await currentUuid(session, profile) : void 0;
1089
- return {
1090
- author: isMe(options.author) ? mine : options.author,
1091
- reviewer: isMe(options.reviewer) ? mine : options.reviewer
1092
- };
1126
+ function renderList(node, ctx, indent, kind) {
1127
+ const start = kind === "ordered" ? toNumber(node.attrs?.["order"]) ?? 1 : 0;
1128
+ return (node.content ?? []).filter((n) => n.type === "listItem").map((item, i) => {
1129
+ return renderListItem(item, ctx, indent, kind === "ordered" ? `${start + i}. ` : "- ");
1130
+ }).join("\n");
1093
1131
  }
1094
- function isMe(value) {
1095
- return value?.toLowerCase() === "me";
1132
+ function renderListItem(item, ctx, indent, marker) {
1133
+ const continuationIndent = `${indent}${" ".repeat(marker.length)}`;
1134
+ return (item.content ?? []).map((block, i) => i === 0 ? `${indent}${marker}${renderBlock(block, ctx, "")}` : renderBlock(block, ctx, continuationIndent)).join("\n");
1096
1135
  }
1097
- async function currentUuid(session, profile) {
1098
- if (session.uuid) return session.uuid;
1099
- const uuid = await withScopeHint(ACCOUNT_SCOPE, () => fetchCurrentUserUuid(session));
1100
- await rememberBitbucketUuid(profile, uuid);
1101
- return uuid;
1136
+ function renderTaskList(node, ctx, indent) {
1137
+ return (node.content ?? []).filter((n) => n.type === "taskItem").map((item) => {
1138
+ return `${indent}- [${item.attrs?.["state"] === "DONE" ? "x" : " "}] ${renderInline(item.content ?? [], ctx)}`;
1139
+ }).join("\n");
1102
1140
  }
1103
- function emptyMessage(options) {
1104
- return options.author || options.reviewer || options.query || options.state || options.all ? "No matching pull requests." : "No open pull requests.";
1141
+ function renderDecisionList(node, ctx, indent) {
1142
+ return (node.content ?? []).filter((n) => n.type === "decisionItem").map((item) => `${indent}- (decision) ${renderInline(item.content ?? [], ctx)}`).join("\n");
1105
1143
  }
1106
- //#endregion
1107
- //#region src/cli/bitbucket.ts
1108
- function registerBitbucket(bitbucket) {
1109
- bitbucket.description("Bitbucket commands");
1110
- bitbucket.command("login").description("Store Bitbucket workspace and API token").action(bareAction(bitbucketLogin));
1111
- bitbucket.command("logout").description("Remove stored Bitbucket credentials").action(bareAction(bitbucketLogout));
1112
- bitbucket.command("status").description("Show the current Bitbucket login").action(bareAction(bitbucketStatus));
1113
- 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));
1114
- 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));
1115
- 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));
1116
- return bitbucket;
1144
+ function renderCodeBlock(node, indent) {
1145
+ const lang = typeof node.attrs?.["language"] === "string" ? node.attrs["language"] : "";
1146
+ const code = (node.content ?? []).map((n) => n.text ?? "").join("");
1147
+ const fence = "```";
1148
+ return prefixLines(`${fence}${lang}\n${code}\n${fence}`, indent);
1117
1149
  }
1118
- //#endregion
1119
- //#region src/adf/from-markdown.ts
1120
- function mediaNode(source, alt) {
1121
- const attrs = { ...source };
1122
- if (alt) attrs["alt"] = alt;
1123
- return {
1124
- type: "mediaSingle",
1125
- attrs: { layout: "center" },
1126
- content: [{
1127
- type: "media",
1128
- attrs
1129
- }]
1130
- };
1150
+ const PANEL_LABELS = {
1151
+ info: "Info",
1152
+ note: "Note",
1153
+ warning: "Warning",
1154
+ success: "Success",
1155
+ error: "Error"
1156
+ };
1157
+ function renderPanel(node, ctx, indent) {
1158
+ const type = typeof node.attrs?.["panelType"] === "string" ? node.attrs["panelType"] : "info";
1159
+ return prefixLines(`**${PANEL_LABELS[type] ?? "Note"}**\n\n${renderBlocks(node.content ?? [], ctx, "")}`, `${indent}> `);
1131
1160
  }
1132
- function imageHrefs(md) {
1133
- const hrefs = /* @__PURE__ */ new Set();
1134
- marked.walkTokens(marked.lexer(md), (token) => {
1135
- if (token.type === "image") hrefs.add(token.href);
1136
- });
1137
- return [...hrefs];
1161
+ function renderExpand(node, ctx, indent) {
1162
+ return `${indent}<details><summary>${typeof node.attrs?.["title"] === "string" ? node.attrs["title"] : "Details"}</summary>\n\n${renderBlocks(node.content ?? [], ctx, "")}\n\n${indent}</details>`;
1138
1163
  }
1139
- function markdownToAdf(md, options = {}) {
1140
- const ctx = { resolveImage: options.resolveImage ?? externalMedia };
1141
- return {
1142
- type: "doc",
1143
- version: 1,
1144
- content: blocks(marked.lexer(md), ctx)
1164
+ function renderTable(node, ctx) {
1165
+ const rows = (node.content ?? []).filter((n) => n.type === "tableRow");
1166
+ if (rows.length === 0) return "";
1167
+ const grid = rows.map((row) => (row.content ?? []).map((cell) => renderCellOnOneLine(cell, ctx)));
1168
+ const cols = Math.max(...grid.map((r) => r.length));
1169
+ const pad = (r) => {
1170
+ const copy = [...r];
1171
+ while (copy.length < cols) copy.push("");
1172
+ return copy;
1145
1173
  };
1174
+ const header = pad(grid[0] ?? []);
1175
+ const lines = [`| ${header.join(" | ")} |`, `| ${header.map(() => "---").join(" | ")} |`];
1176
+ for (const row of grid.slice(1)) lines.push(`| ${pad(row).join(" | ")} |`);
1177
+ return lines.join("\n");
1146
1178
  }
1147
- function blocks(tokens, ctx) {
1148
- const out = [];
1149
- for (const token of tokens) out.push(...block(token, ctx));
1150
- return out;
1179
+ function renderCellOnOneLine(cell, ctx) {
1180
+ return renderBlocks(cell.content ?? [], ctx, "").replace(/\n+/g, " ").replace(/\|/g, "\\|").trim();
1151
1181
  }
1152
- function block(token, ctx) {
1153
- switch (token.type) {
1154
- case "space": return [];
1155
- case "heading": return [{
1156
- type: "heading",
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
- }
1182
+ function prefixLines(text, prefix) {
1183
+ return text.split("\n").map((line) => line.length > 0 ? prefix + line : prefix.trimEnd()).join("\n");
1172
1184
  }
1173
- function paragraphsSplitAtImages(tokens, ctx) {
1185
+ function clampLevel$1(value) {
1186
+ const n = toNumber(value) ?? 1;
1187
+ return Math.min(6, Math.max(1, n));
1188
+ }
1189
+ function toNumber(value) {
1190
+ return typeof value === "number" ? value : void 0;
1191
+ }
1192
+ //#endregion
1193
+ //#region src/markdown/highlight.ts
1194
+ const HEADING = /^#{1,6} /;
1195
+ const QUOTE = /^(\s*)>( |$)/;
1196
+ const LIST = /^(\s*)(-|\d+\.) (\[[ x]\] )?/;
1197
+ const RULE = /^\s*---$/;
1198
+ const TABLE_ROW = /^\|.*\|$/;
1199
+ const TABLE_SEPARATOR = /^\|( --- \|)+$/;
1200
+ const CELL_DIVIDER = /(?<!\\)\|/;
1201
+ const DETAILS_OPEN = /^(\s*)<details><summary>(.*)<\/summary>$/;
1202
+ const DETAILS_CLOSE = /^\s*<\/details>$/;
1203
+ const FENCE_OPEN = /^((?:\s|> |- |\d+\. )*)```(\S*)$/;
1204
+ const FENCE = "```";
1205
+ const INLINE = /(`[^`]+`)|\[([^\]]+)\]\(((?:[^()]|\([^()]*\))+)\)|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(?!\*)(.+?)(?<!\*)\*(?!\*)|~~(.+?)~~/g;
1206
+ const TOKEN_STYLES = {
1207
+ keyword: kleur.magenta,
1208
+ built_in: kleur.magenta,
1209
+ literal: kleur.magenta,
1210
+ string: kleur.green,
1211
+ regexp: kleur.green,
1212
+ addition: kleur.green,
1213
+ number: kleur.yellow,
1214
+ symbol: kleur.yellow,
1215
+ meta: kleur.yellow,
1216
+ attr: kleur.yellow,
1217
+ comment: kleur.dim,
1218
+ quote: kleur.dim,
1219
+ deletion: kleur.dim,
1220
+ title: kleur.cyan,
1221
+ type: kleur.cyan,
1222
+ section: kleur.cyan
1223
+ };
1224
+ const lowlight = createLowlight(common);
1225
+ function highlightMarkdown(md) {
1226
+ const lines = md.split("\n");
1174
1227
  const out = [];
1175
- let buffer = [];
1176
- const flush = () => {
1177
- if (buffer.length === 0) return;
1178
- const nodes = inline(buffer, ctx);
1179
- if (nodes.length > 0) out.push({
1180
- type: "paragraph",
1181
- content: nodes
1228
+ for (let i = 0; i < lines.length; i++) {
1229
+ const line = lines[i] ?? "";
1230
+ const fence = line.match(FENCE_OPEN);
1231
+ if (!fence) {
1232
+ out.push(highlightLine(line, lines[i + 1]));
1233
+ continue;
1234
+ }
1235
+ const [, prefix = "", lang = ""] = fence;
1236
+ const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
1237
+ out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
1238
+ const code = [];
1239
+ const prefixed = [];
1240
+ let j = i + 1;
1241
+ for (; j < lines.length; j++) {
1242
+ const raw = lines[j] ?? "";
1243
+ const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
1244
+ const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
1245
+ if (body === FENCE) break;
1246
+ code.push(body);
1247
+ prefixed.push(hasPrefix);
1248
+ }
1249
+ if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
1250
+ const lead = prefixed[k] ? styledPrefix : "";
1251
+ out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
1182
1252
  });
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;
1253
+ if (j < lines.length) {
1254
+ const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
1255
+ out.push(`${lead}${kleur.dim(FENCE)}`);
1256
+ }
1257
+ i = j;
1258
+ }
1259
+ return out.join("\n");
1193
1260
  }
1194
- function list(token, ctx) {
1195
- if (token.items.length > 0 && token.items.every((i) => i.task)) return {
1196
- type: "taskList",
1197
- attrs: { localId: randomUUID() },
1198
- content: token.items.map((item) => ({
1199
- type: "taskItem",
1200
- attrs: {
1201
- localId: randomUUID(),
1202
- state: item.checked ? "DONE" : "TODO"
1203
- },
1204
- content: inline(taskItemInline(item), ctx)
1205
- }))
1206
- };
1207
- const node = {
1208
- type: token.ordered ? "orderedList" : "bulletList",
1209
- content: token.items.map((item) => listItem(item, ctx))
1210
- };
1211
- const start = Number(token.start);
1212
- if (token.ordered && Number.isFinite(start) && start !== 1) node.attrs = { order: start };
1213
- return node;
1261
+ function highlightCode(code, lang) {
1262
+ if (!lang || !lowlight.registered(lang)) return styleEachLine(code, kleur.yellow);
1263
+ return renderTree(lowlight.highlight(lang, code), (text) => text);
1214
1264
  }
1215
- function listItem(item, ctx) {
1216
- const children = blocks(item.tokens.filter((t) => t.type !== "checkbox"), ctx);
1265
+ function renderTree(node, style) {
1266
+ if (node.type === "text") return styleEachLine(node.value, style);
1267
+ if (node.type !== "root" && node.type !== "element") return "";
1268
+ const own = node.type === "element" ? tokenStyle(node.properties["className"]) : void 0;
1269
+ const next = own ? (text) => style(own(text)) : style;
1270
+ return node.children.map((child) => renderTree(child, next)).join("");
1271
+ }
1272
+ function tokenStyle(className) {
1273
+ if (!Array.isArray(className)) return void 0;
1274
+ for (const name of className) {
1275
+ if (typeof name !== "string" || !name.startsWith("hljs-")) continue;
1276
+ const style = TOKEN_STYLES[name.slice(5)];
1277
+ if (style) return style;
1278
+ }
1279
+ }
1280
+ function styleEachLine(text, style) {
1281
+ return text.split("\n").map((line) => line ? style(line) : line).join("\n");
1282
+ }
1283
+ function highlightLine(line, nextLine) {
1284
+ const quote = line.match(QUOTE);
1285
+ if (quote) {
1286
+ const [whole, indent = ""] = quote;
1287
+ const rest = line.slice(whole.length);
1288
+ const gap = whole.endsWith(" ") ? " " : "";
1289
+ return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
1290
+ }
1291
+ const list = line.match(LIST);
1292
+ if (list) {
1293
+ const [whole, indent = "", marker = "", checkbox] = list;
1294
+ const rest = line.slice(whole.length);
1295
+ const box = checkbox ? `${kleur.bold(checkbox.trim())} ` : "";
1296
+ return `${indent}${kleur.dim(marker)} ${box}${highlightLine(rest, void 0)}`;
1297
+ }
1298
+ if (HEADING.test(line)) return kleur.bold().cyan(line);
1299
+ if (RULE.test(line)) return kleur.dim(line);
1300
+ if (TABLE_SEPARATOR.test(line)) return kleur.dim(line);
1301
+ if (TABLE_ROW.test(line)) return tableRow(line, nextLine !== void 0 && TABLE_SEPARATOR.test(nextLine));
1302
+ const details = line.match(DETAILS_OPEN);
1303
+ if (details) {
1304
+ const [, indent = "", title = ""] = details;
1305
+ return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
1306
+ }
1307
+ if (DETAILS_CLOSE.test(line)) return kleur.dim(line);
1308
+ return highlightInline(line);
1309
+ }
1310
+ function stripPrefix(line, prefix) {
1311
+ return line?.startsWith(prefix) ? line.slice(prefix.length) : void 0;
1312
+ }
1313
+ function tableRow(line, header) {
1314
+ const styled = line.split(CELL_DIVIDER).slice(1, -1).map((cell) => header ? kleur.bold(highlightInline(cell)) : highlightInline(cell));
1315
+ const pipe = kleur.dim("|");
1316
+ return `${pipe}${styled.join(pipe)}${pipe}`;
1317
+ }
1318
+ function highlightInline(text) {
1319
+ return text.replace(INLINE, (match, code, linkText, url, boldItalic, bold, italic, strike) => {
1320
+ if (code !== void 0) return kleur.yellow(code);
1321
+ if (linkText !== void 0 && url !== void 0) return link(linkText, url);
1322
+ if (boldItalic !== void 0) return kleur.bold().italic(match);
1323
+ if (bold !== void 0) return kleur.bold(`**${highlightInline(bold)}**`);
1324
+ if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
1325
+ if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
1326
+ return match;
1327
+ });
1328
+ }
1329
+ function link(text, url) {
1330
+ const open = kleur.dim("[");
1331
+ const middle = kleur.dim("](");
1332
+ return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
1333
+ }
1334
+ //#endregion
1335
+ //#region src/commands/view.ts
1336
+ const INDENT = " ";
1337
+ function fieldLines(pairs) {
1338
+ const kept = pairs.filter(([, value]) => value !== "");
1339
+ const width = Math.max(...kept.map(([label]) => label.length)) + 1;
1340
+ return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
1341
+ }
1342
+ function dateWithAge(iso, nowMs) {
1343
+ if (!iso) return "";
1344
+ return `${formatDate$1(iso)} (${relativeTime(iso, nowMs)})`;
1345
+ }
1346
+ function markdownBody(md) {
1347
+ return md ? ["", highlightMarkdown(md)] : [];
1348
+ }
1349
+ function bodyLines(body) {
1350
+ return markdownBody(adfToMarkdown(body));
1351
+ }
1352
+ function renderedComments(comments) {
1353
+ return comments.map((comment) => [{
1354
+ author: comment.author,
1355
+ created: comment.created,
1356
+ markdown: adfToMarkdown(comment.body)
1357
+ }]);
1358
+ }
1359
+ function commentSection(threads, options) {
1360
+ const total = threads.reduce((running, thread) => running + thread.length, 0);
1361
+ if (total === 0) return [];
1362
+ const visible = options.allComments ? threads : threads.slice(-5);
1363
+ const lines = ["", kleur.bold(commentHeading(total, threads.length, visible.length, options))];
1364
+ for (const thread of visible) thread.forEach((comment, index) => {
1365
+ lines.push("", commentHeader(comment, index > 0), ...commentBody(comment, index > 0), ...commentTrailer(comment));
1366
+ });
1367
+ return lines;
1368
+ }
1369
+ function commentHeading(total, threads, visible, options) {
1370
+ const count = options.truncated ? `${total}+` : String(total);
1371
+ if (visible === threads) return `Comments (${count})`;
1372
+ if (threads === total) return `Comments (${count}, showing last ${visible} — --all-comments for all)`;
1373
+ return `Comments (${count} in ${threads} threads, showing last ${visible} threads — --all-comments for all)`;
1374
+ }
1375
+ function commentHeader(comment, reply) {
1376
+ const author = kleur.bold(comment.author || "Unknown");
1377
+ const when = comment.created ? ` · ${kleur.dim(formatDateTime(comment.created))}` : "";
1378
+ const where = comment.anchor ? ` · ${kleur.dim(comment.anchor)}` : "";
1379
+ return `${reply ? `${INDENT}↳ ` : "─ "}${author}${when}${where}`;
1380
+ }
1381
+ function commentBody(comment, reply) {
1382
+ if (!comment.markdown) return [];
1383
+ const body = highlightMarkdown(comment.markdown);
1384
+ if (!reply) return [body];
1385
+ return [body.split("\n").map((line) => line ? `${INDENT}${line}` : line).join("\n")];
1386
+ }
1387
+ function commentTrailer(comment) {
1388
+ return comment.trailer ? ["", comment.trailer] : [];
1389
+ }
1390
+ function attachmentSection(attachments) {
1391
+ if (attachments.length === 0) return [];
1392
+ return [
1393
+ "",
1394
+ kleur.bold("Attachments"),
1395
+ ...attachments.map((a) => `- ${a.filename}`)
1396
+ ];
1397
+ }
1398
+ //#endregion
1399
+ //#region src/commands/bitbucket-pr.ts
1400
+ async function bitbucketPr({ session, term }, arg, options) {
1401
+ const ref = parseRef(arg);
1402
+ const repo = ref.repo ?? resolveRepo(options.repo, session);
1403
+ const detail = await withScopeHint(PULL_REQUEST_SCOPE, () => fetchDetail(session, repo, ref.id));
1404
+ const [comments, files] = await Promise.all([withScopeHint(PULL_REQUEST_SCOPE, () => listComments(session, repo, ref.id)), fetchFiles(session, repo, ref.id)]);
1405
+ if (options.json) {
1406
+ term.json({
1407
+ ...detail,
1408
+ comments: comments.items,
1409
+ files: files?.items ?? null,
1410
+ truncated: {
1411
+ comments: comments.truncated,
1412
+ files: files?.truncated ?? false
1413
+ }
1414
+ });
1415
+ return;
1416
+ }
1417
+ const lines = formatPullRequestView(detail, comments, files, Date.now(), options.allComments);
1418
+ await term.page(lines.join("\n"), { pager: options.pager });
1419
+ }
1420
+ function formatPullRequestView(detail, comments, files, nowMs, allComments = false) {
1421
+ const label = detail.draft ? "DRAFT" : detail.state || "-";
1422
+ return [
1423
+ kleur.bold(detail.title),
1424
+ ...fieldLines([
1425
+ ["State", colorForBitbucketState(label)(label)],
1426
+ ["Author", detail.author],
1427
+ ["Branch", branchLine(detail)],
1428
+ ["Approvals", approvalCount(detail.reviewers)],
1429
+ ["Created", dateWithAge(detail.createdOn, nowMs)],
1430
+ ["Updated", dateWithAge(detail.updatedOn, nowMs)],
1431
+ ["Closed by", detail.closedBy],
1432
+ ["URL", detail.url]
1433
+ ]),
1434
+ ...markdownBody(detail.description),
1435
+ ...reviewerSection(detail.reviewers),
1436
+ ...fileSection(files),
1437
+ ...commentSection(renderedPrComments(comments.items, detail.participants), {
1438
+ allComments,
1439
+ truncated: comments.truncated
1440
+ })
1441
+ ];
1442
+ }
1443
+ function reviewerSection(reviewers) {
1444
+ const heading = ["", kleur.bold("Reviewers")];
1445
+ if (reviewers.length === 0) return [...heading, ` ${kleur.dim("None assigned")}`];
1446
+ const width = Math.max(...reviewers.map((r) => r.name.length));
1447
+ return [...heading, ...reviewers.map((r) => ` ${r.name.padEnd(width)} ${reviewerState(r)}`)];
1448
+ }
1449
+ function fileSection(files) {
1450
+ if (!files) return [
1451
+ "",
1452
+ kleur.bold("Files"),
1453
+ ` ${kleur.dim(`Unavailable: the token needs the ${REPOSITORY_SCOPE} scope.`)}`
1454
+ ];
1455
+ if (files.items.length === 0) return [];
1456
+ const rows = files.items.map((file) => ({
1457
+ added: `+${file.added}`,
1458
+ removed: `-${file.removed}`,
1459
+ path: file.path
1460
+ }));
1461
+ const wa = Math.max(...rows.map((r) => r.added.length));
1462
+ const wr = Math.max(...rows.map((r) => r.removed.length));
1463
+ const total = files.total ?? files.items.length;
1464
+ const added = sum(files.items, (f) => f.added);
1465
+ const removed = sum(files.items, (f) => f.removed);
1466
+ return [
1467
+ "",
1468
+ kleur.bold(`Files (${total}, +${added} -${removed})`),
1469
+ ...rows.map((r) => ` ${kleur.green(r.added.padStart(wa))} ${kleur.red(r.removed.padStart(wr))} ${r.path}`),
1470
+ ...moreFilesLine(files)
1471
+ ];
1472
+ }
1473
+ function moreFilesLine(files) {
1474
+ if (!files.truncated) return [];
1475
+ const rest = files.total === void 0 ? void 0 : files.total - files.items.length;
1476
+ return [kleur.dim(rest === void 0 ? " ... more files not shown" : ` ... and ${rest} more`)];
1477
+ }
1478
+ function sum(files, pick) {
1479
+ return files.reduce((running, file) => running + pick(file), 0);
1480
+ }
1481
+ function reviewerState(reviewer) {
1482
+ if (!reviewer.state) return kleur.dim("—");
1483
+ return colorForBitbucketState(reviewer.state)(reviewer.state);
1484
+ }
1485
+ function approvalCount(reviewers) {
1486
+ return `${reviewers.filter((r) => r.state === "APPROVED").length}/${reviewers.length}`;
1487
+ }
1488
+ function branchLine(detail) {
1489
+ if (!detail.sourceBranch || !detail.destinationBranch) return detail.sourceBranch || detail.destinationBranch;
1490
+ return `${detail.sourceBranch} → ${detail.destinationBranch}`;
1491
+ }
1492
+ function parseRef(arg) {
1493
+ const ref = parsePullRequestRef(arg ?? "");
1494
+ if (!ref) throw new Error(`Invalid pull request "${arg ?? ""}". Expected a PR number, e.g. 42, or a Bitbucket pull request URL.`);
1495
+ return ref;
1496
+ }
1497
+ async function fetchFiles(session, repo, id) {
1498
+ try {
1499
+ return await listChangedFiles(session, repo, id);
1500
+ } catch (err) {
1501
+ if (err instanceof HttpError && (err.status === 401 || err.status === 403)) return null;
1502
+ throw err;
1503
+ }
1504
+ }
1505
+ async function fetchDetail(session, repo, id) {
1506
+ try {
1507
+ return await getPullRequest(session, repo, id);
1508
+ } catch (err) {
1509
+ if (err instanceof HttpError && err.status === 404) throw new Error(`Pull request #${id} not found in ${repo.workspace}/${repo.repo}.`);
1510
+ throw err;
1511
+ }
1512
+ }
1513
+ //#endregion
1514
+ //#region src/util/link.ts
1515
+ const OSC8 = "\x1B]8;;";
1516
+ const BEL = "\x07";
1517
+ function hyperlink(text, url) {
1518
+ if (!kleur.enabled || !url) return text;
1519
+ return `${OSC8}${url}${BEL}${text}${OSC8}${BEL}`;
1520
+ }
1521
+ //#endregion
1522
+ //#region src/commands/search-run.ts
1523
+ function alignedRows(items, nowMs, cells) {
1524
+ const rows = items.map((item) => {
1525
+ const cell = cells(item);
1526
+ return {
1527
+ ...cell,
1528
+ age: relativeTime(cell.timestamp, nowMs),
1529
+ json: { ...item }
1530
+ };
1531
+ });
1532
+ const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
1533
+ const idWidth = width((row) => row.id);
1534
+ const labelWidth = width((row) => row.label);
1535
+ const ageWidth = width((row) => row.age);
1536
+ return rows.map((row) => ({
1537
+ id: row.id,
1538
+ url: row.url,
1539
+ fixedColumns: `${row.id.padEnd(idWidth)} ${row.color(row.label.padEnd(labelWidth))} ${row.age.padEnd(ageWidth)}`,
1540
+ freeText: row.text,
1541
+ json: row.json
1542
+ }));
1543
+ }
1544
+ function formatRows(rows, options) {
1545
+ if (rows.length === 0) return [options.empty];
1546
+ const lines = rows.map((row) => formatRow(row, options.width));
1547
+ return options.footer ? [...lines, options.footer] : lines;
1548
+ }
1549
+ function writeRows(term, rows, options) {
1550
+ if (options.json) term.json(rows.map((r) => r.json));
1551
+ else term.out(formatRows(rows, {
1552
+ ...options,
1553
+ width: term.width
1554
+ }));
1555
+ }
1556
+ const COPY_CONCURRENCY = 5;
1557
+ async function runSearch(term, rows, options, noun, copyOne) {
1558
+ if (!(options.copy && !options.json && rows.length > 0)) {
1559
+ writeRows(term, rows, options);
1560
+ return;
1561
+ }
1562
+ if (options.out?.endsWith(".md")) throw new Error("--out must be a directory when using --copy; a .md file path would overwrite each selection.");
1563
+ await copySelected(term, rows, noun, copyOne);
1564
+ }
1565
+ function searchFooter(limit) {
1566
+ return `
1567
+ Showing first ${limit}; refine with flags or raise --limit.`;
1568
+ }
1569
+ async function copySelected(term, rows, noun, copyOne) {
1570
+ if (!term.interactive) throw new Error("--copy requires an interactive terminal.");
1571
+ const selected = await term.ask.pickMany({
1572
+ message: `Select ${noun.plural} to copy:`,
1573
+ choices: rows.map((r) => ({
1574
+ name: formatRow(r, term.width),
1575
+ value: r.id
1576
+ })),
1577
+ pageSize: 20
1578
+ });
1579
+ if (selected.length === 0) {
1580
+ term.out("Nothing selected.");
1581
+ return;
1582
+ }
1583
+ let copied = 0;
1584
+ const failures = [];
1585
+ const queue = [...selected];
1586
+ async function worker() {
1587
+ for (let id = queue.shift(); id !== void 0; id = queue.shift()) try {
1588
+ await copyOne(id);
1589
+ copied++;
1590
+ } catch (err) {
1591
+ failures.push(`${id} (${err instanceof Error ? err.message : String(err)})`);
1592
+ }
1593
+ }
1594
+ await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, selected.length) }, worker));
1595
+ const summary = `Copied ${copied} ${copied === 1 ? noun.singular : noun.plural}`;
1596
+ if (failures.length === 0) term.out(summary);
1597
+ else term.out(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
1598
+ }
1599
+ function formatRow(row, width) {
1600
+ const room = width - stripVTControlCharacters(row.fixedColumns).length - 2;
1601
+ const text = room > 0 ? truncate(row.freeText, room) : "";
1602
+ const columns = hyperlink(row.id, row.url) + row.fixedColumns.slice(row.id.length);
1603
+ return text ? `${columns} ${hyperlink(text, row.url)}` : columns;
1604
+ }
1605
+ function truncate(text, max) {
1606
+ const clean = text.replace(/\s+/g, " ").trim();
1607
+ return clean.length <= max ? clean : `${clean.slice(0, Math.max(0, max - 3))}...`;
1608
+ }
1609
+ //#endregion
1610
+ //#region src/commands/bitbucket-prs.ts
1611
+ async function bitbucketPrs({ session, term, profile }, options) {
1612
+ if (options.query && (options.author || options.reviewer)) throw new Error("--query cannot be combined with --author or --reviewer.");
1613
+ const states = pullRequestStates(options);
1614
+ const ref = resolveRepo(options.repo, session);
1615
+ const limit = parseLimit(options.limit);
1616
+ const query = pullRequestQuery({
1617
+ ...await resolvePrincipals(session, profile, options),
1618
+ query: options.query
1619
+ });
1620
+ const prs = await withScopeHint(PULL_REQUEST_SCOPE, () => listPullRequests(session, ref, {
1621
+ limit,
1622
+ states,
1623
+ query
1624
+ }));
1625
+ writeRows(term, pullRequestRows(prs, Date.now()), {
1626
+ json: options.json,
1627
+ empty: emptyMessage(options),
1628
+ footer: prs.length === limit ? searchFooter(limit) : void 0
1629
+ });
1630
+ }
1631
+ function pullRequestStates(options) {
1632
+ if (!options.all) return parsePullRequestStates(options.state);
1633
+ if (options.state) throw new Error("--all cannot be combined with --state.");
1634
+ return allPullRequestStates();
1635
+ }
1636
+ function pullRequestRows(prs, nowMs) {
1637
+ return alignedRows(prs, nowMs, (pr) => {
1638
+ const label = pr.draft ? "DRAFT" : pr.state || "-";
1639
+ return {
1640
+ id: `#${pr.id}`,
1641
+ url: pr.url,
1642
+ label,
1643
+ color: colorForBitbucketState(label),
1644
+ text: pr.title,
1645
+ timestamp: pr.updatedOn
1646
+ };
1647
+ });
1648
+ }
1649
+ async function resolvePrincipals(session, profile, options) {
1650
+ const mine = isMe(options.author) || isMe(options.reviewer) ? await currentUuid(session, profile) : void 0;
1217
1651
  return {
1218
- type: "listItem",
1219
- content: children.length > 0 ? children : [emptyParagraph()]
1652
+ author: isMe(options.author) ? mine : options.author,
1653
+ reviewer: isMe(options.reviewer) ? mine : options.reviewer
1220
1654
  };
1221
1655
  }
1222
- function emptyParagraph() {
1223
- return {
1224
- type: "paragraph",
1225
- content: []
1226
- };
1656
+ function isMe(value) {
1657
+ return value?.toLowerCase() === "me";
1658
+ }
1659
+ async function currentUuid(session, profile) {
1660
+ if (session.uuid) return session.uuid;
1661
+ const uuid = await withScopeHint(ACCOUNT_SCOPE, () => fetchCurrentUserUuid(session));
1662
+ await rememberBitbucketUuid(profile, uuid);
1663
+ return uuid;
1664
+ }
1665
+ function emptyMessage(options) {
1666
+ return options.author || options.reviewer || options.query || options.state || options.all ? "No matching pull requests." : "No open pull requests.";
1667
+ }
1668
+ //#endregion
1669
+ //#region src/api/bitbucket-pipelines.ts
1670
+ const BUILD_NUMBER_SCAN_LIMIT = 1e3;
1671
+ function pipelineStatus(state) {
1672
+ if (!state) return "";
1673
+ if (state.name === "COMPLETED") return state.result?.name ?? "COMPLETED";
1674
+ return state.name ?? "";
1675
+ }
1676
+ function pipelinesQuery(limit) {
1677
+ return new URLSearchParams({
1678
+ sort: "-created_on",
1679
+ pagelen: String(Math.min(Math.max(limit, 1), 100))
1680
+ }).toString();
1681
+ }
1682
+ function wallClockSeconds(startOn, endOn) {
1683
+ if (!startOn || !endOn) return null;
1684
+ const start = Date.parse(startOn);
1685
+ const end = Date.parse(endOn);
1686
+ if (Number.isNaN(start) || Number.isNaN(end)) return null;
1687
+ return Math.floor((end - start) / 1e3);
1688
+ }
1689
+ function shortHash(hash) {
1690
+ return hash?.slice(0, 7) ?? "";
1227
1691
  }
1228
- function taskItemInline(item) {
1229
- const first = item.tokens.find((t) => t.type === "text");
1230
- if (first && "tokens" in first && first.tokens) return first.tokens;
1231
- return [textToken(item.text)];
1692
+ function pipelineUrl(ref, buildNumber) {
1693
+ return `${BITBUCKET_WEB_ORIGIN}/${ref.workspace}/${ref.repo}/pipelines/results/${buildNumber}`;
1232
1694
  }
1233
- function codeBlock(token) {
1695
+ function toPipelineSummary(ref, p) {
1234
1696
  return {
1235
- type: "codeBlock",
1236
- attrs: token.lang ? { language: token.lang } : {},
1237
- content: token.text.length > 0 ? [{
1238
- type: "text",
1239
- text: token.text
1240
- }] : []
1697
+ buildNumber: p.build_number,
1698
+ status: pipelineStatus(p.state),
1699
+ ref: p.target?.ref_name ?? "",
1700
+ commit: shortHash(p.target?.commit?.hash),
1701
+ durationSeconds: wallClockSeconds(p.created_on, p.completed_on),
1702
+ createdOn: p.created_on ?? "",
1703
+ creator: p.creator?.display_name ?? "",
1704
+ uuid: p.uuid,
1705
+ url: pipelineUrl(ref, p.build_number)
1241
1706
  };
1242
1707
  }
1243
- function table(token, ctx) {
1708
+ function toPipelineDetail(ref, p) {
1244
1709
  return {
1245
- type: "table",
1246
- content: [{
1247
- type: "tableRow",
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
- }))]
1710
+ ...toPipelineSummary(ref, p),
1711
+ repo: p.repository?.full_name ?? "",
1712
+ trigger: p.trigger?.name ?? ""
1253
1713
  };
1254
1714
  }
1255
- function tableCell(cell, type, ctx) {
1715
+ function toStepSummary(step) {
1256
1716
  return {
1257
- type,
1258
- content: [{
1259
- type: "paragraph",
1260
- content: inline(cell.tokens, ctx)
1261
- }]
1717
+ name: step.name ?? "",
1718
+ status: pipelineStatus(step.state),
1719
+ durationSeconds: wallClockSeconds(step.started_on, step.completed_on)
1262
1720
  };
1263
1721
  }
1264
- function inline(tokens, ctx, marks = []) {
1265
- const out = [];
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
- }
1722
+ function pipelinesPath(ref) {
1723
+ return repoPath(ref, "pipelines");
1292
1724
  }
1293
- function textNode(text, marks) {
1294
- if (text.length === 0) return [];
1295
- const node = {
1296
- type: "text",
1297
- text
1298
- };
1299
- if (marks.length > 0) node.marks = marks;
1300
- return [node];
1725
+ async function listPipelines(client, ref, limit) {
1726
+ return collectPages(client, `${pipelinesPath(ref)}?${pipelinesQuery(limit)}`, limit, (value) => toPipelineSummary(ref, value));
1301
1727
  }
1302
- function withUniqueMark(marks, mark) {
1303
- return [...marks.filter((m) => m.type !== mark.type), mark];
1728
+ async function getPipeline(client, ref, buildNumber) {
1729
+ const found = await fetchByBuildNumber(client, ref, buildNumber) ?? await findInRecentRuns(client, ref, buildNumber);
1730
+ if (!found) throw new Error(`Could not find pipeline #${buildNumber} in the ${BUILD_NUMBER_SCAN_LIMIT} most recent runs. It may be too old.`);
1731
+ return toPipelineDetail(ref, found);
1304
1732
  }
1305
- function externalMedia(href, alt) {
1306
- return mediaNode({
1307
- type: "external",
1308
- url: href
1309
- }, alt);
1733
+ async function fetchByBuildNumber(client, ref, buildNumber) {
1734
+ try {
1735
+ return await client.getJson(`${pipelinesPath(ref)}/${encodeURIComponent(String(buildNumber))}`);
1736
+ } catch (err) {
1737
+ if (err instanceof HttpError && (err.status === 400 || err.status === 404)) return null;
1738
+ throw err;
1739
+ }
1310
1740
  }
1311
- function textToken(text) {
1312
- return {
1313
- type: "text",
1314
- raw: text,
1315
- text,
1316
- escaped: false
1317
- };
1741
+ async function findInRecentRuns(client, ref, buildNumber) {
1742
+ const first = `${pipelinesPath(ref)}?${pipelinesQuery(100)}`;
1743
+ let scanned = 0;
1744
+ for await (const pipeline of walkPages(client, first)) {
1745
+ if (pipeline.build_number === buildNumber) return pipeline;
1746
+ if (++scanned >= BUILD_NUMBER_SCAN_LIMIT) break;
1747
+ }
1748
+ return null;
1318
1749
  }
1319
- function clampLevel$1(value) {
1320
- return Math.min(6, Math.max(1, Math.trunc(value) || 1));
1750
+ async function listSteps(client, ref, pipelineId) {
1751
+ const out = [];
1752
+ const first = `${pipelinesPath(ref)}/${encodeURIComponent(pipelineId)}/steps`;
1753
+ for await (const step of walkPages(client, first)) out.push(toStepSummary(step));
1754
+ return out;
1321
1755
  }
1322
1756
  //#endregion
1323
- //#region src/api/confluence-attachments.ts
1324
- const UNKNOWN_SIZE = -1;
1325
- async function listAttachments(client, id) {
1326
- return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.map((a) => ({
1327
- filename: a.title ?? a.id,
1328
- fileId: mediaNodeId(a),
1329
- size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
1330
- }));
1331
- }
1332
- async function uploadAttachment(client, pageId, filename, bytes) {
1333
- return (await client.postMultipart(`/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment`, filename, bytes)).results?.[0]?.extensions?.fileId ?? await fileIdByListing(client, pageId, filename);
1757
+ //#region src/commands/bitbucket.ts
1758
+ async function bitbucketPipelines({ session, term }, options) {
1759
+ const ref = resolveRepo(options.repo, session);
1760
+ const limit = parseLimit(options.limit);
1761
+ writeRows(term, pipelineRows(await withScopeHint(PIPELINE_SCOPE, () => listPipelines(session, ref, limit)), Date.now()), {
1762
+ json: options.json,
1763
+ empty: "No pipelines found."
1764
+ });
1334
1765
  }
1335
- async function fileIdByListing(client, pageId, filename) {
1336
- const match = (await listAttachments(client, pageId)).find((a) => a.filename === filename);
1337
- if (match) return match.fileId;
1338
- throw new Error(`Upload of "${filename}" did not return a fileId.`);
1766
+ async function bitbucketPipeline({ session, term }, arg, options) {
1767
+ const buildNumber = parseBuildNumber(arg);
1768
+ const ref = resolveRepo(options.repo, session);
1769
+ const detail = await withScopeHint(PIPELINE_SCOPE, () => getPipeline(session, ref, buildNumber));
1770
+ const steps = await withScopeHint(PIPELINE_SCOPE, () => listSteps(session, ref, detail.uuid));
1771
+ term.out(formatPipelineDetail(detail, steps, Date.now()));
1339
1772
  }
1340
- async function fetchAttachments(client, id) {
1341
- return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.filter((a) => a.downloadLink).map((a) => ({
1342
- mediaId: mediaNodeId(a),
1343
- filename: a.title ?? a.id,
1344
- url: withWikiContextPath(a.downloadLink ?? "")
1773
+ function pipelineRows(pipelines, nowMs) {
1774
+ return alignedRows(pipelines, nowMs, (p) => ({
1775
+ id: `#${p.buildNumber}`,
1776
+ url: p.url,
1777
+ label: p.status || "-",
1778
+ color: colorForBitbucketState(p.status),
1779
+ text: `${refWithCommit(p)} · ${formatDuration(p.durationSeconds)}`,
1780
+ timestamp: p.createdOn
1345
1781
  }));
1346
1782
  }
1347
- function mediaNodeId(a) {
1348
- return a.fileId ?? a.id;
1349
- }
1350
- function withWikiContextPath(link) {
1351
- if (link.startsWith("http") || link.startsWith("/wiki")) return link;
1352
- return `/wiki${link}`;
1353
- }
1354
- //#endregion
1355
- //#region src/api/confluence-pages.ts
1356
- async function fetchPage(client, site, id) {
1357
- const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
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
- };
1379
- }
1380
- async function fetchPageState(client, id) {
1381
- const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
1382
- return {
1383
- version: page.version?.number ?? 0,
1384
- title: page.title,
1385
- body: parseAdfJson(page.body?.atlas_doc_format?.value)
1386
- };
1387
- }
1388
- async function updatePage(client, id, params) {
1389
- return (await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
1390
- id,
1391
- status: "current",
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;
1783
+ function formatStepRows(steps) {
1784
+ const rows = steps.map((s) => ({
1785
+ name: s.name || "-",
1786
+ status: s.status || "-",
1787
+ dur: formatDuration(s.durationSeconds)
1788
+ }));
1789
+ const wn = Math.max(...rows.map((r) => r.name.length));
1790
+ const ws = Math.max(...rows.map((r) => r.status.length));
1791
+ return rows.map((r) => ` ${r.name.padEnd(wn)} ${r.status.padEnd(ws)} ${r.dur}`);
1402
1792
  }
1403
- async function fetchSpaceKey(client, spaceId) {
1404
- if (!spaceId) return "";
1405
- try {
1406
- return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
1407
- } catch {
1408
- return "";
1409
- }
1793
+ function formatPipelineDetail(detail, steps, nowMs) {
1794
+ const by = detail.creator ? ` by ${detail.creator}` : "";
1795
+ return [
1796
+ `Pipeline #${detail.buildNumber} ${detail.status || "-"}`,
1797
+ ...detail.repo ? [`Repo: ${detail.repo}`] : [],
1798
+ `Ref: ${refWithCommit(detail)}`,
1799
+ ...detail.trigger ? [`Trigger: ${detail.trigger}`] : [],
1800
+ `Duration: ${formatDuration(detail.durationSeconds)}`,
1801
+ `Created: ${relativeTime(detail.createdOn, nowMs)}${by}`,
1802
+ ...steps.length > 0 ? [
1803
+ "",
1804
+ "Steps:",
1805
+ ...formatStepRows(steps)
1806
+ ] : []
1807
+ ];
1410
1808
  }
1411
- async function fetchComments$1(client, id, names) {
1412
- const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
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
- })));
1809
+ function refWithCommit(pipeline) {
1810
+ if (!pipeline.ref) return pipeline.commit || "-";
1811
+ return pipeline.commit ? `${pipeline.ref} (${pipeline.commit})` : pipeline.ref;
1418
1812
  }
1419
- function parseAdfJson(value) {
1420
- if (!value) return null;
1421
- try {
1422
- return JSON.parse(value);
1423
- } catch {
1424
- return null;
1425
- }
1813
+ function parseBuildNumber(arg) {
1814
+ const raw = (arg ?? "").replace(/^#/, "").trim();
1815
+ if (!/^\d+$/.test(raw)) throw new Error(`Invalid pipeline number "${arg ?? ""}". Expected a build number, e.g. 123.`);
1816
+ return Number.parseInt(raw, 10);
1426
1817
  }
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
1818
  //#endregion
1450
- //#region src/util/html.ts
1451
- const MAX_CODE_POINT = 1114111;
1452
- const NAMED = {
1453
- amp: "&",
1454
- lt: "<",
1455
- gt: ">",
1456
- quot: "\"",
1457
- apos: "'",
1458
- nbsp: " "
1459
- };
1460
- function decodeEntities(text) {
1461
- return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
1462
- if (body[0] === "#") {
1463
- const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
1464
- return isCodePoint(code) ? String.fromCodePoint(code) : match;
1465
- }
1466
- return NAMED[body.toLowerCase()] ?? match;
1467
- });
1468
- }
1469
- function isCodePoint(code) {
1470
- return Number.isFinite(code) && code >= 0 && code <= MAX_CODE_POINT;
1819
+ //#region src/cli/bitbucket.ts
1820
+ function registerBitbucket(bitbucket) {
1821
+ bitbucket.description("Bitbucket commands");
1822
+ bitbucket.command("login").description("Store Bitbucket workspace and API token").action(bareAction(bitbucketLogin));
1823
+ bitbucket.command("logout").description("Remove stored Bitbucket credentials").action(bareAction(bitbucketLogout));
1824
+ bitbucket.command("status").description("Show the current Bitbucket login").action(bareAction(bitbucketStatus));
1825
+ 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));
1826
+ 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));
1827
+ 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));
1828
+ 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));
1829
+ return bitbucket;
1471
1830
  }
1472
1831
  //#endregion
1473
- //#region src/api/confluence-search.ts
1474
- async function searchPages(client, site, params) {
1475
- const cql = buildCql(params);
1476
- const query = new URLSearchParams({
1477
- cql,
1478
- limit: String(params.limit),
1479
- expand: "space"
1480
- });
1481
- const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
1482
- const serverPageWasFull = results.length === params.limit;
1832
+ //#region src/adf/from-markdown.ts
1833
+ function mediaNode(source, alt) {
1834
+ const attrs = { ...source };
1835
+ if (alt) attrs["alt"] = alt;
1483
1836
  return {
1484
- pages: results.filter((r) => r.content?.id).map((r) => ({
1485
- id: r.content?.id ?? "",
1486
- space: r.space?.key ?? r.resultGlobalContainer?.title ?? "",
1487
- title: decodeEntities(r.content?.title ?? r.title ?? ""),
1488
- updated: r.lastModified ?? "",
1489
- url: r.url ? `${site}/wiki${r.url}` : ""
1490
- })),
1491
- hasMore: serverPageWasFull
1837
+ type: "mediaSingle",
1838
+ attrs: { layout: "center" },
1839
+ content: [{
1840
+ type: "media",
1841
+ attrs
1842
+ }]
1492
1843
  };
1493
1844
  }
1494
- function buildCql(params) {
1495
- if (params.cql) return params.cql;
1496
- const clauses = ["type = page"];
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")}"`;
1504
- }
1505
- //#endregion
1506
- //#region src/commands/resolve-ref.ts
1507
- async function resolveRef(ask, arg, kind) {
1508
- const raw = arg ?? await ask.text({
1509
- message: kind.message,
1510
- flag: kind.flag,
1511
- required: true
1845
+ function imageHrefs(md) {
1846
+ const hrefs = /* @__PURE__ */ new Set();
1847
+ marked.walkTokens(marked.lexer(md), (token) => {
1848
+ if (token.type === "image") hrefs.add(token.href);
1512
1849
  });
1513
- const ref = kind.parse(raw);
1514
- if (!ref) throw new Error(kind.notFound(raw));
1515
- return ref;
1850
+ return [...hrefs];
1516
1851
  }
1517
- //#endregion
1518
- //#region src/adf/to-markdown-inline.ts
1519
- function renderMedia(node, ctx) {
1520
- const attrs = node.attrs ?? {};
1521
- const alt = attrs.alt ?? "";
1522
- const resolved = ctx.resolveMedia?.(attrs);
1523
- if (resolved) return `![${alt}](${resolved})`;
1524
- return `[embedded media: ${alt || attrs.id || "media"}]`;
1852
+ function markdownToAdf(md, options = {}) {
1853
+ const ctx = { resolveImage: options.resolveImage ?? externalMedia };
1854
+ return {
1855
+ type: "doc",
1856
+ version: 1,
1857
+ content: blocks(marked.lexer(md), ctx)
1858
+ };
1525
1859
  }
1526
- function renderInline(nodes, ctx) {
1527
- return nodes.map((n) => renderInlineNode(n, ctx)).join("");
1860
+ function blocks(tokens, ctx) {
1861
+ const out = [];
1862
+ for (const token of tokens) out.push(...block(token, ctx));
1863
+ return out;
1528
1864
  }
1529
- function renderInlineNode(node, ctx) {
1530
- switch (node.type) {
1531
- case "text": return applyMarks(node.text ?? "", node.marks ?? []);
1532
- case "hardBreak": return " \n";
1533
- case "mention": return (typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : "") || "@unknown";
1534
- case "emoji": {
1535
- const text = node.attrs?.["text"];
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 ?? "";
1865
+ function block(token, ctx) {
1866
+ switch (token.type) {
1867
+ case "space": return [];
1868
+ case "heading": return [{
1869
+ type: "heading",
1870
+ attrs: { level: clampLevel(token.depth) },
1871
+ content: inline(token.tokens ?? [], ctx)
1872
+ }];
1873
+ case "paragraph": return paragraphsSplitAtImages(token.tokens ?? [], ctx);
1874
+ case "text": return paragraphsSplitAtImages(token.tokens ?? [textToken(token.text)], ctx);
1875
+ case "list": return [list(token, ctx)];
1876
+ case "code": return [codeBlock(token)];
1877
+ case "blockquote": return [{
1878
+ type: "blockquote",
1879
+ content: blocks(token.tokens ?? [], ctx)
1880
+ }];
1881
+ case "table": return [table(token, ctx)];
1882
+ case "hr": return [{ type: "rule" }];
1883
+ default: return [];
1549
1884
  }
1550
1885
  }
1551
- function applyMarks(text, marks) {
1552
- if (text.length === 0) return text;
1553
- let out = text;
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})`;
1886
+ function paragraphsSplitAtImages(tokens, ctx) {
1887
+ const out = [];
1888
+ let buffer = [];
1889
+ const flush = () => {
1890
+ if (buffer.length === 0) return;
1891
+ const nodes = inline(buffer, ctx);
1892
+ if (nodes.length > 0) out.push({
1893
+ type: "paragraph",
1894
+ content: nodes
1895
+ });
1896
+ buffer = [];
1897
+ };
1898
+ for (const token of tokens) if (token.type === "image") {
1899
+ flush();
1900
+ const image = token;
1901
+ const node = ctx.resolveImage(image.href, image.text ?? "");
1902
+ if (node) out.push(node);
1903
+ } else buffer.push(token);
1904
+ flush();
1575
1905
  return out;
1576
1906
  }
1577
- function formatDate(timestamp) {
1578
- const ms = typeof timestamp === "string" ? Number(timestamp) : typeof timestamp === "number" ? timestamp : NaN;
1579
- if (Number.isNaN(ms)) return "";
1580
- return new Date(ms).toISOString().slice(0, 10);
1907
+ function list(token, ctx) {
1908
+ if (token.items.length > 0 && token.items.every((i) => i.task)) return {
1909
+ type: "taskList",
1910
+ attrs: { localId: randomUUID() },
1911
+ content: token.items.map((item) => ({
1912
+ type: "taskItem",
1913
+ attrs: {
1914
+ localId: randomUUID(),
1915
+ state: item.checked ? "DONE" : "TODO"
1916
+ },
1917
+ content: inline(taskItemInline(item), ctx)
1918
+ }))
1919
+ };
1920
+ const node = {
1921
+ type: token.ordered ? "orderedList" : "bulletList",
1922
+ content: token.items.map((item) => listItem(item, ctx))
1923
+ };
1924
+ const start = Number(token.start);
1925
+ if (token.ordered && Number.isFinite(start) && start !== 1) node.attrs = { order: start };
1926
+ return node;
1927
+ }
1928
+ function listItem(item, ctx) {
1929
+ const children = blocks(item.tokens.filter((t) => t.type !== "checkbox"), ctx);
1930
+ return {
1931
+ type: "listItem",
1932
+ content: children.length > 0 ? children : [emptyParagraph()]
1933
+ };
1581
1934
  }
1582
- //#endregion
1583
- //#region src/adf/to-markdown.ts
1584
- function adfToMarkdown(doc, options = {}) {
1585
- if (!doc) return "";
1586
- const ctx = { resolveMedia: options.resolveMedia };
1587
- return (doc.type === "doc" ? renderBlocks(doc.content ?? [], ctx, "") : renderBlock(doc, ctx, "")).trim();
1935
+ function emptyParagraph() {
1936
+ return {
1937
+ type: "paragraph",
1938
+ content: []
1939
+ };
1588
1940
  }
1589
- function renderBlocks(nodes, ctx, indent) {
1590
- return nodes.map((n) => renderBlock(n, ctx, indent)).filter((s) => s.length > 0).join("\n\n");
1941
+ function taskItemInline(item) {
1942
+ const first = item.tokens.find((t) => t.type === "text");
1943
+ if (first && "tokens" in first && first.tokens) return first.tokens;
1944
+ return [textToken(item.text)];
1591
1945
  }
1592
- function renderBlock(node, ctx, indent) {
1593
- switch (node.type) {
1594
- case "paragraph": return indent + renderInline(node.content ?? [], ctx);
1595
- case "heading": {
1596
- const level = clampLevel(node.attrs?.["level"]);
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
- }
1946
+ function codeBlock(token) {
1947
+ return {
1948
+ type: "codeBlock",
1949
+ attrs: token.lang ? { language: token.lang } : {},
1950
+ content: token.text.length > 0 ? [{
1951
+ type: "text",
1952
+ text: token.text
1953
+ }] : []
1954
+ };
1617
1955
  }
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");
1956
+ function table(token, ctx) {
1957
+ return {
1958
+ type: "table",
1959
+ content: [{
1960
+ type: "tableRow",
1961
+ content: token.header.map((cell) => tableCell(cell, "tableHeader", ctx))
1962
+ }, ...token.rows.map((row) => ({
1963
+ type: "tableRow",
1964
+ content: row.map((cell) => tableCell(cell, "tableCell", ctx))
1965
+ }))]
1966
+ };
1623
1967
  }
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");
1968
+ function tableCell(cell, type, ctx) {
1969
+ return {
1970
+ type,
1971
+ content: [{
1972
+ type: "paragraph",
1973
+ content: inline(cell.tokens, ctx)
1974
+ }]
1975
+ };
1627
1976
  }
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");
1977
+ function inline(tokens, ctx, marks = []) {
1978
+ const out = [];
1979
+ for (const token of tokens) out.push(...inlineNode(token, ctx, marks));
1980
+ return out;
1632
1981
  }
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");
1982
+ function inlineNode(token, ctx, marks) {
1983
+ switch (token.type) {
1984
+ case "text":
1985
+ case "escape": {
1986
+ const t = token;
1987
+ if ("tokens" in t && t.tokens?.length) return inline(t.tokens, ctx, marks);
1988
+ return textNode(t.text, marks);
1989
+ }
1990
+ case "strong": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strong" }));
1991
+ case "em": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "em" }));
1992
+ case "del": return inline(token.tokens, ctx, withUniqueMark(marks, { type: "strike" }));
1993
+ case "codespan": return textNode(token.text, withUniqueMark(marks, { type: "code" }));
1994
+ case "link": {
1995
+ const link = token;
1996
+ return inline(link.tokens, ctx, withUniqueMark(marks, {
1997
+ type: "link",
1998
+ attrs: { href: link.href }
1999
+ }));
2000
+ }
2001
+ case "br": return [{ type: "hardBreak" }];
2002
+ case "html": return textNode(token.text, marks);
2003
+ default: return "text" in token && token.text ? textNode(token.text, marks) : [];
2004
+ }
1635
2005
  }
1636
- function renderCodeBlock(node, indent) {
1637
- const lang = typeof node.attrs?.["language"] === "string" ? node.attrs["language"] : "";
1638
- const code = (node.content ?? []).map((n) => n.text ?? "").join("");
1639
- const fence = "```";
1640
- return prefixLines(`${fence}${lang}\n${code}\n${fence}`, indent);
2006
+ function textNode(text, marks) {
2007
+ if (text.length === 0) return [];
2008
+ const node = {
2009
+ type: "text",
2010
+ text
2011
+ };
2012
+ if (marks.length > 0) node.marks = marks;
2013
+ return [node];
1641
2014
  }
1642
- const PANEL_LABELS = {
1643
- info: "Info",
1644
- note: "Note",
1645
- warning: "Warning",
1646
- success: "Success",
1647
- error: "Error"
1648
- };
1649
- function renderPanel(node, ctx, indent) {
1650
- const type = typeof node.attrs?.["panelType"] === "string" ? node.attrs["panelType"] : "info";
1651
- return prefixLines(`**${PANEL_LABELS[type] ?? "Note"}**\n\n${renderBlocks(node.content ?? [], ctx, "")}`, `${indent}> `);
2015
+ function withUniqueMark(marks, mark) {
2016
+ return [...marks.filter((m) => m.type !== mark.type), mark];
1652
2017
  }
1653
- function renderExpand(node, ctx, indent) {
1654
- return `${indent}<details><summary>${typeof node.attrs?.["title"] === "string" ? node.attrs["title"] : "Details"}</summary>\n\n${renderBlocks(node.content ?? [], ctx, "")}\n\n${indent}</details>`;
2018
+ function externalMedia(href, alt) {
2019
+ return mediaNode({
2020
+ type: "external",
2021
+ url: href
2022
+ }, alt);
1655
2023
  }
1656
- function renderTable(node, ctx) {
1657
- const rows = (node.content ?? []).filter((n) => n.type === "tableRow");
1658
- if (rows.length === 0) return "";
1659
- const grid = rows.map((row) => (row.content ?? []).map((cell) => renderCellOnOneLine(cell, ctx)));
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;
2024
+ function textToken(text) {
2025
+ return {
2026
+ type: "text",
2027
+ raw: text,
2028
+ text,
2029
+ escaped: false
1665
2030
  };
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
- }
1671
- function renderCellOnOneLine(cell, ctx) {
1672
- return renderBlocks(cell.content ?? [], ctx, "").replace(/\n+/g, " ").replace(/\|/g, "\\|").trim();
1673
- }
1674
- function prefixLines(text, prefix) {
1675
- return text.split("\n").map((line) => line.length > 0 ? prefix + line : prefix.trimEnd()).join("\n");
1676
2031
  }
1677
2032
  function clampLevel(value) {
1678
- const n = toNumber(value) ?? 1;
1679
- return Math.min(6, Math.max(1, n));
1680
- }
1681
- function toNumber(value) {
1682
- return typeof value === "number" ? value : void 0;
2033
+ return Math.min(6, Math.max(1, Math.trunc(value) || 1));
1683
2034
  }
1684
2035
  //#endregion
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");
1719
- const out = [];
1720
- for (let i = 0; i < lines.length; i++) {
1721
- const line = lines[i] ?? "";
1722
- const fence = line.match(FENCE_OPEN);
1723
- if (!fence) {
1724
- out.push(highlightLine(line, lines[i + 1]));
1725
- continue;
1726
- }
1727
- const [, prefix = "", lang = ""] = fence;
1728
- const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
1729
- out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
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);
1740
- }
1741
- if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
1742
- const lead = prefixed[k] ? styledPrefix : "";
1743
- out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
1744
- });
1745
- if (j < lines.length) {
1746
- const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
1747
- out.push(`${lead}${kleur.dim(FENCE)}`);
1748
- }
1749
- i = j;
1750
- }
1751
- return out.join("\n");
2036
+ //#region src/api/confluence-attachments.ts
2037
+ const UNKNOWN_SIZE = -1;
2038
+ async function listAttachments(client, id) {
2039
+ return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.map((a) => ({
2040
+ filename: a.title ?? a.id,
2041
+ fileId: mediaNodeId(a),
2042
+ size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
2043
+ }));
1752
2044
  }
1753
- function highlightCode(code, lang) {
1754
- if (!lang || !lowlight.registered(lang)) return styleEachLine(code, kleur.yellow);
1755
- return renderTree(lowlight.highlight(lang, code), (text) => text);
2045
+ async function uploadAttachment(client, pageId, filename, bytes) {
2046
+ return (await client.postMultipart(`/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment`, filename, bytes)).results?.[0]?.extensions?.fileId ?? await fileIdByListing(client, pageId, filename);
1756
2047
  }
1757
- function renderTree(node, style) {
1758
- if (node.type === "text") return styleEachLine(node.value, style);
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("");
2048
+ async function fileIdByListing(client, pageId, filename) {
2049
+ const match = (await listAttachments(client, pageId)).find((a) => a.filename === filename);
2050
+ if (match) return match.fileId;
2051
+ throw new Error(`Upload of "${filename}" did not return a fileId.`);
1763
2052
  }
1764
- function tokenStyle(className) {
1765
- if (!Array.isArray(className)) return void 0;
1766
- for (const name of className) {
1767
- if (typeof name !== "string" || !name.startsWith("hljs-")) continue;
1768
- const style = TOKEN_STYLES[name.slice(5)];
1769
- if (style) return style;
1770
- }
2053
+ async function fetchAttachments(client, id) {
2054
+ return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.filter((a) => a.downloadLink).map((a) => ({
2055
+ mediaId: mediaNodeId(a),
2056
+ filename: a.title ?? a.id,
2057
+ url: withWikiContextPath(a.downloadLink ?? "")
2058
+ }));
1771
2059
  }
1772
- function styleEachLine(text, style) {
1773
- return text.split("\n").map((line) => line ? style(line) : line).join("\n");
2060
+ function mediaNodeId(a) {
2061
+ return a.fileId ?? a.id;
1774
2062
  }
1775
- function highlightLine(line, nextLine) {
1776
- const quote = line.match(QUOTE);
1777
- if (quote) {
1778
- const [whole, indent = ""] = quote;
1779
- const rest = line.slice(whole.length);
1780
- const gap = whole.endsWith(" ") ? " " : "";
1781
- return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
1782
- }
1783
- const list = line.match(LIST);
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);
2063
+ function withWikiContextPath(link) {
2064
+ if (link.startsWith("http") || link.startsWith("/wiki")) return link;
2065
+ return `/wiki${link}`;
1801
2066
  }
1802
- function stripPrefix(line, prefix) {
1803
- return line?.startsWith(prefix) ? line.slice(prefix.length) : void 0;
2067
+ //#endregion
2068
+ //#region src/api/confluence-pages.ts
2069
+ async function fetchPage(client, site, id) {
2070
+ const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
2071
+ const names = new UserNames(client);
2072
+ const [spaceKey, attachments, comments, author] = await Promise.all([
2073
+ fetchSpaceKey(client, page.spaceId),
2074
+ fetchAttachments(client, id),
2075
+ fetchComments$1(client, id, names),
2076
+ names.resolve(page.version?.authorId ?? page.authorId)
2077
+ ]);
2078
+ const webui = page._links?.webui ?? "";
2079
+ return {
2080
+ id: page.id,
2081
+ title: page.title,
2082
+ spaceKey,
2083
+ version: page.version?.number ?? 0,
2084
+ author,
2085
+ createdAt: page.createdAt ?? "",
2086
+ updatedAt: page.version?.createdAt ?? "",
2087
+ url: webui ? `${site}/wiki${webui}` : "",
2088
+ body: parseAdfJson(page.body?.atlas_doc_format?.value),
2089
+ attachments,
2090
+ comments
2091
+ };
1804
2092
  }
1805
- function tableRow(line, header) {
1806
- const styled = line.split(CELL_DIVIDER).slice(1, -1).map((cell) => header ? kleur.bold(highlightInline(cell)) : highlightInline(cell));
1807
- const pipe = kleur.dim("|");
1808
- return `${pipe}${styled.join(pipe)}${pipe}`;
2093
+ async function fetchPageState(client, id) {
2094
+ const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
2095
+ return {
2096
+ version: page.version?.number ?? 0,
2097
+ title: page.title,
2098
+ body: parseAdfJson(page.body?.atlas_doc_format?.value)
2099
+ };
1809
2100
  }
1810
- function highlightInline(text) {
1811
- return text.replace(INLINE, (match, code, linkText, url, boldItalic, bold, italic, strike) => {
1812
- if (code !== void 0) return kleur.yellow(code);
1813
- if (linkText !== void 0 && url !== void 0) return link(linkText, url);
1814
- if (boldItalic !== void 0) return kleur.bold().italic(match);
1815
- if (bold !== void 0) return kleur.bold(`**${highlightInline(bold)}**`);
1816
- if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
1817
- if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
1818
- return match;
1819
- });
2101
+ async function updatePage(client, id, params) {
2102
+ return (await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
2103
+ id,
2104
+ status: "current",
2105
+ title: params.title,
2106
+ body: {
2107
+ representation: "atlas_doc_format",
2108
+ value: JSON.stringify(params.body)
2109
+ },
2110
+ version: {
2111
+ number: params.nextVersion,
2112
+ message: params.message
2113
+ }
2114
+ })).version?.number ?? params.nextVersion;
1820
2115
  }
1821
- function link(text, url) {
1822
- const open = kleur.dim("[");
1823
- const middle = kleur.dim("](");
1824
- return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
2116
+ async function fetchSpaceKey(client, spaceId) {
2117
+ if (!spaceId) return "";
2118
+ try {
2119
+ return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
2120
+ } catch {
2121
+ return "";
2122
+ }
2123
+ }
2124
+ async function fetchComments$1(client, id, names) {
2125
+ const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
2126
+ return Promise.all(res.results.map(async (c) => ({
2127
+ author: await names.resolve(c.version?.authorId),
2128
+ created: c.version?.createdAt ?? "",
2129
+ body: parseAdfJson(c.body?.atlas_doc_format?.value)
2130
+ })));
2131
+ }
2132
+ function parseAdfJson(value) {
2133
+ if (!value) return null;
2134
+ try {
2135
+ return JSON.parse(value);
2136
+ } catch {
2137
+ return null;
2138
+ }
1825
2139
  }
2140
+ var UserNames = class {
2141
+ client;
2142
+ cache = /* @__PURE__ */ new Map();
2143
+ constructor(client) {
2144
+ this.client = client;
2145
+ }
2146
+ async resolve(accountId) {
2147
+ if (!accountId) return "";
2148
+ const cached = this.cache.get(accountId);
2149
+ if (cached !== void 0) return cached;
2150
+ const name = await this.fetchDisplayName(accountId) ?? accountId;
2151
+ this.cache.set(accountId, name);
2152
+ return name;
2153
+ }
2154
+ async fetchDisplayName(accountId) {
2155
+ try {
2156
+ return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
2157
+ } catch {
2158
+ return null;
2159
+ }
2160
+ }
2161
+ };
1826
2162
  //#endregion
1827
- //#region src/commands/view.ts
1828
- function fieldLines(pairs) {
1829
- const kept = pairs.filter(([, value]) => value !== "");
1830
- const width = Math.max(...kept.map(([label]) => label.length)) + 1;
1831
- return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
2163
+ //#region src/util/html.ts
2164
+ const MAX_CODE_POINT = 1114111;
2165
+ const NAMED = {
2166
+ amp: "&",
2167
+ lt: "<",
2168
+ gt: ">",
2169
+ quot: "\"",
2170
+ apos: "'",
2171
+ nbsp: " "
2172
+ };
2173
+ function decodeEntities(text) {
2174
+ return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
2175
+ if (body[0] === "#") {
2176
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
2177
+ return isCodePoint(code) ? String.fromCodePoint(code) : match;
2178
+ }
2179
+ return NAMED[body.toLowerCase()] ?? match;
2180
+ });
1832
2181
  }
1833
- function dateWithAge(iso, nowMs) {
1834
- if (!iso) return "";
1835
- return `${formatDateTime(iso).slice(0, 10)} (${relativeTime(iso, nowMs)})`;
2182
+ function isCodePoint(code) {
2183
+ return Number.isFinite(code) && code >= 0 && code <= MAX_CODE_POINT;
1836
2184
  }
1837
- function bodyLines(body) {
1838
- const md = adfToMarkdown(body);
1839
- return md ? ["", highlightMarkdown(md)] : [];
2185
+ //#endregion
2186
+ //#region src/api/confluence-search.ts
2187
+ async function searchPages(client, site, params) {
2188
+ const cql = buildCql(params);
2189
+ const query = new URLSearchParams({
2190
+ cql,
2191
+ limit: String(params.limit),
2192
+ expand: "space"
2193
+ });
2194
+ const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
2195
+ const serverPageWasFull = results.length === params.limit;
2196
+ return {
2197
+ pages: results.filter((r) => r.content?.id).map((r) => ({
2198
+ id: r.content?.id ?? "",
2199
+ space: r.space?.key ?? r.resultGlobalContainer?.title ?? "",
2200
+ title: decodeEntities(r.content?.title ?? r.title ?? ""),
2201
+ updated: r.lastModified ?? "",
2202
+ url: r.url ? `${site}/wiki${r.url}` : ""
2203
+ })),
2204
+ hasMore: serverPageWasFull
2205
+ };
1840
2206
  }
1841
- function commentSection(comments, allComments) {
1842
- if (comments.length === 0) return [];
1843
- const visible = allComments ? comments : comments.slice(-5);
1844
- const heading = comments.length - visible.length > 0 ? `Comments (${comments.length}, showing last ${visible.length} — --all-comments for all)` : `Comments (${comments.length})`;
1845
- const lines = ["", kleur.bold(heading)];
1846
- for (const comment of visible) {
1847
- const body = adfToMarkdown(comment.body);
1848
- lines.push("", commentHeader(comment), ...body ? [highlightMarkdown(body)] : []);
1849
- }
1850
- return lines;
2207
+ function buildCql(params) {
2208
+ if (params.cql) return params.cql;
2209
+ const clauses = ["type = page"];
2210
+ if (params.starred) clauses.push("favourite = currentUser()");
2211
+ if (params.space) clauses.push(`space = ${cqlStringLiteral(params.space)}`);
2212
+ if (params.text) clauses.push(`text ~ ${cqlStringLiteral(params.text)}`);
2213
+ return `${clauses.join(" AND ")} ORDER BY lastmodified DESC`;
1851
2214
  }
1852
- function commentHeader(comment) {
1853
- return `─ ${kleur.bold(comment.author || "Unknown")}${comment.created ? ` · ${kleur.dim(formatDateTime(comment.created))}` : ""}`;
2215
+ function cqlStringLiteral(value) {
2216
+ return `"${value.replace(/(["\\])/g, "\\$1")}"`;
1854
2217
  }
1855
- function attachmentSection(attachments) {
1856
- if (attachments.length === 0) return [];
1857
- return [
1858
- "",
1859
- kleur.bold("Attachments"),
1860
- ...attachments.map((a) => `- ${a.filename}`)
1861
- ];
2218
+ //#endregion
2219
+ //#region src/commands/resolve-ref.ts
2220
+ async function resolveRef(ask, arg, kind) {
2221
+ const raw = arg ?? await ask.text({
2222
+ message: kind.message,
2223
+ flag: kind.flag,
2224
+ required: true
2225
+ });
2226
+ const ref = kind.parse(raw);
2227
+ if (!ref) throw new Error(kind.notFound(raw));
2228
+ return ref;
1862
2229
  }
1863
2230
  //#endregion
1864
2231
  //#region src/api/attachments.ts
@@ -2376,7 +2743,7 @@ function formatPageView(page, nowMs, allComments) {
2376
2743
  ["URL", page.url]
2377
2744
  ]),
2378
2745
  ...bodyLines(page.body),
2379
- ...commentSection(page.comments, allComments),
2746
+ ...commentSection(renderedComments(page.comments), { allComments }),
2380
2747
  ...attachmentSection(page.attachments)
2381
2748
  ];
2382
2749
  }
@@ -3496,7 +3863,7 @@ function formatIssueView(issue, nowMs, allComments) {
3496
3863
  ["URL", issue.url]
3497
3864
  ]),
3498
3865
  ...bodyLines(issue.description),
3499
- ...commentSection(issue.comments, allComments),
3866
+ ...commentSection(renderedComments(issue.comments), { allComments }),
3500
3867
  ...attachmentSection(issue.attachments)
3501
3868
  ];
3502
3869
  }
@@ -3602,7 +3969,7 @@ function registerJira(jira) {
3602
3969
  }
3603
3970
  //#endregion
3604
3971
  //#region package.json
3605
- var version = "1.13.0";
3972
+ var version = "1.14.0";
3606
3973
  //#endregion
3607
3974
  //#region src/cli/build.ts
3608
3975
  const PRODUCTS = {