atlass 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,12 +44,19 @@ atlass bitbucket logout
44
44
  ```bash
45
45
  atlass jira view PROJ-123
46
46
  atlass jira view https://acme.atlassian.net/browse/PROJ-123
47
+ atlass confluence view 123456
48
+ atlass confluence view https://acme.atlassian.net/wiki/spaces/DEV/pages/123456/Title
47
49
  ```
48
50
 
49
- Prints the issue to the terminal: fields, description, the last five comments
50
- (`--all-comments` for all of them), and attachment names. The status is colored
51
- by category and the Markdown is syntax highlighted, code blocks included; color
52
- turns off automatically when piping, or with `NO_COLOR`, leaving plain Markdown.
51
+ Prints the issue or page to the terminal: fields, the body, the last five
52
+ comments (`--all-comments` for all of them), and attachment names. The Jira
53
+ status is colored by category and the Markdown is syntax highlighted, code
54
+ blocks included; color turns off automatically when piping, or with `NO_COLOR`,
55
+ leaving plain Markdown.
56
+
57
+ Output taller than the terminal goes through a pager, `$PAGER` or `less`, with
58
+ `LESS=FRX` unless you have set `LESS` yourself. Pass `--no-pager` to print
59
+ directly. Piped output is never paged.
53
60
 
54
61
  ## Copy
55
62
 
@@ -96,12 +103,18 @@ body sent is everything between the H1 and `## Comments`.
96
103
  atlass jira list
97
104
  atlass jira list --project PROJ
98
105
  atlass jira list --all
106
+ atlass confluence list
107
+ atlass confluence list --space DOCS
99
108
  ```
100
109
 
101
- Lists issues assigned to you as `KEY Status Age Summary`, with In Progress
102
- first, then To Do, and the most recently updated at the top of each group. Done
103
- issues are left out; `--all` adds those updated in the last 30 days. `--json`
104
- and `--copy` work as for search.
110
+ `jira list` shows issues assigned to you as `KEY Status Age Summary`, with
111
+ In Progress first, then To Do, and the most recently updated at the top of each
112
+ group. Done issues are left out; `--all` adds those updated in the last 30 days.
113
+
114
+ `confluence list` shows the pages you starred as `ID SPACE Age Title`, most
115
+ recently updated first. `--limit` works as for search.
116
+
117
+ For both, `--json` and `--copy` work as for search.
105
118
 
106
119
  ## Search
107
120
 
@@ -113,10 +126,11 @@ atlass confluence search "onboarding" --space DOCS
113
126
  atlass confluence search --cql "label = runbook ORDER BY created DESC"
114
127
  ```
115
128
 
116
- Filters combine with AND and sort by most recently updated. `--jql` and `--cql`
117
- replace the friendly filters. `--limit` defaults to 25, max 100. `--json` prints
118
- machine output. Add `--copy` to pick results interactively and copy each one,
119
- with `--out` as the target directory.
129
+ Filters combine with AND and sort by most recently updated. Rows use the same
130
+ columns as `jira list` and `confluence list`. `--jql` and `--cql` replace the
131
+ friendly filters. `--limit` defaults to 25, max 100. `--json` prints machine
132
+ output. Add `--copy` to pick results interactively and copy each one, with
133
+ `--out` as the target directory.
120
134
 
121
135
  Discovery aids for the filters:
122
136
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as buildStandalone, r as fail } from "./build-DhED0Zw_.mjs";
2
+ import { n as buildStandalone, r as fail } from "./build-Colq1d4N.mjs";
3
3
  //#region src/bitbucket.ts
4
4
  buildStandalone("bitbucket").parseAsync().catch(fail);
5
5
  //#endregion
@@ -4,11 +4,12 @@ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
6
  import { Entry } from "@napi-rs/keyring";
7
+ import kleur from "kleur";
7
8
  import { marked } from "marked";
8
9
  import { randomUUID } from "node:crypto";
9
10
  import { stripVTControlCharacters } from "node:util";
10
- import kleur from "kleur";
11
11
  import { common, createLowlight } from "lowlight";
12
+ import { spawn } from "node:child_process";
12
13
  //#region src/cli/run.ts
13
14
  const SIGINT_EXIT_CODE = 130;
14
15
  function run(fn) {
@@ -925,6 +926,7 @@ async function searchPages(client, site, params) {
925
926
  id: r.content?.id ?? "",
926
927
  space: r.space?.key ?? r.resultGlobalContainer?.title ?? "",
927
928
  title: decodeEntities(r.content?.title ?? r.title ?? ""),
929
+ updated: r.lastModified ?? "",
928
930
  url: r.url ? `${site}/wiki${r.url}` : ""
929
931
  })),
930
932
  hasMore: serverPageWasFull
@@ -933,6 +935,7 @@ async function searchPages(client, site, params) {
933
935
  function buildCql(params) {
934
936
  if (params.cql) return params.cql;
935
937
  const clauses = ["type = page"];
938
+ if (params.starred) clauses.push("favourite = currentUser()");
936
939
  if (params.space) clauses.push(`space = ${cqlStringLiteral(params.space)}`);
937
940
  if (params.text) clauses.push(`text ~ ${cqlStringLiteral(params.text)}`);
938
941
  return `${clauses.join(" AND ")} ORDER BY lastmodified DESC`;
@@ -1013,6 +1016,23 @@ async function resolveRef(arg, kind) {
1013
1016
  }
1014
1017
  //#endregion
1015
1018
  //#region src/commands/search-run.ts
1019
+ function alignedRows(items, nowMs, cells) {
1020
+ const rows = items.map((item) => ({
1021
+ ...cells(item),
1022
+ age: relativeTime(item.updated, nowMs),
1023
+ json: { ...item }
1024
+ }));
1025
+ const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
1026
+ const idWidth = width((row) => row.id);
1027
+ const labelWidth = width((row) => row.label);
1028
+ const ageWidth = width((row) => row.age);
1029
+ return rows.map((row) => ({
1030
+ id: row.id,
1031
+ fixedColumns: `${row.id.padEnd(idWidth)} ${row.color(row.label.padEnd(labelWidth))} ${row.age.padEnd(ageWidth)}`,
1032
+ freeText: row.text,
1033
+ json: row.json
1034
+ }));
1035
+ }
1016
1036
  const COPY_CONCURRENCY = 5;
1017
1037
  async function runSearch(rows, options, noun, copyOne) {
1018
1038
  if (options.json) {
@@ -1240,6 +1260,185 @@ function formatDate(timestamp) {
1240
1260
  return new Date(ms).toISOString().slice(0, 10);
1241
1261
  }
1242
1262
  //#endregion
1263
+ //#region src/markdown/highlight.ts
1264
+ const HEADING = /^#{1,6} /;
1265
+ const QUOTE = /^(\s*)>( |$)/;
1266
+ const LIST = /^(\s*)(-|\d+\.) (\[[ x]\] )?/;
1267
+ const RULE = /^\s*---$/;
1268
+ const TABLE_ROW = /^\|.*\|$/;
1269
+ const TABLE_SEPARATOR = /^\|( --- \|)+$/;
1270
+ const CELL_DIVIDER = /(?<!\\)\|/;
1271
+ const DETAILS_OPEN = /^(\s*)<details><summary>(.*)<\/summary>$/;
1272
+ const DETAILS_CLOSE = /^\s*<\/details>$/;
1273
+ const FENCE_OPEN = /^((?:\s|> |- |\d+\. )*)```(\S*)$/;
1274
+ const FENCE = "```";
1275
+ const INLINE = /(`[^`]+`)|\[([^\]]+)\]\(((?:[^()]|\([^()]*\))+)\)|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(?!\*)(.+?)(?<!\*)\*(?!\*)|~~(.+?)~~/g;
1276
+ const TOKEN_STYLES = {
1277
+ keyword: kleur.magenta,
1278
+ built_in: kleur.magenta,
1279
+ literal: kleur.magenta,
1280
+ string: kleur.green,
1281
+ regexp: kleur.green,
1282
+ addition: kleur.green,
1283
+ number: kleur.yellow,
1284
+ symbol: kleur.yellow,
1285
+ meta: kleur.yellow,
1286
+ attr: kleur.yellow,
1287
+ comment: kleur.dim,
1288
+ quote: kleur.dim,
1289
+ deletion: kleur.dim,
1290
+ title: kleur.cyan,
1291
+ type: kleur.cyan,
1292
+ section: kleur.cyan
1293
+ };
1294
+ const lowlight = createLowlight(common);
1295
+ function highlightMarkdown(md) {
1296
+ const lines = md.split("\n");
1297
+ const out = [];
1298
+ for (let i = 0; i < lines.length; i++) {
1299
+ const line = lines[i] ?? "";
1300
+ const fence = line.match(FENCE_OPEN);
1301
+ if (!fence) {
1302
+ out.push(highlightLine(line, lines[i + 1]));
1303
+ continue;
1304
+ }
1305
+ const [, prefix = "", lang = ""] = fence;
1306
+ const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
1307
+ out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
1308
+ const code = [];
1309
+ const prefixed = [];
1310
+ let j = i + 1;
1311
+ for (; j < lines.length; j++) {
1312
+ const raw = lines[j] ?? "";
1313
+ const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
1314
+ const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
1315
+ if (body === FENCE) break;
1316
+ code.push(body);
1317
+ prefixed.push(hasPrefix);
1318
+ }
1319
+ if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
1320
+ const lead = prefixed[k] ? styledPrefix : "";
1321
+ out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
1322
+ });
1323
+ if (j < lines.length) {
1324
+ const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
1325
+ out.push(`${lead}${kleur.dim(FENCE)}`);
1326
+ }
1327
+ i = j;
1328
+ }
1329
+ return out.join("\n");
1330
+ }
1331
+ function highlightCode(code, lang) {
1332
+ if (!lang || !lowlight.registered(lang)) return styleEachLine(code, kleur.yellow);
1333
+ return renderTree(lowlight.highlight(lang, code), (text) => text);
1334
+ }
1335
+ function renderTree(node, style) {
1336
+ if (node.type === "text") return styleEachLine(node.value, style);
1337
+ if (node.type !== "root" && node.type !== "element") return "";
1338
+ const own = node.type === "element" ? tokenStyle(node.properties["className"]) : void 0;
1339
+ const next = own ? (text) => style(own(text)) : style;
1340
+ return node.children.map((child) => renderTree(child, next)).join("");
1341
+ }
1342
+ function tokenStyle(className) {
1343
+ if (!Array.isArray(className)) return void 0;
1344
+ for (const name of className) {
1345
+ if (typeof name !== "string" || !name.startsWith("hljs-")) continue;
1346
+ const style = TOKEN_STYLES[name.slice(5)];
1347
+ if (style) return style;
1348
+ }
1349
+ }
1350
+ function styleEachLine(text, style) {
1351
+ return text.split("\n").map((line) => line ? style(line) : line).join("\n");
1352
+ }
1353
+ function highlightLine(line, nextLine) {
1354
+ const quote = line.match(QUOTE);
1355
+ if (quote) {
1356
+ const [whole, indent = ""] = quote;
1357
+ const rest = line.slice(whole.length);
1358
+ const gap = whole.endsWith(" ") ? " " : "";
1359
+ return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
1360
+ }
1361
+ const list = line.match(LIST);
1362
+ if (list) {
1363
+ const [whole, indent = "", marker = "", checkbox] = list;
1364
+ const rest = line.slice(whole.length);
1365
+ const box = checkbox ? `${kleur.bold(checkbox.trim())} ` : "";
1366
+ return `${indent}${kleur.dim(marker)} ${box}${highlightLine(rest, void 0)}`;
1367
+ }
1368
+ if (HEADING.test(line)) return kleur.bold().cyan(line);
1369
+ if (RULE.test(line)) return kleur.dim(line);
1370
+ if (TABLE_SEPARATOR.test(line)) return kleur.dim(line);
1371
+ if (TABLE_ROW.test(line)) return tableRow(line, nextLine !== void 0 && TABLE_SEPARATOR.test(nextLine));
1372
+ const details = line.match(DETAILS_OPEN);
1373
+ if (details) {
1374
+ const [, indent = "", title = ""] = details;
1375
+ return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
1376
+ }
1377
+ if (DETAILS_CLOSE.test(line)) return kleur.dim(line);
1378
+ return highlightInline(line);
1379
+ }
1380
+ function stripPrefix(line, prefix) {
1381
+ return line?.startsWith(prefix) ? line.slice(prefix.length) : void 0;
1382
+ }
1383
+ function tableRow(line, header) {
1384
+ const styled = line.split(CELL_DIVIDER).slice(1, -1).map((cell) => header ? kleur.bold(highlightInline(cell)) : highlightInline(cell));
1385
+ const pipe = kleur.dim("|");
1386
+ return `${pipe}${styled.join(pipe)}${pipe}`;
1387
+ }
1388
+ function highlightInline(text) {
1389
+ return text.replace(INLINE, (match, code, linkText, url, boldItalic, bold, italic, strike) => {
1390
+ if (code !== void 0) return kleur.yellow(code);
1391
+ if (linkText !== void 0 && url !== void 0) return link(linkText, url);
1392
+ if (boldItalic !== void 0) return kleur.bold().italic(match);
1393
+ if (bold !== void 0) return kleur.bold(`**${highlightInline(bold)}**`);
1394
+ if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
1395
+ if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
1396
+ return match;
1397
+ });
1398
+ }
1399
+ function link(text, url) {
1400
+ const open = kleur.dim("[");
1401
+ const middle = kleur.dim("](");
1402
+ return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
1403
+ }
1404
+ //#endregion
1405
+ //#region src/commands/view.ts
1406
+ function fieldLines(pairs) {
1407
+ const kept = pairs.filter(([, value]) => value !== "");
1408
+ const width = Math.max(...kept.map(([label]) => label.length)) + 1;
1409
+ return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
1410
+ }
1411
+ function dateWithAge(iso, nowMs) {
1412
+ if (!iso) return "";
1413
+ return `${formatDateTime(iso).slice(0, 10)} (${relativeTime(iso, nowMs)})`;
1414
+ }
1415
+ function bodyLines(body) {
1416
+ const md = adfToMarkdown(body);
1417
+ return md ? ["", highlightMarkdown(md)] : [];
1418
+ }
1419
+ function commentSection(comments, allComments) {
1420
+ if (comments.length === 0) return [];
1421
+ const visible = allComments ? comments : comments.slice(-5);
1422
+ const heading = comments.length - visible.length > 0 ? `Comments (${comments.length}, showing last ${visible.length} — --all-comments for all)` : `Comments (${comments.length})`;
1423
+ const lines = ["", kleur.bold(heading)];
1424
+ for (const comment of visible) {
1425
+ const body = adfToMarkdown(comment.body);
1426
+ lines.push("", commentHeader(comment), ...body ? [highlightMarkdown(body)] : []);
1427
+ }
1428
+ return lines;
1429
+ }
1430
+ function commentHeader(comment) {
1431
+ return `─ ${kleur.bold(comment.author || "Unknown")}${comment.created ? ` · ${kleur.dim(formatDateTime(comment.created))}` : ""}`;
1432
+ }
1433
+ function attachmentSection(attachments) {
1434
+ if (attachments.length === 0) return [];
1435
+ return [
1436
+ "",
1437
+ kleur.bold("Attachments"),
1438
+ ...attachments.map((a) => `- ${a.filename}`)
1439
+ ];
1440
+ }
1441
+ //#endregion
1243
1442
  //#region src/api/attachments.ts
1244
1443
  function mediaResolver(downloaded) {
1245
1444
  const byMediaId = new Map(downloaded.map((d) => [d.mediaId, d.relativePath]));
@@ -1734,7 +1933,79 @@ async function runPlan(plan, options, push) {
1734
1933
  await push();
1735
1934
  }
1736
1935
  //#endregion
1936
+ //#region src/util/pager.ts
1937
+ const DEFAULT_ROWS = 24;
1938
+ const DEFAULT_COLUMNS = 80;
1939
+ const DEFAULT_PAGER = "less";
1940
+ const DEFAULT_LESS = "FRX";
1941
+ function renderedHeight(text, columns) {
1942
+ const width = Math.max(1, columns);
1943
+ return text.split("\n").map((line) => Math.max(1, Math.ceil(stripVTControlCharacters(line).length / width))).reduce((sum, rows) => sum + rows, 0);
1944
+ }
1945
+ function shouldPage(text, term) {
1946
+ if (!term.isTTY) return false;
1947
+ const rows = term.rows ?? DEFAULT_ROWS;
1948
+ return renderedHeight(text, term.columns ?? DEFAULT_COLUMNS) > rows;
1949
+ }
1950
+ function pagerCommand(env) {
1951
+ const [command = DEFAULT_PAGER, ...args] = (env["PAGER"] ?? "").trim().split(/\s+/).filter(Boolean);
1952
+ return {
1953
+ command,
1954
+ args,
1955
+ env: {
1956
+ ...env,
1957
+ LESS: env["LESS"] ?? DEFAULT_LESS
1958
+ }
1959
+ };
1960
+ }
1961
+ async function printPaged(text, options = {}) {
1962
+ const term = options.term ?? process.stdout;
1963
+ if (options.pager === false || !shouldPage(text, term)) {
1964
+ console.log(text);
1965
+ return;
1966
+ }
1967
+ if (!await pipeToPager(text, pagerCommand(options.env ?? process.env))) console.log(text);
1968
+ }
1969
+ function pipeToPager(text, pager) {
1970
+ return new Promise((resolve) => {
1971
+ const child = spawn(pager.command, pager.args, {
1972
+ stdio: [
1973
+ "pipe",
1974
+ "inherit",
1975
+ "inherit"
1976
+ ],
1977
+ env: pager.env
1978
+ });
1979
+ child.once("error", () => resolve(false));
1980
+ child.once("close", () => resolve(true));
1981
+ child.stdin.once("error", () => {});
1982
+ child.stdin.end(`${text}\n`);
1983
+ });
1984
+ }
1985
+ //#endregion
1737
1986
  //#region src/commands/confluence.ts
1987
+ async function confluenceView(arg, options) {
1988
+ const auth = await requireAuth();
1989
+ const id = await resolveRef(arg, PAGE_REF);
1990
+ await printPaged(formatPageView(await fetchPage(new AtlassianClient(auth), auth.site, id), Date.now(), options.allComments ?? false).join("\n"), { pager: options.pager });
1991
+ }
1992
+ function formatPageView(page, nowMs, allComments) {
1993
+ return [
1994
+ kleur.bold(page.title),
1995
+ ...fieldLines([
1996
+ ["Space", page.spaceKey],
1997
+ ["ID", page.id],
1998
+ ["Version", page.version ? String(page.version) : ""],
1999
+ ["Author", page.author],
2000
+ ["Created", dateWithAge(page.createdAt, nowMs)],
2001
+ ["Updated", dateWithAge(page.updatedAt, nowMs)],
2002
+ ["URL", page.url]
2003
+ ]),
2004
+ ...bodyLines(page.body),
2005
+ ...commentSection(page.comments, allComments),
2006
+ ...attachmentSection(page.attachments)
2007
+ ];
2008
+ }
1738
2009
  async function confluenceCopy(arg, options) {
1739
2010
  const auth = await requireAuth();
1740
2011
  const id = await resolveRef(arg, PAGE_REF);
@@ -1786,37 +2057,59 @@ async function fileSize(path) {
1786
2057
  }
1787
2058
  async function confluenceSearch(query, options) {
1788
2059
  if (options.cql && (query || options.space)) throw new Error("--cql cannot be combined with a text query or --space.");
2060
+ await listPages({
2061
+ text: query,
2062
+ space: options.space,
2063
+ cql: options.cql
2064
+ }, "No matching pages.", options);
2065
+ }
2066
+ async function confluenceList(options) {
2067
+ await listPages({
2068
+ starred: true,
2069
+ space: options.space
2070
+ }, options.space ? `No starred pages in ${options.space}.` : "No starred pages.", options);
2071
+ }
2072
+ async function listPages(filter, empty, options) {
1789
2073
  if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
1790
2074
  const auth = await requireAuth();
1791
2075
  const client = new AtlassianClient(auth);
1792
2076
  const limit = parseLimit(options.limit);
1793
2077
  const { pages, hasMore } = await searchPages(client, auth.site, {
1794
- text: query,
1795
- space: options.space,
1796
- cql: options.cql,
2078
+ ...filter,
1797
2079
  limit
1798
2080
  });
1799
- await runSearch(pages.map((p) => ({
1800
- id: p.id,
1801
- fixedColumns: `${p.id} ${p.space}`,
1802
- freeText: p.title,
1803
- json: {
1804
- id: p.id,
1805
- space: p.space,
1806
- title: p.title,
1807
- url: p.url
1808
- }
1809
- })), {
2081
+ await runSearch(formatPageRows(pages, Date.now()), {
1810
2082
  json: options.json,
1811
2083
  copy: options.copy,
1812
2084
  out: options.out,
1813
- empty: "No matching pages.",
2085
+ empty,
1814
2086
  footer: hasMore ? searchFooter(limit) : void 0
1815
- }, {
1816
- singular: "page",
1817
- plural: "pages"
1818
- }, (id) => copyPage(client, auth.site, id, options.out));
2087
+ }, PAGE_NOUN, (id) => copyPage(client, auth.site, id, options.out));
2088
+ }
2089
+ function formatPageRows(pages, nowMs) {
2090
+ return alignedRows(pages, nowMs, (p) => ({
2091
+ id: p.id,
2092
+ label: p.space,
2093
+ color: colorForSpace(p.space),
2094
+ text: p.title
2095
+ }));
1819
2096
  }
2097
+ const SPACE_COLORS = [
2098
+ kleur.cyan,
2099
+ kleur.yellow,
2100
+ kleur.green,
2101
+ kleur.magenta,
2102
+ kleur.blue
2103
+ ];
2104
+ function colorForSpace(space) {
2105
+ let hash = 0;
2106
+ for (const char of space) hash = hash * 31 + (char.codePointAt(0) ?? 0) >>> 0;
2107
+ return SPACE_COLORS[hash % SPACE_COLORS.length] ?? kleur.cyan;
2108
+ }
2109
+ const PAGE_NOUN = {
2110
+ singular: "page",
2111
+ plural: "pages"
2112
+ };
1820
2113
  async function copyPage(client, site, id, out) {
1821
2114
  console.log(`Fetching page ${id} ...`);
1822
2115
  await runCopy(planPageCopy(await fetchPage(client, site, id), out), (url) => client.getBinary(url));
@@ -1830,8 +2123,10 @@ const PAGE_REF = {
1830
2123
  //#region src/cli/confluence.ts
1831
2124
  function registerConfluence(confluence) {
1832
2125
  confluence.description("Confluence commands");
2126
+ 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(run(confluenceView));
1833
2127
  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(run(confluenceCopy));
1834
2128
  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(run(confluenceUpdate));
2129
+ 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(run(confluenceList));
1835
2130
  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(run(confluenceSearch));
1836
2131
  return confluence;
1837
2132
  }
@@ -2034,148 +2329,6 @@ async function fetchComments(client, key) {
2034
2329
  }));
2035
2330
  }
2036
2331
  //#endregion
2037
- //#region src/markdown/highlight.ts
2038
- const HEADING = /^#{1,6} /;
2039
- const QUOTE = /^(\s*)>( |$)/;
2040
- const LIST = /^(\s*)(-|\d+\.) (\[[ x]\] )?/;
2041
- const RULE = /^\s*---$/;
2042
- const TABLE_ROW = /^\|.*\|$/;
2043
- const TABLE_SEPARATOR = /^\|( --- \|)+$/;
2044
- const CELL_DIVIDER = /(?<!\\)\|/;
2045
- const DETAILS_OPEN = /^(\s*)<details><summary>(.*)<\/summary>$/;
2046
- const DETAILS_CLOSE = /^\s*<\/details>$/;
2047
- const FENCE_OPEN = /^((?:\s|> |- |\d+\. )*)```(\S*)$/;
2048
- const FENCE = "```";
2049
- const INLINE = /(`[^`]+`)|\[([^\]]+)\]\(((?:[^()]|\([^()]*\))+)\)|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(?!\*)(.+?)(?<!\*)\*(?!\*)|~~(.+?)~~/g;
2050
- const TOKEN_STYLES = {
2051
- keyword: kleur.magenta,
2052
- built_in: kleur.magenta,
2053
- literal: kleur.magenta,
2054
- string: kleur.green,
2055
- regexp: kleur.green,
2056
- addition: kleur.green,
2057
- number: kleur.yellow,
2058
- symbol: kleur.yellow,
2059
- meta: kleur.yellow,
2060
- attr: kleur.yellow,
2061
- comment: kleur.dim,
2062
- quote: kleur.dim,
2063
- deletion: kleur.dim,
2064
- title: kleur.cyan,
2065
- type: kleur.cyan,
2066
- section: kleur.cyan
2067
- };
2068
- const lowlight = createLowlight(common);
2069
- function highlightMarkdown(md) {
2070
- const lines = md.split("\n");
2071
- const out = [];
2072
- for (let i = 0; i < lines.length; i++) {
2073
- const line = lines[i] ?? "";
2074
- const fence = line.match(FENCE_OPEN);
2075
- if (!fence) {
2076
- out.push(highlightLine(line, lines[i + 1]));
2077
- continue;
2078
- }
2079
- const [, prefix = "", lang = ""] = fence;
2080
- const styledPrefix = prefix.replace(/\S+/g, (marker) => kleur.dim(marker));
2081
- out.push(`${styledPrefix}${kleur.dim(`${FENCE}${lang}`)}`);
2082
- const code = [];
2083
- const prefixed = [];
2084
- let j = i + 1;
2085
- for (; j < lines.length; j++) {
2086
- const raw = lines[j] ?? "";
2087
- const hasPrefix = raw.startsWith(prefix) || raw === prefix.trimEnd();
2088
- const body = raw.startsWith(prefix) ? raw.slice(prefix.length) : hasPrefix ? "" : raw;
2089
- if (body === FENCE) break;
2090
- code.push(body);
2091
- prefixed.push(hasPrefix);
2092
- }
2093
- if (code.length > 0) highlightCode(code.join("\n"), lang).split("\n").forEach((codeLine, k) => {
2094
- const lead = prefixed[k] ? styledPrefix : "";
2095
- out.push(codeLine ? `${lead}${codeLine}` : lead.trimEnd());
2096
- });
2097
- if (j < lines.length) {
2098
- const lead = (lines[j] ?? "").startsWith(prefix) ? styledPrefix : "";
2099
- out.push(`${lead}${kleur.dim(FENCE)}`);
2100
- }
2101
- i = j;
2102
- }
2103
- return out.join("\n");
2104
- }
2105
- function highlightCode(code, lang) {
2106
- if (!lang || !lowlight.registered(lang)) return styleEachLine(code, kleur.yellow);
2107
- return renderTree(lowlight.highlight(lang, code), (text) => text);
2108
- }
2109
- function renderTree(node, style) {
2110
- if (node.type === "text") return styleEachLine(node.value, style);
2111
- if (node.type !== "root" && node.type !== "element") return "";
2112
- const own = node.type === "element" ? tokenStyle(node.properties["className"]) : void 0;
2113
- const next = own ? (text) => style(own(text)) : style;
2114
- return node.children.map((child) => renderTree(child, next)).join("");
2115
- }
2116
- function tokenStyle(className) {
2117
- if (!Array.isArray(className)) return void 0;
2118
- for (const name of className) {
2119
- if (typeof name !== "string" || !name.startsWith("hljs-")) continue;
2120
- const style = TOKEN_STYLES[name.slice(5)];
2121
- if (style) return style;
2122
- }
2123
- }
2124
- function styleEachLine(text, style) {
2125
- return text.split("\n").map((line) => line ? style(line) : line).join("\n");
2126
- }
2127
- function highlightLine(line, nextLine) {
2128
- const quote = line.match(QUOTE);
2129
- if (quote) {
2130
- const [whole, indent = ""] = quote;
2131
- const rest = line.slice(whole.length);
2132
- const gap = whole.endsWith(" ") ? " " : "";
2133
- return `${indent}${kleur.dim(">")}${gap}${highlightLine(rest, stripPrefix(nextLine, whole))}`;
2134
- }
2135
- const list = line.match(LIST);
2136
- if (list) {
2137
- const [whole, indent = "", marker = "", checkbox] = list;
2138
- const rest = line.slice(whole.length);
2139
- const box = checkbox ? `${kleur.bold(checkbox.trim())} ` : "";
2140
- return `${indent}${kleur.dim(marker)} ${box}${highlightLine(rest, void 0)}`;
2141
- }
2142
- if (HEADING.test(line)) return kleur.bold().cyan(line);
2143
- if (RULE.test(line)) return kleur.dim(line);
2144
- if (TABLE_SEPARATOR.test(line)) return kleur.dim(line);
2145
- if (TABLE_ROW.test(line)) return tableRow(line, nextLine !== void 0 && TABLE_SEPARATOR.test(nextLine));
2146
- const details = line.match(DETAILS_OPEN);
2147
- if (details) {
2148
- const [, indent = "", title = ""] = details;
2149
- return `${indent}${kleur.dim("<details><summary>")}${highlightInline(title)}${kleur.dim("</summary>")}`;
2150
- }
2151
- if (DETAILS_CLOSE.test(line)) return kleur.dim(line);
2152
- return highlightInline(line);
2153
- }
2154
- function stripPrefix(line, prefix) {
2155
- return line?.startsWith(prefix) ? line.slice(prefix.length) : void 0;
2156
- }
2157
- function tableRow(line, header) {
2158
- const styled = line.split(CELL_DIVIDER).slice(1, -1).map((cell) => header ? kleur.bold(highlightInline(cell)) : highlightInline(cell));
2159
- const pipe = kleur.dim("|");
2160
- return `${pipe}${styled.join(pipe)}${pipe}`;
2161
- }
2162
- function highlightInline(text) {
2163
- return text.replace(INLINE, (match, code, linkText, url, boldItalic, bold, italic, strike) => {
2164
- if (code !== void 0) return kleur.yellow(code);
2165
- if (linkText !== void 0 && url !== void 0) return link(linkText, url);
2166
- if (boldItalic !== void 0) return kleur.bold().italic(match);
2167
- if (bold !== void 0) return kleur.bold(`**${highlightInline(bold)}**`);
2168
- if (italic !== void 0) return kleur.italic(`*${highlightInline(italic)}*`);
2169
- if (strike !== void 0) return kleur.strikethrough(`~~${highlightInline(strike)}~~`);
2170
- return match;
2171
- });
2172
- }
2173
- function link(text, url) {
2174
- const open = kleur.dim("[");
2175
- const middle = kleur.dim("](");
2176
- return `${open}${highlightInline(text)}${middle}${kleur.dim().underline(url)}${kleur.dim(")")}`;
2177
- }
2178
- //#endregion
2179
2332
  //#region src/commands/jira.ts
2180
2333
  async function jiraProjects(query, options) {
2181
2334
  const auth = await requireAuth();
@@ -2217,8 +2370,7 @@ function formatStatusRows(statuses) {
2217
2370
  async function jiraView(arg, options) {
2218
2371
  const auth = await requireAuth();
2219
2372
  const key = await resolveRef(arg, ISSUE_REF);
2220
- const issue = await fetchIssue(new AtlassianClient(auth), auth.site, key);
2221
- for (const line of formatIssueView(issue, Date.now(), options.allComments ?? false)) console.log(line);
2373
+ await printPaged(formatIssueView(await fetchIssue(new AtlassianClient(auth), auth.site, key), Date.now(), options.allComments ?? false).join("\n"), { pager: options.pager });
2222
2374
  }
2223
2375
  const CATEGORY_COLORS = {
2224
2376
  new: kleur.cyan,
@@ -2229,59 +2381,25 @@ function colorForCategory(category) {
2229
2381
  return CATEGORY_COLORS[category] ?? ((text) => text);
2230
2382
  }
2231
2383
  function formatIssueView(issue, nowMs, allComments) {
2384
+ const colorStatus = colorForCategory(issue.statusCategory);
2232
2385
  return [
2233
2386
  `${kleur.bold(issue.key)} ${issue.summary}`,
2234
- ...fieldLines(issue, nowMs),
2235
- ...bodyLines(adfToMarkdown(issue.description)),
2387
+ ...fieldLines([
2388
+ ["Type", issue.type],
2389
+ ["Status", issue.status && colorStatus(issue.status)],
2390
+ ["Assignee", issue.assignee],
2391
+ ["Reporter", issue.reporter],
2392
+ ["Priority", issue.priority],
2393
+ ["Labels", issue.labels.join(", ")],
2394
+ ["Created", dateWithAge(issue.created, nowMs)],
2395
+ ["Updated", dateWithAge(issue.updated, nowMs)],
2396
+ ["URL", issue.url]
2397
+ ]),
2398
+ ...bodyLines(issue.description),
2236
2399
  ...commentSection(issue.comments, allComments),
2237
2400
  ...attachmentSection(issue.attachments)
2238
2401
  ];
2239
2402
  }
2240
- function fieldLines(issue, nowMs) {
2241
- const colorStatus = colorForCategory(issue.statusCategory);
2242
- const kept = [
2243
- ["Type", issue.type],
2244
- ["Status", issue.status && colorStatus(issue.status)],
2245
- ["Assignee", issue.assignee],
2246
- ["Reporter", issue.reporter],
2247
- ["Priority", issue.priority],
2248
- ["Labels", issue.labels.join(", ")],
2249
- ["Created", dateWithAge(issue.created, nowMs)],
2250
- ["Updated", dateWithAge(issue.updated, nowMs)],
2251
- ["URL", issue.url]
2252
- ].filter(([, value]) => value !== "");
2253
- const width = Math.max(...kept.map(([label]) => label.length)) + 1;
2254
- return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
2255
- }
2256
- function dateWithAge(iso, nowMs) {
2257
- if (!iso) return "";
2258
- return `${formatDateTime(iso).slice(0, 10)} (${relativeTime(iso, nowMs)})`;
2259
- }
2260
- function bodyLines(body) {
2261
- return body ? ["", highlightMarkdown(body)] : [];
2262
- }
2263
- function commentSection(comments, allComments) {
2264
- if (comments.length === 0) return [];
2265
- const visible = allComments ? comments : comments.slice(-5);
2266
- const heading = comments.length - visible.length > 0 ? `Comments (${comments.length}, showing last ${visible.length} — --all-comments for all)` : `Comments (${comments.length})`;
2267
- const lines = ["", kleur.bold(heading)];
2268
- for (const comment of visible) {
2269
- const body = adfToMarkdown(comment.body);
2270
- lines.push("", commentHeader(comment), ...body ? [highlightMarkdown(body)] : []);
2271
- }
2272
- return lines;
2273
- }
2274
- function commentHeader(comment) {
2275
- return `─ ${kleur.bold(comment.author || "Unknown")}${comment.created ? ` · ${kleur.dim(formatDateTime(comment.created))}` : ""}`;
2276
- }
2277
- function attachmentSection(attachments) {
2278
- if (attachments.length === 0) return [];
2279
- return [
2280
- "",
2281
- kleur.bold("Attachments"),
2282
- ...attachments.map((a) => `- ${a.filename}`)
2283
- ];
2284
- }
2285
2403
  async function jiraCopy(arg, options) {
2286
2404
  const auth = await requireAuth();
2287
2405
  const key = await resolveRef(arg, ISSUE_REF);
@@ -2318,17 +2436,7 @@ async function jiraSearch(query, options) {
2318
2436
  jql: options.jql,
2319
2437
  limit
2320
2438
  });
2321
- await runSearch(issues.map((i) => ({
2322
- id: i.key,
2323
- fixedColumns: `${i.key} ${i.status}`,
2324
- freeText: i.summary,
2325
- json: {
2326
- key: i.key,
2327
- status: i.status,
2328
- summary: i.summary,
2329
- url: i.url
2330
- }
2331
- })), {
2439
+ await runSearch(formatIssueRows(issues, Date.now()), {
2332
2440
  json: options.json,
2333
2441
  copy: options.copy,
2334
2442
  out: options.out,
@@ -2344,7 +2452,7 @@ async function jiraList(options) {
2344
2452
  all: options.all,
2345
2453
  project: options.project
2346
2454
  });
2347
- await runSearch(formatListRows(sortByCategoryThenUpdated(issues), Date.now()), {
2455
+ await runSearch(formatIssueRows(sortByCategoryThenUpdated(issues), Date.now()), {
2348
2456
  json: options.json,
2349
2457
  copy: options.copy,
2350
2458
  out: options.out,
@@ -2352,21 +2460,13 @@ async function jiraList(options) {
2352
2460
  footer: truncated ? `\nShowing the first ${issues.length}; narrow with --project.` : void 0
2353
2461
  }, ISSUE_NOUN, (key) => copyIssue(client, auth.site, key, options.out));
2354
2462
  }
2355
- function formatListRows(issues, nowMs) {
2356
- const ages = issues.map((i) => relativeTime(i.updated, nowMs));
2357
- const width = (values) => Math.max(...values.map((v) => v.length));
2358
- const keyWidth = width(issues.map((i) => i.key));
2359
- const statusWidth = width(issues.map((i) => i.status));
2360
- const ageWidth = width(ages);
2361
- return issues.map((i, n) => {
2362
- const status = colorForCategory(i.statusCategory)(i.status.padEnd(statusWidth));
2363
- return {
2364
- id: i.key,
2365
- fixedColumns: `${i.key.padEnd(keyWidth)} ${status} ${(ages[n] ?? "").padEnd(ageWidth)}`,
2366
- freeText: i.summary,
2367
- json: { ...i }
2368
- };
2369
- });
2463
+ function formatIssueRows(issues, nowMs) {
2464
+ return alignedRows(issues, nowMs, (i) => ({
2465
+ id: i.key,
2466
+ label: i.status,
2467
+ color: colorForCategory(i.statusCategory),
2468
+ text: i.summary
2469
+ }));
2370
2470
  }
2371
2471
  const ISSUE_NOUN = {
2372
2472
  singular: "issue",
@@ -2387,7 +2487,7 @@ function registerJira(jira) {
2387
2487
  jira.description("Jira commands");
2388
2488
  jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(run(jiraProjects));
2389
2489
  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(run(jiraStatuses));
2390
- 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").action(run(jiraView));
2490
+ 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(run(jiraView));
2391
2491
  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(run(jiraCopy));
2392
2492
  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(run(jiraUpdate));
2393
2493
  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(run(jiraList));
@@ -2396,7 +2496,7 @@ function registerJira(jira) {
2396
2496
  }
2397
2497
  //#endregion
2398
2498
  //#region package.json
2399
- var version = "1.9.0";
2499
+ var version = "1.10.0";
2400
2500
  //#endregion
2401
2501
  //#region src/cli/build.ts
2402
2502
  const PRODUCTS = {
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as fail, t as buildAtlass } from "./build-DhED0Zw_.mjs";
2
+ import { r as fail, t as buildAtlass } from "./build-Colq1d4N.mjs";
3
3
  //#region src/cli.ts
4
4
  buildAtlass().parseAsync().catch(fail);
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as buildStandalone, r as fail } from "./build-DhED0Zw_.mjs";
2
+ import { n as buildStandalone, r as fail } from "./build-Colq1d4N.mjs";
3
3
  //#region src/confluence.ts
4
4
  buildStandalone("confluence").parseAsync().catch(fail);
5
5
  //#endregion
package/dist/jira.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as buildStandalone, r as fail } from "./build-DhED0Zw_.mjs";
2
+ import { n as buildStandalone, r as fail } from "./build-Colq1d4N.mjs";
3
3
  //#region src/jira.ts
4
4
  buildStandalone("jira").parseAsync().catch(fail);
5
5
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlass",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "CLI to copy Jira issues and Confluence pages to Markdown, and update Confluence pages.",
5
5
  "license": "MIT",
6
6
  "repository": {