atlass 1.13.0 → 2.0.0

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