@kud/jira 0.3.0 → 0.4.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 +17 -1
- package/dist/index.d.ts +77 -7
- package/dist/index.js +73 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,7 @@ Returns a client object exposing `request` (the raw authenticated fetch wrapper)
|
|
|
116
116
|
| ----------------------------- | ------------------------------------------------------ |
|
|
117
117
|
| `getBoards()` | Lists all boards. |
|
|
118
118
|
| `getBoard(id)` | Fetches one board. |
|
|
119
|
+
| `getBoardConfiguration(id)` | The board's columns and the status ids each one claims. |
|
|
119
120
|
| `getBoardIssues(id, jql?)` | Lists a board's issues, optionally filtered by JQL. |
|
|
120
121
|
| `getBacklog(id)` | Lists a board's backlog issues. |
|
|
121
122
|
| `getSprints(boardId, state?)` | Lists a board's sprints, optionally filtered by state. |
|
|
@@ -153,6 +154,7 @@ Returns a client object exposing `request` (the raw authenticated fetch wrapper)
|
|
|
153
154
|
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
154
155
|
| `jiraApiError(status, method, url, body)` | Builds a typed `JiraApiError` (`Error` with `name: "JiraApiError"`, `status`, `method`, `url`, `body`). Used internally on every non-2xx response. |
|
|
155
156
|
| `isJiraApiError(e)` | Type guard narrowing an unknown catch value to `JiraApiError`. |
|
|
157
|
+
| `errorMessagesOf(e)` | Jira's own words for what went wrong — `errorMessages[]` and `errors{}` from the response body — falling back to the error's message. What a person can act on; `message` is the URL-prefixed envelope. |
|
|
156
158
|
|
|
157
159
|
```ts
|
|
158
160
|
import { isJiraApiError } from "@kud/jira"
|
|
@@ -168,6 +170,20 @@ try {
|
|
|
168
170
|
}
|
|
169
171
|
```
|
|
170
172
|
|
|
173
|
+
### JQL helpers
|
|
174
|
+
|
|
175
|
+
| Export | Description |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `jqlEscape(value)` | Quotes a value for JQL, escaping embedded quotes. |
|
|
178
|
+
| `buildJql(options, defaultProject?)` | Composes `assignee` / `mine` / `project` / `status` / `label` / `sprint` into one query, `ORDER BY updated DESC`; `jql` replaces everything. An empty set means yours. |
|
|
179
|
+
| `looksLikeJql(input)` | Whether a string has the shape of a JQL clause — `identifier operator` — as opposed to plain words. `"in progress"` is words; `"status = Done"` is a query. |
|
|
180
|
+
| `textClause(words)` | `text ~ "…"` with Lucene's specials escaped, so a search for `c++` does not throw. |
|
|
181
|
+
| `searchJql(input, { mode?, scope? })` | What a search box becomes: plain words search within `scope`; JQL replaces it. Returns the query and which reading was taken. |
|
|
182
|
+
|
|
183
|
+
### Issue types
|
|
184
|
+
|
|
185
|
+
`isContainerType(type)` says whether an issue type heads children — Jira Cloud's `hierarchyLevel > 0` when present (so a renamed Epic or an Initiative still counts), the name `Epic` otherwise. Use it rather than comparing names.
|
|
186
|
+
|
|
171
187
|
### ADF conversion
|
|
172
188
|
|
|
173
189
|
| Export | Description |
|
|
@@ -191,7 +207,7 @@ Turns a bare host (`myorg.atlassian.net`) into a full `https://` URL. A no-op on
|
|
|
191
207
|
|
|
192
208
|
### `loadConfig(env?, path?)`
|
|
193
209
|
|
|
194
|
-
Resolves the instance URL, email and token the way every `@kud` Jira surface does, so a host can build a client without re-implementing the lookup. The URL and email come from `ATLASSIAN_BASE_URL` / `ATLASSIAN_USER_EMAIL` or, failing those, from `$XDG_CONFIG_HOME/jira/config.json` (`JIRA_CONFIG_FILE` overrides the path); the token comes from `ATLASSIAN_API_TOKEN` only, and a token found in the file is an error. Returns `{ config }` ready for `createJiraClient`, or `{ missing: string[] }` naming every absent variable at once. `readFileConfig(path?)` and `configPath()` are exported for hosts that need the halves.
|
|
210
|
+
Resolves the instance URL, email and token the way every `@kud` Jira surface does, so a host can build a client without re-implementing the lookup. The URL and email come from `ATLASSIAN_BASE_URL` / `ATLASSIAN_USER_EMAIL` or, failing those, from `$XDG_CONFIG_HOME/jira/config.json` (`JIRA_CONFIG_FILE` overrides the path); the token comes from `ATLASSIAN_API_TOKEN` only, and a token found in the file is an error. Returns `{ config }` ready for `createJiraClient`, or `{ missing: string[] }` naming every absent variable at once. `readFileConfig(path?)` and `configPath()` are exported for hosts that need the halves. The file may also carry `defaultProject`, `defaultBoard`, `customFields`, `sprintField`, and `tabs` — hand-written board tabs (`{ label, statuses: string[] }[]`, statuses by id or name) for a TUI to use over a board's own column config.
|
|
195
211
|
|
|
196
212
|
## 🔧 Development
|
|
197
213
|
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type JiraUser = {
|
|
|
5
5
|
active?: boolean;
|
|
6
6
|
};
|
|
7
7
|
type JiraStatus = {
|
|
8
|
+
id?: string;
|
|
8
9
|
name: string;
|
|
9
10
|
statusCategory?: {
|
|
10
11
|
key: string;
|
|
@@ -17,9 +18,7 @@ type JiraIssueParent = {
|
|
|
17
18
|
fields?: {
|
|
18
19
|
summary?: string;
|
|
19
20
|
status?: JiraStatus;
|
|
20
|
-
issuetype?:
|
|
21
|
-
name: string;
|
|
22
|
-
};
|
|
21
|
+
issuetype?: JiraIssueType;
|
|
23
22
|
};
|
|
24
23
|
};
|
|
25
24
|
type JiraIssueFields = {
|
|
@@ -27,9 +26,7 @@ type JiraIssueFields = {
|
|
|
27
26
|
status?: JiraStatus;
|
|
28
27
|
assignee?: JiraUser | null;
|
|
29
28
|
reporter?: JiraUser | null;
|
|
30
|
-
issuetype?:
|
|
31
|
-
name: string;
|
|
32
|
-
};
|
|
29
|
+
issuetype?: JiraIssueType;
|
|
33
30
|
priority?: {
|
|
34
31
|
name: string;
|
|
35
32
|
} | null;
|
|
@@ -116,9 +113,31 @@ type JiraNamed = {
|
|
|
116
113
|
name: string;
|
|
117
114
|
description?: string;
|
|
118
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* `hierarchyLevel` is Jira Cloud's own word for what an issue type is in the
|
|
118
|
+
* tree — 1 for Epic, 0 for a standard type, -1 for a sub-task — and it is how
|
|
119
|
+
* a container is recognised here, so a renamed Epic or a level-1 Initiative
|
|
120
|
+
* still heads its children. Server/DC may omit it, hence the name fallback.
|
|
121
|
+
*/
|
|
119
122
|
type JiraIssueType = JiraNamed & {
|
|
120
123
|
subtask?: boolean;
|
|
121
124
|
scope?: unknown;
|
|
125
|
+
hierarchyLevel?: number;
|
|
126
|
+
};
|
|
127
|
+
declare const isContainerType: (type: JiraIssueType | undefined) => boolean;
|
|
128
|
+
type JiraBoardColumn = {
|
|
129
|
+
name: string;
|
|
130
|
+
statuses: {
|
|
131
|
+
id: string;
|
|
132
|
+
}[];
|
|
133
|
+
};
|
|
134
|
+
type JiraBoardConfiguration = {
|
|
135
|
+
id: number;
|
|
136
|
+
name: string;
|
|
137
|
+
type?: string;
|
|
138
|
+
columnConfig?: {
|
|
139
|
+
columns: JiraBoardColumn[];
|
|
140
|
+
};
|
|
122
141
|
};
|
|
123
142
|
type JiraVersion = JiraNamed & {
|
|
124
143
|
released?: boolean;
|
|
@@ -195,6 +214,12 @@ type JiraApiError = Error & {
|
|
|
195
214
|
};
|
|
196
215
|
declare const jiraApiError: (status: number, method: string, url: string, body: string) => JiraApiError;
|
|
197
216
|
declare const isJiraApiError: (e: unknown) => e is JiraApiError;
|
|
217
|
+
/**
|
|
218
|
+
* Jira's own words for what went wrong — `errorMessages[]` from the body —
|
|
219
|
+
* which is what a person can act on. `message` is the URL-prefixed, truncated
|
|
220
|
+
* envelope and is the fallback, never the first choice.
|
|
221
|
+
*/
|
|
222
|
+
declare const errorMessagesOf: (e: unknown) => string[];
|
|
198
223
|
/** Accepts `myorg.atlassian.net` as readily as a full URL; a bare host is the
|
|
199
224
|
* common shape of the env var and produces an opaque ERR_INVALID_URL if left. */
|
|
200
225
|
declare const normalizeBaseUrl: (raw: string) => string;
|
|
@@ -259,6 +284,8 @@ declare const createJiraClient: (options: JiraClientOptions) => {
|
|
|
259
284
|
getProjectComponents: (key: string) => Promise<JiraComponent[]>;
|
|
260
285
|
getProjectStatuses: (key: string) => Promise<JiraProjectStatuses[]>;
|
|
261
286
|
getBoard: (id: number) => Promise<JiraBoard>;
|
|
287
|
+
/** The board's columns and which statuses each one claims, by id. */
|
|
288
|
+
getBoardConfiguration: (id: number) => Promise<JiraBoardConfiguration>;
|
|
262
289
|
getBoardIssues: (id: number, jql?: string) => Promise<{
|
|
263
290
|
issues: JiraIssue[];
|
|
264
291
|
}>;
|
|
@@ -381,6 +408,16 @@ type FileConfig = {
|
|
|
381
408
|
sprintField?: string;
|
|
382
409
|
defaultProject?: string;
|
|
383
410
|
defaultBoard?: number;
|
|
411
|
+
/**
|
|
412
|
+
* Hand-written board tabs, each naming the statuses it claims by id or by
|
|
413
|
+
* name. The escape hatch over a board's own column config — for an instance
|
|
414
|
+
* with no board, or a board whose columns are not how you think.
|
|
415
|
+
*/
|
|
416
|
+
tabs?: BoardTab[];
|
|
417
|
+
};
|
|
418
|
+
type BoardTab = {
|
|
419
|
+
label: string;
|
|
420
|
+
statuses: string[];
|
|
384
421
|
};
|
|
385
422
|
type Config = FileConfig & {
|
|
386
423
|
baseUrl: string;
|
|
@@ -407,4 +444,37 @@ declare const loadConfig: (env?: NodeJS.ProcessEnv, path?: string) => {
|
|
|
407
444
|
missing: string[];
|
|
408
445
|
};
|
|
409
446
|
|
|
410
|
-
|
|
447
|
+
declare const jqlEscape: (value: string) => string;
|
|
448
|
+
type ListOptions = {
|
|
449
|
+
assignee?: string;
|
|
450
|
+
mine?: boolean;
|
|
451
|
+
status?: string;
|
|
452
|
+
project?: string;
|
|
453
|
+
sprint?: string;
|
|
454
|
+
label?: string;
|
|
455
|
+
jql?: string;
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* Flags compose into one JQL string rather than each becoming its own command.
|
|
459
|
+
* `--jql` replaces the generated clauses entirely so there is always an escape
|
|
460
|
+
* hatch for anything the flags cannot express.
|
|
461
|
+
*/
|
|
462
|
+
declare const buildJql: (options: ListOptions, defaultProject?: string) => string;
|
|
463
|
+
type SearchMode = "auto" | "jql" | "text";
|
|
464
|
+
declare const looksLikeJql: (input: string) => boolean;
|
|
465
|
+
declare const textClause: (words: string) => string;
|
|
466
|
+
/**
|
|
467
|
+
* What a search box submission becomes. Plain words search WITHIN the scope
|
|
468
|
+
* handed in (the list the user is looking at); JQL replaces it, because the
|
|
469
|
+
* user has asked for something else. The two differ on purpose and the
|
|
470
|
+
* screen names which is in force.
|
|
471
|
+
*/
|
|
472
|
+
declare const searchJql: (input: string, { mode, scope }?: {
|
|
473
|
+
mode?: SearchMode;
|
|
474
|
+
scope?: string;
|
|
475
|
+
}) => {
|
|
476
|
+
jql: string;
|
|
477
|
+
mode: "jql" | "text";
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export { type AttachmentOrigin, type BoardTab, type Config, type CustomFieldRef, type FileConfig, type JiraApiError, type JiraAttachment, type JiraBoard, type JiraBoardColumn, type JiraBoardConfiguration, type JiraChangelogEntry, type JiraClient, type JiraClientOptions, type JiraComment, type JiraComponent, type JiraCreated, type JiraCredentials, type JiraEpic, type JiraField, type JiraFilter, type JiraIssue, type JiraIssueFields, type JiraIssueLinkType, type JiraIssueParent, type JiraIssueType, type JiraNamed, type JiraProject, type JiraProjectStatuses, type JiraSearchPage, type JiraSprint, type JiraStatus, type JiraTransition, type JiraUser, type JiraVersion, type JiraWorklog, type ListOptions, type LocatedAttachment, type MediaRef, type MediaResolver, type SearchMode, type SearchOptions, adfToMarkdown, buildJql, configPath, createJiraClient, downloadAttachment, errorMessagesOf, isContainerType, isJiraApiError, isTextual, jiraApiError, jqlEscape, loadConfig, locateAttachments, looksLikeJql, markdownToAdf, normalizeBaseUrl, readFileConfig, searchJql, textClause };
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,20 @@ var jiraApiError = (status, method, url, body) => Object.assign(
|
|
|
4
4
|
{ name: "JiraApiError", status, method, url, body }
|
|
5
5
|
);
|
|
6
6
|
var isJiraApiError = (e) => e instanceof Error && e.name === "JiraApiError";
|
|
7
|
+
var errorMessagesOf = (e) => {
|
|
8
|
+
if (isJiraApiError(e)) {
|
|
9
|
+
try {
|
|
10
|
+
const body = JSON.parse(e.body);
|
|
11
|
+
const fromBody = [
|
|
12
|
+
...body.errorMessages ?? [],
|
|
13
|
+
...Object.values(body.errors ?? {})
|
|
14
|
+
];
|
|
15
|
+
if (fromBody.length) return fromBody;
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [e instanceof Error ? e.message : String(e)];
|
|
20
|
+
};
|
|
7
21
|
var truncate = (s, max = 400) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
8
22
|
var normalizeBaseUrl = (raw) => {
|
|
9
23
|
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
@@ -182,6 +196,8 @@ var createJiraClient = (options) => {
|
|
|
182
196
|
getProjectStatuses: (key) => request(`/rest/api/3/project/${encodeURIComponent(key)}/statuses`),
|
|
183
197
|
// ── agile ─────────────────────────────────────────────────────────────
|
|
184
198
|
getBoard: (id) => request(`/rest/agile/1.0/board/${id}`),
|
|
199
|
+
/** The board's columns and which statuses each one claims, by id. */
|
|
200
|
+
getBoardConfiguration: (id) => request(`/rest/agile/1.0/board/${id}/configuration`),
|
|
185
201
|
getBoardIssues: (id, jql) => request(
|
|
186
202
|
`/rest/agile/1.0/board/${id}/issue${jql ? `?jql=${encodeURIComponent(jql)}` : ""}`
|
|
187
203
|
),
|
|
@@ -221,6 +237,9 @@ var createJiraClient = (options) => {
|
|
|
221
237
|
};
|
|
222
238
|
};
|
|
223
239
|
|
|
240
|
+
// src/types.ts
|
|
241
|
+
var isContainerType = (type) => !type ? false : type.hierarchyLevel !== void 0 ? type.hierarchyLevel > 0 : type.name.toLowerCase() === "epic";
|
|
242
|
+
|
|
224
243
|
// src/adf.ts
|
|
225
244
|
var markUp = (text, marks) => (marks ?? []).reduce((acc, mark) => {
|
|
226
245
|
if (mark.type === "code") return `\`${acc}\``;
|
|
@@ -501,18 +520,71 @@ var loadConfig = (env = process.env, path = configPath()) => {
|
|
|
501
520
|
}
|
|
502
521
|
};
|
|
503
522
|
};
|
|
523
|
+
|
|
524
|
+
// src/jql.ts
|
|
525
|
+
var jqlEscape = (value) => `"${value.replace(/"/g, '\\"')}"`;
|
|
526
|
+
var buildJql = (options, defaultProject) => {
|
|
527
|
+
if (options.jql) return options.jql;
|
|
528
|
+
const clauses = [];
|
|
529
|
+
if (options.mine) clauses.push("assignee = currentUser()");
|
|
530
|
+
else if (options.assignee)
|
|
531
|
+
clauses.push(
|
|
532
|
+
options.assignee === "none" ? "assignee IS EMPTY" : `assignee = ${jqlEscape(options.assignee)}`
|
|
533
|
+
);
|
|
534
|
+
const project = options.project ?? defaultProject;
|
|
535
|
+
if (project) clauses.push(`project = ${jqlEscape(project)}`);
|
|
536
|
+
if (options.status) clauses.push(`status = ${jqlEscape(options.status)}`);
|
|
537
|
+
if (options.label) clauses.push(`labels = ${jqlEscape(options.label)}`);
|
|
538
|
+
if (options.sprint)
|
|
539
|
+
clauses.push(
|
|
540
|
+
options.sprint === "current" ? "sprint IN openSprints()" : `sprint = ${jqlEscape(options.sprint)}`
|
|
541
|
+
);
|
|
542
|
+
if (clauses.length === 0) clauses.push("assignee = currentUser()");
|
|
543
|
+
return `${clauses.join(" AND ")} ORDER BY updated DESC`;
|
|
544
|
+
};
|
|
545
|
+
var WORD_OPS = [
|
|
546
|
+
String.raw`\s+(not\s+)?in\s*\(`,
|
|
547
|
+
String.raw`\s+is\s+(not\s+)?(empty|null)\b`,
|
|
548
|
+
String.raw`\s+was\s+(not\s+)?(in\s*\(|"|'|empty|null)`,
|
|
549
|
+
String.raw`\s+changed\s*(by|after|before|during|on|from|to|$)`
|
|
550
|
+
].join("|");
|
|
551
|
+
var JQL_SHAPE = new RegExp(
|
|
552
|
+
String.raw`^\(?\s*(not\s+)?\(?\s*[\w.\-[\]"]+\s*(=|!=|~|!~|<=|>=|<|>|` + WORD_OPS + ")",
|
|
553
|
+
"i"
|
|
554
|
+
);
|
|
555
|
+
var ORDER_ONLY = /^order\s+by\s/i;
|
|
556
|
+
var looksLikeJql = (input) => {
|
|
557
|
+
const s = input.trim();
|
|
558
|
+
return JQL_SHAPE.test(s) || ORDER_ONLY.test(s);
|
|
559
|
+
};
|
|
560
|
+
var LUCENE_SPECIALS = /([+\-&|!(){}[\]^~*?:\\])/g;
|
|
561
|
+
var textClause = (words) => `text ~ ${jqlEscape(words.replace(LUCENE_SPECIALS, "\\$1"))}`;
|
|
562
|
+
var searchJql = (input, { mode = "auto", scope } = {}) => {
|
|
563
|
+
const trimmed = input.trim();
|
|
564
|
+
const isJql = mode === "jql" || mode === "auto" && looksLikeJql(trimmed);
|
|
565
|
+
if (isJql) return { jql: trimmed, mode: "jql" };
|
|
566
|
+
const clauses = [scope, textClause(trimmed)].filter(Boolean);
|
|
567
|
+
return { jql: `${clauses.join(" AND ")} ORDER BY updated DESC`, mode: "text" };
|
|
568
|
+
};
|
|
504
569
|
export {
|
|
505
570
|
adfToMarkdown,
|
|
571
|
+
buildJql,
|
|
506
572
|
configPath,
|
|
507
573
|
createJiraClient,
|
|
508
574
|
downloadAttachment,
|
|
575
|
+
errorMessagesOf,
|
|
576
|
+
isContainerType,
|
|
509
577
|
isJiraApiError,
|
|
510
578
|
isTextual,
|
|
511
579
|
jiraApiError,
|
|
580
|
+
jqlEscape,
|
|
512
581
|
loadConfig,
|
|
513
582
|
locateAttachments,
|
|
583
|
+
looksLikeJql,
|
|
514
584
|
markdownToAdf,
|
|
515
585
|
normalizeBaseUrl,
|
|
516
|
-
readFileConfig
|
|
586
|
+
readFileConfig,
|
|
587
|
+
searchJql,
|
|
588
|
+
textClause
|
|
517
589
|
};
|
|
518
590
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/adf.ts","../src/attachments.ts","../src/config.ts"],"sourcesContent":["import type {\n JiraBoard,\n JiraChangelogEntry,\n JiraComment,\n JiraComponent,\n JiraCreated,\n JiraEpic,\n JiraFilter,\n JiraIssueLinkType,\n JiraIssueType,\n JiraNamed,\n JiraProjectStatuses,\n JiraVersion,\n JiraWorklog,\n JiraField,\n JiraIssue,\n JiraProject,\n JiraSearchPage,\n JiraSprint,\n JiraTransition,\n JiraUser,\n SearchOptions,\n} from \"./types.js\"\n\nexport type JiraCredentials = {\n baseUrl: string\n email: string\n token: string\n}\n\nexport type JiraClientOptions = JiraCredentials & {\n /** Custom fields to request and label, discovered per instance via `jira fields`. */\n customFields?: { id: string; label: string }[]\n /** The instance's sprint field id, e.g. customfield_10020. */\n sprintField?: string\n fetch?: typeof globalThis.fetch\n}\n\nexport type JiraApiError = Error & {\n name: \"JiraApiError\"\n status: number\n method: string\n url: string\n body: string\n}\n\nexport const jiraApiError = (\n status: number,\n method: string,\n url: string,\n body: string,\n): JiraApiError =>\n Object.assign(\n new Error(`Jira API ${status} ${method} ${url}: ${truncate(body)}`),\n { name: \"JiraApiError\" as const, status, method, url, body },\n )\n\nexport const isJiraApiError = (e: unknown): e is JiraApiError =>\n e instanceof Error && e.name === \"JiraApiError\"\n\nconst truncate = (s: string, max = 400): string =>\n s.length > max ? `${s.slice(0, max)}…` : s\n\n/** Accepts `myorg.atlassian.net` as readily as a full URL; a bare host is the\n * common shape of the env var and produces an opaque ERR_INVALID_URL if left. */\nexport const normalizeBaseUrl = (raw: string): string => {\n const trimmed = raw.trim().replace(/\\/+$/, \"\")\n return /^https?:\\/\\//.test(trimmed) ? trimmed : `https://${trimmed}`\n}\n\nconst DEFAULT_FIELDS = [\n \"summary\",\n \"status\",\n \"assignee\",\n \"issuetype\",\n \"priority\",\n \"project\",\n \"labels\",\n \"parent\",\n \"updated\",\n]\n\nexport const createJiraClient = (options: JiraClientOptions) => {\n const doFetch = options.fetch ?? globalThis.fetch\n const baseUrl = normalizeBaseUrl(options.baseUrl)\n const customFields = options.customFields ?? []\n const authHeader = `Basic ${Buffer.from(`${options.email}:${options.token}`).toString(\"base64\")}`\n\n const request = async <T>(\n path: string,\n init: RequestInit & { raw?: boolean } = {},\n ): Promise<T> => {\n const url = path.startsWith(\"http\")\n ? path\n : `${baseUrl}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`\n\n const res = await doFetch(url, {\n ...init,\n headers: {\n Authorization: authHeader,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n // Jira localises error bodies from the account's language. Errors here\n // are read by scripts and pasted into issues, so pin them to English.\n \"Accept-Language\": \"en\",\n ...init.headers,\n },\n })\n\n // `raw` hands back the Response untouched — binary downloads and manual\n // redirect handling both need the headers, not a parsed body.\n if (init.raw) return res as T\n\n if (!res.ok) {\n throw jiraApiError(\n res.status,\n init.method ?? \"GET\",\n url,\n await res.text(),\n )\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n /** Every field the caller cares about, including this instance's custom ones. */\n const fieldList = (extra?: string[]): string[] => [\n ...new Set([\n ...(extra ?? DEFAULT_FIELDS),\n ...customFields.map((f) => f.id),\n ...(options.sprintField ? [options.sprintField] : []),\n ]),\n ]\n\n const searchPage = (\n jql: string,\n opts: SearchOptions = {},\n ): Promise<JiraSearchPage> =>\n request<JiraSearchPage>(\"/rest/api/3/search/jql\", {\n method: \"POST\",\n body: JSON.stringify({\n jql,\n fields: fieldList(opts.fields),\n maxResults: opts.maxResults ?? 50,\n ...(opts.nextPageToken ? { nextPageToken: opts.nextPageToken } : {}),\n ...(opts.expand ? { expand: opts.expand } : {}),\n }),\n })\n\n /**\n * Walks the cursor to `limit` issues. Three separate stop conditions, because\n * the cursor is opaque and has been reported to loop: no token, an empty\n * page, or a token we have already followed. Trusting `isLast` alone would\n * page forever against an instance exhibiting that bug.\n */\n const searchIssues = async (\n jql: string,\n opts: SearchOptions & { limit?: number } = {},\n ): Promise<JiraIssue[]> => {\n const limit = opts.limit ?? 50\n const issues: JiraIssue[] = []\n const seenTokens = new Set<string>()\n let token = opts.nextPageToken\n\n while (issues.length < limit) {\n const page = await searchPage(jql, {\n ...opts,\n nextPageToken: token,\n maxResults: Math.min(100, limit - issues.length),\n })\n if (page.issues.length === 0) break\n issues.push(...page.issues)\n\n const next = page.nextPageToken\n if (!next || page.isLast || seenTokens.has(next)) break\n seenTokens.add(next)\n token = next\n }\n\n return issues.slice(0, limit)\n }\n\n return {\n request,\n customFields,\n sprintField: options.sprintField,\n\n searchPage,\n searchIssues,\n\n getIssue: (key: string): Promise<JiraIssue> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}?fields=${fieldList([\n ...DEFAULT_FIELDS,\n \"description\",\n \"comment\",\n \"reporter\",\n \"created\",\n \"attachment\",\n ]).join(\",\")}`,\n ),\n\n getTransitions: (key: string): Promise<{ transitions: JiraTransition[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`),\n\n // `fields` rides along with the transition rather than going through a\n // separate edit: Jira rejects a resolution set on an already-closed issue, so\n // the only moment a screen-required field like `resolution` can be written is\n // the transition itself.\n transitionIssue: (\n key: string,\n transitionId: string,\n fields?: Record<string, unknown>,\n ): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {\n method: \"POST\",\n body: JSON.stringify({\n transition: { id: transitionId },\n ...(fields ? { fields } : {}),\n }),\n }),\n\n addComment: (key: string, body: unknown): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {\n method: \"POST\",\n body: JSON.stringify({ body }),\n }),\n\n getMe: (): Promise<JiraUser> => request(\"/rest/api/3/myself\"),\n\n getFields: (): Promise<JiraField[]> => request(\"/rest/api/3/field\"),\n\n getProjects: (): Promise<JiraProject[]> =>\n request(\"/rest/api/3/project?expand=description\"),\n\n getBoards: (): Promise<{ values: JiraBoard[] }> =>\n request(\"/rest/agile/1.0/board\"),\n\n getSprints: (\n boardId: number,\n state?: string,\n ): Promise<{ values: JiraSprint[] }> =>\n request(\n `/rest/agile/1.0/board/${boardId}/sprint${state ? `?state=${state}` : \"\"}`,\n ),\n\n // ── issues ────────────────────────────────────────────────────────────\n createIssue: (fields: Record<string, unknown>): Promise<JiraCreated> =>\n request(\"/rest/api/3/issue\", {\n method: \"POST\",\n body: JSON.stringify({ fields }),\n }),\n\n updateIssue: (\n key: string,\n fields: Record<string, unknown>,\n ): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}`, {\n method: \"PUT\",\n body: JSON.stringify({ fields }),\n }),\n\n deleteIssue: (key: string, deleteSubtasks = false): Promise<void> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}?deleteSubtasks=${deleteSubtasks}`,\n { method: \"DELETE\" },\n ),\n\n assignIssue: (key: string, accountId: string | null): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/assignee`, {\n method: \"PUT\",\n body: JSON.stringify({ accountId }),\n }),\n\n getComments: (key: string): Promise<{ comments: JiraComment[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`),\n\n deleteComment: (key: string, commentId: string): Promise<void> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}/comment/${encodeURIComponent(commentId)}`,\n { method: \"DELETE\" },\n ),\n\n getWatchers: (key: string): Promise<{ watchers: JiraUser[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/watchers`),\n\n addWatcher: (key: string, accountId: string): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/watchers`, {\n method: \"POST\",\n body: JSON.stringify(accountId),\n }),\n\n removeWatcher: (key: string, accountId: string): Promise<void> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}/watchers?accountId=${encodeURIComponent(accountId)}`,\n { method: \"DELETE\" },\n ),\n\n getWorklogs: (key: string): Promise<{ worklogs: JiraWorklog[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/worklog`),\n\n addWorklog: (\n key: string,\n body: { timeSpent: string; comment?: unknown; started?: string },\n ): Promise<JiraWorklog> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/worklog`, {\n method: \"POST\",\n body: JSON.stringify(body),\n }),\n\n getChangelog: (key: string): Promise<{ values: JiraChangelogEntry[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/changelog`),\n\n getIssueLinkTypes: (): Promise<{ issueLinkTypes: JiraIssueLinkType[] }> =>\n request(\"/rest/api/3/issueLinkType\"),\n\n linkIssues: (\n type: string,\n inwardKey: string,\n outwardKey: string,\n ): Promise<void> =>\n request(\"/rest/api/3/issueLink\", {\n method: \"POST\",\n body: JSON.stringify({\n type: { name: type },\n inwardIssue: { key: inwardKey },\n outwardIssue: { key: outwardKey },\n }),\n }),\n\n // ── projects ──────────────────────────────────────────────────────────\n getProject: (key: string): Promise<JiraProject> =>\n request(\n `/rest/api/3/project/${encodeURIComponent(key)}?expand=description,lead,url`,\n ),\n\n getProjectVersions: (key: string): Promise<JiraVersion[]> =>\n request(`/rest/api/3/project/${encodeURIComponent(key)}/versions`),\n\n getProjectComponents: (key: string): Promise<JiraComponent[]> =>\n request(`/rest/api/3/project/${encodeURIComponent(key)}/components`),\n\n getProjectStatuses: (key: string): Promise<JiraProjectStatuses[]> =>\n request(`/rest/api/3/project/${encodeURIComponent(key)}/statuses`),\n\n // ── agile ─────────────────────────────────────────────────────────────\n getBoard: (id: number): Promise<JiraBoard> =>\n request(`/rest/agile/1.0/board/${id}`),\n\n getBoardIssues: (\n id: number,\n jql?: string,\n ): Promise<{ issues: JiraIssue[] }> =>\n request(\n `/rest/agile/1.0/board/${id}/issue${jql ? `?jql=${encodeURIComponent(jql)}` : \"\"}`,\n ),\n\n getBacklog: (id: number): Promise<{ issues: JiraIssue[] }> =>\n request(`/rest/agile/1.0/board/${id}/backlog`),\n\n getSprint: (id: number): Promise<JiraSprint> =>\n request(`/rest/agile/1.0/sprint/${id}`),\n\n getSprintIssues: (id: number): Promise<{ issues: JiraIssue[] }> =>\n request(`/rest/agile/1.0/sprint/${id}/issue`),\n\n getBoardEpics: (id: number): Promise<{ values: JiraEpic[] }> =>\n request(`/rest/agile/1.0/board/${id}/epic`),\n\n getEpicIssues: (id: string): Promise<{ issues: JiraIssue[] }> =>\n request(`/rest/agile/1.0/epic/${encodeURIComponent(id)}/issue`),\n\n // ── people ────────────────────────────────────────────────────────────\n searchUsers: (query: string, maxResults = 20): Promise<JiraUser[]> =>\n request(\n `/rest/api/3/user/search?query=${encodeURIComponent(query)}&maxResults=${maxResults}`,\n ),\n\n searchAssignableUsers: (\n query: string,\n projectKey: string,\n maxResults = 20,\n ): Promise<JiraUser[]> =>\n request(\n `/rest/api/3/user/assignable/search?query=${encodeURIComponent(query)}&project=${encodeURIComponent(projectKey)}&maxResults=${maxResults}`,\n ),\n\n // ── instance metadata ─────────────────────────────────────────────────\n getIssueTypes: (): Promise<JiraIssueType[]> =>\n request(\"/rest/api/3/issuetype\"),\n\n getPriorities: (): Promise<JiraNamed[]> => request(\"/rest/api/3/priority\"),\n\n getResolutions: (): Promise<JiraNamed[]> =>\n request(\"/rest/api/3/resolution\"),\n\n getStatuses: (): Promise<JiraNamed[]> => request(\"/rest/api/3/status\"),\n\n getLabels: (): Promise<{ values: string[] }> =>\n request(\"/rest/api/3/label?maxResults=1000\"),\n\n getFilters: (): Promise<{ values: JiraFilter[] }> =>\n request(\"/rest/api/3/filter/search?expand=jql&maxResults=50\"),\n\n getDashboards: (): Promise<{ dashboards: JiraNamed[] }> =>\n request(\"/rest/api/3/dashboard\"),\n\n getServerInfo: (): Promise<Record<string, unknown>> =>\n request(\"/rest/api/3/serverInfo\"),\n\n getMyPermissions: (projectKey?: string): Promise<Record<string, unknown>> =>\n request(\n `/rest/api/3/mypermissions${projectKey ? `?projectKey=${encodeURIComponent(projectKey)}` : \"\"}`,\n ),\n\n /**\n * The count the removed `total` field used to give. Deliberately named\n * approximate because that is what Atlassian guarantees — it is an index\n * estimate, not a scan, and will disagree with a full page walk.\n */\n approximateCount: (jql: string): Promise<{ count: number }> =>\n request(\"/rest/api/3/search/approximate-count\", {\n method: \"POST\",\n body: JSON.stringify({ jql }),\n }),\n }\n}\n\nexport type JiraClient = ReturnType<typeof createJiraClient>\n","type AdfNode = {\n type?: string\n text?: string\n content?: AdfNode[]\n attrs?: Record<string, unknown>\n marks?: { type: string; attrs?: Record<string, unknown> }[]\n}\n\nconst markUp = (text: string, marks: AdfNode[\"marks\"]): string =>\n (marks ?? []).reduce((acc, mark) => {\n if (mark.type === \"code\") return `\\`${acc}\\``\n if (mark.type === \"strong\") return `**${acc}**`\n if (mark.type === \"em\") return `_${acc}_`\n if (mark.type === \"strike\") return `~~${acc}~~`\n if (mark.type === \"link\") return `[${acc}](${mark.attrs?.[\"href\"] ?? \"\"})`\n return acc\n }, text)\n\nexport type MediaResolver = (id: string) => string | undefined\n\nlet resolveMedia: MediaResolver = () => undefined\n\nconst children = (node: AdfNode, sep = \"\"): string =>\n (node.content ?? []).map(render).join(sep)\n\nconst listItems = (node: AdfNode, bullet: (i: number) => string): string =>\n (node.content ?? [])\n .map((item, i) => {\n const body = children(item, \"\\n\\n\").trim()\n const [first = \"\", ...rest] = body.split(\"\\n\")\n const marker = bullet(i)\n const indent = \" \".repeat(marker.length)\n return [\n `${marker}${first}`,\n ...rest.map((l) => (l ? `${indent}${l}` : l)),\n ].join(\"\\n\")\n })\n .join(\"\\n\")\n\n/** A table row rendered as a pipe row; the header separator is added by the caller. */\nconst row = (node: AdfNode): string =>\n `| ${(node.content ?? []).map((cell) => children(cell, \" \").trim().replace(/\\n+/g, \" \")).join(\" | \")} |`\n\nconst renderTable = (node: AdfNode): string => {\n const rows = node.content ?? []\n if (rows.length === 0) return \"\"\n const isHeader = (r: AdfNode): boolean =>\n (r.content ?? []).some((c) => c.type === \"tableHeader\")\n const [first] = rows\n const rendered = rows.map(row)\n if (first && isHeader(first)) {\n const columns = (first.content ?? []).length\n rendered.splice(1, 0, `|${\" --- |\".repeat(columns)}`)\n }\n return rendered.join(\"\\n\")\n}\n\nconst render = (node: AdfNode): string => {\n switch (node.type) {\n case \"doc\":\n return children(node, \"\\n\\n\")\n case \"paragraph\":\n return children(node)\n case \"text\":\n return markUp(node.text ?? \"\", node.marks)\n case \"hardBreak\":\n return \"\\n\"\n case \"heading\":\n return `${\"#\".repeat(Number(node.attrs?.[\"level\"] ?? 1))} ${children(node)}`\n case \"bulletList\":\n return listItems(node, () => \"- \")\n case \"orderedList\":\n return listItems(\n node,\n (i) => `${Number(node.attrs?.[\"order\"] ?? 1) + i}. `,\n )\n case \"codeBlock\":\n return `\\`\\`\\`${node.attrs?.[\"language\"] ?? \"\"}\\n${children(node)}\\n\\`\\`\\``\n case \"blockquote\":\n return children(node, \"\\n\\n\")\n .split(\"\\n\")\n .map((l) => `> ${l}`)\n .join(\"\\n\")\n case \"panel\":\n return `> [!${String(node.attrs?.[\"panelType\"] ?? \"note\").toUpperCase()}]\\n${children(\n node,\n \"\\n\\n\",\n )\n .split(\"\\n\")\n .map((l) => `> ${l}`)\n .join(\"\\n\")}`\n case \"rule\":\n return \"---\"\n case \"table\":\n return renderTable(node)\n case \"mediaSingle\":\n case \"mediaGroup\":\n return children(node, \"\\n\")\n case \"media\": {\n // `alt` is the filename Jira stored; the id is a media-platform UUID that\n // means nothing to a reader and does not match the attachment id either.\n const alt = node.attrs?.[\"alt\"]\n if (typeof alt === \"string\" && alt) return `[attachment: ${alt}]`\n const id = String(node.attrs?.[\"id\"] ?? \"\")\n const name = resolveMedia(id)\n return name ? `[attachment: ${name}]` : `[attachment: ${id || \"unknown\"}]`\n }\n case \"mention\": {\n // Jira stores the display text with its own leading @ most of the time,\n // but not always, so normalise rather than assume either way.\n const label = String(node.attrs?.[\"text\"] ?? node.attrs?.[\"id\"] ?? \"\")\n return label.startsWith(\"@\") ? label : `@${label}`\n }\n case \"emoji\":\n return String(node.attrs?.[\"text\"] ?? node.attrs?.[\"shortName\"] ?? \"\")\n case \"date\":\n return String(node.attrs?.[\"timestamp\"] ?? \"\")\n case \"status\":\n return `[${String(node.attrs?.[\"text\"] ?? \"\").toUpperCase()}]`\n case \"inlineCard\":\n return String(node.attrs?.[\"url\"] ?? \"\")\n default:\n return children(node, \"\\n\\n\")\n }\n}\n\n/**\n * Atlassian Document Format to Markdown. Deliberately stops at Markdown rather\n * than emitting ANSI: rendering is the surface's job, so a TUI, a pager and a\n * `--json` consumer all get the same text and only one of them styles it.\n */\nexport const adfToMarkdown = (\n doc: unknown,\n media?: MediaResolver,\n): string => {\n if (typeof doc === \"string\") return doc\n if (!doc || typeof doc !== \"object\") return \"\"\n const previous = resolveMedia\n resolveMedia = media ?? (() => undefined)\n try {\n return render(doc as AdfNode)\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim()\n } finally {\n resolveMedia = previous\n }\n}\n\ntype AdfDoc = { type: \"doc\"; version: 1; content: unknown[] }\n\nconst inline = (text: string): unknown[] => {\n // Only code spans and links are worth parsing: they are the two marks whose\n // absence changes meaning rather than appearance.\n const pattern = /(`[^`]+`)|(\\[[^\\]]+\\]\\([^)]+\\))/g\n const nodes: unknown[] = []\n let cursor = 0\n\n for (const match of text.matchAll(pattern)) {\n const at = match.index\n if (at > cursor)\n nodes.push({ type: \"text\", text: text.slice(cursor, at) })\n\n const token = match[0]\n if (token.startsWith(\"`\")) {\n nodes.push({\n type: \"text\",\n text: token.slice(1, -1),\n marks: [{ type: \"code\" }],\n })\n } else {\n const [, label = \"\", href = \"\"] =\n token.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/) ?? []\n nodes.push({\n type: \"text\",\n text: label,\n marks: [{ type: \"link\", attrs: { href } }],\n })\n }\n cursor = at + token.length\n }\n\n if (cursor < text.length) nodes.push({ type: \"text\", text: text.slice(cursor) })\n return nodes.length > 0 ? nodes : [{ type: \"text\", text }]\n}\n\nconst blockToAdf = (block: string): unknown => {\n const fence = block.match(/^```(\\w*)\\n([\\s\\S]*?)\\n?```$/)\n if (fence)\n return {\n type: \"codeBlock\",\n ...(fence[1] ? { attrs: { language: fence[1] } } : {}),\n content: [{ type: \"text\", text: fence[2] ?? \"\" }],\n }\n\n const heading = block.match(/^(#{1,6})\\s+(.*)$/)\n if (heading)\n return {\n type: \"heading\",\n attrs: { level: heading[1]?.length ?? 1 },\n content: inline(heading[2] ?? \"\"),\n }\n\n const lines = block.split(\"\\n\")\n if (lines.every((l) => /^\\s*[-*]\\s+/.test(l)))\n return {\n type: \"bulletList\",\n content: lines.map((l) => ({\n type: \"listItem\",\n content: [\n { type: \"paragraph\", content: inline(l.replace(/^\\s*[-*]\\s+/, \"\")) },\n ],\n })),\n }\n\n if (lines.every((l) => /^\\s*\\d+[.)]\\s+/.test(l)))\n return {\n type: \"orderedList\",\n content: lines.map((l) => ({\n type: \"listItem\",\n content: [\n { type: \"paragraph\", content: inline(l.replace(/^\\s*\\d+[.)]\\s+/, \"\")) },\n ],\n })),\n }\n\n return { type: \"paragraph\", content: inline(block) }\n}\n\n/**\n * Markdown to ADF, covering what someone actually types into a comment from a\n * terminal: paragraphs, fenced code, headings, lists, links and code spans.\n * Deliberately partial — anything richer is better authored in Jira, and a\n * half-supported table would corrupt more often than it would help.\n */\nexport const markdownToAdf = (text: string): AdfDoc => ({\n type: \"doc\",\n version: 1,\n content: text\n .replace(/\\r\\n/g, \"\\n\")\n .split(/\\n{2,}/)\n .map((b) => b.trim())\n .filter(Boolean)\n .map(blockToAdf),\n})\n","import type { JiraClient } from \"./client.js\"\nimport type { JiraIssue } from \"./types.js\"\n\nexport type JiraAttachment = {\n id: string\n filename: string\n mimeType: string\n size: number\n created?: string\n author?: { displayName: string }\n content?: string\n}\n\n/** Where an attachment was referenced from, so `issue view` can say so. */\nexport type AttachmentOrigin =\n | { kind: \"issue\" }\n | { kind: \"description\" }\n | { kind: \"comment\"; commentId: string; author?: string }\n\nexport type LocatedAttachment = JiraAttachment & { origins: AttachmentOrigin[] }\n\ntype AdfNode = {\n type?: string\n attrs?: Record<string, unknown>\n content?: AdfNode[]\n}\n\nexport type MediaRef = { id?: string; filename?: string }\n\n/**\n * Media nodes carry a media-platform UUID in `attrs.id`, which is a different\n * namespace from the numeric attachment id — joining on it matches nothing,\n * ever. `attrs.alt` holds the original filename and is the only field the two\n * representations share, so it is the real key and the id is the fallback.\n */\nconst mediaRefs = (node: unknown): MediaRef[] => {\n if (!node || typeof node !== \"object\") return []\n const n = node as AdfNode\n const here: MediaRef[] =\n n.type === \"media\"\n ? [\n {\n ...(typeof n.attrs?.[\"id\"] === \"string\"\n ? { id: n.attrs[\"id\"] as string }\n : {}),\n ...(typeof n.attrs?.[\"alt\"] === \"string\"\n ? { filename: n.attrs[\"alt\"] as string }\n : {}),\n },\n ]\n : []\n return [...here, ...(n.content ?? []).flatMap(mediaRefs)]\n}\n\n/**\n * Jira reports attachments once, on the issue, and never says where they were\n * embedded. Walking the description and each comment for media nodes and\n * joining them back is the only way to answer \"which comment did this come\n * from\" — a question the API cannot be asked directly.\n */\nexport const locateAttachments = (issue: JiraIssue): LocatedAttachment[] => {\n const attachments = (issue.fields[\"attachment\"] as JiraAttachment[]) ?? []\n const origins = new Map<string, AttachmentOrigin[]>()\n\n const note = (ref: MediaRef, origin: AttachmentOrigin): void => {\n const match = attachments.find(\n (a) =>\n (ref.filename !== undefined && a.filename === ref.filename) ||\n (ref.id !== undefined && a.id === ref.id),\n )\n if (!match) return\n origins.set(match.id, [...(origins.get(match.id) ?? []), origin])\n }\n\n for (const ref of mediaRefs(issue.fields.description))\n note(ref, { kind: \"description\" })\n\n for (const comment of issue.fields.comment?.comments ?? [])\n for (const ref of mediaRefs(comment.body))\n note(ref, {\n kind: \"comment\",\n commentId: comment.id,\n author: comment.author?.displayName,\n })\n\n return attachments.map((a) => ({\n ...a,\n origins: origins.get(a.id) ?? [{ kind: \"issue\" as const }],\n }))\n}\n\nconst TEXTUAL =\n /^(text\\/|application\\/(json|xml|x-yaml|yaml|javascript|sql|x-sh))/\n\n/** Extensions Jira commonly mislabels as application/octet-stream. */\nconst TEXTUAL_EXTENSIONS =\n /\\.(txt|md|markdown|log|json|ya?ml|csv|tsv|xml|html?|css|jsx?|tsx?|py|rb|go|rs|java|kt|sh|zsh|bash|sql|ini|toml|conf|env|diff|patch)$/i\n\nexport const isTextual = (attachment: JiraAttachment): boolean =>\n TEXTUAL.test(attachment.mimeType) ||\n TEXTUAL_EXTENSIONS.test(attachment.filename)\n\n/**\n * Fetches attachment bytes. The documented content endpoint 302s to a\n * short-lived media host, and the auth header must NOT follow: it is a Jira\n * credential and the redirect target is a different origin that neither needs\n * nor should see it. Hence manual redirect handling rather than fetch's default.\n */\nexport const downloadAttachment = async (\n client: JiraClient,\n id: string,\n fetchImpl: typeof globalThis.fetch = globalThis.fetch,\n): Promise<{ bytes: Uint8Array; mimeType: string | null }> => {\n const res = await client.request<Response>(\n `/rest/api/3/attachment/content/${encodeURIComponent(id)}`,\n { redirect: \"manual\", raw: true },\n )\n\n const location = res.headers.get(\"location\")\n const final =\n res.status >= 300 && res.status < 400 && location\n ? await fetchImpl(location)\n : res\n\n if (!final.ok) {\n throw new Error(\n `could not download attachment ${id}: ${final.status} ${final.statusText}`,\n )\n }\n\n return {\n bytes: new Uint8Array(await final.arrayBuffer()),\n mimeType: final.headers.get(\"content-type\"),\n }\n}\n","import { readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { normalizeBaseUrl } from \"./client.js\"\n\nexport type CustomFieldRef = { id: string; label: string }\n\nexport type FileConfig = {\n baseUrl?: string\n email?: string\n customFields?: CustomFieldRef[]\n sprintField?: string\n defaultProject?: string\n defaultBoard?: number\n}\n\nexport type Config = FileConfig & {\n baseUrl: string\n email: string\n token: string\n}\n\nexport const configPath = (): string =>\n process.env[\"JIRA_CONFIG_FILE\"] ||\n join(\n process.env[\"XDG_CONFIG_HOME\"] || join(homedir(), \".config\"),\n \"jira\",\n \"config.json\",\n )\n\n/**\n * Credentials come from the environment and never from the config file: the\n * file is meant to be readable, diffable and shareable across a team, and a\n * token in it would leak the moment anyone pasted theirs into a gist.\n */\nexport const readFileConfig = (path = configPath()): FileConfig => {\n let raw: string\n try {\n raw = readFileSync(path, \"utf8\")\n } catch {\n return {}\n }\n const parsed = JSON.parse(raw) as FileConfig\n if (parsed.customFields && !Array.isArray(parsed.customFields))\n throw new Error(`${path}: customFields must be an array of {id,label}`)\n if (\"token\" in parsed || \"apiToken\" in parsed)\n throw new Error(\n `${path} contains a token. Tokens belong in ATLASSIAN_API_TOKEN, not in a file that gets shared and backed up — remove it.`,\n )\n return parsed\n}\n\n/**\n * Only the token is a secret, so only the token is env-only. The instance URL\n * and your email are settings, and asking someone to export three variables to\n * run a CLI is friction that buys nothing. Environment still wins where both\n * are present, so a second instance needs one inline override rather than a\n * second config file.\n */\nexport const loadConfig = (\n env: NodeJS.ProcessEnv = process.env,\n path = configPath(),\n): { config: Config } | { missing: string[] } => {\n const file = readFileConfig(path)\n\n const baseUrl = env[\"ATLASSIAN_BASE_URL\"] || file.baseUrl\n const email = env[\"ATLASSIAN_USER_EMAIL\"] || file.email\n const token = env[\"ATLASSIAN_API_TOKEN\"]\n\n const missing = (\n [\n [\"ATLASSIAN_BASE_URL\", baseUrl],\n [\"ATLASSIAN_USER_EMAIL\", email],\n [\"ATLASSIAN_API_TOKEN\", token],\n ] as const\n )\n .filter(([, v]) => !v)\n .map(([k]) => k)\n\n if (missing.length > 0) return { missing }\n\n return {\n config: {\n ...file,\n baseUrl: normalizeBaseUrl(baseUrl as string),\n email: email as string,\n token: token as string,\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/types.ts","../src/adf.ts","../src/attachments.ts","../src/config.ts","../src/jql.ts"],"sourcesContent":["import type {\n JiraBoard,\n JiraBoardConfiguration,\n JiraChangelogEntry,\n JiraComment,\n JiraComponent,\n JiraCreated,\n JiraEpic,\n JiraFilter,\n JiraIssueLinkType,\n JiraIssueType,\n JiraNamed,\n JiraProjectStatuses,\n JiraVersion,\n JiraWorklog,\n JiraField,\n JiraIssue,\n JiraProject,\n JiraSearchPage,\n JiraSprint,\n JiraTransition,\n JiraUser,\n SearchOptions,\n} from \"./types.js\"\n\nexport type JiraCredentials = {\n baseUrl: string\n email: string\n token: string\n}\n\nexport type JiraClientOptions = JiraCredentials & {\n /** Custom fields to request and label, discovered per instance via `jira fields`. */\n customFields?: { id: string; label: string }[]\n /** The instance's sprint field id, e.g. customfield_10020. */\n sprintField?: string\n fetch?: typeof globalThis.fetch\n}\n\nexport type JiraApiError = Error & {\n name: \"JiraApiError\"\n status: number\n method: string\n url: string\n body: string\n}\n\nexport const jiraApiError = (\n status: number,\n method: string,\n url: string,\n body: string,\n): JiraApiError =>\n Object.assign(\n new Error(`Jira API ${status} ${method} ${url}: ${truncate(body)}`),\n { name: \"JiraApiError\" as const, status, method, url, body },\n )\n\nexport const isJiraApiError = (e: unknown): e is JiraApiError =>\n e instanceof Error && e.name === \"JiraApiError\"\n\n/**\n * Jira's own words for what went wrong — `errorMessages[]` from the body —\n * which is what a person can act on. `message` is the URL-prefixed, truncated\n * envelope and is the fallback, never the first choice.\n */\nexport const errorMessagesOf = (e: unknown): string[] => {\n if (isJiraApiError(e)) {\n try {\n const body = JSON.parse(e.body) as {\n errorMessages?: string[]\n errors?: Record<string, string>\n }\n const fromBody = [\n ...(body.errorMessages ?? []),\n ...Object.values(body.errors ?? {}),\n ]\n if (fromBody.length) return fromBody\n } catch {\n // not JSON — the envelope will do\n }\n }\n return [e instanceof Error ? e.message : String(e)]\n}\n\nconst truncate = (s: string, max = 400): string =>\n s.length > max ? `${s.slice(0, max)}…` : s\n\n/** Accepts `myorg.atlassian.net` as readily as a full URL; a bare host is the\n * common shape of the env var and produces an opaque ERR_INVALID_URL if left. */\nexport const normalizeBaseUrl = (raw: string): string => {\n const trimmed = raw.trim().replace(/\\/+$/, \"\")\n return /^https?:\\/\\//.test(trimmed) ? trimmed : `https://${trimmed}`\n}\n\nconst DEFAULT_FIELDS = [\n \"summary\",\n \"status\",\n \"assignee\",\n \"issuetype\",\n \"priority\",\n \"project\",\n \"labels\",\n \"parent\",\n \"updated\",\n]\n\nexport const createJiraClient = (options: JiraClientOptions) => {\n const doFetch = options.fetch ?? globalThis.fetch\n const baseUrl = normalizeBaseUrl(options.baseUrl)\n const customFields = options.customFields ?? []\n const authHeader = `Basic ${Buffer.from(`${options.email}:${options.token}`).toString(\"base64\")}`\n\n const request = async <T>(\n path: string,\n init: RequestInit & { raw?: boolean } = {},\n ): Promise<T> => {\n const url = path.startsWith(\"http\")\n ? path\n : `${baseUrl}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`\n\n const res = await doFetch(url, {\n ...init,\n headers: {\n Authorization: authHeader,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n // Jira localises error bodies from the account's language. Errors here\n // are read by scripts and pasted into issues, so pin them to English.\n \"Accept-Language\": \"en\",\n ...init.headers,\n },\n })\n\n // `raw` hands back the Response untouched — binary downloads and manual\n // redirect handling both need the headers, not a parsed body.\n if (init.raw) return res as T\n\n if (!res.ok) {\n throw jiraApiError(\n res.status,\n init.method ?? \"GET\",\n url,\n await res.text(),\n )\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n /** Every field the caller cares about, including this instance's custom ones. */\n const fieldList = (extra?: string[]): string[] => [\n ...new Set([\n ...(extra ?? DEFAULT_FIELDS),\n ...customFields.map((f) => f.id),\n ...(options.sprintField ? [options.sprintField] : []),\n ]),\n ]\n\n const searchPage = (\n jql: string,\n opts: SearchOptions = {},\n ): Promise<JiraSearchPage> =>\n request<JiraSearchPage>(\"/rest/api/3/search/jql\", {\n method: \"POST\",\n body: JSON.stringify({\n jql,\n fields: fieldList(opts.fields),\n maxResults: opts.maxResults ?? 50,\n ...(opts.nextPageToken ? { nextPageToken: opts.nextPageToken } : {}),\n ...(opts.expand ? { expand: opts.expand } : {}),\n }),\n })\n\n /**\n * Walks the cursor to `limit` issues. Three separate stop conditions, because\n * the cursor is opaque and has been reported to loop: no token, an empty\n * page, or a token we have already followed. Trusting `isLast` alone would\n * page forever against an instance exhibiting that bug.\n */\n const searchIssues = async (\n jql: string,\n opts: SearchOptions & { limit?: number } = {},\n ): Promise<JiraIssue[]> => {\n const limit = opts.limit ?? 50\n const issues: JiraIssue[] = []\n const seenTokens = new Set<string>()\n let token = opts.nextPageToken\n\n while (issues.length < limit) {\n const page = await searchPage(jql, {\n ...opts,\n nextPageToken: token,\n maxResults: Math.min(100, limit - issues.length),\n })\n if (page.issues.length === 0) break\n issues.push(...page.issues)\n\n const next = page.nextPageToken\n if (!next || page.isLast || seenTokens.has(next)) break\n seenTokens.add(next)\n token = next\n }\n\n return issues.slice(0, limit)\n }\n\n return {\n request,\n customFields,\n sprintField: options.sprintField,\n\n searchPage,\n searchIssues,\n\n getIssue: (key: string): Promise<JiraIssue> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}?fields=${fieldList([\n ...DEFAULT_FIELDS,\n \"description\",\n \"comment\",\n \"reporter\",\n \"created\",\n \"attachment\",\n ]).join(\",\")}`,\n ),\n\n getTransitions: (key: string): Promise<{ transitions: JiraTransition[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`),\n\n // `fields` rides along with the transition rather than going through a\n // separate edit: Jira rejects a resolution set on an already-closed issue, so\n // the only moment a screen-required field like `resolution` can be written is\n // the transition itself.\n transitionIssue: (\n key: string,\n transitionId: string,\n fields?: Record<string, unknown>,\n ): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {\n method: \"POST\",\n body: JSON.stringify({\n transition: { id: transitionId },\n ...(fields ? { fields } : {}),\n }),\n }),\n\n addComment: (key: string, body: unknown): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {\n method: \"POST\",\n body: JSON.stringify({ body }),\n }),\n\n getMe: (): Promise<JiraUser> => request(\"/rest/api/3/myself\"),\n\n getFields: (): Promise<JiraField[]> => request(\"/rest/api/3/field\"),\n\n getProjects: (): Promise<JiraProject[]> =>\n request(\"/rest/api/3/project?expand=description\"),\n\n getBoards: (): Promise<{ values: JiraBoard[] }> =>\n request(\"/rest/agile/1.0/board\"),\n\n getSprints: (\n boardId: number,\n state?: string,\n ): Promise<{ values: JiraSprint[] }> =>\n request(\n `/rest/agile/1.0/board/${boardId}/sprint${state ? `?state=${state}` : \"\"}`,\n ),\n\n // ── issues ────────────────────────────────────────────────────────────\n createIssue: (fields: Record<string, unknown>): Promise<JiraCreated> =>\n request(\"/rest/api/3/issue\", {\n method: \"POST\",\n body: JSON.stringify({ fields }),\n }),\n\n updateIssue: (\n key: string,\n fields: Record<string, unknown>,\n ): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}`, {\n method: \"PUT\",\n body: JSON.stringify({ fields }),\n }),\n\n deleteIssue: (key: string, deleteSubtasks = false): Promise<void> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}?deleteSubtasks=${deleteSubtasks}`,\n { method: \"DELETE\" },\n ),\n\n assignIssue: (key: string, accountId: string | null): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/assignee`, {\n method: \"PUT\",\n body: JSON.stringify({ accountId }),\n }),\n\n getComments: (key: string): Promise<{ comments: JiraComment[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`),\n\n deleteComment: (key: string, commentId: string): Promise<void> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}/comment/${encodeURIComponent(commentId)}`,\n { method: \"DELETE\" },\n ),\n\n getWatchers: (key: string): Promise<{ watchers: JiraUser[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/watchers`),\n\n addWatcher: (key: string, accountId: string): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/watchers`, {\n method: \"POST\",\n body: JSON.stringify(accountId),\n }),\n\n removeWatcher: (key: string, accountId: string): Promise<void> =>\n request(\n `/rest/api/3/issue/${encodeURIComponent(key)}/watchers?accountId=${encodeURIComponent(accountId)}`,\n { method: \"DELETE\" },\n ),\n\n getWorklogs: (key: string): Promise<{ worklogs: JiraWorklog[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/worklog`),\n\n addWorklog: (\n key: string,\n body: { timeSpent: string; comment?: unknown; started?: string },\n ): Promise<JiraWorklog> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/worklog`, {\n method: \"POST\",\n body: JSON.stringify(body),\n }),\n\n getChangelog: (key: string): Promise<{ values: JiraChangelogEntry[] }> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/changelog`),\n\n getIssueLinkTypes: (): Promise<{ issueLinkTypes: JiraIssueLinkType[] }> =>\n request(\"/rest/api/3/issueLinkType\"),\n\n linkIssues: (\n type: string,\n inwardKey: string,\n outwardKey: string,\n ): Promise<void> =>\n request(\"/rest/api/3/issueLink\", {\n method: \"POST\",\n body: JSON.stringify({\n type: { name: type },\n inwardIssue: { key: inwardKey },\n outwardIssue: { key: outwardKey },\n }),\n }),\n\n // ── projects ──────────────────────────────────────────────────────────\n getProject: (key: string): Promise<JiraProject> =>\n request(\n `/rest/api/3/project/${encodeURIComponent(key)}?expand=description,lead,url`,\n ),\n\n getProjectVersions: (key: string): Promise<JiraVersion[]> =>\n request(`/rest/api/3/project/${encodeURIComponent(key)}/versions`),\n\n getProjectComponents: (key: string): Promise<JiraComponent[]> =>\n request(`/rest/api/3/project/${encodeURIComponent(key)}/components`),\n\n getProjectStatuses: (key: string): Promise<JiraProjectStatuses[]> =>\n request(`/rest/api/3/project/${encodeURIComponent(key)}/statuses`),\n\n // ── agile ─────────────────────────────────────────────────────────────\n getBoard: (id: number): Promise<JiraBoard> =>\n request(`/rest/agile/1.0/board/${id}`),\n\n /** The board's columns and which statuses each one claims, by id. */\n getBoardConfiguration: (id: number): Promise<JiraBoardConfiguration> =>\n request(`/rest/agile/1.0/board/${id}/configuration`),\n\n getBoardIssues: (\n id: number,\n jql?: string,\n ): Promise<{ issues: JiraIssue[] }> =>\n request(\n `/rest/agile/1.0/board/${id}/issue${jql ? `?jql=${encodeURIComponent(jql)}` : \"\"}`,\n ),\n\n getBacklog: (id: number): Promise<{ issues: JiraIssue[] }> =>\n request(`/rest/agile/1.0/board/${id}/backlog`),\n\n getSprint: (id: number): Promise<JiraSprint> =>\n request(`/rest/agile/1.0/sprint/${id}`),\n\n getSprintIssues: (id: number): Promise<{ issues: JiraIssue[] }> =>\n request(`/rest/agile/1.0/sprint/${id}/issue`),\n\n getBoardEpics: (id: number): Promise<{ values: JiraEpic[] }> =>\n request(`/rest/agile/1.0/board/${id}/epic`),\n\n getEpicIssues: (id: string): Promise<{ issues: JiraIssue[] }> =>\n request(`/rest/agile/1.0/epic/${encodeURIComponent(id)}/issue`),\n\n // ── people ────────────────────────────────────────────────────────────\n searchUsers: (query: string, maxResults = 20): Promise<JiraUser[]> =>\n request(\n `/rest/api/3/user/search?query=${encodeURIComponent(query)}&maxResults=${maxResults}`,\n ),\n\n searchAssignableUsers: (\n query: string,\n projectKey: string,\n maxResults = 20,\n ): Promise<JiraUser[]> =>\n request(\n `/rest/api/3/user/assignable/search?query=${encodeURIComponent(query)}&project=${encodeURIComponent(projectKey)}&maxResults=${maxResults}`,\n ),\n\n // ── instance metadata ─────────────────────────────────────────────────\n getIssueTypes: (): Promise<JiraIssueType[]> =>\n request(\"/rest/api/3/issuetype\"),\n\n getPriorities: (): Promise<JiraNamed[]> => request(\"/rest/api/3/priority\"),\n\n getResolutions: (): Promise<JiraNamed[]> =>\n request(\"/rest/api/3/resolution\"),\n\n getStatuses: (): Promise<JiraNamed[]> => request(\"/rest/api/3/status\"),\n\n getLabels: (): Promise<{ values: string[] }> =>\n request(\"/rest/api/3/label?maxResults=1000\"),\n\n getFilters: (): Promise<{ values: JiraFilter[] }> =>\n request(\"/rest/api/3/filter/search?expand=jql&maxResults=50\"),\n\n getDashboards: (): Promise<{ dashboards: JiraNamed[] }> =>\n request(\"/rest/api/3/dashboard\"),\n\n getServerInfo: (): Promise<Record<string, unknown>> =>\n request(\"/rest/api/3/serverInfo\"),\n\n getMyPermissions: (projectKey?: string): Promise<Record<string, unknown>> =>\n request(\n `/rest/api/3/mypermissions${projectKey ? `?projectKey=${encodeURIComponent(projectKey)}` : \"\"}`,\n ),\n\n /**\n * The count the removed `total` field used to give. Deliberately named\n * approximate because that is what Atlassian guarantees — it is an index\n * estimate, not a scan, and will disagree with a full page walk.\n */\n approximateCount: (jql: string): Promise<{ count: number }> =>\n request(\"/rest/api/3/search/approximate-count\", {\n method: \"POST\",\n body: JSON.stringify({ jql }),\n }),\n }\n}\n\nexport type JiraClient = ReturnType<typeof createJiraClient>\n","export type JiraUser = {\n accountId: string\n displayName: string\n emailAddress?: string\n active?: boolean\n}\n\nexport type JiraStatus = {\n id?: string\n name: string\n statusCategory?: { key: string; name: string }\n}\n\nexport type JiraIssueParent = {\n id: string\n key: string\n fields?: {\n summary?: string\n status?: JiraStatus\n issuetype?: JiraIssueType\n }\n}\n\nexport type JiraIssueFields = {\n summary?: string\n status?: JiraStatus\n assignee?: JiraUser | null\n reporter?: JiraUser | null\n issuetype?: JiraIssueType\n priority?: { name: string } | null\n project?: { key: string; name: string }\n labels?: string[]\n parent?: JiraIssueParent\n created?: string\n updated?: string\n description?: unknown\n comment?: { comments: JiraComment[] }\n [field: string]: unknown\n}\n\nexport type JiraIssue = {\n id: string\n key: string\n self: string\n fields: JiraIssueFields\n}\n\nexport type JiraComment = {\n id: string\n author?: JiraUser\n created?: string\n body?: unknown\n}\n\nexport type JiraTransition = {\n id: string\n name: string\n to?: JiraStatus\n}\n\nexport type JiraBoard = { id: number; name: string; type?: string }\n\nexport type JiraSprint = {\n id: number\n name: string\n state: string\n startDate?: string\n endDate?: string\n}\n\nexport type JiraProject = { id: string; key: string; name: string }\n\nexport type JiraField = {\n id: string\n name: string\n custom: boolean\n schema?: { type?: string; custom?: string }\n}\n\n/**\n * A page of the enhanced JQL search. `total` and `startAt` are deliberately\n * absent: /rest/api/3/search/jql replaced offset paging with an opaque cursor\n * and stopped reporting a count at all.\n */\nexport type JiraSearchPage = {\n issues: JiraIssue[]\n nextPageToken?: string\n isLast?: boolean\n}\n\nexport type SearchOptions = {\n fields?: string[]\n maxResults?: number\n nextPageToken?: string\n expand?: string\n}\n\nexport type JiraCreated = { id: string; key: string; self: string }\n\nexport type JiraNamed = { id: string; name: string; description?: string }\n\n/**\n * `hierarchyLevel` is Jira Cloud's own word for what an issue type is in the\n * tree — 1 for Epic, 0 for a standard type, -1 for a sub-task — and it is how\n * a container is recognised here, so a renamed Epic or a level-1 Initiative\n * still heads its children. Server/DC may omit it, hence the name fallback.\n */\nexport type JiraIssueType = JiraNamed & {\n subtask?: boolean\n scope?: unknown\n hierarchyLevel?: number\n}\n\nexport const isContainerType = (type: JiraIssueType | undefined): boolean =>\n !type\n ? false\n : type.hierarchyLevel !== undefined\n ? type.hierarchyLevel > 0\n : type.name.toLowerCase() === \"epic\"\n\nexport type JiraBoardColumn = { name: string; statuses: { id: string }[] }\n\nexport type JiraBoardConfiguration = {\n id: number\n name: string\n type?: string\n columnConfig?: { columns: JiraBoardColumn[] }\n}\n\nexport type JiraVersion = JiraNamed & {\n released?: boolean\n archived?: boolean\n releaseDate?: string\n}\n\nexport type JiraComponent = JiraNamed & { lead?: JiraUser }\n\nexport type JiraProjectStatuses = {\n id: string\n name: string\n statuses: JiraNamed[]\n}\n\nexport type JiraEpic = {\n id: number\n key: string\n name: string\n summary?: string\n done?: boolean\n}\n\nexport type JiraFilter = {\n id: string\n name: string\n jql?: string\n owner?: JiraUser\n}\n\nexport type JiraWorklog = {\n id: string\n author?: JiraUser\n timeSpent?: string\n timeSpentSeconds?: number\n started?: string\n comment?: unknown\n}\n\nexport type JiraIssueLinkType = {\n id: string\n name: string\n inward: string\n outward: string\n}\n\nexport type JiraChangelogEntry = {\n id: string\n author?: JiraUser\n created?: string\n items?: {\n field: string\n fromString?: string | null\n toString?: string | null\n }[]\n}\n","type AdfNode = {\n type?: string\n text?: string\n content?: AdfNode[]\n attrs?: Record<string, unknown>\n marks?: { type: string; attrs?: Record<string, unknown> }[]\n}\n\nconst markUp = (text: string, marks: AdfNode[\"marks\"]): string =>\n (marks ?? []).reduce((acc, mark) => {\n if (mark.type === \"code\") return `\\`${acc}\\``\n if (mark.type === \"strong\") return `**${acc}**`\n if (mark.type === \"em\") return `_${acc}_`\n if (mark.type === \"strike\") return `~~${acc}~~`\n if (mark.type === \"link\") return `[${acc}](${mark.attrs?.[\"href\"] ?? \"\"})`\n return acc\n }, text)\n\nexport type MediaResolver = (id: string) => string | undefined\n\nlet resolveMedia: MediaResolver = () => undefined\n\nconst children = (node: AdfNode, sep = \"\"): string =>\n (node.content ?? []).map(render).join(sep)\n\nconst listItems = (node: AdfNode, bullet: (i: number) => string): string =>\n (node.content ?? [])\n .map((item, i) => {\n const body = children(item, \"\\n\\n\").trim()\n const [first = \"\", ...rest] = body.split(\"\\n\")\n const marker = bullet(i)\n const indent = \" \".repeat(marker.length)\n return [\n `${marker}${first}`,\n ...rest.map((l) => (l ? `${indent}${l}` : l)),\n ].join(\"\\n\")\n })\n .join(\"\\n\")\n\n/** A table row rendered as a pipe row; the header separator is added by the caller. */\nconst row = (node: AdfNode): string =>\n `| ${(node.content ?? []).map((cell) => children(cell, \" \").trim().replace(/\\n+/g, \" \")).join(\" | \")} |`\n\nconst renderTable = (node: AdfNode): string => {\n const rows = node.content ?? []\n if (rows.length === 0) return \"\"\n const isHeader = (r: AdfNode): boolean =>\n (r.content ?? []).some((c) => c.type === \"tableHeader\")\n const [first] = rows\n const rendered = rows.map(row)\n if (first && isHeader(first)) {\n const columns = (first.content ?? []).length\n rendered.splice(1, 0, `|${\" --- |\".repeat(columns)}`)\n }\n return rendered.join(\"\\n\")\n}\n\nconst render = (node: AdfNode): string => {\n switch (node.type) {\n case \"doc\":\n return children(node, \"\\n\\n\")\n case \"paragraph\":\n return children(node)\n case \"text\":\n return markUp(node.text ?? \"\", node.marks)\n case \"hardBreak\":\n return \"\\n\"\n case \"heading\":\n return `${\"#\".repeat(Number(node.attrs?.[\"level\"] ?? 1))} ${children(node)}`\n case \"bulletList\":\n return listItems(node, () => \"- \")\n case \"orderedList\":\n return listItems(\n node,\n (i) => `${Number(node.attrs?.[\"order\"] ?? 1) + i}. `,\n )\n case \"codeBlock\":\n return `\\`\\`\\`${node.attrs?.[\"language\"] ?? \"\"}\\n${children(node)}\\n\\`\\`\\``\n case \"blockquote\":\n return children(node, \"\\n\\n\")\n .split(\"\\n\")\n .map((l) => `> ${l}`)\n .join(\"\\n\")\n case \"panel\":\n return `> [!${String(node.attrs?.[\"panelType\"] ?? \"note\").toUpperCase()}]\\n${children(\n node,\n \"\\n\\n\",\n )\n .split(\"\\n\")\n .map((l) => `> ${l}`)\n .join(\"\\n\")}`\n case \"rule\":\n return \"---\"\n case \"table\":\n return renderTable(node)\n case \"mediaSingle\":\n case \"mediaGroup\":\n return children(node, \"\\n\")\n case \"media\": {\n // `alt` is the filename Jira stored; the id is a media-platform UUID that\n // means nothing to a reader and does not match the attachment id either.\n const alt = node.attrs?.[\"alt\"]\n if (typeof alt === \"string\" && alt) return `[attachment: ${alt}]`\n const id = String(node.attrs?.[\"id\"] ?? \"\")\n const name = resolveMedia(id)\n return name ? `[attachment: ${name}]` : `[attachment: ${id || \"unknown\"}]`\n }\n case \"mention\": {\n // Jira stores the display text with its own leading @ most of the time,\n // but not always, so normalise rather than assume either way.\n const label = String(node.attrs?.[\"text\"] ?? node.attrs?.[\"id\"] ?? \"\")\n return label.startsWith(\"@\") ? label : `@${label}`\n }\n case \"emoji\":\n return String(node.attrs?.[\"text\"] ?? node.attrs?.[\"shortName\"] ?? \"\")\n case \"date\":\n return String(node.attrs?.[\"timestamp\"] ?? \"\")\n case \"status\":\n return `[${String(node.attrs?.[\"text\"] ?? \"\").toUpperCase()}]`\n case \"inlineCard\":\n return String(node.attrs?.[\"url\"] ?? \"\")\n default:\n return children(node, \"\\n\\n\")\n }\n}\n\n/**\n * Atlassian Document Format to Markdown. Deliberately stops at Markdown rather\n * than emitting ANSI: rendering is the surface's job, so a TUI, a pager and a\n * `--json` consumer all get the same text and only one of them styles it.\n */\nexport const adfToMarkdown = (\n doc: unknown,\n media?: MediaResolver,\n): string => {\n if (typeof doc === \"string\") return doc\n if (!doc || typeof doc !== \"object\") return \"\"\n const previous = resolveMedia\n resolveMedia = media ?? (() => undefined)\n try {\n return render(doc as AdfNode)\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim()\n } finally {\n resolveMedia = previous\n }\n}\n\ntype AdfDoc = { type: \"doc\"; version: 1; content: unknown[] }\n\nconst inline = (text: string): unknown[] => {\n // Only code spans and links are worth parsing: they are the two marks whose\n // absence changes meaning rather than appearance.\n const pattern = /(`[^`]+`)|(\\[[^\\]]+\\]\\([^)]+\\))/g\n const nodes: unknown[] = []\n let cursor = 0\n\n for (const match of text.matchAll(pattern)) {\n const at = match.index\n if (at > cursor)\n nodes.push({ type: \"text\", text: text.slice(cursor, at) })\n\n const token = match[0]\n if (token.startsWith(\"`\")) {\n nodes.push({\n type: \"text\",\n text: token.slice(1, -1),\n marks: [{ type: \"code\" }],\n })\n } else {\n const [, label = \"\", href = \"\"] =\n token.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/) ?? []\n nodes.push({\n type: \"text\",\n text: label,\n marks: [{ type: \"link\", attrs: { href } }],\n })\n }\n cursor = at + token.length\n }\n\n if (cursor < text.length) nodes.push({ type: \"text\", text: text.slice(cursor) })\n return nodes.length > 0 ? nodes : [{ type: \"text\", text }]\n}\n\nconst blockToAdf = (block: string): unknown => {\n const fence = block.match(/^```(\\w*)\\n([\\s\\S]*?)\\n?```$/)\n if (fence)\n return {\n type: \"codeBlock\",\n ...(fence[1] ? { attrs: { language: fence[1] } } : {}),\n content: [{ type: \"text\", text: fence[2] ?? \"\" }],\n }\n\n const heading = block.match(/^(#{1,6})\\s+(.*)$/)\n if (heading)\n return {\n type: \"heading\",\n attrs: { level: heading[1]?.length ?? 1 },\n content: inline(heading[2] ?? \"\"),\n }\n\n const lines = block.split(\"\\n\")\n if (lines.every((l) => /^\\s*[-*]\\s+/.test(l)))\n return {\n type: \"bulletList\",\n content: lines.map((l) => ({\n type: \"listItem\",\n content: [\n { type: \"paragraph\", content: inline(l.replace(/^\\s*[-*]\\s+/, \"\")) },\n ],\n })),\n }\n\n if (lines.every((l) => /^\\s*\\d+[.)]\\s+/.test(l)))\n return {\n type: \"orderedList\",\n content: lines.map((l) => ({\n type: \"listItem\",\n content: [\n { type: \"paragraph\", content: inline(l.replace(/^\\s*\\d+[.)]\\s+/, \"\")) },\n ],\n })),\n }\n\n return { type: \"paragraph\", content: inline(block) }\n}\n\n/**\n * Markdown to ADF, covering what someone actually types into a comment from a\n * terminal: paragraphs, fenced code, headings, lists, links and code spans.\n * Deliberately partial — anything richer is better authored in Jira, and a\n * half-supported table would corrupt more often than it would help.\n */\nexport const markdownToAdf = (text: string): AdfDoc => ({\n type: \"doc\",\n version: 1,\n content: text\n .replace(/\\r\\n/g, \"\\n\")\n .split(/\\n{2,}/)\n .map((b) => b.trim())\n .filter(Boolean)\n .map(blockToAdf),\n})\n","import type { JiraClient } from \"./client.js\"\nimport type { JiraIssue } from \"./types.js\"\n\nexport type JiraAttachment = {\n id: string\n filename: string\n mimeType: string\n size: number\n created?: string\n author?: { displayName: string }\n content?: string\n}\n\n/** Where an attachment was referenced from, so `issue view` can say so. */\nexport type AttachmentOrigin =\n | { kind: \"issue\" }\n | { kind: \"description\" }\n | { kind: \"comment\"; commentId: string; author?: string }\n\nexport type LocatedAttachment = JiraAttachment & { origins: AttachmentOrigin[] }\n\ntype AdfNode = {\n type?: string\n attrs?: Record<string, unknown>\n content?: AdfNode[]\n}\n\nexport type MediaRef = { id?: string; filename?: string }\n\n/**\n * Media nodes carry a media-platform UUID in `attrs.id`, which is a different\n * namespace from the numeric attachment id — joining on it matches nothing,\n * ever. `attrs.alt` holds the original filename and is the only field the two\n * representations share, so it is the real key and the id is the fallback.\n */\nconst mediaRefs = (node: unknown): MediaRef[] => {\n if (!node || typeof node !== \"object\") return []\n const n = node as AdfNode\n const here: MediaRef[] =\n n.type === \"media\"\n ? [\n {\n ...(typeof n.attrs?.[\"id\"] === \"string\"\n ? { id: n.attrs[\"id\"] as string }\n : {}),\n ...(typeof n.attrs?.[\"alt\"] === \"string\"\n ? { filename: n.attrs[\"alt\"] as string }\n : {}),\n },\n ]\n : []\n return [...here, ...(n.content ?? []).flatMap(mediaRefs)]\n}\n\n/**\n * Jira reports attachments once, on the issue, and never says where they were\n * embedded. Walking the description and each comment for media nodes and\n * joining them back is the only way to answer \"which comment did this come\n * from\" — a question the API cannot be asked directly.\n */\nexport const locateAttachments = (issue: JiraIssue): LocatedAttachment[] => {\n const attachments = (issue.fields[\"attachment\"] as JiraAttachment[]) ?? []\n const origins = new Map<string, AttachmentOrigin[]>()\n\n const note = (ref: MediaRef, origin: AttachmentOrigin): void => {\n const match = attachments.find(\n (a) =>\n (ref.filename !== undefined && a.filename === ref.filename) ||\n (ref.id !== undefined && a.id === ref.id),\n )\n if (!match) return\n origins.set(match.id, [...(origins.get(match.id) ?? []), origin])\n }\n\n for (const ref of mediaRefs(issue.fields.description))\n note(ref, { kind: \"description\" })\n\n for (const comment of issue.fields.comment?.comments ?? [])\n for (const ref of mediaRefs(comment.body))\n note(ref, {\n kind: \"comment\",\n commentId: comment.id,\n author: comment.author?.displayName,\n })\n\n return attachments.map((a) => ({\n ...a,\n origins: origins.get(a.id) ?? [{ kind: \"issue\" as const }],\n }))\n}\n\nconst TEXTUAL =\n /^(text\\/|application\\/(json|xml|x-yaml|yaml|javascript|sql|x-sh))/\n\n/** Extensions Jira commonly mislabels as application/octet-stream. */\nconst TEXTUAL_EXTENSIONS =\n /\\.(txt|md|markdown|log|json|ya?ml|csv|tsv|xml|html?|css|jsx?|tsx?|py|rb|go|rs|java|kt|sh|zsh|bash|sql|ini|toml|conf|env|diff|patch)$/i\n\nexport const isTextual = (attachment: JiraAttachment): boolean =>\n TEXTUAL.test(attachment.mimeType) ||\n TEXTUAL_EXTENSIONS.test(attachment.filename)\n\n/**\n * Fetches attachment bytes. The documented content endpoint 302s to a\n * short-lived media host, and the auth header must NOT follow: it is a Jira\n * credential and the redirect target is a different origin that neither needs\n * nor should see it. Hence manual redirect handling rather than fetch's default.\n */\nexport const downloadAttachment = async (\n client: JiraClient,\n id: string,\n fetchImpl: typeof globalThis.fetch = globalThis.fetch,\n): Promise<{ bytes: Uint8Array; mimeType: string | null }> => {\n const res = await client.request<Response>(\n `/rest/api/3/attachment/content/${encodeURIComponent(id)}`,\n { redirect: \"manual\", raw: true },\n )\n\n const location = res.headers.get(\"location\")\n const final =\n res.status >= 300 && res.status < 400 && location\n ? await fetchImpl(location)\n : res\n\n if (!final.ok) {\n throw new Error(\n `could not download attachment ${id}: ${final.status} ${final.statusText}`,\n )\n }\n\n return {\n bytes: new Uint8Array(await final.arrayBuffer()),\n mimeType: final.headers.get(\"content-type\"),\n }\n}\n","import { readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { normalizeBaseUrl } from \"./client.js\"\n\nexport type CustomFieldRef = { id: string; label: string }\n\nexport type FileConfig = {\n baseUrl?: string\n email?: string\n customFields?: CustomFieldRef[]\n sprintField?: string\n defaultProject?: string\n defaultBoard?: number\n /**\n * Hand-written board tabs, each naming the statuses it claims by id or by\n * name. The escape hatch over a board's own column config — for an instance\n * with no board, or a board whose columns are not how you think.\n */\n tabs?: BoardTab[]\n}\n\nexport type BoardTab = { label: string; statuses: string[] }\n\nexport type Config = FileConfig & {\n baseUrl: string\n email: string\n token: string\n}\n\nexport const configPath = (): string =>\n process.env[\"JIRA_CONFIG_FILE\"] ||\n join(\n process.env[\"XDG_CONFIG_HOME\"] || join(homedir(), \".config\"),\n \"jira\",\n \"config.json\",\n )\n\n/**\n * Credentials come from the environment and never from the config file: the\n * file is meant to be readable, diffable and shareable across a team, and a\n * token in it would leak the moment anyone pasted theirs into a gist.\n */\nexport const readFileConfig = (path = configPath()): FileConfig => {\n let raw: string\n try {\n raw = readFileSync(path, \"utf8\")\n } catch {\n return {}\n }\n const parsed = JSON.parse(raw) as FileConfig\n if (parsed.customFields && !Array.isArray(parsed.customFields))\n throw new Error(`${path}: customFields must be an array of {id,label}`)\n if (\"token\" in parsed || \"apiToken\" in parsed)\n throw new Error(\n `${path} contains a token. Tokens belong in ATLASSIAN_API_TOKEN, not in a file that gets shared and backed up — remove it.`,\n )\n return parsed\n}\n\n/**\n * Only the token is a secret, so only the token is env-only. The instance URL\n * and your email are settings, and asking someone to export three variables to\n * run a CLI is friction that buys nothing. Environment still wins where both\n * are present, so a second instance needs one inline override rather than a\n * second config file.\n */\nexport const loadConfig = (\n env: NodeJS.ProcessEnv = process.env,\n path = configPath(),\n): { config: Config } | { missing: string[] } => {\n const file = readFileConfig(path)\n\n const baseUrl = env[\"ATLASSIAN_BASE_URL\"] || file.baseUrl\n const email = env[\"ATLASSIAN_USER_EMAIL\"] || file.email\n const token = env[\"ATLASSIAN_API_TOKEN\"]\n\n const missing = (\n [\n [\"ATLASSIAN_BASE_URL\", baseUrl],\n [\"ATLASSIAN_USER_EMAIL\", email],\n [\"ATLASSIAN_API_TOKEN\", token],\n ] as const\n )\n .filter(([, v]) => !v)\n .map(([k]) => k)\n\n if (missing.length > 0) return { missing }\n\n return {\n config: {\n ...file,\n baseUrl: normalizeBaseUrl(baseUrl as string),\n email: email as string,\n token: token as string,\n },\n }\n}\n","export const jqlEscape = (value: string): string =>\n `\"${value.replace(/\"/g, '\\\\\"')}\"`\n\nexport type ListOptions = {\n assignee?: string\n mine?: boolean\n status?: string\n project?: string\n sprint?: string\n label?: string\n jql?: string\n}\n\n/**\n * Flags compose into one JQL string rather than each becoming its own command.\n * `--jql` replaces the generated clauses entirely so there is always an escape\n * hatch for anything the flags cannot express.\n */\nexport const buildJql = (\n options: ListOptions,\n defaultProject?: string,\n): string => {\n if (options.jql) return options.jql\n\n const clauses: string[] = []\n if (options.mine) clauses.push(\"assignee = currentUser()\")\n else if (options.assignee)\n clauses.push(\n options.assignee === \"none\"\n ? \"assignee IS EMPTY\"\n : `assignee = ${jqlEscape(options.assignee)}`,\n )\n\n const project = options.project ?? defaultProject\n if (project) clauses.push(`project = ${jqlEscape(project)}`)\n if (options.status) clauses.push(`status = ${jqlEscape(options.status)}`)\n if (options.label) clauses.push(`labels = ${jqlEscape(options.label)}`)\n if (options.sprint)\n clauses.push(\n options.sprint === \"current\"\n ? \"sprint IN openSprints()\"\n : `sprint = ${jqlEscape(options.sprint)}`,\n )\n\n // Jira rejects an unbounded query outright, so a bare `issue list` has to\n // mean something. Yours is the only defensible default.\n if (clauses.length === 0) clauses.push(\"assignee = currentUser()\")\n\n return `${clauses.join(\" AND \")} ORDER BY updated DESC`\n}\n\nexport type SearchMode = \"auto\" | \"jql\" | \"text\"\n\n/**\n * The tell for JQL is the SHAPE of the first clause — `identifier operator` —\n * not the presence of an operator somewhere. Every JQL clause opens that way\n * and no natural phrase does, so \"in progress\" stays words while\n * \"status = broken\" is a query (a wrong one, which Jira then says in its own\n * words). Never try-as-JQL: too many phrases parse.\n */\n// The wordy operators are held to their JQL grammar — `is` wants EMPTY or\n// NULL, `in` wants a list — so \"what is the total\" and \"in progress\" stay words.\nconst WORD_OPS = [\n String.raw`\\s+(not\\s+)?in\\s*\\(`,\n String.raw`\\s+is\\s+(not\\s+)?(empty|null)\\b`,\n String.raw`\\s+was\\s+(not\\s+)?(in\\s*\\(|\"|'|empty|null)`,\n String.raw`\\s+changed\\s*(by|after|before|during|on|from|to|$)`,\n].join(\"|\")\nconst JQL_SHAPE = new RegExp(\n String.raw`^\\(?\\s*(not\\s+)?\\(?\\s*[\\w.\\-[\\]\"]+\\s*(=|!=|~|!~|<=|>=|<|>|` + WORD_OPS + \")\",\n \"i\",\n)\nconst ORDER_ONLY = /^order\\s+by\\s/i\n\nexport const looksLikeJql = (input: string): boolean => {\n const s = input.trim()\n return JQL_SHAPE.test(s) || ORDER_ONLY.test(s)\n}\n\n// Jira's `~` hands the value to Lucene, where these are syntax: a search for\n// `c++` throws unless each is escaped.\nconst LUCENE_SPECIALS = /([+\\-&|!(){}[\\]^~*?:\\\\])/g\n\nexport const textClause = (words: string): string =>\n `text ~ ${jqlEscape(words.replace(LUCENE_SPECIALS, \"\\\\$1\"))}`\n\n/**\n * What a search box submission becomes. Plain words search WITHIN the scope\n * handed in (the list the user is looking at); JQL replaces it, because the\n * user has asked for something else. The two differ on purpose and the\n * screen names which is in force.\n */\nexport const searchJql = (\n input: string,\n { mode = \"auto\", scope }: { mode?: SearchMode; scope?: string } = {},\n): { jql: string; mode: \"jql\" | \"text\" } => {\n const trimmed = input.trim()\n const isJql = mode === \"jql\" || (mode === \"auto\" && looksLikeJql(trimmed))\n if (isJql) return { jql: trimmed, mode: \"jql\" }\n const clauses = [scope, textClause(trimmed)].filter(Boolean)\n return { jql: `${clauses.join(\" AND \")} ORDER BY updated DESC`, mode: \"text\" }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kud/jira",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Headless Jira client — issues, comments, attachments, ADF conversion, agile boards and instance metadata, with no environment or process dependencies",
|
|
6
6
|
"exports": {
|