atlass 1.10.0 → 1.11.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 +25 -0
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-Colq1d4N.mjs → build-wKtbjBhd.mjs} +775 -9
- 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
|
@@ -97,6 +97,31 @@ body sent is everything between the H1 and `## Comments`.
|
|
|
97
97
|
- Confluence uploads local images referenced in the body as attachments. Jira
|
|
98
98
|
update does not support image changes yet.
|
|
99
99
|
|
|
100
|
+
## Create
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
atlass jira create # prompts for everything
|
|
104
|
+
atlass jira create BSC Bug --summary "Login loops" --priority High --label auth
|
|
105
|
+
atlass jira create BSC Task -s "Rotate keys" --description-file notes.md --assignee me
|
|
106
|
+
atlass jira create BSC Defect -s "..." --component API --field severity=S2 --dry-run
|
|
107
|
+
atlass jira fields BSC # issue types you can create
|
|
108
|
+
atlass jira fields BSC Defect # the create form for one type
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
With no field flags on a terminal, `create` walks through the create screen:
|
|
112
|
+
required fields, a pick list of optional ones, then a review and confirm.
|
|
113
|
+
Multi-line fields open `$EDITOR` for Markdown.
|
|
114
|
+
|
|
115
|
+
Any field flag, `--no-input`, or a non-terminal switches to strict mode:
|
|
116
|
+
nothing is prompted, and the command fails before creating anything if a
|
|
117
|
+
required field is missing, a value is not allowed, or a field is not on the
|
|
118
|
+
create screen. `--field NAME=VALUE` takes the display name or id. Multi-value
|
|
119
|
+
fields take commas or repeated flags; cascading selects take `Parent > Child`;
|
|
120
|
+
assignees take `me`, an account id, or a name matching one assignable user.
|
|
121
|
+
|
|
122
|
+
`--dry-run` prints the resolved payload. `--json` prints the created key, id,
|
|
123
|
+
and URL. `jira fields` shows what each field expects.
|
|
124
|
+
|
|
100
125
|
## List
|
|
101
126
|
|
|
102
127
|
```bash
|
package/dist/bitbucket.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { checkbox, confirm, input, password } from "@inquirer/prompts";
|
|
2
|
+
import { checkbox, confirm, editor, input, password, search, select } from "@inquirer/prompts";
|
|
3
3
|
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";
|
|
@@ -58,6 +58,16 @@ var AtlassianClient = class {
|
|
|
58
58
|
async getJson(path) {
|
|
59
59
|
return (await this.request(path, { headers: { Accept: "application/json" } })).json();
|
|
60
60
|
}
|
|
61
|
+
async postJson(path, body) {
|
|
62
|
+
return (await this.request(path, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
Accept: "application/json",
|
|
66
|
+
"Content-Type": "application/json"
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(body)
|
|
69
|
+
})).json();
|
|
70
|
+
}
|
|
61
71
|
async putJson(path, body) {
|
|
62
72
|
return (await this.request(path, {
|
|
63
73
|
method: "PUT",
|
|
@@ -103,9 +113,14 @@ function pathAndQuery(absoluteUrl) {
|
|
|
103
113
|
}
|
|
104
114
|
var HttpError = class extends Error {
|
|
105
115
|
status;
|
|
106
|
-
|
|
116
|
+
jira;
|
|
117
|
+
constructor(status, message, jira = {
|
|
118
|
+
errorMessages: [],
|
|
119
|
+
errors: {}
|
|
120
|
+
}) {
|
|
107
121
|
super(message);
|
|
108
122
|
this.status = status;
|
|
123
|
+
this.jira = jira;
|
|
109
124
|
this.name = "HttpError";
|
|
110
125
|
}
|
|
111
126
|
};
|
|
@@ -114,17 +129,26 @@ function httpError(status, path, body = "") {
|
|
|
114
129
|
if (status === 404) return new HttpError(status, `Not found (404): ${path}`);
|
|
115
130
|
if (status === 409) return new HttpError(status, `Conflict (409): ${extractErrorMessage(body) || "the page changed on the server"}`);
|
|
116
131
|
if (status === 413) return new HttpError(status, "Payload too large (413): the page or an attachment exceeds the size limit.");
|
|
117
|
-
if (status === 400) return new HttpError(status, `Bad request (400): ${extractErrorMessage(body) || path}
|
|
118
|
-
return new HttpError(status, `Request failed (${status}): ${extractErrorMessage(body) || path}
|
|
132
|
+
if (status === 400) return new HttpError(status, `Bad request (400): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
133
|
+
return new HttpError(status, `Request failed (${status}): ${extractErrorMessage(body) || path}`, jiraErrors(body));
|
|
119
134
|
}
|
|
120
135
|
function extractErrorMessage(body) {
|
|
121
136
|
if (!body) return "";
|
|
122
137
|
const json = tryParseJson(body);
|
|
123
|
-
|
|
138
|
+
const { errorMessages, errors } = jiraErrors(body);
|
|
139
|
+
const fromJira = [...errorMessages, ...Object.entries(errors).map(([f, m]) => `${f}: ${m}`)];
|
|
140
|
+
if (fromJira.length) return fromJira.join("; ");
|
|
124
141
|
if (json?.message) return json.message;
|
|
125
142
|
if (json?.error?.message) return json.error.message;
|
|
126
143
|
return body.slice(0, 300);
|
|
127
144
|
}
|
|
145
|
+
function jiraErrors(body) {
|
|
146
|
+
const json = tryParseJson(body);
|
|
147
|
+
return {
|
|
148
|
+
errorMessages: json?.errorMessages ?? [],
|
|
149
|
+
errors: json?.errors ?? {}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
128
152
|
function tryParseJson(text) {
|
|
129
153
|
try {
|
|
130
154
|
return JSON.parse(text);
|
|
@@ -1015,6 +1039,14 @@ async function resolveRef(arg, kind) {
|
|
|
1015
1039
|
return ref;
|
|
1016
1040
|
}
|
|
1017
1041
|
//#endregion
|
|
1042
|
+
//#region src/util/link.ts
|
|
1043
|
+
const OSC8 = "\x1B]8;;";
|
|
1044
|
+
const BEL = "\x07";
|
|
1045
|
+
function hyperlink(text, url) {
|
|
1046
|
+
if (!kleur.enabled || !url) return text;
|
|
1047
|
+
return `${OSC8}${url}${BEL}${text}${OSC8}${BEL}`;
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1018
1050
|
//#region src/commands/search-run.ts
|
|
1019
1051
|
function alignedRows(items, nowMs, cells) {
|
|
1020
1052
|
const rows = items.map((item) => ({
|
|
@@ -1028,6 +1060,7 @@ function alignedRows(items, nowMs, cells) {
|
|
|
1028
1060
|
const ageWidth = width((row) => row.age);
|
|
1029
1061
|
return rows.map((row) => ({
|
|
1030
1062
|
id: row.id,
|
|
1063
|
+
url: row.url,
|
|
1031
1064
|
fixedColumns: `${row.id.padEnd(idWidth)} ${row.color(row.label.padEnd(labelWidth))} ${row.age.padEnd(ageWidth)}`,
|
|
1032
1065
|
freeText: row.text,
|
|
1033
1066
|
json: row.json
|
|
@@ -1085,10 +1118,11 @@ async function copySelected(rows, noun, copyOne) {
|
|
|
1085
1118
|
if (failures.length === 0) console.log(summary);
|
|
1086
1119
|
else console.log(`${summary}, failed ${failures.length}: ${failures.join(", ")}`);
|
|
1087
1120
|
}
|
|
1088
|
-
function formatRow(row) {
|
|
1089
|
-
const room =
|
|
1121
|
+
function formatRow(row, width = process.stdout.columns ?? 80) {
|
|
1122
|
+
const room = width - stripVTControlCharacters(row.fixedColumns).length - 2;
|
|
1090
1123
|
const text = room > 0 ? truncate(row.freeText, room) : "";
|
|
1091
|
-
|
|
1124
|
+
const columns = hyperlink(row.id, row.url) + row.fixedColumns.slice(row.id.length);
|
|
1125
|
+
return text ? `${columns} ${hyperlink(text, row.url)}` : columns;
|
|
1092
1126
|
}
|
|
1093
1127
|
function truncate(text, max) {
|
|
1094
1128
|
const clean = text.replace(/\s+/g, " ").trim();
|
|
@@ -2089,6 +2123,7 @@ async function listPages(filter, empty, options) {
|
|
|
2089
2123
|
function formatPageRows(pages, nowMs) {
|
|
2090
2124
|
return alignedRows(pages, nowMs, (p) => ({
|
|
2091
2125
|
id: p.id,
|
|
2126
|
+
url: p.url,
|
|
2092
2127
|
label: p.space,
|
|
2093
2128
|
color: colorForSpace(p.space),
|
|
2094
2129
|
text: p.title
|
|
@@ -2328,6 +2363,731 @@ async function fetchComments(client, key) {
|
|
|
2328
2363
|
body: c.body ?? null
|
|
2329
2364
|
}));
|
|
2330
2365
|
}
|
|
2366
|
+
const CREATEMETA_PAGE_SIZE = 200;
|
|
2367
|
+
async function fetchCreateIssueTypes(client, project) {
|
|
2368
|
+
return (await pageCreateMeta(client, project, "", (page) => page.issueTypes ?? [])).map((t) => ({
|
|
2369
|
+
id: t.id,
|
|
2370
|
+
name: t.name,
|
|
2371
|
+
description: t.description ?? "",
|
|
2372
|
+
subtask: t.subtask ?? false
|
|
2373
|
+
}));
|
|
2374
|
+
}
|
|
2375
|
+
async function fetchCreateFields(client, project, issueTypeId) {
|
|
2376
|
+
return (await pageCreateMeta(client, project, `/${encodeURIComponent(issueTypeId)}`, (page) => page.fields ?? [])).map((f) => ({
|
|
2377
|
+
...f,
|
|
2378
|
+
name: decodeEntities(f.name),
|
|
2379
|
+
allowedValues: f.allowedValues?.map(decodeAllowedValue)
|
|
2380
|
+
}));
|
|
2381
|
+
}
|
|
2382
|
+
function decodeAllowedValue(v) {
|
|
2383
|
+
return {
|
|
2384
|
+
...v,
|
|
2385
|
+
name: v.name === void 0 ? void 0 : decodeEntities(v.name),
|
|
2386
|
+
value: v.value === void 0 ? void 0 : decodeEntities(v.value),
|
|
2387
|
+
children: v.children?.map(decodeAllowedValue)
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
async function pageCreateMeta(client, project, suffix, items) {
|
|
2391
|
+
const base = `/rest/api/3/issue/createmeta/${encodeURIComponent(project)}/issuetypes${suffix}`;
|
|
2392
|
+
const all = [];
|
|
2393
|
+
for (let startAt = 0;;) {
|
|
2394
|
+
let page;
|
|
2395
|
+
try {
|
|
2396
|
+
page = await client.getJson(`${base}?startAt=${startAt}&maxResults=${CREATEMETA_PAGE_SIZE}`);
|
|
2397
|
+
} catch (err) {
|
|
2398
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`No project found with key "${project}".`);
|
|
2399
|
+
throw err;
|
|
2400
|
+
}
|
|
2401
|
+
const values = items(page);
|
|
2402
|
+
all.push(...values);
|
|
2403
|
+
startAt += values.length;
|
|
2404
|
+
if (values.length === 0 || startAt >= (page.total ?? 0)) return all;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
async function createIssue(client, site, fields) {
|
|
2408
|
+
const res = await client.postJson("/rest/api/3/issue?updateHistory=true", { fields });
|
|
2409
|
+
return {
|
|
2410
|
+
id: res.id,
|
|
2411
|
+
key: res.key,
|
|
2412
|
+
url: browseUrl(site, res.key)
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
async function searchAssignableUsers(client, project, query) {
|
|
2416
|
+
const params = new URLSearchParams({
|
|
2417
|
+
project,
|
|
2418
|
+
query,
|
|
2419
|
+
maxResults: "20"
|
|
2420
|
+
});
|
|
2421
|
+
return (await client.getJson(`/rest/api/3/user/assignable/search?${params.toString()}`)).map(toJiraUser);
|
|
2422
|
+
}
|
|
2423
|
+
async function fetchMyself(client) {
|
|
2424
|
+
return toJiraUser(await client.getJson("/rest/api/3/myself"));
|
|
2425
|
+
}
|
|
2426
|
+
function toJiraUser(u) {
|
|
2427
|
+
return {
|
|
2428
|
+
accountId: u.accountId,
|
|
2429
|
+
displayName: u.displayName ?? "",
|
|
2430
|
+
email: u.emailAddress ?? "",
|
|
2431
|
+
active: u.active ?? true
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
//#endregion
|
|
2435
|
+
//#region src/create/encode.ts
|
|
2436
|
+
const SERVER_FILLED = new Set([
|
|
2437
|
+
"project",
|
|
2438
|
+
"issuetype",
|
|
2439
|
+
"reporter"
|
|
2440
|
+
]);
|
|
2441
|
+
function requiredFields(meta) {
|
|
2442
|
+
return meta.filter((f) => f.required && !f.hasDefaultValue && !SERVER_FILLED.has(f.fieldId));
|
|
2443
|
+
}
|
|
2444
|
+
function findField(meta, name) {
|
|
2445
|
+
const needle = name.trim().toLowerCase();
|
|
2446
|
+
const byId = meta.find((f) => f.fieldId === name.trim());
|
|
2447
|
+
if (byId) return { field: byId };
|
|
2448
|
+
const byName = meta.filter((f) => f.name.toLowerCase() === needle);
|
|
2449
|
+
if (byName.length === 1) return { field: byName[0] };
|
|
2450
|
+
if (byName.length > 1) return { ambiguous: byName };
|
|
2451
|
+
return { unknown: true };
|
|
2452
|
+
}
|
|
2453
|
+
function assign(meta, inputs, problems) {
|
|
2454
|
+
const byField = /* @__PURE__ */ new Map();
|
|
2455
|
+
for (const input of inputs) {
|
|
2456
|
+
const match = findField(meta, input.name);
|
|
2457
|
+
if ("unknown" in match) {
|
|
2458
|
+
problems.push(`"${input.name.trim()}" is not on this create screen.`);
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
if ("ambiguous" in match) {
|
|
2462
|
+
const ids = match.ambiguous.map((f) => f.fieldId).join(", ");
|
|
2463
|
+
problems.push(`"${input.name.trim()}" matches more than one field; use the id: ${ids}.`);
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
const existing = byField.get(match.field.fieldId);
|
|
2467
|
+
if (!existing) byField.set(match.field.fieldId, {
|
|
2468
|
+
field: match.field,
|
|
2469
|
+
values: [...input.values],
|
|
2470
|
+
source: input.source
|
|
2471
|
+
});
|
|
2472
|
+
else if (existing.source !== input.source) problems.push(`${match.field.name} was given by both ${existing.source} and ${input.source}.`);
|
|
2473
|
+
else existing.values.push(...input.values);
|
|
2474
|
+
}
|
|
2475
|
+
return [...byField.values()];
|
|
2476
|
+
}
|
|
2477
|
+
async function encodeCreate(meta, inputs, resolveUser) {
|
|
2478
|
+
const problems = [];
|
|
2479
|
+
const fields = {};
|
|
2480
|
+
const assignments = assign(meta, inputs, problems);
|
|
2481
|
+
for (const a of assignments) {
|
|
2482
|
+
const encoded = await encodeField(a.field, a.values, resolveUser);
|
|
2483
|
+
if ("problem" in encoded) problems.push(encoded.problem);
|
|
2484
|
+
else fields[a.field.fieldId] = encoded.value;
|
|
2485
|
+
}
|
|
2486
|
+
const set = new Set(assignments.map((a) => a.field.fieldId));
|
|
2487
|
+
return {
|
|
2488
|
+
fields,
|
|
2489
|
+
missing: requiredFields(meta).filter((f) => !set.has(f.fieldId)),
|
|
2490
|
+
problems
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
function isMultiLine(schema) {
|
|
2494
|
+
return schema.system === "description" || schema.system === "environment" || (schema.custom?.endsWith(":textarea") ?? false);
|
|
2495
|
+
}
|
|
2496
|
+
function isLabels(schema) {
|
|
2497
|
+
return schema.system === "labels" || (schema.custom?.endsWith(":labels") ?? false);
|
|
2498
|
+
}
|
|
2499
|
+
async function encodeField(field, values, resolveUser) {
|
|
2500
|
+
const { schema } = field;
|
|
2501
|
+
if (schema.type === "array") {
|
|
2502
|
+
const items = schema.items ?? "string";
|
|
2503
|
+
const out = [];
|
|
2504
|
+
for (const token of splitCommas(values)) {
|
|
2505
|
+
const encoded = await encodeScalar(field, items, token, resolveUser);
|
|
2506
|
+
if ("problem" in encoded) return encoded;
|
|
2507
|
+
out.push(encoded.value);
|
|
2508
|
+
}
|
|
2509
|
+
return { value: out };
|
|
2510
|
+
}
|
|
2511
|
+
if (values.length !== 1) return { problem: `${field.name} takes one value.` };
|
|
2512
|
+
return encodeScalar(field, schema.type, values[0], resolveUser);
|
|
2513
|
+
}
|
|
2514
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2515
|
+
const DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/i;
|
|
2516
|
+
const NAMED_TYPES = new Set([
|
|
2517
|
+
"priority",
|
|
2518
|
+
"version",
|
|
2519
|
+
"component",
|
|
2520
|
+
"securitylevel",
|
|
2521
|
+
"issuetype"
|
|
2522
|
+
]);
|
|
2523
|
+
async function encodeScalar(field, type, raw, resolveUser) {
|
|
2524
|
+
const fail = (text) => ({ problem: `${field.name} ${text}` });
|
|
2525
|
+
const token = raw.trim();
|
|
2526
|
+
const { schema } = field;
|
|
2527
|
+
if (type === "option-with-child") return matchCascading(field, token);
|
|
2528
|
+
if (field.allowedValues && field.allowedValues.length > 0) return matchAllowed(field, token);
|
|
2529
|
+
switch (type) {
|
|
2530
|
+
case "string":
|
|
2531
|
+
if (isMultiLine(schema)) return { value: markdownToAdf(raw) };
|
|
2532
|
+
if (isLabels(schema) && /\s/.test(token)) return fail(`cannot contain spaces: "${token}".`);
|
|
2533
|
+
return { value: schema.type === "array" ? token : raw };
|
|
2534
|
+
case "number": {
|
|
2535
|
+
const n = Number(token);
|
|
2536
|
+
if (token === "" || Number.isNaN(n)) return fail(`must be a number, not "${token}".`);
|
|
2537
|
+
return { value: n };
|
|
2538
|
+
}
|
|
2539
|
+
case "date":
|
|
2540
|
+
if (!DATE.test(token)) return fail(`must be YYYY-MM-DD, not "${token}".`);
|
|
2541
|
+
return { value: token };
|
|
2542
|
+
case "datetime": {
|
|
2543
|
+
const ms = DATETIME.test(token) ? Date.parse(token) : NaN;
|
|
2544
|
+
if (Number.isNaN(ms)) return fail(`must be an ISO 8601 datetime, not "${token}".`);
|
|
2545
|
+
return { value: formatJiraDateTime(new Date(ms)) };
|
|
2546
|
+
}
|
|
2547
|
+
case "option": return { value: { value: token } };
|
|
2548
|
+
case "project": return { value: { key: token.toUpperCase() } };
|
|
2549
|
+
case "user": try {
|
|
2550
|
+
return { value: { accountId: await resolveUser(token) } };
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
return { problem: `${field.name}: ${err instanceof Error ? err.message : String(err)}` };
|
|
2553
|
+
}
|
|
2554
|
+
case "group": return { value: { name: token } };
|
|
2555
|
+
case "issuelink": return { value: { key: token.toUpperCase() } };
|
|
2556
|
+
default:
|
|
2557
|
+
if (NAMED_TYPES.has(type)) return { value: { name: token } };
|
|
2558
|
+
return { value: looseValue(raw) };
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
function looseValue(raw) {
|
|
2562
|
+
const trimmed = raw.trim();
|
|
2563
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return raw;
|
|
2564
|
+
try {
|
|
2565
|
+
return JSON.parse(trimmed);
|
|
2566
|
+
} catch {
|
|
2567
|
+
return raw;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function formatJiraDateTime(date) {
|
|
2571
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
2572
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
2573
|
+
const sign = offsetMinutes < 0 ? "-" : "+";
|
|
2574
|
+
const abs = Math.abs(offsetMinutes);
|
|
2575
|
+
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
2576
|
+
const hms = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
2577
|
+
const offset = `${sign}${pad(Math.floor(abs / 60))}${pad(abs % 60)}`;
|
|
2578
|
+
return `${ymd}T${hms}.${pad(date.getMilliseconds(), 3)}${offset}`;
|
|
2579
|
+
}
|
|
2580
|
+
function splitCommas(values) {
|
|
2581
|
+
return values.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
2582
|
+
}
|
|
2583
|
+
function allowedLabel(v) {
|
|
2584
|
+
return v.value ?? v.name ?? v.key ?? v.id ?? "";
|
|
2585
|
+
}
|
|
2586
|
+
function findAllowed(options, token) {
|
|
2587
|
+
const needle = token.toLowerCase();
|
|
2588
|
+
return options.find((v) => v.id === token) ?? options.find((v) => allowedLabel(v).toLowerCase() === needle);
|
|
2589
|
+
}
|
|
2590
|
+
function allowedList(options) {
|
|
2591
|
+
return options.map(allowedLabel).join(", ");
|
|
2592
|
+
}
|
|
2593
|
+
function matchAllowed(field, token) {
|
|
2594
|
+
const options = field.allowedValues ?? [];
|
|
2595
|
+
const hit = findAllowed(options, token);
|
|
2596
|
+
if (!hit) return { problem: `${field.name} has no value "${token}"; allowed: ${allowedList(options)}.` };
|
|
2597
|
+
return { value: { id: hit.id } };
|
|
2598
|
+
}
|
|
2599
|
+
function matchCascading(field, value) {
|
|
2600
|
+
const options = field.allowedValues ?? [];
|
|
2601
|
+
const [parentText = "", childText] = value.split(">").map((s) => s.trim());
|
|
2602
|
+
const parent = findAllowed(options, parentText);
|
|
2603
|
+
if (!parent) return { problem: `${field.name} has no value "${parentText}"; allowed: ${allowedList(options)}.` };
|
|
2604
|
+
if (!childText) return { value: { id: parent.id } };
|
|
2605
|
+
const children = parent.children ?? [];
|
|
2606
|
+
const child = findAllowed(children, childText);
|
|
2607
|
+
if (!child) {
|
|
2608
|
+
const under = allowedLabel(parent);
|
|
2609
|
+
return { problem: `${field.name} has no value "${childText}" under ${under}; allowed: ${allowedList(children)}.` };
|
|
2610
|
+
}
|
|
2611
|
+
return { value: {
|
|
2612
|
+
id: parent.id,
|
|
2613
|
+
child: { id: child.id }
|
|
2614
|
+
} };
|
|
2615
|
+
}
|
|
2616
|
+
//#endregion
|
|
2617
|
+
//#region src/create/format.ts
|
|
2618
|
+
const SCALAR_LABELS = {
|
|
2619
|
+
string: "text",
|
|
2620
|
+
number: "number",
|
|
2621
|
+
date: "date",
|
|
2622
|
+
datetime: "datetime",
|
|
2623
|
+
option: "option",
|
|
2624
|
+
"option-with-child": "cascading option",
|
|
2625
|
+
user: "user",
|
|
2626
|
+
group: "group",
|
|
2627
|
+
priority: "priority",
|
|
2628
|
+
issuetype: "issue type",
|
|
2629
|
+
project: "project",
|
|
2630
|
+
version: "version",
|
|
2631
|
+
component: "component",
|
|
2632
|
+
issuelink: "issue key",
|
|
2633
|
+
securitylevel: "security level",
|
|
2634
|
+
timetracking: "time tracking"
|
|
2635
|
+
};
|
|
2636
|
+
const PLURAL_LABELS = {
|
|
2637
|
+
string: "text list",
|
|
2638
|
+
option: "options",
|
|
2639
|
+
user: "users",
|
|
2640
|
+
group: "groups",
|
|
2641
|
+
version: "versions",
|
|
2642
|
+
component: "components"
|
|
2643
|
+
};
|
|
2644
|
+
function fieldTypeLabel(schema) {
|
|
2645
|
+
if (isMultiLine(schema)) return "multi-line text";
|
|
2646
|
+
if (isLabels(schema)) return "labels";
|
|
2647
|
+
if (schema.type === "array") {
|
|
2648
|
+
const items = schema.items ?? "string";
|
|
2649
|
+
return PLURAL_LABELS[items] ?? `${SCALAR_LABELS[items] ?? items} list`;
|
|
2650
|
+
}
|
|
2651
|
+
const known = SCALAR_LABELS[schema.type];
|
|
2652
|
+
if (known) return known;
|
|
2653
|
+
return schema.custom?.split(/[:/]/).pop() || schema.type;
|
|
2654
|
+
}
|
|
2655
|
+
const ALLOWED_SHOWN = 8;
|
|
2656
|
+
function allowedSummary(field) {
|
|
2657
|
+
const options = field.allowedValues ?? [];
|
|
2658
|
+
if (options.length === 0) return void 0;
|
|
2659
|
+
const shown = options.slice(0, ALLOWED_SHOWN).map(allowedLabel);
|
|
2660
|
+
const rest = options.length - shown.length;
|
|
2661
|
+
if (rest > 0) shown.push(`+${rest} more`);
|
|
2662
|
+
return `allowed: ${shown.join(", ")}`;
|
|
2663
|
+
}
|
|
2664
|
+
function defaultSummary(field) {
|
|
2665
|
+
if (!field.hasDefaultValue || field.defaultValue == null) return void 0;
|
|
2666
|
+
return `default: ${valueLabel(field.defaultValue)}`;
|
|
2667
|
+
}
|
|
2668
|
+
function valueLabel(value) {
|
|
2669
|
+
if (Array.isArray(value)) return value.map(valueLabel).join(", ");
|
|
2670
|
+
if (typeof value === "object" && value !== null) return allowedLabel(value);
|
|
2671
|
+
return String(value);
|
|
2672
|
+
}
|
|
2673
|
+
const DEDICATED_FLAGS = {
|
|
2674
|
+
summary: "--summary",
|
|
2675
|
+
description: "--description",
|
|
2676
|
+
assignee: "--assignee",
|
|
2677
|
+
priority: "--priority",
|
|
2678
|
+
labels: "--label",
|
|
2679
|
+
components: "--component",
|
|
2680
|
+
parent: "--parent"
|
|
2681
|
+
};
|
|
2682
|
+
function flagFor(field) {
|
|
2683
|
+
return DEDICATED_FLAGS[field.fieldId] ?? "--field";
|
|
2684
|
+
}
|
|
2685
|
+
function formatMissingFields(project, type, missing) {
|
|
2686
|
+
const nameWidth = Math.max(...missing.map((f) => f.name.length));
|
|
2687
|
+
const idWidth = Math.max(...missing.map((f) => f.fieldId.length + 2));
|
|
2688
|
+
const rows = missing.map((f) => {
|
|
2689
|
+
const note = allowedSummary(f) ?? fieldTypeLabel(f.schema);
|
|
2690
|
+
return ` ${f.name.padEnd(nameWidth)} ${`(${f.fieldId})`.padEnd(idWidth)} ${note}`;
|
|
2691
|
+
});
|
|
2692
|
+
const flags = [...new Set(missing.map(flagFor))].join("/");
|
|
2693
|
+
return [
|
|
2694
|
+
`${project} ${type} needs these fields:`,
|
|
2695
|
+
...rows,
|
|
2696
|
+
`Pass them with ${flags}, or run \`jira fields ${project} ${type}\` to see the form.`
|
|
2697
|
+
];
|
|
2698
|
+
}
|
|
2699
|
+
function formatFieldRows(fields) {
|
|
2700
|
+
const cells = fields.map((f) => ({
|
|
2701
|
+
name: f.name,
|
|
2702
|
+
id: f.fieldId,
|
|
2703
|
+
type: fieldTypeLabel(f.schema),
|
|
2704
|
+
required: f.required ? "required" : "",
|
|
2705
|
+
notes: [defaultSummary(f), allowedSummary(f)].filter((n) => n !== void 0)
|
|
2706
|
+
}));
|
|
2707
|
+
const width = (pick) => Math.max(...cells.map((c) => pick(c).length));
|
|
2708
|
+
const nameWidth = width((c) => c.name);
|
|
2709
|
+
const idWidth = width((c) => c.id);
|
|
2710
|
+
const typeWidth = width((c) => c.type);
|
|
2711
|
+
const requiredWidth = width((c) => c.required);
|
|
2712
|
+
return cells.map((c) => [
|
|
2713
|
+
c.name.padEnd(nameWidth),
|
|
2714
|
+
c.id.padEnd(idWidth),
|
|
2715
|
+
c.type.padEnd(typeWidth),
|
|
2716
|
+
c.required.padEnd(requiredWidth),
|
|
2717
|
+
...c.notes
|
|
2718
|
+
].join(" ").trimEnd());
|
|
2719
|
+
}
|
|
2720
|
+
function formatIssueTypeRows(types) {
|
|
2721
|
+
const width = Math.max(...types.map((t) => t.name.length));
|
|
2722
|
+
return types.map((t) => {
|
|
2723
|
+
const tail = [t.subtask ? "(subtask)" : "", t.description].filter(Boolean).join(" ");
|
|
2724
|
+
return `${t.name.padEnd(width)} ${tail}`.trimEnd();
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
//#endregion
|
|
2728
|
+
//#region src/create/prompt.ts
|
|
2729
|
+
const PROMPT_SOURCE = "prompt";
|
|
2730
|
+
async function walkFields(meta, deps) {
|
|
2731
|
+
const firstPass = meta.filter((f) => f.required && !SERVER_FILLED.has(f.fieldId));
|
|
2732
|
+
const inputs = [];
|
|
2733
|
+
for (const field of firstPass) {
|
|
2734
|
+
const answer = await promptField(field, deps, true);
|
|
2735
|
+
if (answer) inputs.push(answer);
|
|
2736
|
+
}
|
|
2737
|
+
const optional = meta.filter((f) => !firstPass.includes(f) && !SERVER_FILLED.has(f.fieldId));
|
|
2738
|
+
if (optional.length === 0) return inputs;
|
|
2739
|
+
const chosen = await checkbox({
|
|
2740
|
+
message: "Set any optional fields?",
|
|
2741
|
+
choices: optional.map((f) => ({
|
|
2742
|
+
name: `${f.name} (${fieldTypeLabel(f.schema)})`,
|
|
2743
|
+
value: f.fieldId
|
|
2744
|
+
})),
|
|
2745
|
+
pageSize: 15
|
|
2746
|
+
});
|
|
2747
|
+
for (const field of optional.filter((f) => chosen.includes(f.fieldId))) {
|
|
2748
|
+
const answer = await promptField(field, deps, false);
|
|
2749
|
+
if (answer) inputs.push(answer);
|
|
2750
|
+
}
|
|
2751
|
+
return inputs;
|
|
2752
|
+
}
|
|
2753
|
+
async function promptField(field, deps, required) {
|
|
2754
|
+
const answer = await askValues(field, deps, required);
|
|
2755
|
+
if (answer.values.length === 0) return void 0;
|
|
2756
|
+
return {
|
|
2757
|
+
name: field.fieldId,
|
|
2758
|
+
values: answer.values,
|
|
2759
|
+
source: PROMPT_SOURCE,
|
|
2760
|
+
display: answer.display ?? displayFor(field, answer.values)
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
const SKIP = {
|
|
2764
|
+
name: "(skip)",
|
|
2765
|
+
value: ""
|
|
2766
|
+
};
|
|
2767
|
+
async function askValues(field, deps, required) {
|
|
2768
|
+
const { schema } = field;
|
|
2769
|
+
const message = `${field.name}:`;
|
|
2770
|
+
const options = field.allowedValues ?? [];
|
|
2771
|
+
if (schema.type === "option-with-child") return askCascading(field, required);
|
|
2772
|
+
if (options.length > 0 && schema.type === "array") {
|
|
2773
|
+
const defaults = new Set(defaultIds(field));
|
|
2774
|
+
return { values: await checkbox({
|
|
2775
|
+
message,
|
|
2776
|
+
choices: options.map((v) => ({
|
|
2777
|
+
...choice(v),
|
|
2778
|
+
checked: defaults.has(v.id ?? "")
|
|
2779
|
+
})),
|
|
2780
|
+
required,
|
|
2781
|
+
pageSize: 15
|
|
2782
|
+
}) };
|
|
2783
|
+
}
|
|
2784
|
+
if (options.length > 0) {
|
|
2785
|
+
const choices = options.map(choice);
|
|
2786
|
+
const picked = await select({
|
|
2787
|
+
message,
|
|
2788
|
+
choices: required ? choices : [SKIP, ...choices],
|
|
2789
|
+
default: defaultIds(field)[0],
|
|
2790
|
+
pageSize: 15
|
|
2791
|
+
});
|
|
2792
|
+
return { values: picked ? [picked] : [] };
|
|
2793
|
+
}
|
|
2794
|
+
if (schema.type === "user") return askUser(field, deps, required);
|
|
2795
|
+
if (isMultiLine(schema)) {
|
|
2796
|
+
const text = await editor({
|
|
2797
|
+
message: `${field.name} (opens your editor, Markdown):`,
|
|
2798
|
+
postfix: ".md",
|
|
2799
|
+
default: primitiveDefault(field),
|
|
2800
|
+
validate: (v) => !required || v.trim().length > 0 ? true : `${field.name} is required.`
|
|
2801
|
+
});
|
|
2802
|
+
return { values: text.trim() ? [text] : [] };
|
|
2803
|
+
}
|
|
2804
|
+
const hint = inputHint(field);
|
|
2805
|
+
const text = await input({
|
|
2806
|
+
message: hint ? `${field.name} (${hint}):` : message,
|
|
2807
|
+
default: primitiveDefault(field),
|
|
2808
|
+
validate: async (v) => {
|
|
2809
|
+
if (v.trim() === "") return required ? `${field.name} is required.` : true;
|
|
2810
|
+
return deps.validate(field, v);
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
return { values: text.trim() ? [text] : [] };
|
|
2814
|
+
}
|
|
2815
|
+
function choice(v) {
|
|
2816
|
+
return {
|
|
2817
|
+
name: allowedLabel(v),
|
|
2818
|
+
value: v.id ?? allowedLabel(v)
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
function inputHint(field) {
|
|
2822
|
+
const type = field.schema.type === "array" ? field.schema.items : field.schema.type;
|
|
2823
|
+
const list = field.schema.type === "array" ? ", comma separated" : "";
|
|
2824
|
+
switch (type) {
|
|
2825
|
+
case "date": return `YYYY-MM-DD${list}`;
|
|
2826
|
+
case "datetime": return `ISO 8601${list}`;
|
|
2827
|
+
case "number": return `number${list}`;
|
|
2828
|
+
case "user": return `name, email, or me${list}`;
|
|
2829
|
+
case "string": return list ? "comma separated" : void 0;
|
|
2830
|
+
default: return fieldTypeLabel(field.schema);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
function defaultIds(field) {
|
|
2834
|
+
if (!field.hasDefaultValue || field.defaultValue == null) return [];
|
|
2835
|
+
const d = field.defaultValue;
|
|
2836
|
+
return (Array.isArray(d) ? d : [d]).map((v) => v.id).filter((id) => id !== void 0);
|
|
2837
|
+
}
|
|
2838
|
+
function primitiveDefault(field) {
|
|
2839
|
+
const d = field.defaultValue;
|
|
2840
|
+
if (!field.hasDefaultValue || d == null) return void 0;
|
|
2841
|
+
return typeof d === "string" || typeof d === "number" ? String(d) : void 0;
|
|
2842
|
+
}
|
|
2843
|
+
async function askCascading(field, required) {
|
|
2844
|
+
const options = field.allowedValues ?? [];
|
|
2845
|
+
const parentChoices = options.map(choice);
|
|
2846
|
+
const parentId = await select({
|
|
2847
|
+
message: `${field.name}:`,
|
|
2848
|
+
choices: required ? parentChoices : [SKIP, ...parentChoices],
|
|
2849
|
+
pageSize: 15
|
|
2850
|
+
});
|
|
2851
|
+
if (!parentId) return { values: [] };
|
|
2852
|
+
const parent = options.find((v) => v.id === parentId);
|
|
2853
|
+
const children = parent?.children ?? [];
|
|
2854
|
+
if (children.length === 0) return { values: [parentId] };
|
|
2855
|
+
const childId = await select({
|
|
2856
|
+
message: `${field.name} > ${allowedLabel(parent)}:`,
|
|
2857
|
+
choices: [{
|
|
2858
|
+
name: "(none)",
|
|
2859
|
+
value: ""
|
|
2860
|
+
}, ...children.map(choice)],
|
|
2861
|
+
pageSize: 15
|
|
2862
|
+
});
|
|
2863
|
+
return { values: [childId ? `${parentId} > ${childId}` : parentId] };
|
|
2864
|
+
}
|
|
2865
|
+
const ME = {
|
|
2866
|
+
name: "me",
|
|
2867
|
+
value: "me"
|
|
2868
|
+
};
|
|
2869
|
+
const UNASSIGNED = {
|
|
2870
|
+
name: "(unassigned)",
|
|
2871
|
+
value: ""
|
|
2872
|
+
};
|
|
2873
|
+
async function askUser(field, deps, required) {
|
|
2874
|
+
const fixed = required ? [ME] : [ME, UNASSIGNED];
|
|
2875
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2876
|
+
const picked = await search({
|
|
2877
|
+
message: `${field.name} (type to search):`,
|
|
2878
|
+
source: async (term) => {
|
|
2879
|
+
if (!term) return fixed;
|
|
2880
|
+
const users = await deps.searchUsers(term);
|
|
2881
|
+
for (const u of users) seen.set(u.accountId, u.displayName);
|
|
2882
|
+
return [...fixed, ...users.filter((u) => u.active).map((u) => ({
|
|
2883
|
+
name: u.email ? `${u.displayName} <${u.email}>` : u.displayName,
|
|
2884
|
+
value: u.accountId
|
|
2885
|
+
}))];
|
|
2886
|
+
},
|
|
2887
|
+
pageSize: 10
|
|
2888
|
+
});
|
|
2889
|
+
if (!picked) return { values: [] };
|
|
2890
|
+
return {
|
|
2891
|
+
values: [picked],
|
|
2892
|
+
display: seen.get(picked) ?? picked
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
function displayFor(field, values) {
|
|
2896
|
+
const options = field.allowedValues ?? [];
|
|
2897
|
+
if (options.length === 0) return isMultiLine(field.schema) ? summarizeText(values[0] ?? "") : values.join(", ");
|
|
2898
|
+
return values.map((v) => v.split(">").map((part) => {
|
|
2899
|
+
const id = part.trim();
|
|
2900
|
+
const hit = options.find((o) => o.id === id) ?? findChild(options, id);
|
|
2901
|
+
return hit ? allowedLabel(hit) : id;
|
|
2902
|
+
}).join(" > ")).join(", ");
|
|
2903
|
+
}
|
|
2904
|
+
function findChild(options, id) {
|
|
2905
|
+
for (const o of options) {
|
|
2906
|
+
const hit = o.children?.find((c) => c.id === id);
|
|
2907
|
+
if (hit) return hit;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
function summarizeText(text) {
|
|
2911
|
+
const lines = text.trim().split(/\r?\n/);
|
|
2912
|
+
const first = lines[0] ?? "";
|
|
2913
|
+
const more = lines.length - 1;
|
|
2914
|
+
return more > 0 ? `${first} (+${more} more lines)` : first;
|
|
2915
|
+
}
|
|
2916
|
+
//#endregion
|
|
2917
|
+
//#region src/commands/jira-create.ts
|
|
2918
|
+
async function flagInputs(options) {
|
|
2919
|
+
const inputs = [];
|
|
2920
|
+
const one = (name, value, source) => {
|
|
2921
|
+
if (value !== void 0) inputs.push({
|
|
2922
|
+
name,
|
|
2923
|
+
values: [value],
|
|
2924
|
+
source
|
|
2925
|
+
});
|
|
2926
|
+
};
|
|
2927
|
+
one("summary", options.summary, "--summary");
|
|
2928
|
+
one("description", options.description, "--description");
|
|
2929
|
+
if (options.descriptionFile !== void 0) {
|
|
2930
|
+
if (options.description !== void 0) throw new Error("--description and --description-file cannot be used together.");
|
|
2931
|
+
one("description", await readFile(options.descriptionFile, "utf8"), "--description-file");
|
|
2932
|
+
}
|
|
2933
|
+
one("assignee", options.assignee, "--assignee");
|
|
2934
|
+
one("priority", options.priority, "--priority");
|
|
2935
|
+
one("parent", options.parent, "--parent");
|
|
2936
|
+
if (options.label?.length) inputs.push({
|
|
2937
|
+
name: "labels",
|
|
2938
|
+
values: options.label,
|
|
2939
|
+
source: "--label"
|
|
2940
|
+
});
|
|
2941
|
+
if (options.component?.length) inputs.push({
|
|
2942
|
+
name: "components",
|
|
2943
|
+
values: options.component,
|
|
2944
|
+
source: "--component"
|
|
2945
|
+
});
|
|
2946
|
+
for (const raw of options.field ?? []) inputs.push(parseFieldFlag(raw));
|
|
2947
|
+
return inputs;
|
|
2948
|
+
}
|
|
2949
|
+
function parseFieldFlag(raw) {
|
|
2950
|
+
const eq = raw.indexOf("=");
|
|
2951
|
+
if (eq <= 0) throw new Error(`--field expects NAME=VALUE, got "${raw}".`);
|
|
2952
|
+
return {
|
|
2953
|
+
name: raw.slice(0, eq),
|
|
2954
|
+
values: [raw.slice(eq + 1)],
|
|
2955
|
+
source: "--field"
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
async function jiraCreate(projectArg, typeArg, options) {
|
|
2959
|
+
const inputs = await flagInputs(options);
|
|
2960
|
+
const strict = inputs.length > 0 || options.input === false || !process.stdin.isTTY;
|
|
2961
|
+
const auth = await requireAuth();
|
|
2962
|
+
const client = new AtlassianClient(auth);
|
|
2963
|
+
const project = await resolveProject(client, auth.site, projectArg, strict);
|
|
2964
|
+
const type = await resolveType(project, await fetchCreateIssueTypes(client, project), typeArg, strict);
|
|
2965
|
+
const meta = await fetchCreateFields(client, project, type.id);
|
|
2966
|
+
const resolveUser = userResolver(client, project);
|
|
2967
|
+
if (!strict) inputs.push(...await walkFields(meta, {
|
|
2968
|
+
searchUsers: (query) => searchAssignableUsers(client, project, query),
|
|
2969
|
+
validate: async (field, value) => {
|
|
2970
|
+
return (await encodeCreate([field], [{
|
|
2971
|
+
name: field.fieldId,
|
|
2972
|
+
values: [value],
|
|
2973
|
+
source: "prompt"
|
|
2974
|
+
}], resolveUser)).problems[0] ?? true;
|
|
2975
|
+
}
|
|
2976
|
+
}));
|
|
2977
|
+
const result = await encodeCreate(meta, inputs, resolveUser);
|
|
2978
|
+
if (result.problems.length > 0) throw new Error(result.problems.join("\n"));
|
|
2979
|
+
if (result.missing.length > 0) throw new Error(formatMissingFields(project, type.name, result.missing).join("\n"));
|
|
2980
|
+
const fields = {
|
|
2981
|
+
project: { key: project },
|
|
2982
|
+
issuetype: { id: type.id },
|
|
2983
|
+
...result.fields
|
|
2984
|
+
};
|
|
2985
|
+
if (options.dryRun) {
|
|
2986
|
+
console.log(JSON.stringify({ fields }, null, 2));
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (!strict) {
|
|
2990
|
+
for (const line of formatReview(meta, inputs)) console.log(line);
|
|
2991
|
+
if (!await confirm({
|
|
2992
|
+
message: `Create ${project} ${type.name}?`,
|
|
2993
|
+
default: true
|
|
2994
|
+
})) {
|
|
2995
|
+
console.log("Aborted.");
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
try {
|
|
3000
|
+
const created = await createIssue(client, auth.site, fields);
|
|
3001
|
+
if (options.json) console.log(JSON.stringify(created, null, 2));
|
|
3002
|
+
else console.log(`Created ${created.key} ${created.url}`);
|
|
3003
|
+
} catch (err) {
|
|
3004
|
+
throw describeRejection(err, meta);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
function formatReview(meta, inputs) {
|
|
3008
|
+
const width = Math.max(...inputs.map((i) => nameOf(meta, i.name).length));
|
|
3009
|
+
return inputs.map((i) => `${`${nameOf(meta, i.name)}:`.padEnd(width + 1)} ${i.display ?? i.values.join(", ")}`);
|
|
3010
|
+
}
|
|
3011
|
+
function nameOf(meta, idOrName) {
|
|
3012
|
+
return meta.find((f) => f.fieldId === idOrName)?.name ?? idOrName;
|
|
3013
|
+
}
|
|
3014
|
+
function describeRejection(err, meta) {
|
|
3015
|
+
if (!(err instanceof HttpError)) return err;
|
|
3016
|
+
const { errorMessages, errors } = err.jira;
|
|
3017
|
+
const perField = Object.entries(errors).map(([id, msg]) => {
|
|
3018
|
+
const name = nameOf(meta, id);
|
|
3019
|
+
return name === id ? ` ${id}: ${msg}` : ` ${name} (${id}): ${msg}`;
|
|
3020
|
+
});
|
|
3021
|
+
if (errorMessages.length === 0 && perField.length === 0) return err;
|
|
3022
|
+
return new Error([
|
|
3023
|
+
"Jira rejected the issue:",
|
|
3024
|
+
...errorMessages.map((m) => ` ${m}`),
|
|
3025
|
+
...perField
|
|
3026
|
+
].join("\n"));
|
|
3027
|
+
}
|
|
3028
|
+
async function resolveProject(client, site, arg, strict) {
|
|
3029
|
+
if (arg) return arg.toUpperCase();
|
|
3030
|
+
if (strict) throw new Error("A project key is required, e.g. `jira create BSC Bug --summary ...`.");
|
|
3031
|
+
return search({
|
|
3032
|
+
message: "Project:",
|
|
3033
|
+
source: async (term) => (await listProjects(client, site, term)).map((p) => ({
|
|
3034
|
+
name: `${p.key} ${p.name}`,
|
|
3035
|
+
value: p.key
|
|
3036
|
+
})),
|
|
3037
|
+
pageSize: 15
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
async function resolveType(project, types, arg, strict) {
|
|
3041
|
+
if (types.length === 0) throw new Error(`You cannot create issues in ${project}.`);
|
|
3042
|
+
if (arg) {
|
|
3043
|
+
const found = matchType(types, arg);
|
|
3044
|
+
if (found) return found;
|
|
3045
|
+
const names = types.map((t) => t.name).join(", ");
|
|
3046
|
+
throw new Error(`${project} has no issue type "${arg}"; available: ${names}.`);
|
|
3047
|
+
}
|
|
3048
|
+
if (strict) throw new Error(`An issue type is required; ${project} has: ${types.map((t) => t.name).join(", ")}.`);
|
|
3049
|
+
return select({
|
|
3050
|
+
message: "Issue type:",
|
|
3051
|
+
choices: types.map((t) => ({
|
|
3052
|
+
name: t.name,
|
|
3053
|
+
value: t,
|
|
3054
|
+
description: t.description
|
|
3055
|
+
})),
|
|
3056
|
+
pageSize: 15
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
function matchType(types, arg) {
|
|
3060
|
+
const needle = arg.trim().toLowerCase();
|
|
3061
|
+
return types.find((t) => t.id === arg.trim()) ?? types.find((t) => t.name.toLowerCase() === needle);
|
|
3062
|
+
}
|
|
3063
|
+
const ACCOUNT_ID = /^(?:[0-9a-f]{24}|[a-z0-9]+:[0-9a-f-]{36}(?::[0-9a-f-]{36})?)$/i;
|
|
3064
|
+
function userResolver(client, project) {
|
|
3065
|
+
return async (query) => {
|
|
3066
|
+
if (query === "me") return (await fetchMyself(client)).accountId;
|
|
3067
|
+
if (ACCOUNT_ID.test(query)) return query;
|
|
3068
|
+
const users = (await searchAssignableUsers(client, project, query)).filter((u) => u.active);
|
|
3069
|
+
const exact = users.filter((u) => u.displayName.toLowerCase() === query.toLowerCase() || u.email.toLowerCase() === query.toLowerCase());
|
|
3070
|
+
const candidates = exact.length === 1 ? exact : users;
|
|
3071
|
+
if (candidates.length === 1) return candidates[0].accountId;
|
|
3072
|
+
if (candidates.length === 0) throw new Error(`no assignable user matches "${query}".`);
|
|
3073
|
+
const names = candidates.map((u) => u.displayName).join(", ");
|
|
3074
|
+
throw new Error(`"${query}" matches ${candidates.length} users: ${names}.`);
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
async function jiraFields(projectArg, typeArg, options) {
|
|
3078
|
+
const client = new AtlassianClient(await requireAuth());
|
|
3079
|
+
const project = projectArg.toUpperCase();
|
|
3080
|
+
const types = await fetchCreateIssueTypes(client, project);
|
|
3081
|
+
if (!typeArg) {
|
|
3082
|
+
if (options.json) console.log(JSON.stringify(types, null, 2));
|
|
3083
|
+
else if (types.length === 0) console.log(`You cannot create issues in ${project}.`);
|
|
3084
|
+
else for (const line of formatIssueTypeRows(types)) console.log(line);
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
const fields = await fetchCreateFields(client, project, (await resolveType(project, types, typeArg, true)).id);
|
|
3088
|
+
if (options.json) console.log(JSON.stringify(fields, null, 2));
|
|
3089
|
+
else for (const line of formatFieldRows(fields)) console.log(line);
|
|
3090
|
+
}
|
|
2331
3091
|
//#endregion
|
|
2332
3092
|
//#region src/commands/jira.ts
|
|
2333
3093
|
async function jiraProjects(query, options) {
|
|
@@ -2463,6 +3223,7 @@ async function jiraList(options) {
|
|
|
2463
3223
|
function formatIssueRows(issues, nowMs) {
|
|
2464
3224
|
return alignedRows(issues, nowMs, (i) => ({
|
|
2465
3225
|
id: i.key,
|
|
3226
|
+
url: i.url,
|
|
2466
3227
|
label: i.status,
|
|
2467
3228
|
color: colorForCategory(i.statusCategory),
|
|
2468
3229
|
text: i.summary
|
|
@@ -2483,10 +3244,15 @@ const ISSUE_REF = {
|
|
|
2483
3244
|
};
|
|
2484
3245
|
//#endregion
|
|
2485
3246
|
//#region src/cli/jira.ts
|
|
3247
|
+
function collect(value, previous = []) {
|
|
3248
|
+
return [...previous, value];
|
|
3249
|
+
}
|
|
2486
3250
|
function registerJira(jira) {
|
|
2487
3251
|
jira.description("Jira commands");
|
|
2488
3252
|
jira.command("projects [query]").description("List projects (optionally filtered by key or name)").option("--json", "output results as JSON").action(run(jiraProjects));
|
|
2489
3253
|
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));
|
|
3254
|
+
jira.command("create [project] [type]").description("Create a Jira issue, prompting for fields or taking them all as flags").option("-s, --summary <text>", "issue summary").option("-d, --description <markdown>", "description as Markdown").option("--description-file <path>", "read the description from a Markdown file").option("-a, --assignee <who>", "assignee: me, an account id, or a name to look up").option("--priority <name>", "priority name").option("-l, --label <label>", "label (repeatable, or comma separated)", collect).option("-c, --component <name>", "component (repeatable, or comma separated)", collect).option("--parent <key>", "parent issue key for subtasks").option("-f, --field <name=value>", "any other create-screen field (repeatable)", collect).option("--no-input", "never prompt; fail if a required field is missing").option("--dry-run", "print the resolved payload instead of creating").option("--json", "print the created issue as JSON").action(run(jiraCreate));
|
|
3255
|
+
jira.command("fields <project> [type]").description("Show the issue types you can create in a project, or the create form for one type").option("--json", "output results as JSON").action(run(jiraFields));
|
|
2490
3256
|
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));
|
|
2491
3257
|
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));
|
|
2492
3258
|
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));
|
|
@@ -2496,7 +3262,7 @@ function registerJira(jira) {
|
|
|
2496
3262
|
}
|
|
2497
3263
|
//#endregion
|
|
2498
3264
|
//#region package.json
|
|
2499
|
-
var version = "1.
|
|
3265
|
+
var version = "1.11.0";
|
|
2500
3266
|
//#endregion
|
|
2501
3267
|
//#region src/cli/build.ts
|
|
2502
3268
|
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-wKtbjBhd.mjs";
|
|
3
3
|
//#region src/confluence.ts
|
|
4
4
|
buildStandalone("confluence").parseAsync().catch(fail);
|
|
5
5
|
//#endregion
|
package/dist/jira.mjs
CHANGED