atlass 1.7.0 → 1.8.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 +13 -0
- package/dist/cli.mjs +111 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,19 @@ body sent is everything between the H1 and `## Comments`.
|
|
|
86
86
|
- Confluence uploads local images referenced in the body as attachments. Jira
|
|
87
87
|
update does not support image changes yet.
|
|
88
88
|
|
|
89
|
+
## List
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
atlass jira list
|
|
93
|
+
atlass jira list --project PROJ
|
|
94
|
+
atlass jira list --all
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Lists issues assigned to you as `KEY Status Age Summary`, with In Progress
|
|
98
|
+
first, then To Do, and the most recently updated at the top of each group. Done
|
|
99
|
+
issues are left out; `--all` adds those updated in the last 30 days. `--json`
|
|
100
|
+
and `--copy` work as for search.
|
|
101
|
+
|
|
89
102
|
## Search
|
|
90
103
|
|
|
91
104
|
```bash
|
package/dist/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
7
7
|
import { Entry } from "@napi-rs/keyring";
|
|
8
8
|
import { marked } from "marked";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { stripVTControlCharacters } from "node:util";
|
|
10
11
|
import kleur from "kleur";
|
|
11
12
|
import { common, createLowlight } from "lowlight";
|
|
12
13
|
//#region src/api/client.ts
|
|
@@ -980,7 +981,7 @@ async function runSearch(rows, options, noun, copyOne) {
|
|
|
980
981
|
return;
|
|
981
982
|
}
|
|
982
983
|
if (rows.length === 0) {
|
|
983
|
-
console.log(
|
|
984
|
+
console.log(options.empty);
|
|
984
985
|
return;
|
|
985
986
|
}
|
|
986
987
|
if (options.copy) {
|
|
@@ -989,7 +990,11 @@ async function runSearch(rows, options, noun, copyOne) {
|
|
|
989
990
|
return;
|
|
990
991
|
}
|
|
991
992
|
for (const row of rows) console.log(formatRow(row));
|
|
992
|
-
if (options.
|
|
993
|
+
if (options.footer) console.log(options.footer);
|
|
994
|
+
}
|
|
995
|
+
function searchFooter(limit) {
|
|
996
|
+
return `
|
|
997
|
+
Showing first ${limit}; refine with flags or raise --limit.`;
|
|
993
998
|
}
|
|
994
999
|
async function copySelected(rows, noun, copyOne) {
|
|
995
1000
|
if (!process.stdin.isTTY) throw new Error("--copy requires an interactive terminal.");
|
|
@@ -1022,7 +1027,7 @@ async function copySelected(rows, noun, copyOne) {
|
|
|
1022
1027
|
else console.log(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
|
|
1023
1028
|
}
|
|
1024
1029
|
function formatRow(row) {
|
|
1025
|
-
const room = (process.stdout.columns ?? 80) - row.fixedColumns.length - 2;
|
|
1030
|
+
const room = (process.stdout.columns ?? 80) - stripVTControlCharacters(row.fixedColumns).length - 2;
|
|
1026
1031
|
const text = room > 0 ? truncate(row.freeText, room) : "";
|
|
1027
1032
|
return text ? `${row.fixedColumns} ${text}` : row.fixedColumns;
|
|
1028
1033
|
}
|
|
@@ -1765,9 +1770,9 @@ async function confluenceSearch(query, options) {
|
|
|
1765
1770
|
})), {
|
|
1766
1771
|
json: options.json,
|
|
1767
1772
|
copy: options.copy,
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1773
|
+
out: options.out,
|
|
1774
|
+
empty: "No matching pages.",
|
|
1775
|
+
footer: hasMore ? searchFooter(limit) : void 0
|
|
1771
1776
|
}, {
|
|
1772
1777
|
singular: "page",
|
|
1773
1778
|
plural: "pages"
|
|
@@ -1832,19 +1837,28 @@ async function updateIssue(client, key, update) {
|
|
|
1832
1837
|
if (update.summary !== void 0) fields["summary"] = update.summary;
|
|
1833
1838
|
await client.putNoContent(`/rest/api/3/issue/${encodeURIComponent(key)}`, { fields });
|
|
1834
1839
|
}
|
|
1840
|
+
const SEARCH_FIELDS = "summary,status,updated";
|
|
1835
1841
|
async function searchIssues(client, site, params) {
|
|
1836
|
-
|
|
1842
|
+
return ((await fetchSearchPage(client, buildJql(params), params.limit)).issues ?? []).map((i) => toIssueSummary(site, i));
|
|
1843
|
+
}
|
|
1844
|
+
async function fetchSearchPage(client, jql, maxResults, nextPageToken) {
|
|
1837
1845
|
const query = new URLSearchParams({
|
|
1838
1846
|
jql,
|
|
1839
|
-
maxResults: String(
|
|
1840
|
-
fields:
|
|
1847
|
+
maxResults: String(maxResults),
|
|
1848
|
+
fields: SEARCH_FIELDS
|
|
1841
1849
|
});
|
|
1842
|
-
|
|
1850
|
+
if (nextPageToken) query.set("nextPageToken", nextPageToken);
|
|
1851
|
+
return client.getJson(`/rest/api/3/search/jql?${query.toString()}`);
|
|
1852
|
+
}
|
|
1853
|
+
function toIssueSummary(site, i) {
|
|
1854
|
+
return {
|
|
1843
1855
|
key: i.key,
|
|
1844
1856
|
status: i.fields?.status?.name ?? "",
|
|
1857
|
+
statusCategory: i.fields?.status?.statusCategory?.key ?? "",
|
|
1845
1858
|
summary: decodeEntities(i.fields?.summary ?? ""),
|
|
1859
|
+
updated: i.fields?.updated ?? "",
|
|
1846
1860
|
url: browseUrl(site, i.key)
|
|
1847
|
-
}
|
|
1861
|
+
};
|
|
1848
1862
|
}
|
|
1849
1863
|
function buildJql(params) {
|
|
1850
1864
|
if (params.jql) return params.jql;
|
|
@@ -1856,6 +1870,46 @@ function buildJql(params) {
|
|
|
1856
1870
|
if (clauses.length === 0) clauses.push(RECENT_ISSUES_CLAUSE);
|
|
1857
1871
|
return `${clauses.join(" AND ")} ORDER BY updated DESC`;
|
|
1858
1872
|
}
|
|
1873
|
+
const OPEN_CLAUSE = "statusCategory != Done";
|
|
1874
|
+
function buildListJql(params) {
|
|
1875
|
+
const clauses = ["assignee = currentUser()"];
|
|
1876
|
+
if (params.project) clauses.push(`project = ${jqlStringLiteral(params.project)}`);
|
|
1877
|
+
clauses.push(params.all ? `(${OPEN_CLAUSE} OR ${RECENT_ISSUES_CLAUSE})` : OPEN_CLAUSE);
|
|
1878
|
+
return `${clauses.join(" AND ")} ORDER BY updated DESC`;
|
|
1879
|
+
}
|
|
1880
|
+
const LIST_PAGE_SIZE = 100;
|
|
1881
|
+
const LIST_CAP = 500;
|
|
1882
|
+
async function listAssignedIssues(client, site, params) {
|
|
1883
|
+
const jql = buildListJql(params);
|
|
1884
|
+
const issues = [];
|
|
1885
|
+
let nextPageToken;
|
|
1886
|
+
for (;;) {
|
|
1887
|
+
const room = LIST_CAP - issues.length;
|
|
1888
|
+
const res = await fetchSearchPage(client, jql, Math.min(LIST_PAGE_SIZE, room), nextPageToken);
|
|
1889
|
+
const page = res.issues ?? [];
|
|
1890
|
+
issues.push(...page.map((i) => toIssueSummary(site, i)));
|
|
1891
|
+
if (res.isLast || !res.nextPageToken || page.length === 0) return {
|
|
1892
|
+
issues,
|
|
1893
|
+
truncated: false
|
|
1894
|
+
};
|
|
1895
|
+
if (issues.length >= LIST_CAP) return {
|
|
1896
|
+
issues,
|
|
1897
|
+
truncated: true
|
|
1898
|
+
};
|
|
1899
|
+
nextPageToken = res.nextPageToken;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const LIST_CATEGORY_ORDER = {
|
|
1903
|
+
indeterminate: 0,
|
|
1904
|
+
new: 1,
|
|
1905
|
+
done: 2
|
|
1906
|
+
};
|
|
1907
|
+
const LIST_UNKNOWN_CATEGORY_RANK = Object.keys(LIST_CATEGORY_ORDER).length;
|
|
1908
|
+
function sortByCategoryThenUpdated(issues) {
|
|
1909
|
+
const rank = (i) => LIST_CATEGORY_ORDER[i.statusCategory] ?? LIST_UNKNOWN_CATEGORY_RANK;
|
|
1910
|
+
const updatedMs = (i) => Date.parse(i.updated) || 0;
|
|
1911
|
+
return [...issues].sort((a, b) => rank(a) - rank(b) || updatedMs(b) - updatedMs(a));
|
|
1912
|
+
}
|
|
1859
1913
|
const PROJECT_PAGE_SIZE = 50;
|
|
1860
1914
|
async function listProjects(client, site, query) {
|
|
1861
1915
|
const projects = [];
|
|
@@ -2123,6 +2177,9 @@ const CATEGORY_COLORS = {
|
|
|
2123
2177
|
indeterminate: kleur.yellow,
|
|
2124
2178
|
done: kleur.green
|
|
2125
2179
|
};
|
|
2180
|
+
function colorForCategory(category) {
|
|
2181
|
+
return CATEGORY_COLORS[category] ?? ((text) => text);
|
|
2182
|
+
}
|
|
2126
2183
|
function formatIssueView(issue, nowMs, allComments) {
|
|
2127
2184
|
return [
|
|
2128
2185
|
`${kleur.bold(issue.key)} ${issue.summary}`,
|
|
@@ -2133,7 +2190,7 @@ function formatIssueView(issue, nowMs, allComments) {
|
|
|
2133
2190
|
];
|
|
2134
2191
|
}
|
|
2135
2192
|
function fieldLines(issue, nowMs) {
|
|
2136
|
-
const colorStatus =
|
|
2193
|
+
const colorStatus = colorForCategory(issue.statusCategory);
|
|
2137
2194
|
const kept = [
|
|
2138
2195
|
["Type", issue.type],
|
|
2139
2196
|
["Status", issue.status && colorStatus(issue.status)],
|
|
@@ -2226,14 +2283,47 @@ async function jiraSearch(query, options) {
|
|
|
2226
2283
|
})), {
|
|
2227
2284
|
json: options.json,
|
|
2228
2285
|
copy: options.copy,
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
},
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2286
|
+
out: options.out,
|
|
2287
|
+
empty: "No matching issues.",
|
|
2288
|
+
footer: issues.length === limit ? searchFooter(limit) : void 0
|
|
2289
|
+
}, ISSUE_NOUN, (key) => copyIssue(client, auth.site, key, options.out));
|
|
2290
|
+
}
|
|
2291
|
+
async function jiraList(options) {
|
|
2292
|
+
if (options.json && options.copy) throw new Error("--json and --copy cannot be used together.");
|
|
2293
|
+
const auth = await requireAuth();
|
|
2294
|
+
const client = new AtlassianClient(auth);
|
|
2295
|
+
const { issues, truncated } = await listAssignedIssues(client, auth.site, {
|
|
2296
|
+
all: options.all,
|
|
2297
|
+
project: options.project
|
|
2298
|
+
});
|
|
2299
|
+
await runSearch(formatListRows(sortByCategoryThenUpdated(issues), Date.now()), {
|
|
2300
|
+
json: options.json,
|
|
2301
|
+
copy: options.copy,
|
|
2302
|
+
out: options.out,
|
|
2303
|
+
empty: options.all ? "No issues assigned to you." : "No open issues assigned to you.",
|
|
2304
|
+
footer: truncated ? `\nShowing the first ${issues.length}; narrow with --project.` : void 0
|
|
2305
|
+
}, ISSUE_NOUN, (key) => copyIssue(client, auth.site, key, options.out));
|
|
2306
|
+
}
|
|
2307
|
+
function formatListRows(issues, nowMs) {
|
|
2308
|
+
const ages = issues.map((i) => relativeTime(i.updated, nowMs));
|
|
2309
|
+
const width = (values) => Math.max(...values.map((v) => v.length));
|
|
2310
|
+
const keyWidth = width(issues.map((i) => i.key));
|
|
2311
|
+
const statusWidth = width(issues.map((i) => i.status));
|
|
2312
|
+
const ageWidth = width(ages);
|
|
2313
|
+
return issues.map((i, n) => {
|
|
2314
|
+
const status = colorForCategory(i.statusCategory)(i.status.padEnd(statusWidth));
|
|
2315
|
+
return {
|
|
2316
|
+
id: i.key,
|
|
2317
|
+
fixedColumns: `${i.key.padEnd(keyWidth)} ${status} ${(ages[n] ?? "").padEnd(ageWidth)}`,
|
|
2318
|
+
freeText: i.summary,
|
|
2319
|
+
json: { ...i }
|
|
2320
|
+
};
|
|
2321
|
+
});
|
|
2236
2322
|
}
|
|
2323
|
+
const ISSUE_NOUN = {
|
|
2324
|
+
singular: "issue",
|
|
2325
|
+
plural: "issues"
|
|
2326
|
+
};
|
|
2237
2327
|
async function copyIssue(client, site, key, out) {
|
|
2238
2328
|
console.log(`Fetching ${key} ...`);
|
|
2239
2329
|
await runCopy(planIssueCopy(await fetchIssue(client, site, key), out), (url) => client.getBinary(url));
|
|
@@ -2245,7 +2335,7 @@ const ISSUE_REF = {
|
|
|
2245
2335
|
};
|
|
2246
2336
|
//#endregion
|
|
2247
2337
|
//#region package.json
|
|
2248
|
-
var version = "1.
|
|
2338
|
+
var version = "1.8.0";
|
|
2249
2339
|
//#endregion
|
|
2250
2340
|
//#region src/cli.ts
|
|
2251
2341
|
const program = new Command();
|
|
@@ -2260,6 +2350,7 @@ jira.command("statuses [query]").description("List statuses (optionally filtered
|
|
|
2260
2350
|
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));
|
|
2261
2351
|
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));
|
|
2262
2352
|
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));
|
|
2353
|
+
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));
|
|
2263
2354
|
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(run(jiraSearch));
|
|
2264
2355
|
const confluence = program.command("confluence").description("Confluence commands");
|
|
2265
2356
|
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));
|