atlass 2.1.0 → 2.2.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 +56 -4
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-_4VuIcqF.mjs → build-H7eTLrf3.mjs} +495 -140
- package/dist/cli.mjs +1 -1
- package/dist/confluence.mjs +1 -1
- package/dist/jira.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,6 +157,7 @@ atlass confluence list --space DOCS
|
|
|
157
157
|
`jira list` shows issues assigned to you as `KEY Status Age Summary`, with
|
|
158
158
|
In Progress first, then To Do, and the most recently updated at the top of each
|
|
159
159
|
group. Done issues are left out; `--all` adds those updated in the last 30 days.
|
|
160
|
+
`--limit` keeps the first rows of that order; without it, all are shown.
|
|
160
161
|
|
|
161
162
|
`confluence list` shows the pages you starred as `ID SPACE Age Title`, most
|
|
162
163
|
recently updated first. `--limit` works as for search. Star or unstar a page
|
|
@@ -175,10 +176,12 @@ atlass confluence search "onboarding" --space DOCS --label runbook
|
|
|
175
176
|
```
|
|
176
177
|
|
|
177
178
|
Jira filters on `--project`, `--assignee`, `--reporter`, `--status`, `--type`,
|
|
178
|
-
`--label`, `--updated` and `--open`; Confluence on `--space`,
|
|
179
|
-
`--updated` and `--starred`. Repeating a flag matches any of its
|
|
180
|
-
different flags must all match. `--assignee` and `--reporter` take `me`,
|
|
181
|
-
account id, or a name to look up. `--
|
|
179
|
+
`--label`, `--sprint`, `--updated` and `--open`; Confluence on `--space`,
|
|
180
|
+
`--label`, `--updated` and `--starred`. Repeating a flag matches any of its
|
|
181
|
+
values; different flags must all match. `--assignee` and `--reporter` take `me`,
|
|
182
|
+
an account id, or a name to look up. `--sprint` takes `active`, `future`, a
|
|
183
|
+
sprint id, or a sprint name; names can repeat across boards, an id cannot.
|
|
184
|
+
`--updated` takes `7d`, `2w`, `3m` or
|
|
182
185
|
`YYYY-MM-DD`. Results sort by most recently updated and use the same columns
|
|
183
186
|
as `jira list` and `confluence list`.
|
|
184
187
|
|
|
@@ -193,6 +196,19 @@ atlass confluence cql "label = runbook AND creator = currentUser()"
|
|
|
193
196
|
to pick results interactively and copy each one, with `--out` as the target
|
|
194
197
|
directory. All four work on both search commands.
|
|
195
198
|
|
|
199
|
+
`--field` adds a field's value to each issue on `jira search`, `jira jql` and
|
|
200
|
+
`jira list`:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
atlass jira search --assignee me --status Closed --field "Story Points" --json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
It takes a field id or a name, ignoring case, and repeats for more fields. A
|
|
207
|
+
name several fields share fails and lists their ids; `atlass jira fields
|
|
208
|
+
--search` finds the one you want. Each field becomes a column after the status.
|
|
209
|
+
With `--json`, each issue gains a `fields` object of raw values keyed by the
|
|
210
|
+
name you passed.
|
|
211
|
+
|
|
196
212
|
Discovery aids for the filters:
|
|
197
213
|
|
|
198
214
|
```bash
|
|
@@ -200,6 +216,42 @@ atlass jira projects [query]
|
|
|
200
216
|
atlass jira statuses [query] [--project PROJ]
|
|
201
217
|
```
|
|
202
218
|
|
|
219
|
+
## Boards
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
atlass jira boards [query] [--project PROJ]
|
|
223
|
+
atlass jira board <id|url|name> [--json]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`boards` lists every board as `ID Type Name Project`, filtered by name and
|
|
227
|
+
project key. Boards that span projects leave the project cell empty.
|
|
228
|
+
|
|
229
|
+
`board` shows one board: the field it estimates with, its filter JQL, and each
|
|
230
|
+
column's statuses with their categories. A name must match exactly, ignoring
|
|
231
|
+
case; if several boards share it, use the id.
|
|
232
|
+
|
|
233
|
+
## Sprints
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
atlass jira sprints <id|url|name> [--state active|future|closed ...] [--limit N] [--json]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Lists a board's sprints as `ID State Name Start End`, active only unless
|
|
240
|
+
`--state` says otherwise. Active sprints come first, then future, then closed
|
|
241
|
+
newest first. The board takes the same forms as `board`.
|
|
242
|
+
|
|
243
|
+
## Fields
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
atlass jira fields [--search TEXT] [--json]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
With no project, `fields` lists every field on the site as `ID Name Type`,
|
|
250
|
+
sorted by name. `--search` filters by name or id, ignoring case. The type is the
|
|
251
|
+
custom field's plugin type, such as `float` or `gh-sprint`, or the schema type
|
|
252
|
+
for system fields. `--json` carries the full schema, plugin key included. With a
|
|
253
|
+
project, `fields` shows the create form instead (see Create).
|
|
254
|
+
|
|
203
255
|
## Bitbucket
|
|
204
256
|
|
|
205
257
|
`--repo` takes `workspace/slug` or a bare slug under the configured workspace,
|
package/dist/bitbucket.mjs
CHANGED
|
@@ -29,7 +29,7 @@ var HttpError = class extends Error {
|
|
|
29
29
|
};
|
|
30
30
|
function httpError(status, path, body, loginHint) {
|
|
31
31
|
if (status === 401 || status === 403) return new HttpError(status, `Authentication failed (401/403). Run \`${loginHint}\` to update your token.`, void 0, path);
|
|
32
|
-
if (status === 404) return new HttpError(status, `Not found (404): ${path}
|
|
32
|
+
if (status === 404) return new HttpError(status, `Not found (404): ${path}`, void 0, path);
|
|
33
33
|
if (status === 409) return new HttpError(status, `Conflict (409): ${extractErrorMessage(body) || "the page changed on the server"}`);
|
|
34
34
|
if (status === 413) return new HttpError(status, "Payload too large (413): the page or an attachment exceeds the size limit.");
|
|
35
35
|
if (status === 400) return new HttpError(status, `Bad request (400): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
@@ -447,14 +447,58 @@ function guarded(fn) {
|
|
|
447
447
|
};
|
|
448
448
|
}
|
|
449
449
|
function fail(err) {
|
|
450
|
-
if (isPromptInterrupt(err))
|
|
450
|
+
if (isPromptInterrupt(err)) {
|
|
451
|
+
process.exitCode = SIGINT_EXIT_CODE;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
451
454
|
ttyTerminal().err(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
452
|
-
process.
|
|
455
|
+
process.exitCode = 1;
|
|
453
456
|
}
|
|
454
457
|
function isPromptInterrupt(err) {
|
|
455
458
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
456
459
|
}
|
|
457
460
|
//#endregion
|
|
461
|
+
//#region src/util/format.ts
|
|
462
|
+
function formatDuration(seconds) {
|
|
463
|
+
if (seconds == null) return "-";
|
|
464
|
+
if (seconds < 60) return `${seconds}s`;
|
|
465
|
+
const m = Math.floor(seconds / 60);
|
|
466
|
+
const s = seconds % 60;
|
|
467
|
+
if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
|
|
468
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
469
|
+
}
|
|
470
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
471
|
+
function formatDate$1(iso) {
|
|
472
|
+
const date = new Date(iso);
|
|
473
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
474
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
475
|
+
}
|
|
476
|
+
function formatDateTime(iso) {
|
|
477
|
+
const date = new Date(iso);
|
|
478
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
479
|
+
return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
480
|
+
}
|
|
481
|
+
const MINUTE = 60;
|
|
482
|
+
const HOUR = 3600;
|
|
483
|
+
const DAY = 24 * HOUR;
|
|
484
|
+
function relativeTime(iso, nowMs) {
|
|
485
|
+
if (!iso) return "-";
|
|
486
|
+
const then = Date.parse(iso);
|
|
487
|
+
if (Number.isNaN(then)) return "-";
|
|
488
|
+
const secs = Math.floor((nowMs - then) / 1e3);
|
|
489
|
+
if (secs < MINUTE) return "just now";
|
|
490
|
+
if (secs < HOUR) return `${Math.floor(secs / MINUTE)}m ago`;
|
|
491
|
+
if (secs < DAY) return `${Math.floor(secs / HOUR)}h ago`;
|
|
492
|
+
const days = Math.floor(secs / DAY);
|
|
493
|
+
if (days < 45) return `${days}d ago`;
|
|
494
|
+
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
|
|
495
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
496
|
+
}
|
|
497
|
+
function orList(items) {
|
|
498
|
+
if (items.length < 3) return items.join(" or ");
|
|
499
|
+
return `${items.slice(0, -1).join(", ")}, or ${items.at(-1)}`;
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
458
502
|
//#region src/util/parse.ts
|
|
459
503
|
function parseIssueKey(input) {
|
|
460
504
|
const match = input.toUpperCase().match(/[A-Z][A-Z0-9]+-\d+/);
|
|
@@ -469,6 +513,13 @@ function parsePageId(input) {
|
|
|
469
513
|
if (fromQuery) return fromQuery[1];
|
|
470
514
|
return null;
|
|
471
515
|
}
|
|
516
|
+
function parseBoardId(input) {
|
|
517
|
+
const trimmed = input.trim();
|
|
518
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
519
|
+
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
520
|
+
const match = trimmed.match(/\/boards\/(\d+)(?:[/?#]|$)/) ?? trimmed.match(/[?&]rapidView=(\d+)/);
|
|
521
|
+
return match ? Number(match[1]) : null;
|
|
522
|
+
}
|
|
472
523
|
const PULL_REQUEST_HOSTS = ["bitbucket.org", "www.bitbucket.org"];
|
|
473
524
|
const PULL_REQUEST_PATH = /^\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)(?:\/|$)/;
|
|
474
525
|
function parsePullRequestRef(input) {
|
|
@@ -522,6 +573,19 @@ function resolveRepo(flag, config) {
|
|
|
522
573
|
};
|
|
523
574
|
throw new Error(`No repo given. Pass --repo workspace/slug (or a bare slug), or set a default repo at \`atlass bitbucket login\`.`);
|
|
524
575
|
}
|
|
576
|
+
function parseChoices(values, choices, flag) {
|
|
577
|
+
const picked = /* @__PURE__ */ new Set();
|
|
578
|
+
const raw = (values ?? []).flatMap((v) => v.split(",")).map((v) => v.trim());
|
|
579
|
+
for (const value of raw.filter(Boolean)) {
|
|
580
|
+
const choice = choices.find((c) => c.toLowerCase() === value.toLowerCase());
|
|
581
|
+
if (!choice) {
|
|
582
|
+
const expected = orList(choices.map((c) => c.toLowerCase()));
|
|
583
|
+
throw new Error(`Invalid ${flag} "${value}". Expected ${expected}.`);
|
|
584
|
+
}
|
|
585
|
+
picked.add(choice);
|
|
586
|
+
}
|
|
587
|
+
return choices.filter((c) => picked.has(c));
|
|
588
|
+
}
|
|
525
589
|
const DEFAULT_LIMIT = 25;
|
|
526
590
|
const MAX_LIMIT = 100;
|
|
527
591
|
function parseLimit(value) {
|
|
@@ -768,15 +832,7 @@ const PULL_REQUEST_STATES = [
|
|
|
768
832
|
"SUPERSEDED"
|
|
769
833
|
];
|
|
770
834
|
function parsePullRequestStates(values) {
|
|
771
|
-
|
|
772
|
-
const states = [];
|
|
773
|
-
for (const value of raw) {
|
|
774
|
-
const state = value.trim().toUpperCase();
|
|
775
|
-
if (!state) continue;
|
|
776
|
-
if (!PULL_REQUEST_STATES.includes(state)) throw new Error(`Invalid --state "${value.trim()}". Expected ${PULL_REQUEST_STATES.map((s) => s.toLowerCase()).join(", ").replace(/, (?=[^,]*$)/, ", or ")}.`);
|
|
777
|
-
if (!states.includes(state)) states.push(state);
|
|
778
|
-
}
|
|
779
|
-
return states;
|
|
835
|
+
return parseChoices(values, PULL_REQUEST_STATES, "--state");
|
|
780
836
|
}
|
|
781
837
|
function allPullRequestStates() {
|
|
782
838
|
return [...PULL_REQUEST_STATES];
|
|
@@ -942,43 +998,6 @@ async function listChangedFiles(client, ref, id) {
|
|
|
942
998
|
return collectCapped(client, `${pullRequestPath(ref, id, "diffstat")}?pagelen=${PAGELEN}`, 50, toChangedFile);
|
|
943
999
|
}
|
|
944
1000
|
//#endregion
|
|
945
|
-
//#region src/util/format.ts
|
|
946
|
-
function formatDuration(seconds) {
|
|
947
|
-
if (seconds == null) return "-";
|
|
948
|
-
if (seconds < 60) return `${seconds}s`;
|
|
949
|
-
const m = Math.floor(seconds / 60);
|
|
950
|
-
const s = seconds % 60;
|
|
951
|
-
if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`;
|
|
952
|
-
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
953
|
-
}
|
|
954
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
955
|
-
function formatDate$1(iso) {
|
|
956
|
-
const date = new Date(iso);
|
|
957
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
958
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
959
|
-
}
|
|
960
|
-
function formatDateTime(iso) {
|
|
961
|
-
const date = new Date(iso);
|
|
962
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
963
|
-
return `${formatDate$1(iso)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
964
|
-
}
|
|
965
|
-
const MINUTE = 60;
|
|
966
|
-
const HOUR = 3600;
|
|
967
|
-
const DAY = 24 * HOUR;
|
|
968
|
-
function relativeTime(iso, nowMs) {
|
|
969
|
-
if (!iso) return "-";
|
|
970
|
-
const then = Date.parse(iso);
|
|
971
|
-
if (Number.isNaN(then)) return "-";
|
|
972
|
-
const secs = Math.floor((nowMs - then) / 1e3);
|
|
973
|
-
if (secs < MINUTE) return "just now";
|
|
974
|
-
if (secs < HOUR) return `${Math.floor(secs / MINUTE)}m ago`;
|
|
975
|
-
if (secs < DAY) return `${Math.floor(secs / HOUR)}h ago`;
|
|
976
|
-
const days = Math.floor(secs / DAY);
|
|
977
|
-
if (days < 45) return `${days}d ago`;
|
|
978
|
-
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
|
|
979
|
-
return `${Math.floor(days / 365)}y ago`;
|
|
980
|
-
}
|
|
981
|
-
//#endregion
|
|
982
1001
|
//#region src/commands/bitbucket-comments.ts
|
|
983
1002
|
const MENTION = /@\{([^}]+)\}/g;
|
|
984
1003
|
function renderedPrComments(comments, participants) {
|
|
@@ -1580,6 +1599,7 @@ function alignedRows(items, nowMs, cells) {
|
|
|
1580
1599
|
const cell = cells(item);
|
|
1581
1600
|
return {
|
|
1582
1601
|
...cell,
|
|
1602
|
+
columns: cell.columns ?? [],
|
|
1583
1603
|
age: relativeTime(cell.timestamp, nowMs),
|
|
1584
1604
|
json: { ...item }
|
|
1585
1605
|
};
|
|
@@ -1587,11 +1607,17 @@ function alignedRows(items, nowMs, cells) {
|
|
|
1587
1607
|
const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
|
|
1588
1608
|
const idWidth = width((row) => row.id);
|
|
1589
1609
|
const labelWidth = width((row) => row.label);
|
|
1610
|
+
const columnWidths = (rows[0]?.columns ?? []).map((_, i) => width((row) => row.columns[i]));
|
|
1590
1611
|
const ageWidth = width((row) => row.age);
|
|
1591
1612
|
return rows.map((row) => ({
|
|
1592
1613
|
id: row.id,
|
|
1593
1614
|
url: row.url,
|
|
1594
|
-
fixedColumns:
|
|
1615
|
+
fixedColumns: [
|
|
1616
|
+
row.id.padEnd(idWidth),
|
|
1617
|
+
row.color(row.label.padEnd(labelWidth)),
|
|
1618
|
+
...row.columns.map((column, i) => column.padEnd(columnWidths[i])),
|
|
1619
|
+
row.age.padEnd(ageWidth)
|
|
1620
|
+
].join(" "),
|
|
1595
1621
|
freeText: row.text,
|
|
1596
1622
|
json: row.json
|
|
1597
1623
|
}));
|
|
@@ -1894,8 +1920,8 @@ function moved(flags, command) {
|
|
|
1894
1920
|
throw new Error(`${flag} is now its own command: \`atlass ${command} "<query>"\`.`);
|
|
1895
1921
|
});
|
|
1896
1922
|
}
|
|
1897
|
-
function outputOptions(command) {
|
|
1898
|
-
return command.option("-l, --limit <n>",
|
|
1923
|
+
function outputOptions(command, limitHelp = "max results (default 25, max 100)") {
|
|
1924
|
+
return command.option("-l, --limit <n>", limitHelp).option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy");
|
|
1899
1925
|
}
|
|
1900
1926
|
//#endregion
|
|
1901
1927
|
//#region src/api/confluence-create.ts
|
|
@@ -3074,6 +3100,11 @@ function inClause(field, given) {
|
|
|
3074
3100
|
function userClause(field, given) {
|
|
3075
3101
|
return renderClause(field, values(given).map((v) => v === "me" ? "currentUser()" : quote(v)));
|
|
3076
3102
|
}
|
|
3103
|
+
function anyOf(terms) {
|
|
3104
|
+
if (terms.length === 0) return null;
|
|
3105
|
+
if (terms.length === 1) return terms[0];
|
|
3106
|
+
return `(${terms.join(" OR ")})`;
|
|
3107
|
+
}
|
|
3077
3108
|
function renderClause(field, rendered) {
|
|
3078
3109
|
if (rendered.length === 0) return null;
|
|
3079
3110
|
if (rendered.length === 1) return `${field} = ${rendered[0]}`;
|
|
@@ -3336,10 +3367,203 @@ function registerConfluence(confluence) {
|
|
|
3336
3367
|
return confluence;
|
|
3337
3368
|
}
|
|
3338
3369
|
//#endregion
|
|
3370
|
+
//#region src/api/jira-statuses.ts
|
|
3371
|
+
async function listStatuses(client, project) {
|
|
3372
|
+
return dedupeAndSortStatuses((project ? await fetchProjectStatuses(client, project) : await fetchAllStatuses(client)).map(toStatusSummary));
|
|
3373
|
+
}
|
|
3374
|
+
async function statusesById(client) {
|
|
3375
|
+
const raw = await fetchAllStatuses(client);
|
|
3376
|
+
return new Map(raw.map((s) => [s.id, toStatusSummary(s)]));
|
|
3377
|
+
}
|
|
3378
|
+
function fetchAllStatuses(client) {
|
|
3379
|
+
return client.getJson("/rest/api/3/status");
|
|
3380
|
+
}
|
|
3381
|
+
async function fetchProjectStatuses(client, project) {
|
|
3382
|
+
let groups;
|
|
3383
|
+
try {
|
|
3384
|
+
groups = await client.getJson(`/rest/api/3/project/${encodeURIComponent(project)}/statuses`);
|
|
3385
|
+
} catch (err) {
|
|
3386
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`No project found with key "${project}".`);
|
|
3387
|
+
throw err;
|
|
3388
|
+
}
|
|
3389
|
+
return groups.flatMap((g) => g.statuses ?? []);
|
|
3390
|
+
}
|
|
3391
|
+
function toStatusSummary(s) {
|
|
3392
|
+
return {
|
|
3393
|
+
name: s.name,
|
|
3394
|
+
id: s.id,
|
|
3395
|
+
category: s.statusCategory?.name ?? "",
|
|
3396
|
+
categoryKey: s.statusCategory?.key ?? ""
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
const CATEGORY_ORDER = {
|
|
3400
|
+
new: 0,
|
|
3401
|
+
indeterminate: 1,
|
|
3402
|
+
done: 2
|
|
3403
|
+
};
|
|
3404
|
+
const UNKNOWN_CATEGORY_RANK = Object.keys(CATEGORY_ORDER).length;
|
|
3405
|
+
function dedupeAndSortStatuses(statuses) {
|
|
3406
|
+
const byNameCategory = /* @__PURE__ */ new Map();
|
|
3407
|
+
for (const s of statuses) {
|
|
3408
|
+
const key = `${s.name}\0${s.categoryKey}`;
|
|
3409
|
+
if (!byNameCategory.has(key)) byNameCategory.set(key, s);
|
|
3410
|
+
}
|
|
3411
|
+
return [...byNameCategory.values()].sort((a, b) => {
|
|
3412
|
+
const rank = (s) => CATEGORY_ORDER[s.categoryKey] ?? UNKNOWN_CATEGORY_RANK;
|
|
3413
|
+
return rank(a) - rank(b) || a.name.localeCompare(b.name);
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
//#endregion
|
|
3339
3417
|
//#region src/api/jira-url.ts
|
|
3340
3418
|
function browseUrl(site, key) {
|
|
3341
3419
|
return `${site}/browse/${key}`;
|
|
3342
3420
|
}
|
|
3421
|
+
function boardUrl(site, id) {
|
|
3422
|
+
return `${site}/secure/RapidBoard.jspa?rapidView=${id}`;
|
|
3423
|
+
}
|
|
3424
|
+
//#endregion
|
|
3425
|
+
//#region src/api/jira-board.ts
|
|
3426
|
+
async function fetchBoard(client, site, id) {
|
|
3427
|
+
const [config, statuses] = await Promise.all([client.getJson(`/rest/agile/1.0/board/${id}/configuration`), statusesById(client)]);
|
|
3428
|
+
const statusFor = (statusId) => statuses.get(statusId) ?? {
|
|
3429
|
+
id: statusId,
|
|
3430
|
+
name: statusId,
|
|
3431
|
+
category: "",
|
|
3432
|
+
categoryKey: ""
|
|
3433
|
+
};
|
|
3434
|
+
return {
|
|
3435
|
+
id: config.id,
|
|
3436
|
+
name: config.name,
|
|
3437
|
+
type: config.type,
|
|
3438
|
+
url: boardUrl(site, config.id),
|
|
3439
|
+
estimation: estimationOf(config),
|
|
3440
|
+
filter: {
|
|
3441
|
+
id: config.filter.id,
|
|
3442
|
+
jql: await visibleFilterJql(client, config.filter.id),
|
|
3443
|
+
subQuery: config.subQuery?.query || null
|
|
3444
|
+
},
|
|
3445
|
+
columns: (config.columnConfig?.columns ?? []).map((c) => ({
|
|
3446
|
+
name: c.name,
|
|
3447
|
+
statuses: (c.statuses ?? []).map((s) => statusFor(s.id))
|
|
3448
|
+
}))
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
async function visibleFilterJql(client, filterId) {
|
|
3452
|
+
try {
|
|
3453
|
+
return (await client.getJson(`/rest/api/3/filter/${filterId}`)).jql ?? null;
|
|
3454
|
+
} catch (err) {
|
|
3455
|
+
if (err instanceof HttpError && err.status === 404) return null;
|
|
3456
|
+
throw err;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function estimationOf(config) {
|
|
3460
|
+
const field = config.estimation?.type === "field" ? config.estimation.field : void 0;
|
|
3461
|
+
return field ? {
|
|
3462
|
+
fieldId: field.fieldId,
|
|
3463
|
+
fieldName: field.displayName
|
|
3464
|
+
} : null;
|
|
3465
|
+
}
|
|
3466
|
+
//#endregion
|
|
3467
|
+
//#region src/api/offset-pages.ts
|
|
3468
|
+
async function allOffsetPages(client, pathAt) {
|
|
3469
|
+
const all = [];
|
|
3470
|
+
for (let startAt = 0;;) {
|
|
3471
|
+
const res = await client.getJson(pathAt(startAt));
|
|
3472
|
+
const values = res.values ?? [];
|
|
3473
|
+
all.push(...values);
|
|
3474
|
+
if (res.isLast || values.length === 0) return all;
|
|
3475
|
+
startAt += values.length;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
//#endregion
|
|
3479
|
+
//#region src/api/jira-boards.ts
|
|
3480
|
+
async function listBoards(client, site) {
|
|
3481
|
+
return (await allOffsetPages(client, (startAt) => `/rest/agile/1.0/board?startAt=${startAt}&maxResults=50`)).map((b) => ({
|
|
3482
|
+
id: b.id,
|
|
3483
|
+
name: b.name,
|
|
3484
|
+
type: b.type,
|
|
3485
|
+
project: b.location?.projectKey ?? null,
|
|
3486
|
+
url: boardUrl(site, b.id)
|
|
3487
|
+
}));
|
|
3488
|
+
}
|
|
3489
|
+
//#endregion
|
|
3490
|
+
//#region src/commands/board-ref.ts
|
|
3491
|
+
async function withBoard(session, ref, load) {
|
|
3492
|
+
const id = await resolveBoardId(session, ref);
|
|
3493
|
+
try {
|
|
3494
|
+
return await load(id);
|
|
3495
|
+
} catch (err) {
|
|
3496
|
+
if (isMissingBoard(err, id)) throw unknownBoard(ref);
|
|
3497
|
+
throw err;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
async function resolveBoardId(session, ref) {
|
|
3501
|
+
const id = parseBoardId(ref);
|
|
3502
|
+
if (id !== null) return id;
|
|
3503
|
+
const name = ref.trim().toLowerCase();
|
|
3504
|
+
const matches = (await listBoards(session, session.site)).filter((b) => b.name.toLowerCase() === name);
|
|
3505
|
+
const [only, ...others] = matches;
|
|
3506
|
+
if (!only) throw unknownBoard(ref);
|
|
3507
|
+
if (others.length === 0) return only.id;
|
|
3508
|
+
const width = Math.max(...matches.map((b) => String(b.id).length));
|
|
3509
|
+
throw new Error([`"${ref}" matches several boards. Use an id:`, ...matches.map((b) => ` ${String(b.id).padEnd(width)} ${b.name}`)].join("\n"));
|
|
3510
|
+
}
|
|
3511
|
+
function isMissingBoard(err, id) {
|
|
3512
|
+
return err instanceof HttpError && err.status === 404 && err.path.startsWith(`/rest/agile/1.0/board/${id}/`);
|
|
3513
|
+
}
|
|
3514
|
+
function unknownBoard(ref) {
|
|
3515
|
+
return /* @__PURE__ */ new Error(`No board matches "${ref}". Run \`atlass jira boards\` to list boards.`);
|
|
3516
|
+
}
|
|
3517
|
+
//#endregion
|
|
3518
|
+
//#region src/commands/jira-board.ts
|
|
3519
|
+
async function jiraBoard({ session, term }, ref, options) {
|
|
3520
|
+
const board = await withBoard(session, ref, (id) => fetchBoard(session, session.site, id));
|
|
3521
|
+
if (options.json) term.json(board);
|
|
3522
|
+
else term.out(formatBoard(board));
|
|
3523
|
+
}
|
|
3524
|
+
function formatBoard(board) {
|
|
3525
|
+
return [
|
|
3526
|
+
board.name,
|
|
3527
|
+
...fieldLines$1([
|
|
3528
|
+
["ID", String(board.id)],
|
|
3529
|
+
["Type", board.type],
|
|
3530
|
+
["Estimation", estimationLine(board)],
|
|
3531
|
+
["Filter", board.filter.jql ?? `not visible to you (filter ${board.filter.id})`],
|
|
3532
|
+
["Sub-filter", board.filter.subQuery ?? ""],
|
|
3533
|
+
["URL", board.url]
|
|
3534
|
+
]),
|
|
3535
|
+
"",
|
|
3536
|
+
...columnLines(board)
|
|
3537
|
+
];
|
|
3538
|
+
}
|
|
3539
|
+
function estimationLine({ estimation }) {
|
|
3540
|
+
return estimation ? `${estimation.fieldName} (${estimation.fieldId})` : "none, this board does not estimate with a field";
|
|
3541
|
+
}
|
|
3542
|
+
function columnLines({ columns }) {
|
|
3543
|
+
const statuses = columns.flatMap((c) => c.statuses);
|
|
3544
|
+
const width = Math.max(0, ...statuses.map((s) => s.name.length));
|
|
3545
|
+
return columns.flatMap((c) => [c.name, ...c.statuses.map((s) => ` ${s.name.padEnd(width)} ${s.category}`.trimEnd())]);
|
|
3546
|
+
}
|
|
3547
|
+
//#endregion
|
|
3548
|
+
//#region src/commands/jira-boards.ts
|
|
3549
|
+
async function jiraBoards({ session, term }, query, options) {
|
|
3550
|
+
const boards = filterBoards(await listBoards(session, session.site), query, options.project);
|
|
3551
|
+
if (options.json) term.json(boards);
|
|
3552
|
+
else if (boards.length === 0) term.out("No matching boards.");
|
|
3553
|
+
else term.out(formatBoardRows(boards));
|
|
3554
|
+
}
|
|
3555
|
+
function filterBoards(boards, query, project) {
|
|
3556
|
+
const needle = query?.toLowerCase();
|
|
3557
|
+
const key = project?.toUpperCase();
|
|
3558
|
+
return boards.filter((b) => (!needle || b.name.toLowerCase().includes(needle)) && (!key || b.project?.toUpperCase() === key));
|
|
3559
|
+
}
|
|
3560
|
+
function formatBoardRows(boards) {
|
|
3561
|
+
const width = (pick) => Math.max(...boards.map((b) => pick(b).length));
|
|
3562
|
+
const idWidth = width((b) => String(b.id));
|
|
3563
|
+
const typeWidth = width((b) => b.type);
|
|
3564
|
+
const nameWidth = width((b) => b.name);
|
|
3565
|
+
return boards.map((b) => `${String(b.id).padEnd(idWidth)} ${b.type.padEnd(typeWidth)} ${b.name.padEnd(nameWidth)} ${b.project ?? ""}`.trimEnd());
|
|
3566
|
+
}
|
|
3343
3567
|
//#endregion
|
|
3344
3568
|
//#region src/api/jira-createmeta.ts
|
|
3345
3569
|
const CREATEMETA_PAGE_SIZE = 200;
|
|
@@ -3432,28 +3656,19 @@ function toJiraUser(u) {
|
|
|
3432
3656
|
}
|
|
3433
3657
|
//#endregion
|
|
3434
3658
|
//#region src/api/jira-projects.ts
|
|
3435
|
-
const PROJECT_PAGE_SIZE = 50;
|
|
3436
3659
|
async function listProjects(client, site, query) {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
id: p.id,
|
|
3445
|
-
type: p.projectTypeKey ?? "",
|
|
3446
|
-
url: browseUrl(site, p.key)
|
|
3447
|
-
});
|
|
3448
|
-
if (res.isLast || values.length === 0) break;
|
|
3449
|
-
startAt += values.length;
|
|
3450
|
-
}
|
|
3451
|
-
return projects;
|
|
3660
|
+
return (await allOffsetPages(client, (startAt) => `/rest/api/3/project/search?${projectSearchQuery(query, startAt)}`)).map((p) => ({
|
|
3661
|
+
key: p.key,
|
|
3662
|
+
name: p.name,
|
|
3663
|
+
id: p.id,
|
|
3664
|
+
type: p.projectTypeKey ?? "",
|
|
3665
|
+
url: browseUrl(site, p.key)
|
|
3666
|
+
}));
|
|
3452
3667
|
}
|
|
3453
3668
|
function projectSearchQuery(query, startAt) {
|
|
3454
3669
|
const params = new URLSearchParams({
|
|
3455
3670
|
orderBy: "key",
|
|
3456
|
-
maxResults: String(
|
|
3671
|
+
maxResults: String(50),
|
|
3457
3672
|
startAt: String(startAt)
|
|
3458
3673
|
});
|
|
3459
3674
|
if (query) params.set("query", query);
|
|
@@ -4098,17 +4313,55 @@ function describeRejection(err, meta) {
|
|
|
4098
4313
|
...perField
|
|
4099
4314
|
].join("\n"));
|
|
4100
4315
|
}
|
|
4101
|
-
|
|
4316
|
+
//#endregion
|
|
4317
|
+
//#region src/api/jira-fields.ts
|
|
4318
|
+
async function listSiteFields(client) {
|
|
4319
|
+
return (await client.getJson("/rest/api/3/field")).map((f) => ({
|
|
4320
|
+
id: f.id,
|
|
4321
|
+
name: decodeEntities(f.name),
|
|
4322
|
+
custom: f.custom ?? false,
|
|
4323
|
+
schema: f.schema ?? null
|
|
4324
|
+
}));
|
|
4325
|
+
}
|
|
4326
|
+
//#endregion
|
|
4327
|
+
//#region src/commands/jira-fields.ts
|
|
4328
|
+
async function jiraFields(env, projectArg, typeArg, options) {
|
|
4329
|
+
if (!projectArg) return siteFields(env, options);
|
|
4102
4330
|
const project = projectArg.toUpperCase();
|
|
4331
|
+
if (options.search !== void 0) throw new Error(`--search lists site fields and takes no project. Drop ${project}, or run \`atlass jira fields ${project}\` alone.`);
|
|
4332
|
+
return createForm$1(env, project, typeArg, options);
|
|
4333
|
+
}
|
|
4334
|
+
async function siteFields({ session, term }, options) {
|
|
4335
|
+
const fields = matchingFields(await listSiteFields(session), options.search).sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
|
|
4336
|
+
if (options.json) term.json(fields);
|
|
4337
|
+
else if (fields.length === 0) term.out("No matching fields.");
|
|
4338
|
+
else term.out(formatSiteFieldRows(fields));
|
|
4339
|
+
}
|
|
4340
|
+
function matchingFields(fields, search) {
|
|
4341
|
+
const needle = search?.toLowerCase();
|
|
4342
|
+
return fields.filter((f) => !needle || f.name.toLowerCase().includes(needle) || f.id.toLowerCase().includes(needle));
|
|
4343
|
+
}
|
|
4344
|
+
function formatSiteFieldRows(fields) {
|
|
4345
|
+
const width = (pick) => Math.max(...fields.map((f) => pick(f).length));
|
|
4346
|
+
const idWidth = width((f) => f.id);
|
|
4347
|
+
const nameWidth = width((f) => f.name);
|
|
4348
|
+
return fields.map((f) => `${f.id.padEnd(idWidth)} ${f.name.padEnd(nameWidth)} ${shortFieldType(f)}`.trimEnd());
|
|
4349
|
+
}
|
|
4350
|
+
function shortFieldType(field) {
|
|
4351
|
+
const plugin = field.schema?.custom;
|
|
4352
|
+
if (plugin) return plugin.slice(plugin.lastIndexOf(":") + 1);
|
|
4353
|
+
return field.schema?.type ?? "";
|
|
4354
|
+
}
|
|
4355
|
+
async function createForm$1({ session, term }, project, typeArg, { json }) {
|
|
4103
4356
|
const types = await fetchCreateIssueTypes(session, project);
|
|
4104
4357
|
if (!typeArg) {
|
|
4105
|
-
if (
|
|
4358
|
+
if (json) term.json(types);
|
|
4106
4359
|
else if (types.length === 0) term.out(`You cannot create issues in ${project}.`);
|
|
4107
4360
|
else term.out(formatIssueTypeRows(types));
|
|
4108
4361
|
return;
|
|
4109
4362
|
}
|
|
4110
4363
|
const fields = await fetchCreateFields(session, project, (await resolveType(term.ask, project, types, typeArg, true)).id);
|
|
4111
|
-
if (
|
|
4364
|
+
if (json) term.json(fields);
|
|
4112
4365
|
else term.out(formatFieldRows(fields));
|
|
4113
4366
|
}
|
|
4114
4367
|
//#endregion
|
|
@@ -4116,23 +4369,23 @@ async function jiraFields({ session, term }, projectArg, typeArg, options) {
|
|
|
4116
4369
|
const RECENT_ISSUES_CLAUSE = "updated >= -30d";
|
|
4117
4370
|
const OPEN_CLAUSE = "statusCategory != Done";
|
|
4118
4371
|
const SEARCH_FIELDS = "summary,status,updated";
|
|
4119
|
-
async function searchIssues(client, site, params) {
|
|
4120
|
-
return searchIssuesByJql(client, site, buildJql(params), params.limit);
|
|
4372
|
+
async function searchIssues(client, site, params, fieldIds = []) {
|
|
4373
|
+
return searchIssuesByJql(client, site, buildJql(params), params.limit, fieldIds);
|
|
4121
4374
|
}
|
|
4122
|
-
async function searchIssuesByJql(client, site, jql, limit) {
|
|
4123
|
-
return ((await fetchSearchPage(client, jql, limit)).issues ?? []).map((i) => toIssueSummary(site, i));
|
|
4375
|
+
async function searchIssuesByJql(client, site, jql, limit, fieldIds = []) {
|
|
4376
|
+
return ((await fetchSearchPage(client, jql, limit, fieldIds)).issues ?? []).map((i) => toIssueSummary(site, i, fieldIds));
|
|
4124
4377
|
}
|
|
4125
|
-
async function fetchSearchPage(client, jql, maxResults, nextPageToken) {
|
|
4378
|
+
async function fetchSearchPage(client, jql, maxResults, fieldIds, nextPageToken) {
|
|
4126
4379
|
const query = new URLSearchParams({
|
|
4127
4380
|
jql,
|
|
4128
4381
|
maxResults: String(maxResults),
|
|
4129
|
-
fields: SEARCH_FIELDS
|
|
4382
|
+
fields: [SEARCH_FIELDS, ...fieldIds].join(",")
|
|
4130
4383
|
});
|
|
4131
4384
|
if (nextPageToken) query.set("nextPageToken", nextPageToken);
|
|
4132
4385
|
return client.getJson(`/rest/api/3/search/jql?${query.toString()}`);
|
|
4133
4386
|
}
|
|
4134
|
-
function toIssueSummary(site, i) {
|
|
4135
|
-
|
|
4387
|
+
function toIssueSummary(site, i, fieldIds) {
|
|
4388
|
+
const summary = {
|
|
4136
4389
|
key: i.key,
|
|
4137
4390
|
status: i.fields?.status?.name ?? "",
|
|
4138
4391
|
statusCategory: i.fields?.status?.statusCategory?.key ?? "",
|
|
@@ -4140,6 +4393,11 @@ function toIssueSummary(site, i) {
|
|
|
4140
4393
|
updated: i.fields?.updated ?? "",
|
|
4141
4394
|
url: browseUrl(site, i.key)
|
|
4142
4395
|
};
|
|
4396
|
+
if (fieldIds.length === 0) return summary;
|
|
4397
|
+
return {
|
|
4398
|
+
...summary,
|
|
4399
|
+
fields: Object.fromEntries(fieldIds.map((id) => [id, i.fields?.[id] ?? null]))
|
|
4400
|
+
};
|
|
4143
4401
|
}
|
|
4144
4402
|
const ISSUE_ORDER = "updated DESC";
|
|
4145
4403
|
function buildJql(params) {
|
|
@@ -4151,12 +4409,22 @@ function buildJql(params) {
|
|
|
4151
4409
|
userClause("assignee", params.assignee),
|
|
4152
4410
|
userClause("reporter", params.reporter),
|
|
4153
4411
|
inClause("labels", params.label),
|
|
4412
|
+
anyOf(values(params.sprint).map(sprintTerm)),
|
|
4154
4413
|
params.updatedSince ? `updated >= ${quote(params.updatedSince)}` : null,
|
|
4155
4414
|
params.text ? `text ~ ${quote(params.text)}` : null
|
|
4156
4415
|
].filter((c) => c !== null);
|
|
4157
4416
|
if (clauses.length === 0) clauses.push(RECENT_ISSUES_CLAUSE);
|
|
4158
4417
|
return joinClauses(clauses, ISSUE_ORDER);
|
|
4159
4418
|
}
|
|
4419
|
+
const SPRINT_FUNCTIONS = {
|
|
4420
|
+
active: "openSprints()",
|
|
4421
|
+
future: "futureSprints()"
|
|
4422
|
+
};
|
|
4423
|
+
function sprintTerm(sprint) {
|
|
4424
|
+
const sprintFunction = SPRINT_FUNCTIONS[sprint];
|
|
4425
|
+
if (sprintFunction) return `sprint in ${sprintFunction}`;
|
|
4426
|
+
return `sprint = ${/^\d+$/.test(sprint) ? sprint : quote(sprint)}`;
|
|
4427
|
+
}
|
|
4160
4428
|
function buildListJql(params) {
|
|
4161
4429
|
return joinClauses([
|
|
4162
4430
|
"assignee = currentUser()",
|
|
@@ -4166,15 +4434,15 @@ function buildListJql(params) {
|
|
|
4166
4434
|
}
|
|
4167
4435
|
const LIST_PAGE_SIZE = 100;
|
|
4168
4436
|
const LIST_CAP = 500;
|
|
4169
|
-
async function listAssignedIssues(client, site, params) {
|
|
4437
|
+
async function listAssignedIssues(client, site, params, fieldIds = []) {
|
|
4170
4438
|
const jql = buildListJql(params);
|
|
4171
4439
|
const issues = [];
|
|
4172
4440
|
let nextPageToken;
|
|
4173
4441
|
for (;;) {
|
|
4174
4442
|
const room = LIST_CAP - issues.length;
|
|
4175
|
-
const res = await fetchSearchPage(client, jql, Math.min(LIST_PAGE_SIZE, room), nextPageToken);
|
|
4443
|
+
const res = await fetchSearchPage(client, jql, Math.min(LIST_PAGE_SIZE, room), fieldIds, nextPageToken);
|
|
4176
4444
|
const page = res.issues ?? [];
|
|
4177
|
-
issues.push(...page.map((i) => toIssueSummary(site, i)));
|
|
4445
|
+
issues.push(...page.map((i) => toIssueSummary(site, i, fieldIds)));
|
|
4178
4446
|
if (res.isLast || !res.nextPageToken || page.length === 0) return {
|
|
4179
4447
|
issues,
|
|
4180
4448
|
truncated: false
|
|
@@ -4198,6 +4466,54 @@ function sortByCategoryThenUpdated(issues) {
|
|
|
4198
4466
|
return [...issues].sort((a, b) => rank(a) - rank(b) || updatedMs(b) - updatedMs(a));
|
|
4199
4467
|
}
|
|
4200
4468
|
//#endregion
|
|
4469
|
+
//#region src/commands/jira-field-columns.ts
|
|
4470
|
+
async function resolveFieldColumns(session, given) {
|
|
4471
|
+
const names = values(given);
|
|
4472
|
+
if (names.length === 0) return [];
|
|
4473
|
+
const fields = await listSiteFields(session);
|
|
4474
|
+
return names.map((name) => ({
|
|
4475
|
+
name,
|
|
4476
|
+
id: resolveFieldId(fields, name)
|
|
4477
|
+
}));
|
|
4478
|
+
}
|
|
4479
|
+
function resolveFieldId(fields, name) {
|
|
4480
|
+
const byId = fields.find((f) => f.id === name);
|
|
4481
|
+
if (byId) return byId.id;
|
|
4482
|
+
const lower = name.toLowerCase();
|
|
4483
|
+
const matches = fields.filter((f) => f.name.toLowerCase() === lower);
|
|
4484
|
+
const [only, ...others] = matches;
|
|
4485
|
+
if (!only) throw new Error(`No field matches "${name}". Run \`atlass jira fields --search ${shellWord(name)}\` to find it.`);
|
|
4486
|
+
if (others.length === 0) return only.id;
|
|
4487
|
+
const width = Math.max(...matches.map((f) => f.id.length));
|
|
4488
|
+
throw new Error([`"${name}" matches several fields. Use an id:`, ...matches.map((f) => ` ${f.id.padEnd(width)} ${f.name}`)].join("\n"));
|
|
4489
|
+
}
|
|
4490
|
+
function shellWord(text) {
|
|
4491
|
+
return /\s/.test(text) ? `"${text}"` : text;
|
|
4492
|
+
}
|
|
4493
|
+
function fieldIds(columns) {
|
|
4494
|
+
return [...new Set(columns.map((c) => c.id))];
|
|
4495
|
+
}
|
|
4496
|
+
function withNamedFields(issues, columns) {
|
|
4497
|
+
if (columns.length === 0) return issues;
|
|
4498
|
+
return issues.map((issue) => ({
|
|
4499
|
+
...issue,
|
|
4500
|
+
fields: Object.fromEntries(columns.map((c) => [c.name, issue.fields?.[c.id] ?? null]))
|
|
4501
|
+
}));
|
|
4502
|
+
}
|
|
4503
|
+
const EMPTY_CELL = "-";
|
|
4504
|
+
function fieldCell(value) {
|
|
4505
|
+
const text = (Array.isArray(value) ? value.map(shownValue) : [shownValue(value)]).filter((p) => p !== "").join(", ");
|
|
4506
|
+
return text === "" ? EMPTY_CELL : text;
|
|
4507
|
+
}
|
|
4508
|
+
function shownValue(value) {
|
|
4509
|
+
if (value === null || value === void 0) return "";
|
|
4510
|
+
if (typeof value === "string") return value;
|
|
4511
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4512
|
+
const named = value;
|
|
4513
|
+
const shown = named.value ?? named.displayName ?? named.name;
|
|
4514
|
+
return shown === void 0 ? JSON.stringify(value) : shownValue(shown);
|
|
4515
|
+
}
|
|
4516
|
+
//#endregion
|
|
4201
4517
|
//#region src/commands/jira-filters.ts
|
|
4202
4518
|
const FILTER_FLAGS = [
|
|
4203
4519
|
"project",
|
|
@@ -4205,7 +4521,8 @@ const FILTER_FLAGS = [
|
|
|
4205
4521
|
"reporter",
|
|
4206
4522
|
"status",
|
|
4207
4523
|
"type",
|
|
4208
|
-
"label"
|
|
4524
|
+
"label",
|
|
4525
|
+
"sprint"
|
|
4209
4526
|
];
|
|
4210
4527
|
const ISSUE_KEY = /^[A-Z][A-Z0-9]+-\d+$/i;
|
|
4211
4528
|
function hasFilters(filters) {
|
|
@@ -4224,6 +4541,7 @@ async function searchParams(client, query, filters, limit, nowMs) {
|
|
|
4224
4541
|
status: filters.status,
|
|
4225
4542
|
type: filters.type,
|
|
4226
4543
|
label: filters.label,
|
|
4544
|
+
sprint: filters.sprint,
|
|
4227
4545
|
updatedSince: filters.updated ? parseSince(filters.updated, nowMs) : void 0,
|
|
4228
4546
|
open: filters.open,
|
|
4229
4547
|
limit
|
|
@@ -4307,46 +4625,6 @@ async function fetchComments(client, key) {
|
|
|
4307
4625
|
}));
|
|
4308
4626
|
}
|
|
4309
4627
|
//#endregion
|
|
4310
|
-
//#region src/api/jira-statuses.ts
|
|
4311
|
-
async function listStatuses(client, project) {
|
|
4312
|
-
return dedupeAndSortStatuses((project ? await fetchProjectStatuses(client, project) : await client.getJson("/rest/api/3/status")).map(toStatusSummary));
|
|
4313
|
-
}
|
|
4314
|
-
async function fetchProjectStatuses(client, project) {
|
|
4315
|
-
let groups;
|
|
4316
|
-
try {
|
|
4317
|
-
groups = await client.getJson(`/rest/api/3/project/${encodeURIComponent(project)}/statuses`);
|
|
4318
|
-
} catch (err) {
|
|
4319
|
-
if (err instanceof HttpError && err.status === 404) throw new Error(`No project found with key "${project}".`);
|
|
4320
|
-
throw err;
|
|
4321
|
-
}
|
|
4322
|
-
return groups.flatMap((g) => g.statuses ?? []);
|
|
4323
|
-
}
|
|
4324
|
-
function toStatusSummary(s) {
|
|
4325
|
-
return {
|
|
4326
|
-
name: s.name,
|
|
4327
|
-
id: s.id,
|
|
4328
|
-
category: s.statusCategory?.name ?? "",
|
|
4329
|
-
categoryKey: s.statusCategory?.key ?? ""
|
|
4330
|
-
};
|
|
4331
|
-
}
|
|
4332
|
-
const CATEGORY_ORDER = {
|
|
4333
|
-
new: 0,
|
|
4334
|
-
indeterminate: 1,
|
|
4335
|
-
done: 2
|
|
4336
|
-
};
|
|
4337
|
-
const UNKNOWN_CATEGORY_RANK = Object.keys(CATEGORY_ORDER).length;
|
|
4338
|
-
function dedupeAndSortStatuses(statuses) {
|
|
4339
|
-
const byNameCategory = /* @__PURE__ */ new Map();
|
|
4340
|
-
for (const s of statuses) {
|
|
4341
|
-
const key = `${s.name}\0${s.categoryKey}`;
|
|
4342
|
-
if (!byNameCategory.has(key)) byNameCategory.set(key, s);
|
|
4343
|
-
}
|
|
4344
|
-
return [...byNameCategory.values()].sort((a, b) => {
|
|
4345
|
-
const rank = (s) => CATEGORY_ORDER[s.categoryKey] ?? UNKNOWN_CATEGORY_RANK;
|
|
4346
|
-
return rank(a) - rank(b) || a.name.localeCompare(b.name);
|
|
4347
|
-
});
|
|
4348
|
-
}
|
|
4349
|
-
//#endregion
|
|
4350
4628
|
//#region src/commands/jira.ts
|
|
4351
4629
|
async function jiraProjects({ session, term }, query, options) {
|
|
4352
4630
|
const projects = await listProjects(session, session.site, query);
|
|
@@ -4443,18 +4721,20 @@ async function jiraSearch(env, query, options) {
|
|
|
4443
4721
|
const { session } = env;
|
|
4444
4722
|
checkQuery(query, options);
|
|
4445
4723
|
const limit = checkedLimit(options);
|
|
4724
|
+
const columns = await resolveFieldColumns(session, options.field);
|
|
4446
4725
|
const params = await searchParams(session, query, options, limit, Date.now());
|
|
4447
|
-
await showIssues(env, await searchIssues(session, session.site, params).catch((err) => {
|
|
4726
|
+
await showIssues(env, withNamedFields(await searchIssues(session, session.site, params, fieldIds(columns)).catch((err) => {
|
|
4448
4727
|
throw searchHint(err, options.project);
|
|
4449
|
-
}), limit, options);
|
|
4728
|
+
}), columns), columns, limit, options);
|
|
4450
4729
|
}
|
|
4451
4730
|
async function jiraJql(env, query, options) {
|
|
4452
4731
|
const { session } = env;
|
|
4453
4732
|
const limit = checkedLimit(options);
|
|
4454
|
-
|
|
4733
|
+
const columns = await resolveFieldColumns(session, options.field);
|
|
4734
|
+
await showIssues(env, withNamedFields(await searchIssuesByJql(session, session.site, query, limit, fieldIds(columns)), columns), columns, limit, options);
|
|
4455
4735
|
}
|
|
4456
|
-
async function showIssues(env, issues, limit, options, empty = "No matching issues.") {
|
|
4457
|
-
await showRows(env.term, formatIssueRows(issues, Date.now()), {
|
|
4736
|
+
async function showIssues(env, issues, columns, limit, options, empty = "No matching issues.") {
|
|
4737
|
+
await showRows(env.term, formatIssueRows(issues, Date.now(), columns), {
|
|
4458
4738
|
empty,
|
|
4459
4739
|
hasMore: issues.length === limit,
|
|
4460
4740
|
limit
|
|
@@ -4462,27 +4742,36 @@ async function showIssues(env, issues, limit, options, empty = "No matching issu
|
|
|
4462
4742
|
}
|
|
4463
4743
|
async function jiraList(env, options) {
|
|
4464
4744
|
const { session, term } = env;
|
|
4465
|
-
checkedLimit(options);
|
|
4745
|
+
const limit = checkedLimit(options);
|
|
4746
|
+
const columns = await resolveFieldColumns(session, options.field);
|
|
4466
4747
|
const { issues, truncated } = await listAssignedIssues(session, session.site, {
|
|
4467
4748
|
all: options.all,
|
|
4468
4749
|
project: options.project
|
|
4469
|
-
});
|
|
4470
|
-
|
|
4750
|
+
}, fieldIds(columns));
|
|
4751
|
+
const sorted = sortByCategoryThenUpdated(withNamedFields(issues, columns));
|
|
4752
|
+
const shown = options.limit === void 0 ? sorted : sorted.slice(0, limit);
|
|
4753
|
+
await runSearch(term, formatIssueRows(shown, Date.now(), columns), {
|
|
4471
4754
|
json: options.json,
|
|
4472
4755
|
copy: options.copy,
|
|
4473
4756
|
out: options.out,
|
|
4474
4757
|
empty: options.all ? "No issues assigned to you." : "No open issues assigned to you.",
|
|
4475
|
-
footer:
|
|
4758
|
+
footer: listFooter(shown.length, sorted.length, truncated, limit)
|
|
4476
4759
|
}, ISSUE_NOUN, (key) => copyIssue(env, key, options.out));
|
|
4477
4760
|
}
|
|
4478
|
-
function
|
|
4761
|
+
function listFooter(shown, fetched, truncated, limit) {
|
|
4762
|
+
if (shown < fetched) return searchFooter(limit);
|
|
4763
|
+
if (truncated) return `
|
|
4764
|
+
Showing the first ${shown}; narrow with --project.`;
|
|
4765
|
+
}
|
|
4766
|
+
function formatIssueRows(issues, nowMs, columns = []) {
|
|
4479
4767
|
return alignedRows(issues, nowMs, (i) => ({
|
|
4480
4768
|
id: i.key,
|
|
4481
4769
|
url: i.url,
|
|
4482
4770
|
label: i.status,
|
|
4483
4771
|
color: colorForCategory(i.statusCategory),
|
|
4484
4772
|
text: i.summary,
|
|
4485
|
-
timestamp: i.updated
|
|
4773
|
+
timestamp: i.updated,
|
|
4774
|
+
columns: columns.map((c) => fieldCell(i.fields?.[c.name]))
|
|
4486
4775
|
}));
|
|
4487
4776
|
}
|
|
4488
4777
|
const ISSUE_NOUN = {
|
|
@@ -4490,24 +4779,90 @@ const ISSUE_NOUN = {
|
|
|
4490
4779
|
plural: "issues"
|
|
4491
4780
|
};
|
|
4492
4781
|
//#endregion
|
|
4782
|
+
//#region src/api/jira-types.ts
|
|
4783
|
+
const SPRINT_STATES = [
|
|
4784
|
+
"active",
|
|
4785
|
+
"future",
|
|
4786
|
+
"closed"
|
|
4787
|
+
];
|
|
4788
|
+
//#endregion
|
|
4789
|
+
//#region src/api/jira-sprints.ts
|
|
4790
|
+
async function listSprints(client, boardId, states) {
|
|
4791
|
+
const state = states.join(",");
|
|
4792
|
+
return (await allOffsetPages(client, (startAt) => `/rest/agile/1.0/board/${boardId}/sprint?state=${state}&startAt=${startAt}&maxResults=50`)).toSorted(byRelevance).map((s) => ({
|
|
4793
|
+
id: s.id,
|
|
4794
|
+
state: s.state,
|
|
4795
|
+
name: s.name,
|
|
4796
|
+
start: s.startDate ?? null,
|
|
4797
|
+
end: s.endDate ?? null,
|
|
4798
|
+
boardId: s.originBoardId ?? boardId
|
|
4799
|
+
}));
|
|
4800
|
+
}
|
|
4801
|
+
function byRelevance(a, b) {
|
|
4802
|
+
const rank = SPRINT_STATES.indexOf(a.state) - SPRINT_STATES.indexOf(b.state);
|
|
4803
|
+
if (rank !== 0 || a.state !== "closed") return rank;
|
|
4804
|
+
return finishedAt(b) - finishedAt(a);
|
|
4805
|
+
}
|
|
4806
|
+
function finishedAt(sprint) {
|
|
4807
|
+
return Date.parse(sprint.completeDate ?? sprint.endDate ?? "") || 0;
|
|
4808
|
+
}
|
|
4809
|
+
//#endregion
|
|
4810
|
+
//#region src/commands/jira-sprints.ts
|
|
4811
|
+
async function jiraSprints({ session, term }, ref, options) {
|
|
4812
|
+
const chosen = parseChoices(options.state, SPRINT_STATES, "--state");
|
|
4813
|
+
const states = chosen.length > 0 ? chosen : ["active"];
|
|
4814
|
+
const limit = parseLimit(options.limit);
|
|
4815
|
+
const sprints = await withBoard(session, ref, (id) => listSprints(session, id, states).catch((err) => {
|
|
4816
|
+
throw withoutSprints(err, id);
|
|
4817
|
+
}));
|
|
4818
|
+
const shown = sprints.slice(0, limit);
|
|
4819
|
+
if (options.json) term.json(shown);
|
|
4820
|
+
else if (shown.length === 0) term.out(`No ${orList(states)} sprints.`);
|
|
4821
|
+
else if (sprints.length > limit) term.out([...formatSprintRows(shown), searchFooter(limit)]);
|
|
4822
|
+
else term.out(formatSprintRows(shown));
|
|
4823
|
+
}
|
|
4824
|
+
function withoutSprints(err, id) {
|
|
4825
|
+
if (!(err instanceof HttpError) || err.status !== 400) return err;
|
|
4826
|
+
return /* @__PURE__ */ new Error(`Board ${id} does not support sprints. Only scrum boards have them.`);
|
|
4827
|
+
}
|
|
4828
|
+
function formatSprintRows(sprints) {
|
|
4829
|
+
const cells = sprints.map((s) => [
|
|
4830
|
+
String(s.id),
|
|
4831
|
+
s.state,
|
|
4832
|
+
s.name,
|
|
4833
|
+
s.start ? formatDate$1(s.start) : "-",
|
|
4834
|
+
s.end ? formatDate$1(s.end) : "-"
|
|
4835
|
+
]);
|
|
4836
|
+
const widths = cells[0].map((_, i) => Math.max(...cells.map((row) => row[i].length)));
|
|
4837
|
+
return cells.map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd());
|
|
4838
|
+
}
|
|
4839
|
+
//#endregion
|
|
4493
4840
|
//#region src/cli/jira.ts
|
|
4841
|
+
const FIELD_OPTION = [
|
|
4842
|
+
"-f, --field <name|id>",
|
|
4843
|
+
"add a field's value to each issue (repeatable)",
|
|
4844
|
+
collect
|
|
4845
|
+
];
|
|
4494
4846
|
function registerJira(jira) {
|
|
4495
4847
|
jira.description("Jira commands");
|
|
4496
4848
|
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(jiraAction(jiraProjects));
|
|
4497
4849
|
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));
|
|
4850
|
+
jira.command("boards [query]").description("List boards (optionally filtered by name, scoped with --project)").option("-p, --project <key>", "limit to boards located in a project").option("--json", "output results as JSON").action(jiraAction(jiraBoards));
|
|
4851
|
+
jira.command("board <ref>").description("Show a board's estimation field, filter JQL and columns (id, URL or exact name)").option("--json", "output results as JSON").action(jiraAction(jiraBoard));
|
|
4852
|
+
jira.command("sprints <ref>").description("List a board's sprints, active by default (id, URL or exact name)").option("-s, --state <state>", "active, future, or closed (repeatable)", collect).option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").action(jiraAction(jiraSprints));
|
|
4498
4853
|
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));
|
|
4499
|
-
jira.command("fields
|
|
4854
|
+
jira.command("fields [project] [type]").description("List site fields, or the issue types a project offers, or the create form for one type").option("--search <text>", "site fields only: filter by name or id").option("--json", "output results as JSON").action(jiraAction(jiraFields));
|
|
4500
4855
|
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));
|
|
4501
4856
|
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));
|
|
4502
4857
|
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));
|
|
4503
|
-
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));
|
|
4504
|
-
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));
|
|
4505
|
-
outputOptions(jira.command("jql <query>").description("Search Jira issues with a raw JQL query")).action(jiraAction(jiraJql));
|
|
4858
|
+
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").option(...FIELD_OPTION), "max results (default all, max 100)").action(jiraAction(jiraList));
|
|
4859
|
+
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("--sprint <sprint>", "limit to a sprint: active, future, an id, or a name (repeatable)", collect).option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").option("--open", "exclude issues in the Done category").option(...FIELD_OPTION).addOption(moved("--jql <jql>", "jira jql"))).action(jiraAction(jiraSearch));
|
|
4860
|
+
outputOptions(jira.command("jql <query>").description("Search Jira issues with a raw JQL query").option(...FIELD_OPTION)).action(jiraAction(jiraJql));
|
|
4506
4861
|
return jira;
|
|
4507
4862
|
}
|
|
4508
4863
|
//#endregion
|
|
4509
4864
|
//#region package.json
|
|
4510
|
-
var version = "2.
|
|
4865
|
+
var version = "2.2.0";
|
|
4511
4866
|
//#endregion
|
|
4512
4867
|
//#region src/cli/build.ts
|
|
4513
4868
|
const PRODUCTS = {
|
package/dist/cli.mjs
CHANGED
package/dist/confluence.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as buildStandalone, r as fail } from "./build-
|
|
2
|
+
import { n as buildStandalone, r as fail } from "./build-H7eTLrf3.mjs";
|
|
3
3
|
//#region src/confluence.ts
|
|
4
4
|
buildStandalone("confluence").parseAsync().catch(fail);
|
|
5
5
|
//#endregion
|
package/dist/jira.mjs
CHANGED