@kud/jira 0.1.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.d.ts +361 -0
- package/dist/index.js +461 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erwann Mest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
๐ซ
|
|
4
|
+
|
|
5
|
+
# Jira
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
**Headless Jira client โ issues, comments, attachments, ADF conversion, agile boards and instance metadata, with no environment or process dependencies**
|
|
13
|
+
|
|
14
|
+
[Features](#-features) โข [Quick Start](#-quick-start) โข [API Reference](#-api-reference) โข [Development](#-development)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
## ๐ Features
|
|
19
|
+
|
|
20
|
+
- ๐ญ **Factory, not a class** โ `createJiraClient(options)` returns a plain object of methods; no `new`, no inheritance, nothing to extend.
|
|
21
|
+
- ๐ **~45 methods, one client** โ issues, comments, worklogs, watchers, links, projects, agile boards/sprints/epics, people search, and instance metadata (fields, priorities, statuses, labels, filters, dashboards, server info, permissions) all come off the same object.
|
|
22
|
+
- ๐งผ **Zero environment coupling** โ no `process.env` reads, no `process.exit`. Every credential and setting comes in through the options object, so the same client works identically in a CLI, an MCP server, a TUI, or a test.
|
|
23
|
+
- ๐งช **Injectable `fetch`** โ defaults to `globalThis.fetch`, but a caller can pass a fake for tests or a wrapped one for logging/retries.
|
|
24
|
+
- ๐ **ADF โ Markdown, both directions** โ `adfToMarkdown` and `markdownToAdf` convert Jira's Atlassian Document Format to and from plain Markdown, without ever touching ANSI or HTML.
|
|
25
|
+
- ๐ **Attachment origin tracking** โ `locateAttachments` maps each attachment back to where it was actually referenced (issue body, description, or a specific comment), something Jira's API never says directly.
|
|
26
|
+
- โ ๏ธ **Typed API errors** โ `jiraApiError` / `isJiraApiError` give callers a narrowed error shape instead of a bare thrown string.
|
|
27
|
+
- ๐ฆ **Zero runtime dependencies** โ pure `fetch`-based client using only Node/Web platform APIs (`Buffer`, `fetch`).
|
|
28
|
+
|
|
29
|
+
## ๐ Quick Start
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install @kud/jira
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createJiraClient } from "@kud/jira"
|
|
37
|
+
|
|
38
|
+
const jira = createJiraClient({
|
|
39
|
+
baseUrl: "myorg.atlassian.net",
|
|
40
|
+
email: "me@myorg.com",
|
|
41
|
+
token: process.env["JIRA_TOKEN"]!,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const issue = await jira.getIssue("PROJ-123")
|
|
45
|
+
console.log(issue.fields.summary)
|
|
46
|
+
|
|
47
|
+
const results = await jira.searchIssues(
|
|
48
|
+
"project = PROJ AND status = 'In Progress'",
|
|
49
|
+
{ limit: 20 },
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`baseUrl` accepts a bare host (`myorg.atlassian.net`) as readily as a full URL โ `normalizeBaseUrl` fills in `https://` when it's missing, since a bare host is the common shape a config value or env var arrives in.
|
|
54
|
+
|
|
55
|
+
## ๐ API Reference
|
|
56
|
+
|
|
57
|
+
### `createJiraClient(options)`
|
|
58
|
+
|
|
59
|
+
The library's single entry point. Everything else is returned from it.
|
|
60
|
+
|
|
61
|
+
| Option | Type | Description |
|
|
62
|
+
| -------------- | --------------------------------- | ----------------------------------------------------------------------------- |
|
|
63
|
+
| `baseUrl` | `string` | Instance host or URL. Passed through `normalizeBaseUrl`. |
|
|
64
|
+
| `email` | `string` | Account email for HTTP Basic auth. |
|
|
65
|
+
| `token` | `string` | API token for HTTP Basic auth. |
|
|
66
|
+
| `customFields` | `{ id: string; label: string }[]` | Optional. Custom fields to request on every issue fetch/search. |
|
|
67
|
+
| `sprintField` | `string` | Optional. This instance's sprint field id (e.g. `customfield_10020`). |
|
|
68
|
+
| `fetch` | `typeof globalThis.fetch` | Optional. Defaults to `globalThis.fetch` โ override to fake or wrap requests. |
|
|
69
|
+
|
|
70
|
+
Returns a client object exposing `request` (the raw authenticated fetch wrapper), `customFields`, `sprintField`, and the methods below.
|
|
71
|
+
|
|
72
|
+
### Search & issues
|
|
73
|
+
|
|
74
|
+
| Method | Description |
|
|
75
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
76
|
+
| `searchPage(jql, opts?)` | One page of the enhanced JQL search (`/search/jql`), cursor-based. |
|
|
77
|
+
| `searchIssues(jql, opts?)` | Walks the cursor up to `opts.limit` (default 50) issues, guarding against a looping token. |
|
|
78
|
+
| `getIssue(key)` | Fetches an issue with summary, status, description, comments, and attachments. |
|
|
79
|
+
| `createIssue(fields)` | Creates an issue from a Jira fields object. |
|
|
80
|
+
| `updateIssue(key, fields)` | Updates an issue's fields. |
|
|
81
|
+
| `deleteIssue(key, deleteSubtasks?)` | Deletes an issue. |
|
|
82
|
+
| `assignIssue(key, accountId)` | Assigns (or unassigns, with `null`) an issue. |
|
|
83
|
+
| `getTransitions(key)` | Lists the transitions available for an issue's current status. |
|
|
84
|
+
| `transitionIssue(key, transitionId)` | Executes a transition. |
|
|
85
|
+
| `approximateCount(jql)` | An index-estimate count for a JQL query โ not a scan, will disagree with a full page walk. |
|
|
86
|
+
|
|
87
|
+
### Comments, worklogs, watchers & links
|
|
88
|
+
|
|
89
|
+
| Method | Description |
|
|
90
|
+
| ----------------------------------------- | ------------------------------------------------------------ |
|
|
91
|
+
| `getComments(key)` | Lists an issue's comments. |
|
|
92
|
+
| `addComment(key, body)` | Adds a comment (ADF body). |
|
|
93
|
+
| `deleteComment(key, commentId)` | Deletes a comment. |
|
|
94
|
+
| `getWatchers(key)` | Lists an issue's watchers. |
|
|
95
|
+
| `addWatcher(key, accountId)` | Adds a watcher. |
|
|
96
|
+
| `removeWatcher(key, accountId)` | Removes a watcher. |
|
|
97
|
+
| `getWorklogs(key)` | Lists an issue's worklogs. |
|
|
98
|
+
| `addWorklog(key, body)` | Adds a worklog (`timeSpent`, optional `comment`, `started`). |
|
|
99
|
+
| `getChangelog(key)` | Lists an issue's changelog entries. |
|
|
100
|
+
| `getIssueLinkTypes()` | Lists the instance's issue link types. |
|
|
101
|
+
| `linkIssues(type, inwardKey, outwardKey)` | Links two issues by link type name. |
|
|
102
|
+
|
|
103
|
+
### Projects
|
|
104
|
+
|
|
105
|
+
| Method | Description |
|
|
106
|
+
| --------------------------- | --------------------------------------------------------- |
|
|
107
|
+
| `getProjects()` | Lists all projects, with description expanded. |
|
|
108
|
+
| `getProject(key)` | Fetches one project, with description/lead/url expanded. |
|
|
109
|
+
| `getProjectVersions(key)` | Lists a project's versions. |
|
|
110
|
+
| `getProjectComponents(key)` | Lists a project's components. |
|
|
111
|
+
| `getProjectStatuses(key)` | Lists the statuses available per issue type in a project. |
|
|
112
|
+
|
|
113
|
+
### Agile (boards, sprints, epics)
|
|
114
|
+
|
|
115
|
+
| Method | Description |
|
|
116
|
+
| ----------------------------- | ------------------------------------------------------ |
|
|
117
|
+
| `getBoards()` | Lists all boards. |
|
|
118
|
+
| `getBoard(id)` | Fetches one board. |
|
|
119
|
+
| `getBoardIssues(id, jql?)` | Lists a board's issues, optionally filtered by JQL. |
|
|
120
|
+
| `getBacklog(id)` | Lists a board's backlog issues. |
|
|
121
|
+
| `getSprints(boardId, state?)` | Lists a board's sprints, optionally filtered by state. |
|
|
122
|
+
| `getSprint(id)` | Fetches one sprint. |
|
|
123
|
+
| `getSprintIssues(id)` | Lists a sprint's issues. |
|
|
124
|
+
| `getBoardEpics(id)` | Lists a board's epics. |
|
|
125
|
+
| `getEpicIssues(id)` | Lists an epic's issues. |
|
|
126
|
+
|
|
127
|
+
### People
|
|
128
|
+
|
|
129
|
+
| Method | Description |
|
|
130
|
+
| ------------------------------------------------------- | --------------------------------------------- |
|
|
131
|
+
| `searchUsers(query, maxResults?)` | Searches users instance-wide. |
|
|
132
|
+
| `searchAssignableUsers(query, projectKey, maxResults?)` | Searches users assignable to a given project. |
|
|
133
|
+
|
|
134
|
+
### Instance metadata
|
|
135
|
+
|
|
136
|
+
| Method | Description |
|
|
137
|
+
| ------------------------------- | --------------------------------------------------------------------- |
|
|
138
|
+
| `getMe()` | The authenticated user. |
|
|
139
|
+
| `getFields()` | All fields, standard and custom, on the instance. |
|
|
140
|
+
| `getIssueTypes()` | All issue types. |
|
|
141
|
+
| `getPriorities()` | All priorities. |
|
|
142
|
+
| `getResolutions()` | All resolutions. |
|
|
143
|
+
| `getStatuses()` | All statuses. |
|
|
144
|
+
| `getLabels()` | Up to 1000 labels used on the instance. |
|
|
145
|
+
| `getFilters()` | Up to 50 saved filters, with JQL expanded. |
|
|
146
|
+
| `getDashboards()` | All dashboards. |
|
|
147
|
+
| `getServerInfo()` | Server/instance info. |
|
|
148
|
+
| `getMyPermissions(projectKey?)` | The authenticated user's permissions, optionally scoped to a project. |
|
|
149
|
+
|
|
150
|
+
### Error handling
|
|
151
|
+
|
|
152
|
+
| Export | Description |
|
|
153
|
+
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
154
|
+
| `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
|
+
| `isJiraApiError(e)` | Type guard narrowing an unknown catch value to `JiraApiError`. |
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { isJiraApiError } from "@kud/jira"
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await jira.getIssue("PROJ-999")
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (isJiraApiError(e) && e.status === 404) {
|
|
164
|
+
console.error("no such issue")
|
|
165
|
+
} else {
|
|
166
|
+
throw e
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### ADF conversion
|
|
172
|
+
|
|
173
|
+
| Export | Description |
|
|
174
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| `adfToMarkdown(doc, media?)` | Converts an Atlassian Document Format node tree to Markdown. Handles headings, lists, tables, code blocks, panels, mentions, emoji, and media references. |
|
|
176
|
+
| `markdownToAdf(text)` | Converts Markdown back to ADF โ paragraphs, fenced code, headings, lists, links, and code spans. Deliberately partial: richer formatting is better authored in Jira directly. |
|
|
177
|
+
|
|
178
|
+
Both stop at Markdown rather than emitting ANSI or HTML โ rendering is the caller's job, so a TUI, a pager, and a `--json` consumer all get the same text.
|
|
179
|
+
|
|
180
|
+
### Attachments
|
|
181
|
+
|
|
182
|
+
| Export | Description |
|
|
183
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
184
|
+
| `locateAttachments(issue)` | Maps an issue's attachments back to where they were referenced โ the issue body, the description, or a specific comment โ since Jira's API never states this directly. |
|
|
185
|
+
| `isTextual(attachment)` | Mime/extension sniffing for attachments that are safe to render as text. |
|
|
186
|
+
| `downloadAttachment(client, id, fetchImpl?)` | Downloads attachment bytes, handling Jira's redirect-to-media-host flow without forwarding the `Authorization` header cross-origin. |
|
|
187
|
+
|
|
188
|
+
### `normalizeBaseUrl(raw)`
|
|
189
|
+
|
|
190
|
+
Turns a bare host (`myorg.atlassian.net`) into a full `https://` URL. A no-op on an already-complete URL.
|
|
191
|
+
|
|
192
|
+
## ๐ง Development
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
src/
|
|
196
|
+
โโโ index.ts # public API surface โ re-exports everything below
|
|
197
|
+
โโโ client.ts # createJiraClient factory + all client methods
|
|
198
|
+
โโโ types.ts # Jira REST/Agile response shapes
|
|
199
|
+
โโโ adf.ts # ADF โ Markdown conversion
|
|
200
|
+
โโโ attachments.ts # attachment origin tracking, sniffing, download
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| Script | What it does |
|
|
204
|
+
| --------------------- | ----------------------------------- |
|
|
205
|
+
| `npm run build` | Bundles `src/` to `dist/` via tsup. |
|
|
206
|
+
| `npm run build:watch` | Same, in watch mode. |
|
|
207
|
+
| `npm run typecheck` | `tsc --noEmit`. |
|
|
208
|
+
| `npm test` | Runs the vitest suite once. |
|
|
209
|
+
| `npm run test:watch` | Runs vitest in watch mode. |
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
git clone https://github.com/kud/jira.git
|
|
213
|
+
cd jira
|
|
214
|
+
npm install
|
|
215
|
+
npm run build
|
|
216
|
+
npm test
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## ๐ Tech Stack
|
|
220
|
+
|
|
221
|
+
| Category | Choice |
|
|
222
|
+
| ------------ | ----------------------------- |
|
|
223
|
+
| Language | TypeScript |
|
|
224
|
+
| Runtime | Node.js โฅ 20 |
|
|
225
|
+
| Build | tsup |
|
|
226
|
+
| Tests | vitest |
|
|
227
|
+
| Dependencies | none (runtime) โ pure `fetch` |
|
|
228
|
+
|
|
229
|
+
Consumed today by [`@kud/jira-cli`](https://github.com/kud/jira-cli), which extracted this package's logic from its own `src/api/` layer so a second surface โ an MCP server, a TUI โ could consume the same client without going through the CLI.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
MIT ยฉ [kud](https://github.com/kud) โ Made with โค๏ธ
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
type JiraUser = {
|
|
2
|
+
accountId: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
emailAddress?: string;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
};
|
|
7
|
+
type JiraStatus = {
|
|
8
|
+
name: string;
|
|
9
|
+
statusCategory?: {
|
|
10
|
+
key: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
type JiraIssueFields = {
|
|
15
|
+
summary?: string;
|
|
16
|
+
status?: JiraStatus;
|
|
17
|
+
assignee?: JiraUser | null;
|
|
18
|
+
reporter?: JiraUser | null;
|
|
19
|
+
issuetype?: {
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
priority?: {
|
|
23
|
+
name: string;
|
|
24
|
+
} | null;
|
|
25
|
+
project?: {
|
|
26
|
+
key: string;
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
labels?: string[];
|
|
30
|
+
created?: string;
|
|
31
|
+
updated?: string;
|
|
32
|
+
description?: unknown;
|
|
33
|
+
comment?: {
|
|
34
|
+
comments: JiraComment[];
|
|
35
|
+
};
|
|
36
|
+
[field: string]: unknown;
|
|
37
|
+
};
|
|
38
|
+
type JiraIssue = {
|
|
39
|
+
id: string;
|
|
40
|
+
key: string;
|
|
41
|
+
self: string;
|
|
42
|
+
fields: JiraIssueFields;
|
|
43
|
+
};
|
|
44
|
+
type JiraComment = {
|
|
45
|
+
id: string;
|
|
46
|
+
author?: JiraUser;
|
|
47
|
+
created?: string;
|
|
48
|
+
body?: unknown;
|
|
49
|
+
};
|
|
50
|
+
type JiraTransition = {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
to?: JiraStatus;
|
|
54
|
+
};
|
|
55
|
+
type JiraBoard = {
|
|
56
|
+
id: number;
|
|
57
|
+
name: string;
|
|
58
|
+
type?: string;
|
|
59
|
+
};
|
|
60
|
+
type JiraSprint = {
|
|
61
|
+
id: number;
|
|
62
|
+
name: string;
|
|
63
|
+
state: string;
|
|
64
|
+
startDate?: string;
|
|
65
|
+
endDate?: string;
|
|
66
|
+
};
|
|
67
|
+
type JiraProject = {
|
|
68
|
+
id: string;
|
|
69
|
+
key: string;
|
|
70
|
+
name: string;
|
|
71
|
+
};
|
|
72
|
+
type JiraField = {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
custom: boolean;
|
|
76
|
+
schema?: {
|
|
77
|
+
type?: string;
|
|
78
|
+
custom?: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* A page of the enhanced JQL search. `total` and `startAt` are deliberately
|
|
83
|
+
* absent: /rest/api/3/search/jql replaced offset paging with an opaque cursor
|
|
84
|
+
* and stopped reporting a count at all.
|
|
85
|
+
*/
|
|
86
|
+
type JiraSearchPage = {
|
|
87
|
+
issues: JiraIssue[];
|
|
88
|
+
nextPageToken?: string;
|
|
89
|
+
isLast?: boolean;
|
|
90
|
+
};
|
|
91
|
+
type SearchOptions = {
|
|
92
|
+
fields?: string[];
|
|
93
|
+
maxResults?: number;
|
|
94
|
+
nextPageToken?: string;
|
|
95
|
+
expand?: string;
|
|
96
|
+
};
|
|
97
|
+
type JiraCreated = {
|
|
98
|
+
id: string;
|
|
99
|
+
key: string;
|
|
100
|
+
self: string;
|
|
101
|
+
};
|
|
102
|
+
type JiraNamed = {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
};
|
|
107
|
+
type JiraIssueType = JiraNamed & {
|
|
108
|
+
subtask?: boolean;
|
|
109
|
+
scope?: unknown;
|
|
110
|
+
};
|
|
111
|
+
type JiraVersion = JiraNamed & {
|
|
112
|
+
released?: boolean;
|
|
113
|
+
archived?: boolean;
|
|
114
|
+
releaseDate?: string;
|
|
115
|
+
};
|
|
116
|
+
type JiraComponent = JiraNamed & {
|
|
117
|
+
lead?: JiraUser;
|
|
118
|
+
};
|
|
119
|
+
type JiraProjectStatuses = {
|
|
120
|
+
id: string;
|
|
121
|
+
name: string;
|
|
122
|
+
statuses: JiraNamed[];
|
|
123
|
+
};
|
|
124
|
+
type JiraEpic = {
|
|
125
|
+
id: number;
|
|
126
|
+
key: string;
|
|
127
|
+
name: string;
|
|
128
|
+
summary?: string;
|
|
129
|
+
done?: boolean;
|
|
130
|
+
};
|
|
131
|
+
type JiraFilter = {
|
|
132
|
+
id: string;
|
|
133
|
+
name: string;
|
|
134
|
+
jql?: string;
|
|
135
|
+
owner?: JiraUser;
|
|
136
|
+
};
|
|
137
|
+
type JiraWorklog = {
|
|
138
|
+
id: string;
|
|
139
|
+
author?: JiraUser;
|
|
140
|
+
timeSpent?: string;
|
|
141
|
+
timeSpentSeconds?: number;
|
|
142
|
+
started?: string;
|
|
143
|
+
comment?: unknown;
|
|
144
|
+
};
|
|
145
|
+
type JiraIssueLinkType = {
|
|
146
|
+
id: string;
|
|
147
|
+
name: string;
|
|
148
|
+
inward: string;
|
|
149
|
+
outward: string;
|
|
150
|
+
};
|
|
151
|
+
type JiraChangelogEntry = {
|
|
152
|
+
id: string;
|
|
153
|
+
author?: JiraUser;
|
|
154
|
+
created?: string;
|
|
155
|
+
items?: {
|
|
156
|
+
field: string;
|
|
157
|
+
fromString?: string | null;
|
|
158
|
+
toString?: string | null;
|
|
159
|
+
}[];
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
type JiraCredentials = {
|
|
163
|
+
baseUrl: string;
|
|
164
|
+
email: string;
|
|
165
|
+
token: string;
|
|
166
|
+
};
|
|
167
|
+
type JiraClientOptions = JiraCredentials & {
|
|
168
|
+
/** Custom fields to request and label, discovered per instance via `jira fields`. */
|
|
169
|
+
customFields?: {
|
|
170
|
+
id: string;
|
|
171
|
+
label: string;
|
|
172
|
+
}[];
|
|
173
|
+
/** The instance's sprint field id, e.g. customfield_10020. */
|
|
174
|
+
sprintField?: string;
|
|
175
|
+
fetch?: typeof globalThis.fetch;
|
|
176
|
+
};
|
|
177
|
+
type JiraApiError = Error & {
|
|
178
|
+
name: "JiraApiError";
|
|
179
|
+
status: number;
|
|
180
|
+
method: string;
|
|
181
|
+
url: string;
|
|
182
|
+
body: string;
|
|
183
|
+
};
|
|
184
|
+
declare const jiraApiError: (status: number, method: string, url: string, body: string) => JiraApiError;
|
|
185
|
+
declare const isJiraApiError: (e: unknown) => e is JiraApiError;
|
|
186
|
+
/** Accepts `myorg.atlassian.net` as readily as a full URL; a bare host is the
|
|
187
|
+
* common shape of the env var and produces an opaque ERR_INVALID_URL if left. */
|
|
188
|
+
declare const normalizeBaseUrl: (raw: string) => string;
|
|
189
|
+
declare const createJiraClient: (options: JiraClientOptions) => {
|
|
190
|
+
request: <T>(path: string, init?: RequestInit & {
|
|
191
|
+
raw?: boolean;
|
|
192
|
+
}) => Promise<T>;
|
|
193
|
+
customFields: {
|
|
194
|
+
id: string;
|
|
195
|
+
label: string;
|
|
196
|
+
}[];
|
|
197
|
+
sprintField: string | undefined;
|
|
198
|
+
searchPage: (jql: string, opts?: SearchOptions) => Promise<JiraSearchPage>;
|
|
199
|
+
searchIssues: (jql: string, opts?: SearchOptions & {
|
|
200
|
+
limit?: number;
|
|
201
|
+
}) => Promise<JiraIssue[]>;
|
|
202
|
+
getIssue: (key: string) => Promise<JiraIssue>;
|
|
203
|
+
getTransitions: (key: string) => Promise<{
|
|
204
|
+
transitions: JiraTransition[];
|
|
205
|
+
}>;
|
|
206
|
+
transitionIssue: (key: string, transitionId: string) => Promise<void>;
|
|
207
|
+
addComment: (key: string, body: unknown) => Promise<void>;
|
|
208
|
+
getMe: () => Promise<JiraUser>;
|
|
209
|
+
getFields: () => Promise<JiraField[]>;
|
|
210
|
+
getProjects: () => Promise<JiraProject[]>;
|
|
211
|
+
getBoards: () => Promise<{
|
|
212
|
+
values: JiraBoard[];
|
|
213
|
+
}>;
|
|
214
|
+
getSprints: (boardId: number, state?: string) => Promise<{
|
|
215
|
+
values: JiraSprint[];
|
|
216
|
+
}>;
|
|
217
|
+
createIssue: (fields: Record<string, unknown>) => Promise<JiraCreated>;
|
|
218
|
+
updateIssue: (key: string, fields: Record<string, unknown>) => Promise<void>;
|
|
219
|
+
deleteIssue: (key: string, deleteSubtasks?: boolean) => Promise<void>;
|
|
220
|
+
assignIssue: (key: string, accountId: string | null) => Promise<void>;
|
|
221
|
+
getComments: (key: string) => Promise<{
|
|
222
|
+
comments: JiraComment[];
|
|
223
|
+
}>;
|
|
224
|
+
deleteComment: (key: string, commentId: string) => Promise<void>;
|
|
225
|
+
getWatchers: (key: string) => Promise<{
|
|
226
|
+
watchers: JiraUser[];
|
|
227
|
+
}>;
|
|
228
|
+
addWatcher: (key: string, accountId: string) => Promise<void>;
|
|
229
|
+
removeWatcher: (key: string, accountId: string) => Promise<void>;
|
|
230
|
+
getWorklogs: (key: string) => Promise<{
|
|
231
|
+
worklogs: JiraWorklog[];
|
|
232
|
+
}>;
|
|
233
|
+
addWorklog: (key: string, body: {
|
|
234
|
+
timeSpent: string;
|
|
235
|
+
comment?: unknown;
|
|
236
|
+
started?: string;
|
|
237
|
+
}) => Promise<JiraWorklog>;
|
|
238
|
+
getChangelog: (key: string) => Promise<{
|
|
239
|
+
values: JiraChangelogEntry[];
|
|
240
|
+
}>;
|
|
241
|
+
getIssueLinkTypes: () => Promise<{
|
|
242
|
+
issueLinkTypes: JiraIssueLinkType[];
|
|
243
|
+
}>;
|
|
244
|
+
linkIssues: (type: string, inwardKey: string, outwardKey: string) => Promise<void>;
|
|
245
|
+
getProject: (key: string) => Promise<JiraProject>;
|
|
246
|
+
getProjectVersions: (key: string) => Promise<JiraVersion[]>;
|
|
247
|
+
getProjectComponents: (key: string) => Promise<JiraComponent[]>;
|
|
248
|
+
getProjectStatuses: (key: string) => Promise<JiraProjectStatuses[]>;
|
|
249
|
+
getBoard: (id: number) => Promise<JiraBoard>;
|
|
250
|
+
getBoardIssues: (id: number, jql?: string) => Promise<{
|
|
251
|
+
issues: JiraIssue[];
|
|
252
|
+
}>;
|
|
253
|
+
getBacklog: (id: number) => Promise<{
|
|
254
|
+
issues: JiraIssue[];
|
|
255
|
+
}>;
|
|
256
|
+
getSprint: (id: number) => Promise<JiraSprint>;
|
|
257
|
+
getSprintIssues: (id: number) => Promise<{
|
|
258
|
+
issues: JiraIssue[];
|
|
259
|
+
}>;
|
|
260
|
+
getBoardEpics: (id: number) => Promise<{
|
|
261
|
+
values: JiraEpic[];
|
|
262
|
+
}>;
|
|
263
|
+
getEpicIssues: (id: string) => Promise<{
|
|
264
|
+
issues: JiraIssue[];
|
|
265
|
+
}>;
|
|
266
|
+
searchUsers: (query: string, maxResults?: number) => Promise<JiraUser[]>;
|
|
267
|
+
searchAssignableUsers: (query: string, projectKey: string, maxResults?: number) => Promise<JiraUser[]>;
|
|
268
|
+
getIssueTypes: () => Promise<JiraIssueType[]>;
|
|
269
|
+
getPriorities: () => Promise<JiraNamed[]>;
|
|
270
|
+
getResolutions: () => Promise<JiraNamed[]>;
|
|
271
|
+
getStatuses: () => Promise<JiraNamed[]>;
|
|
272
|
+
getLabels: () => Promise<{
|
|
273
|
+
values: string[];
|
|
274
|
+
}>;
|
|
275
|
+
getFilters: () => Promise<{
|
|
276
|
+
values: JiraFilter[];
|
|
277
|
+
}>;
|
|
278
|
+
getDashboards: () => Promise<{
|
|
279
|
+
dashboards: JiraNamed[];
|
|
280
|
+
}>;
|
|
281
|
+
getServerInfo: () => Promise<Record<string, unknown>>;
|
|
282
|
+
getMyPermissions: (projectKey?: string) => Promise<Record<string, unknown>>;
|
|
283
|
+
/**
|
|
284
|
+
* The count the removed `total` field used to give. Deliberately named
|
|
285
|
+
* approximate because that is what Atlassian guarantees โ it is an index
|
|
286
|
+
* estimate, not a scan, and will disagree with a full page walk.
|
|
287
|
+
*/
|
|
288
|
+
approximateCount: (jql: string) => Promise<{
|
|
289
|
+
count: number;
|
|
290
|
+
}>;
|
|
291
|
+
};
|
|
292
|
+
type JiraClient = ReturnType<typeof createJiraClient>;
|
|
293
|
+
|
|
294
|
+
type MediaResolver = (id: string) => string | undefined;
|
|
295
|
+
/**
|
|
296
|
+
* Atlassian Document Format to Markdown. Deliberately stops at Markdown rather
|
|
297
|
+
* than emitting ANSI: rendering is the surface's job, so a TUI, a pager and a
|
|
298
|
+
* `--json` consumer all get the same text and only one of them styles it.
|
|
299
|
+
*/
|
|
300
|
+
declare const adfToMarkdown: (doc: unknown, media?: MediaResolver) => string;
|
|
301
|
+
type AdfDoc = {
|
|
302
|
+
type: "doc";
|
|
303
|
+
version: 1;
|
|
304
|
+
content: unknown[];
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Markdown to ADF, covering what someone actually types into a comment from a
|
|
308
|
+
* terminal: paragraphs, fenced code, headings, lists, links and code spans.
|
|
309
|
+
* Deliberately partial โ anything richer is better authored in Jira, and a
|
|
310
|
+
* half-supported table would corrupt more often than it would help.
|
|
311
|
+
*/
|
|
312
|
+
declare const markdownToAdf: (text: string) => AdfDoc;
|
|
313
|
+
|
|
314
|
+
type JiraAttachment = {
|
|
315
|
+
id: string;
|
|
316
|
+
filename: string;
|
|
317
|
+
mimeType: string;
|
|
318
|
+
size: number;
|
|
319
|
+
created?: string;
|
|
320
|
+
author?: {
|
|
321
|
+
displayName: string;
|
|
322
|
+
};
|
|
323
|
+
content?: string;
|
|
324
|
+
};
|
|
325
|
+
/** Where an attachment was referenced from, so `issue view` can say so. */
|
|
326
|
+
type AttachmentOrigin = {
|
|
327
|
+
kind: "issue";
|
|
328
|
+
} | {
|
|
329
|
+
kind: "description";
|
|
330
|
+
} | {
|
|
331
|
+
kind: "comment";
|
|
332
|
+
commentId: string;
|
|
333
|
+
author?: string;
|
|
334
|
+
};
|
|
335
|
+
type LocatedAttachment = JiraAttachment & {
|
|
336
|
+
origins: AttachmentOrigin[];
|
|
337
|
+
};
|
|
338
|
+
type MediaRef = {
|
|
339
|
+
id?: string;
|
|
340
|
+
filename?: string;
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Jira reports attachments once, on the issue, and never says where they were
|
|
344
|
+
* embedded. Walking the description and each comment for media nodes and
|
|
345
|
+
* joining them back is the only way to answer "which comment did this come
|
|
346
|
+
* from" โ a question the API cannot be asked directly.
|
|
347
|
+
*/
|
|
348
|
+
declare const locateAttachments: (issue: JiraIssue) => LocatedAttachment[];
|
|
349
|
+
declare const isTextual: (attachment: JiraAttachment) => boolean;
|
|
350
|
+
/**
|
|
351
|
+
* Fetches attachment bytes. The documented content endpoint 302s to a
|
|
352
|
+
* short-lived media host, and the auth header must NOT follow: it is a Jira
|
|
353
|
+
* credential and the redirect target is a different origin that neither needs
|
|
354
|
+
* nor should see it. Hence manual redirect handling rather than fetch's default.
|
|
355
|
+
*/
|
|
356
|
+
declare const downloadAttachment: (client: JiraClient, id: string, fetchImpl?: typeof globalThis.fetch) => Promise<{
|
|
357
|
+
bytes: Uint8Array;
|
|
358
|
+
mimeType: string | null;
|
|
359
|
+
}>;
|
|
360
|
+
|
|
361
|
+
export { type AttachmentOrigin, type JiraApiError, type JiraAttachment, type JiraBoard, 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 JiraIssueType, type JiraNamed, type JiraProject, type JiraProjectStatuses, type JiraSearchPage, type JiraSprint, type JiraStatus, type JiraTransition, type JiraUser, type JiraVersion, type JiraWorklog, type LocatedAttachment, type MediaRef, type MediaResolver, type SearchOptions, adfToMarkdown, createJiraClient, downloadAttachment, isJiraApiError, isTextual, jiraApiError, locateAttachments, markdownToAdf, normalizeBaseUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var jiraApiError = (status, method, url, body) => Object.assign(
|
|
3
|
+
new Error(`Jira API ${status} ${method} ${url}: ${truncate(body)}`),
|
|
4
|
+
{ name: "JiraApiError", status, method, url, body }
|
|
5
|
+
);
|
|
6
|
+
var isJiraApiError = (e) => e instanceof Error && e.name === "JiraApiError";
|
|
7
|
+
var truncate = (s, max = 400) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
8
|
+
var normalizeBaseUrl = (raw) => {
|
|
9
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
10
|
+
return /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_FIELDS = [
|
|
13
|
+
"summary",
|
|
14
|
+
"status",
|
|
15
|
+
"assignee",
|
|
16
|
+
"issuetype",
|
|
17
|
+
"priority",
|
|
18
|
+
"project",
|
|
19
|
+
"labels",
|
|
20
|
+
"updated"
|
|
21
|
+
];
|
|
22
|
+
var createJiraClient = (options) => {
|
|
23
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
24
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
25
|
+
const customFields = options.customFields ?? [];
|
|
26
|
+
const authHeader = `Basic ${Buffer.from(`${options.email}:${options.token}`).toString("base64")}`;
|
|
27
|
+
const request = async (path, init = {}) => {
|
|
28
|
+
const url = path.startsWith("http") ? path : `${baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
29
|
+
const res = await doFetch(url, {
|
|
30
|
+
...init,
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: authHeader,
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
// Jira localises error bodies from the account's language. Errors here
|
|
36
|
+
// are read by scripts and pasted into issues, so pin them to English.
|
|
37
|
+
"Accept-Language": "en",
|
|
38
|
+
...init.headers
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
if (init.raw) return res;
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw jiraApiError(
|
|
44
|
+
res.status,
|
|
45
|
+
init.method ?? "GET",
|
|
46
|
+
url,
|
|
47
|
+
await res.text()
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (res.status === 204) return void 0;
|
|
51
|
+
return await res.json();
|
|
52
|
+
};
|
|
53
|
+
const fieldList = (extra) => [
|
|
54
|
+
.../* @__PURE__ */ new Set([
|
|
55
|
+
...extra ?? DEFAULT_FIELDS,
|
|
56
|
+
...customFields.map((f) => f.id),
|
|
57
|
+
...options.sprintField ? [options.sprintField] : []
|
|
58
|
+
])
|
|
59
|
+
];
|
|
60
|
+
const searchPage = (jql, opts = {}) => request("/rest/api/3/search/jql", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
jql,
|
|
64
|
+
fields: fieldList(opts.fields),
|
|
65
|
+
maxResults: opts.maxResults ?? 50,
|
|
66
|
+
...opts.nextPageToken ? { nextPageToken: opts.nextPageToken } : {},
|
|
67
|
+
...opts.expand ? { expand: opts.expand } : {}
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
const searchIssues = async (jql, opts = {}) => {
|
|
71
|
+
const limit = opts.limit ?? 50;
|
|
72
|
+
const issues = [];
|
|
73
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
74
|
+
let token = opts.nextPageToken;
|
|
75
|
+
while (issues.length < limit) {
|
|
76
|
+
const page = await searchPage(jql, {
|
|
77
|
+
...opts,
|
|
78
|
+
nextPageToken: token,
|
|
79
|
+
maxResults: Math.min(100, limit - issues.length)
|
|
80
|
+
});
|
|
81
|
+
if (page.issues.length === 0) break;
|
|
82
|
+
issues.push(...page.issues);
|
|
83
|
+
const next = page.nextPageToken;
|
|
84
|
+
if (!next || page.isLast || seenTokens.has(next)) break;
|
|
85
|
+
seenTokens.add(next);
|
|
86
|
+
token = next;
|
|
87
|
+
}
|
|
88
|
+
return issues.slice(0, limit);
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
request,
|
|
92
|
+
customFields,
|
|
93
|
+
sprintField: options.sprintField,
|
|
94
|
+
searchPage,
|
|
95
|
+
searchIssues,
|
|
96
|
+
getIssue: (key) => request(
|
|
97
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${fieldList([
|
|
98
|
+
...DEFAULT_FIELDS,
|
|
99
|
+
"description",
|
|
100
|
+
"comment",
|
|
101
|
+
"reporter",
|
|
102
|
+
"created",
|
|
103
|
+
"attachment"
|
|
104
|
+
]).join(",")}`
|
|
105
|
+
),
|
|
106
|
+
getTransitions: (key) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`),
|
|
107
|
+
transitionIssue: (key, transitionId) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: JSON.stringify({ transition: { id: transitionId } })
|
|
110
|
+
}),
|
|
111
|
+
addComment: (key, body) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
body: JSON.stringify({ body })
|
|
114
|
+
}),
|
|
115
|
+
getMe: () => request("/rest/api/3/myself"),
|
|
116
|
+
getFields: () => request("/rest/api/3/field"),
|
|
117
|
+
getProjects: () => request("/rest/api/3/project?expand=description"),
|
|
118
|
+
getBoards: () => request("/rest/agile/1.0/board"),
|
|
119
|
+
getSprints: (boardId, state) => request(
|
|
120
|
+
`/rest/agile/1.0/board/${boardId}/sprint${state ? `?state=${state}` : ""}`
|
|
121
|
+
),
|
|
122
|
+
// โโ issues โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
123
|
+
createIssue: (fields) => request("/rest/api/3/issue", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
body: JSON.stringify({ fields })
|
|
126
|
+
}),
|
|
127
|
+
updateIssue: (key, fields) => request(`/rest/api/3/issue/${encodeURIComponent(key)}`, {
|
|
128
|
+
method: "PUT",
|
|
129
|
+
body: JSON.stringify({ fields })
|
|
130
|
+
}),
|
|
131
|
+
deleteIssue: (key, deleteSubtasks = false) => request(
|
|
132
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}?deleteSubtasks=${deleteSubtasks}`,
|
|
133
|
+
{ method: "DELETE" }
|
|
134
|
+
),
|
|
135
|
+
assignIssue: (key, accountId) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/assignee`, {
|
|
136
|
+
method: "PUT",
|
|
137
|
+
body: JSON.stringify({ accountId })
|
|
138
|
+
}),
|
|
139
|
+
getComments: (key) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`),
|
|
140
|
+
deleteComment: (key, commentId) => request(
|
|
141
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}/comment/${encodeURIComponent(commentId)}`,
|
|
142
|
+
{ method: "DELETE" }
|
|
143
|
+
),
|
|
144
|
+
getWatchers: (key) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/watchers`),
|
|
145
|
+
addWatcher: (key, accountId) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/watchers`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: JSON.stringify(accountId)
|
|
148
|
+
}),
|
|
149
|
+
removeWatcher: (key, accountId) => request(
|
|
150
|
+
`/rest/api/3/issue/${encodeURIComponent(key)}/watchers?accountId=${encodeURIComponent(accountId)}`,
|
|
151
|
+
{ method: "DELETE" }
|
|
152
|
+
),
|
|
153
|
+
getWorklogs: (key) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/worklog`),
|
|
154
|
+
addWorklog: (key, body) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/worklog`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
body: JSON.stringify(body)
|
|
157
|
+
}),
|
|
158
|
+
getChangelog: (key) => request(`/rest/api/3/issue/${encodeURIComponent(key)}/changelog`),
|
|
159
|
+
getIssueLinkTypes: () => request("/rest/api/3/issueLinkType"),
|
|
160
|
+
linkIssues: (type, inwardKey, outwardKey) => request("/rest/api/3/issueLink", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
type: { name: type },
|
|
164
|
+
inwardIssue: { key: inwardKey },
|
|
165
|
+
outwardIssue: { key: outwardKey }
|
|
166
|
+
})
|
|
167
|
+
}),
|
|
168
|
+
// โโ projects โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
169
|
+
getProject: (key) => request(
|
|
170
|
+
`/rest/api/3/project/${encodeURIComponent(key)}?expand=description,lead,url`
|
|
171
|
+
),
|
|
172
|
+
getProjectVersions: (key) => request(`/rest/api/3/project/${encodeURIComponent(key)}/versions`),
|
|
173
|
+
getProjectComponents: (key) => request(`/rest/api/3/project/${encodeURIComponent(key)}/components`),
|
|
174
|
+
getProjectStatuses: (key) => request(`/rest/api/3/project/${encodeURIComponent(key)}/statuses`),
|
|
175
|
+
// โโ agile โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
176
|
+
getBoard: (id) => request(`/rest/agile/1.0/board/${id}`),
|
|
177
|
+
getBoardIssues: (id, jql) => request(
|
|
178
|
+
`/rest/agile/1.0/board/${id}/issue${jql ? `?jql=${encodeURIComponent(jql)}` : ""}`
|
|
179
|
+
),
|
|
180
|
+
getBacklog: (id) => request(`/rest/agile/1.0/board/${id}/backlog`),
|
|
181
|
+
getSprint: (id) => request(`/rest/agile/1.0/sprint/${id}`),
|
|
182
|
+
getSprintIssues: (id) => request(`/rest/agile/1.0/sprint/${id}/issue`),
|
|
183
|
+
getBoardEpics: (id) => request(`/rest/agile/1.0/board/${id}/epic`),
|
|
184
|
+
getEpicIssues: (id) => request(`/rest/agile/1.0/epic/${encodeURIComponent(id)}/issue`),
|
|
185
|
+
// โโ people โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
186
|
+
searchUsers: (query, maxResults = 20) => request(
|
|
187
|
+
`/rest/api/3/user/search?query=${encodeURIComponent(query)}&maxResults=${maxResults}`
|
|
188
|
+
),
|
|
189
|
+
searchAssignableUsers: (query, projectKey, maxResults = 20) => request(
|
|
190
|
+
`/rest/api/3/user/assignable/search?query=${encodeURIComponent(query)}&project=${encodeURIComponent(projectKey)}&maxResults=${maxResults}`
|
|
191
|
+
),
|
|
192
|
+
// โโ instance metadata โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
193
|
+
getIssueTypes: () => request("/rest/api/3/issuetype"),
|
|
194
|
+
getPriorities: () => request("/rest/api/3/priority"),
|
|
195
|
+
getResolutions: () => request("/rest/api/3/resolution"),
|
|
196
|
+
getStatuses: () => request("/rest/api/3/status"),
|
|
197
|
+
getLabels: () => request("/rest/api/3/label?maxResults=1000"),
|
|
198
|
+
getFilters: () => request("/rest/api/3/filter/search?expand=jql&maxResults=50"),
|
|
199
|
+
getDashboards: () => request("/rest/api/3/dashboard"),
|
|
200
|
+
getServerInfo: () => request("/rest/api/3/serverInfo"),
|
|
201
|
+
getMyPermissions: (projectKey) => request(
|
|
202
|
+
`/rest/api/3/mypermissions${projectKey ? `?projectKey=${encodeURIComponent(projectKey)}` : ""}`
|
|
203
|
+
),
|
|
204
|
+
/**
|
|
205
|
+
* The count the removed `total` field used to give. Deliberately named
|
|
206
|
+
* approximate because that is what Atlassian guarantees โ it is an index
|
|
207
|
+
* estimate, not a scan, and will disagree with a full page walk.
|
|
208
|
+
*/
|
|
209
|
+
approximateCount: (jql) => request("/rest/api/3/search/approximate-count", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
body: JSON.stringify({ jql })
|
|
212
|
+
})
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/adf.ts
|
|
217
|
+
var markUp = (text, marks) => (marks ?? []).reduce((acc, mark) => {
|
|
218
|
+
if (mark.type === "code") return `\`${acc}\``;
|
|
219
|
+
if (mark.type === "strong") return `**${acc}**`;
|
|
220
|
+
if (mark.type === "em") return `_${acc}_`;
|
|
221
|
+
if (mark.type === "strike") return `~~${acc}~~`;
|
|
222
|
+
if (mark.type === "link") return `[${acc}](${mark.attrs?.["href"] ?? ""})`;
|
|
223
|
+
return acc;
|
|
224
|
+
}, text);
|
|
225
|
+
var resolveMedia = () => void 0;
|
|
226
|
+
var children = (node, sep = "") => (node.content ?? []).map(render).join(sep);
|
|
227
|
+
var listItems = (node, bullet) => (node.content ?? []).map((item, i) => {
|
|
228
|
+
const body = children(item, "\n\n").trim();
|
|
229
|
+
const [first = "", ...rest] = body.split("\n");
|
|
230
|
+
const marker = bullet(i);
|
|
231
|
+
const indent = " ".repeat(marker.length);
|
|
232
|
+
return [
|
|
233
|
+
`${marker}${first}`,
|
|
234
|
+
...rest.map((l) => l ? `${indent}${l}` : l)
|
|
235
|
+
].join("\n");
|
|
236
|
+
}).join("\n");
|
|
237
|
+
var row = (node) => `| ${(node.content ?? []).map((cell) => children(cell, " ").trim().replace(/\n+/g, " ")).join(" | ")} |`;
|
|
238
|
+
var renderTable = (node) => {
|
|
239
|
+
const rows = node.content ?? [];
|
|
240
|
+
if (rows.length === 0) return "";
|
|
241
|
+
const isHeader = (r) => (r.content ?? []).some((c) => c.type === "tableHeader");
|
|
242
|
+
const [first] = rows;
|
|
243
|
+
const rendered = rows.map(row);
|
|
244
|
+
if (first && isHeader(first)) {
|
|
245
|
+
const columns = (first.content ?? []).length;
|
|
246
|
+
rendered.splice(1, 0, `|${" --- |".repeat(columns)}`);
|
|
247
|
+
}
|
|
248
|
+
return rendered.join("\n");
|
|
249
|
+
};
|
|
250
|
+
var render = (node) => {
|
|
251
|
+
switch (node.type) {
|
|
252
|
+
case "doc":
|
|
253
|
+
return children(node, "\n\n");
|
|
254
|
+
case "paragraph":
|
|
255
|
+
return children(node);
|
|
256
|
+
case "text":
|
|
257
|
+
return markUp(node.text ?? "", node.marks);
|
|
258
|
+
case "hardBreak":
|
|
259
|
+
return "\n";
|
|
260
|
+
case "heading":
|
|
261
|
+
return `${"#".repeat(Number(node.attrs?.["level"] ?? 1))} ${children(node)}`;
|
|
262
|
+
case "bulletList":
|
|
263
|
+
return listItems(node, () => "- ");
|
|
264
|
+
case "orderedList":
|
|
265
|
+
return listItems(
|
|
266
|
+
node,
|
|
267
|
+
(i) => `${Number(node.attrs?.["order"] ?? 1) + i}. `
|
|
268
|
+
);
|
|
269
|
+
case "codeBlock":
|
|
270
|
+
return `\`\`\`${node.attrs?.["language"] ?? ""}
|
|
271
|
+
${children(node)}
|
|
272
|
+
\`\`\``;
|
|
273
|
+
case "blockquote":
|
|
274
|
+
return children(node, "\n\n").split("\n").map((l) => `> ${l}`).join("\n");
|
|
275
|
+
case "panel":
|
|
276
|
+
return `> [!${String(node.attrs?.["panelType"] ?? "note").toUpperCase()}]
|
|
277
|
+
${children(
|
|
278
|
+
node,
|
|
279
|
+
"\n\n"
|
|
280
|
+
).split("\n").map((l) => `> ${l}`).join("\n")}`;
|
|
281
|
+
case "rule":
|
|
282
|
+
return "---";
|
|
283
|
+
case "table":
|
|
284
|
+
return renderTable(node);
|
|
285
|
+
case "mediaSingle":
|
|
286
|
+
case "mediaGroup":
|
|
287
|
+
return children(node, "\n");
|
|
288
|
+
case "media": {
|
|
289
|
+
const alt = node.attrs?.["alt"];
|
|
290
|
+
if (typeof alt === "string" && alt) return `[attachment: ${alt}]`;
|
|
291
|
+
const id = String(node.attrs?.["id"] ?? "");
|
|
292
|
+
const name = resolveMedia(id);
|
|
293
|
+
return name ? `[attachment: ${name}]` : `[attachment: ${id || "unknown"}]`;
|
|
294
|
+
}
|
|
295
|
+
case "mention": {
|
|
296
|
+
const label = String(node.attrs?.["text"] ?? node.attrs?.["id"] ?? "");
|
|
297
|
+
return label.startsWith("@") ? label : `@${label}`;
|
|
298
|
+
}
|
|
299
|
+
case "emoji":
|
|
300
|
+
return String(node.attrs?.["text"] ?? node.attrs?.["shortName"] ?? "");
|
|
301
|
+
case "date":
|
|
302
|
+
return String(node.attrs?.["timestamp"] ?? "");
|
|
303
|
+
case "status":
|
|
304
|
+
return `[${String(node.attrs?.["text"] ?? "").toUpperCase()}]`;
|
|
305
|
+
case "inlineCard":
|
|
306
|
+
return String(node.attrs?.["url"] ?? "");
|
|
307
|
+
default:
|
|
308
|
+
return children(node, "\n\n");
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var adfToMarkdown = (doc, media) => {
|
|
312
|
+
if (typeof doc === "string") return doc;
|
|
313
|
+
if (!doc || typeof doc !== "object") return "";
|
|
314
|
+
const previous = resolveMedia;
|
|
315
|
+
resolveMedia = media ?? (() => void 0);
|
|
316
|
+
try {
|
|
317
|
+
return render(doc).replace(/\n{3,}/g, "\n\n").trim();
|
|
318
|
+
} finally {
|
|
319
|
+
resolveMedia = previous;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var inline = (text) => {
|
|
323
|
+
const pattern = /(`[^`]+`)|(\[[^\]]+\]\([^)]+\))/g;
|
|
324
|
+
const nodes = [];
|
|
325
|
+
let cursor = 0;
|
|
326
|
+
for (const match of text.matchAll(pattern)) {
|
|
327
|
+
const at = match.index;
|
|
328
|
+
if (at > cursor)
|
|
329
|
+
nodes.push({ type: "text", text: text.slice(cursor, at) });
|
|
330
|
+
const token = match[0];
|
|
331
|
+
if (token.startsWith("`")) {
|
|
332
|
+
nodes.push({
|
|
333
|
+
type: "text",
|
|
334
|
+
text: token.slice(1, -1),
|
|
335
|
+
marks: [{ type: "code" }]
|
|
336
|
+
});
|
|
337
|
+
} else {
|
|
338
|
+
const [, label = "", href = ""] = token.match(/\[([^\]]+)\]\(([^)]+)\)/) ?? [];
|
|
339
|
+
nodes.push({
|
|
340
|
+
type: "text",
|
|
341
|
+
text: label,
|
|
342
|
+
marks: [{ type: "link", attrs: { href } }]
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
cursor = at + token.length;
|
|
346
|
+
}
|
|
347
|
+
if (cursor < text.length) nodes.push({ type: "text", text: text.slice(cursor) });
|
|
348
|
+
return nodes.length > 0 ? nodes : [{ type: "text", text }];
|
|
349
|
+
};
|
|
350
|
+
var blockToAdf = (block) => {
|
|
351
|
+
const fence = block.match(/^```(\w*)\n([\s\S]*?)\n?```$/);
|
|
352
|
+
if (fence)
|
|
353
|
+
return {
|
|
354
|
+
type: "codeBlock",
|
|
355
|
+
...fence[1] ? { attrs: { language: fence[1] } } : {},
|
|
356
|
+
content: [{ type: "text", text: fence[2] ?? "" }]
|
|
357
|
+
};
|
|
358
|
+
const heading = block.match(/^(#{1,6})\s+(.*)$/);
|
|
359
|
+
if (heading)
|
|
360
|
+
return {
|
|
361
|
+
type: "heading",
|
|
362
|
+
attrs: { level: heading[1]?.length ?? 1 },
|
|
363
|
+
content: inline(heading[2] ?? "")
|
|
364
|
+
};
|
|
365
|
+
const lines = block.split("\n");
|
|
366
|
+
if (lines.every((l) => /^\s*[-*]\s+/.test(l)))
|
|
367
|
+
return {
|
|
368
|
+
type: "bulletList",
|
|
369
|
+
content: lines.map((l) => ({
|
|
370
|
+
type: "listItem",
|
|
371
|
+
content: [
|
|
372
|
+
{ type: "paragraph", content: inline(l.replace(/^\s*[-*]\s+/, "")) }
|
|
373
|
+
]
|
|
374
|
+
}))
|
|
375
|
+
};
|
|
376
|
+
if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l)))
|
|
377
|
+
return {
|
|
378
|
+
type: "orderedList",
|
|
379
|
+
content: lines.map((l) => ({
|
|
380
|
+
type: "listItem",
|
|
381
|
+
content: [
|
|
382
|
+
{ type: "paragraph", content: inline(l.replace(/^\s*\d+[.)]\s+/, "")) }
|
|
383
|
+
]
|
|
384
|
+
}))
|
|
385
|
+
};
|
|
386
|
+
return { type: "paragraph", content: inline(block) };
|
|
387
|
+
};
|
|
388
|
+
var markdownToAdf = (text) => ({
|
|
389
|
+
type: "doc",
|
|
390
|
+
version: 1,
|
|
391
|
+
content: text.replace(/\r\n/g, "\n").split(/\n{2,}/).map((b) => b.trim()).filter(Boolean).map(blockToAdf)
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// src/attachments.ts
|
|
395
|
+
var mediaRefs = (node) => {
|
|
396
|
+
if (!node || typeof node !== "object") return [];
|
|
397
|
+
const n = node;
|
|
398
|
+
const here = n.type === "media" ? [
|
|
399
|
+
{
|
|
400
|
+
...typeof n.attrs?.["id"] === "string" ? { id: n.attrs["id"] } : {},
|
|
401
|
+
...typeof n.attrs?.["alt"] === "string" ? { filename: n.attrs["alt"] } : {}
|
|
402
|
+
}
|
|
403
|
+
] : [];
|
|
404
|
+
return [...here, ...(n.content ?? []).flatMap(mediaRefs)];
|
|
405
|
+
};
|
|
406
|
+
var locateAttachments = (issue) => {
|
|
407
|
+
const attachments = issue.fields["attachment"] ?? [];
|
|
408
|
+
const origins = /* @__PURE__ */ new Map();
|
|
409
|
+
const note = (ref, origin) => {
|
|
410
|
+
const match = attachments.find(
|
|
411
|
+
(a) => ref.filename !== void 0 && a.filename === ref.filename || ref.id !== void 0 && a.id === ref.id
|
|
412
|
+
);
|
|
413
|
+
if (!match) return;
|
|
414
|
+
origins.set(match.id, [...origins.get(match.id) ?? [], origin]);
|
|
415
|
+
};
|
|
416
|
+
for (const ref of mediaRefs(issue.fields.description))
|
|
417
|
+
note(ref, { kind: "description" });
|
|
418
|
+
for (const comment of issue.fields.comment?.comments ?? [])
|
|
419
|
+
for (const ref of mediaRefs(comment.body))
|
|
420
|
+
note(ref, {
|
|
421
|
+
kind: "comment",
|
|
422
|
+
commentId: comment.id,
|
|
423
|
+
author: comment.author?.displayName
|
|
424
|
+
});
|
|
425
|
+
return attachments.map((a) => ({
|
|
426
|
+
...a,
|
|
427
|
+
origins: origins.get(a.id) ?? [{ kind: "issue" }]
|
|
428
|
+
}));
|
|
429
|
+
};
|
|
430
|
+
var TEXTUAL = /^(text\/|application\/(json|xml|x-yaml|yaml|javascript|sql|x-sh))/;
|
|
431
|
+
var TEXTUAL_EXTENSIONS = /\.(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;
|
|
432
|
+
var isTextual = (attachment) => TEXTUAL.test(attachment.mimeType) || TEXTUAL_EXTENSIONS.test(attachment.filename);
|
|
433
|
+
var downloadAttachment = async (client, id, fetchImpl = globalThis.fetch) => {
|
|
434
|
+
const res = await client.request(
|
|
435
|
+
`/rest/api/3/attachment/content/${encodeURIComponent(id)}`,
|
|
436
|
+
{ redirect: "manual", raw: true }
|
|
437
|
+
);
|
|
438
|
+
const location = res.headers.get("location");
|
|
439
|
+
const final = res.status >= 300 && res.status < 400 && location ? await fetchImpl(location) : res;
|
|
440
|
+
if (!final.ok) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`could not download attachment ${id}: ${final.status} ${final.statusText}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
bytes: new Uint8Array(await final.arrayBuffer()),
|
|
447
|
+
mimeType: final.headers.get("content-type")
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
export {
|
|
451
|
+
adfToMarkdown,
|
|
452
|
+
createJiraClient,
|
|
453
|
+
downloadAttachment,
|
|
454
|
+
isJiraApiError,
|
|
455
|
+
isTextual,
|
|
456
|
+
jiraApiError,
|
|
457
|
+
locateAttachments,
|
|
458
|
+
markdownToAdf,
|
|
459
|
+
normalizeBaseUrl
|
|
460
|
+
};
|
|
461
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/adf.ts","../src/attachments.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 \"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 transitionIssue: (key: string, transitionId: string): Promise<void> =>\n request(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {\n method: \"POST\",\n body: JSON.stringify({ transition: { id: transitionId } }),\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"],"mappings":";AA8CO,IAAM,eAAe,CAC1B,QACA,QACA,KACA,SAEA,OAAO;AAAA,EACL,IAAI,MAAM,YAAY,MAAM,IAAI,MAAM,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,EAAE;AAAA,EAClE,EAAE,MAAM,gBAAyB,QAAQ,QAAQ,KAAK,KAAK;AAC7D;AAEK,IAAM,iBAAiB,CAAC,MAC7B,aAAa,SAAS,EAAE,SAAS;AAEnC,IAAM,WAAW,CAAC,GAAW,MAAM,QACjC,EAAE,SAAS,MAAM,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,WAAM;AAIpC,IAAM,mBAAmB,CAAC,QAAwB;AACvD,QAAM,UAAU,IAAI,KAAK,EAAE,QAAQ,QAAQ,EAAE;AAC7C,SAAO,eAAe,KAAK,OAAO,IAAI,UAAU,WAAW,OAAO;AACpE;AAEA,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,mBAAmB,CAAC,YAA+B;AAC9D,QAAM,UAAU,QAAQ,SAAS,WAAW;AAC5C,QAAM,UAAU,iBAAiB,QAAQ,OAAO;AAChD,QAAM,eAAe,QAAQ,gBAAgB,CAAC;AAC9C,QAAM,aAAa,SAAS,OAAO,KAAK,GAAG,QAAQ,KAAK,IAAI,QAAQ,KAAK,EAAE,EAAE,SAAS,QAAQ,CAAC;AAE/F,QAAM,UAAU,OACd,MACA,OAAwC,CAAC,MAC1B;AACf,UAAM,MAAM,KAAK,WAAW,MAAM,IAC9B,OACA,GAAG,OAAO,GAAG,KAAK,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,IAAI;AAEvD,UAAM,MAAM,MAAM,QAAQ,KAAK;AAAA,MAC7B,GAAG;AAAA,MACH,SAAS;AAAA,QACP,eAAe;AAAA,QACf,gBAAgB;AAAA,QAChB,QAAQ;AAAA;AAAA;AAAA,QAGR,mBAAmB;AAAA,QACnB,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAID,QAAI,KAAK,IAAK,QAAO;AAErB,QAAI,CAAC,IAAI,IAAI;AACX,YAAM;AAAA,QACJ,IAAI;AAAA,QACJ,KAAK,UAAU;AAAA,QACf;AAAA,QACA,MAAM,IAAI,KAAK;AAAA,MACjB;AAAA,IACF;AACA,QAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAGA,QAAM,YAAY,CAAC,UAA+B;AAAA,IAChD,GAAG,oBAAI,IAAI;AAAA,MACT,GAAI,SAAS;AAAA,MACb,GAAG,aAAa,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,MAC/B,GAAI,QAAQ,cAAc,CAAC,QAAQ,WAAW,IAAI,CAAC;AAAA,IACrD,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,CACjB,KACA,OAAsB,CAAC,MAEvB,QAAwB,0BAA0B;AAAA,IAChD,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,QAAQ,UAAU,KAAK,MAAM;AAAA,MAC7B,YAAY,KAAK,cAAc;AAAA,MAC/B,GAAI,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,IAAI,CAAC;AAAA,MAClE,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,IAC/C,CAAC;AAAA,EACH,CAAC;AAQH,QAAM,eAAe,OACnB,KACA,OAA2C,CAAC,MACnB;AACzB,UAAM,QAAQ,KAAK,SAAS;AAC5B,UAAM,SAAsB,CAAC;AAC7B,UAAM,aAAa,oBAAI,IAAY;AACnC,QAAI,QAAQ,KAAK;AAEjB,WAAO,OAAO,SAAS,OAAO;AAC5B,YAAM,OAAO,MAAM,WAAW,KAAK;AAAA,QACjC,GAAG;AAAA,QACH,eAAe;AAAA,QACf,YAAY,KAAK,IAAI,KAAK,QAAQ,OAAO,MAAM;AAAA,MACjD,CAAC;AACD,UAAI,KAAK,OAAO,WAAW,EAAG;AAC9B,aAAO,KAAK,GAAG,KAAK,MAAM;AAE1B,YAAM,OAAO,KAAK;AAClB,UAAI,CAAC,QAAQ,KAAK,UAAU,WAAW,IAAI,IAAI,EAAG;AAClD,iBAAW,IAAI,IAAI;AACnB,cAAQ;AAAA,IACV;AAEA,WAAO,OAAO,MAAM,GAAG,KAAK;AAAA,EAC9B;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa,QAAQ;AAAA,IAErB;AAAA,IACA;AAAA,IAEA,UAAU,CAAC,QACT;AAAA,MACE,qBAAqB,mBAAmB,GAAG,CAAC,WAAW,UAAU;AAAA,QAC/D,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,IACd;AAAA,IAEF,gBAAgB,CAAC,QACf,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,cAAc;AAAA,IAEpE,iBAAiB,CAAC,KAAa,iBAC7B,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,gBAAgB;AAAA,MAClE,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,YAAY,EAAE,IAAI,aAAa,EAAE,CAAC;AAAA,IAC3D,CAAC;AAAA,IAEH,YAAY,CAAC,KAAa,SACxB,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,YAAY;AAAA,MAC9D,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,KAAK,CAAC;AAAA,IAC/B,CAAC;AAAA,IAEH,OAAO,MAAyB,QAAQ,oBAAoB;AAAA,IAE5D,WAAW,MAA4B,QAAQ,mBAAmB;AAAA,IAElE,aAAa,MACX,QAAQ,wCAAwC;AAAA,IAElD,WAAW,MACT,QAAQ,uBAAuB;AAAA,IAEjC,YAAY,CACV,SACA,UAEA;AAAA,MACE,yBAAyB,OAAO,UAAU,QAAQ,UAAU,KAAK,KAAK,EAAE;AAAA,IAC1E;AAAA;AAAA,IAGF,aAAa,CAAC,WACZ,QAAQ,qBAAqB;AAAA,MAC3B,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,IACjC,CAAC;AAAA,IAEH,aAAa,CACX,KACA,WAEA,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,IACjC,CAAC;AAAA,IAEH,aAAa,CAAC,KAAa,iBAAiB,UAC1C;AAAA,MACE,qBAAqB,mBAAmB,GAAG,CAAC,mBAAmB,cAAc;AAAA,MAC7E,EAAE,QAAQ,SAAS;AAAA,IACrB;AAAA,IAEF,aAAa,CAAC,KAAa,cACzB,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,aAAa;AAAA,MAC/D,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;AAAA,IACpC,CAAC;AAAA,IAEH,aAAa,CAAC,QACZ,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,UAAU;AAAA,IAEhE,eAAe,CAAC,KAAa,cAC3B;AAAA,MACE,qBAAqB,mBAAmB,GAAG,CAAC,YAAY,mBAAmB,SAAS,CAAC;AAAA,MACrF,EAAE,QAAQ,SAAS;AAAA,IACrB;AAAA,IAEF,aAAa,CAAC,QACZ,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,WAAW;AAAA,IAEjE,YAAY,CAAC,KAAa,cACxB,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,aAAa;AAAA,MAC/D,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,SAAS;AAAA,IAChC,CAAC;AAAA,IAEH,eAAe,CAAC,KAAa,cAC3B;AAAA,MACE,qBAAqB,mBAAmB,GAAG,CAAC,uBAAuB,mBAAmB,SAAS,CAAC;AAAA,MAChG,EAAE,QAAQ,SAAS;AAAA,IACrB;AAAA,IAEF,aAAa,CAAC,QACZ,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,UAAU;AAAA,IAEhE,YAAY,CACV,KACA,SAEA,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,YAAY;AAAA,MAC9D,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AAAA,IAEH,cAAc,CAAC,QACb,QAAQ,qBAAqB,mBAAmB,GAAG,CAAC,YAAY;AAAA,IAElE,mBAAmB,MACjB,QAAQ,2BAA2B;AAAA,IAErC,YAAY,CACV,MACA,WACA,eAEA,QAAQ,yBAAyB;AAAA,MAC/B,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,QACnB,MAAM,EAAE,MAAM,KAAK;AAAA,QACnB,aAAa,EAAE,KAAK,UAAU;AAAA,QAC9B,cAAc,EAAE,KAAK,WAAW;AAAA,MAClC,CAAC;AAAA,IACH,CAAC;AAAA;AAAA,IAGH,YAAY,CAAC,QACX;AAAA,MACE,uBAAuB,mBAAmB,GAAG,CAAC;AAAA,IAChD;AAAA,IAEF,oBAAoB,CAAC,QACnB,QAAQ,uBAAuB,mBAAmB,GAAG,CAAC,WAAW;AAAA,IAEnE,sBAAsB,CAAC,QACrB,QAAQ,uBAAuB,mBAAmB,GAAG,CAAC,aAAa;AAAA,IAErE,oBAAoB,CAAC,QACnB,QAAQ,uBAAuB,mBAAmB,GAAG,CAAC,WAAW;AAAA;AAAA,IAGnE,UAAU,CAAC,OACT,QAAQ,yBAAyB,EAAE,EAAE;AAAA,IAEvC,gBAAgB,CACd,IACA,QAEA;AAAA,MACE,yBAAyB,EAAE,SAAS,MAAM,QAAQ,mBAAmB,GAAG,CAAC,KAAK,EAAE;AAAA,IAClF;AAAA,IAEF,YAAY,CAAC,OACX,QAAQ,yBAAyB,EAAE,UAAU;AAAA,IAE/C,WAAW,CAAC,OACV,QAAQ,0BAA0B,EAAE,EAAE;AAAA,IAExC,iBAAiB,CAAC,OAChB,QAAQ,0BAA0B,EAAE,QAAQ;AAAA,IAE9C,eAAe,CAAC,OACd,QAAQ,yBAAyB,EAAE,OAAO;AAAA,IAE5C,eAAe,CAAC,OACd,QAAQ,wBAAwB,mBAAmB,EAAE,CAAC,QAAQ;AAAA;AAAA,IAGhE,aAAa,CAAC,OAAe,aAAa,OACxC;AAAA,MACE,iCAAiC,mBAAmB,KAAK,CAAC,eAAe,UAAU;AAAA,IACrF;AAAA,IAEF,uBAAuB,CACrB,OACA,YACA,aAAa,OAEb;AAAA,MACE,4CAA4C,mBAAmB,KAAK,CAAC,YAAY,mBAAmB,UAAU,CAAC,eAAe,UAAU;AAAA,IAC1I;AAAA;AAAA,IAGF,eAAe,MACb,QAAQ,uBAAuB;AAAA,IAEjC,eAAe,MAA4B,QAAQ,sBAAsB;AAAA,IAEzE,gBAAgB,MACd,QAAQ,wBAAwB;AAAA,IAElC,aAAa,MAA4B,QAAQ,oBAAoB;AAAA,IAErE,WAAW,MACT,QAAQ,mCAAmC;AAAA,IAE7C,YAAY,MACV,QAAQ,oDAAoD;AAAA,IAE9D,eAAe,MACb,QAAQ,uBAAuB;AAAA,IAEjC,eAAe,MACb,QAAQ,wBAAwB;AAAA,IAElC,kBAAkB,CAAC,eACjB;AAAA,MACE,4BAA4B,aAAa,eAAe,mBAAmB,UAAU,CAAC,KAAK,EAAE;AAAA,IAC/F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOF,kBAAkB,CAAC,QACjB,QAAQ,wCAAwC;AAAA,MAC9C,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,IAC9B,CAAC;AAAA,EACL;AACF;;;ACtZA,IAAM,SAAS,CAAC,MAAc,WAC3B,SAAS,CAAC,GAAG,OAAO,CAAC,KAAK,SAAS;AAClC,MAAI,KAAK,SAAS,OAAQ,QAAO,KAAK,GAAG;AACzC,MAAI,KAAK,SAAS,SAAU,QAAO,KAAK,GAAG;AAC3C,MAAI,KAAK,SAAS,KAAM,QAAO,IAAI,GAAG;AACtC,MAAI,KAAK,SAAS,SAAU,QAAO,KAAK,GAAG;AAC3C,MAAI,KAAK,SAAS,OAAQ,QAAO,IAAI,GAAG,KAAK,KAAK,QAAQ,MAAM,KAAK,EAAE;AACvE,SAAO;AACT,GAAG,IAAI;AAIT,IAAI,eAA8B,MAAM;AAExC,IAAM,WAAW,CAAC,MAAe,MAAM,QACpC,KAAK,WAAW,CAAC,GAAG,IAAI,MAAM,EAAE,KAAK,GAAG;AAE3C,IAAM,YAAY,CAAC,MAAe,YAC/B,KAAK,WAAW,CAAC,GACf,IAAI,CAAC,MAAM,MAAM;AAChB,QAAM,OAAO,SAAS,MAAM,MAAM,EAAE,KAAK;AACzC,QAAM,CAAC,QAAQ,IAAI,GAAG,IAAI,IAAI,KAAK,MAAM,IAAI;AAC7C,QAAM,SAAS,OAAO,CAAC;AACvB,QAAM,SAAS,IAAI,OAAO,OAAO,MAAM;AACvC,SAAO;AAAA,IACL,GAAG,MAAM,GAAG,KAAK;AAAA,IACjB,GAAG,KAAK,IAAI,CAAC,MAAO,IAAI,GAAG,MAAM,GAAG,CAAC,KAAK,CAAE;AAAA,EAC9C,EAAE,KAAK,IAAI;AACb,CAAC,EACA,KAAK,IAAI;AAGd,IAAM,MAAM,CAAC,SACX,MAAM,KAAK,WAAW,CAAC,GAAG,IAAI,CAAC,SAAS,SAAS,MAAM,GAAG,EAAE,KAAK,EAAE,QAAQ,QAAQ,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC;AAEtG,IAAM,cAAc,CAAC,SAA0B;AAC7C,QAAM,OAAO,KAAK,WAAW,CAAC;AAC9B,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,WAAW,CAAC,OACf,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,SAAS,aAAa;AACxD,QAAM,CAAC,KAAK,IAAI;AAChB,QAAM,WAAW,KAAK,IAAI,GAAG;AAC7B,MAAI,SAAS,SAAS,KAAK,GAAG;AAC5B,UAAM,WAAW,MAAM,WAAW,CAAC,GAAG;AACtC,aAAS,OAAO,GAAG,GAAG,IAAI,SAAS,OAAO,OAAO,CAAC,EAAE;AAAA,EACtD;AACA,SAAO,SAAS,KAAK,IAAI;AAC3B;AAEA,IAAM,SAAS,CAAC,SAA0B;AACxC,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,SAAS,MAAM,MAAM;AAAA,IAC9B,KAAK;AACH,aAAO,SAAS,IAAI;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,KAAK;AAAA,IAC3C,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,GAAG,IAAI,OAAO,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,SAAS,IAAI,CAAC;AAAA,IAC5E,KAAK;AACH,aAAO,UAAU,MAAM,MAAM,IAAI;AAAA,IACnC,KAAK;AACH,aAAO;AAAA,QACL;AAAA,QACA,CAAC,MAAM,GAAG,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC,IAAI,CAAC;AAAA,MAClD;AAAA,IACF,KAAK;AACH,aAAO,SAAS,KAAK,QAAQ,UAAU,KAAK,EAAE;AAAA,EAAK,SAAS,IAAI,CAAC;AAAA;AAAA,IACnE,KAAK;AACH,aAAO,SAAS,MAAM,MAAM,EACzB,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EACnB,KAAK,IAAI;AAAA,IACd,KAAK;AACH,aAAO,OAAO,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM,EAAE,YAAY,CAAC;AAAA,EAAM;AAAA,QAC3E;AAAA,QACA;AAAA,MACF,EACG,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EACnB,KAAK,IAAI,CAAC;AAAA,IACf,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,YAAY,IAAI;AAAA,IACzB,KAAK;AAAA,IACL,KAAK;AACH,aAAO,SAAS,MAAM,IAAI;AAAA,IAC5B,KAAK,SAAS;AAGZ,YAAM,MAAM,KAAK,QAAQ,KAAK;AAC9B,UAAI,OAAO,QAAQ,YAAY,IAAK,QAAO,gBAAgB,GAAG;AAC9D,YAAM,KAAK,OAAO,KAAK,QAAQ,IAAI,KAAK,EAAE;AAC1C,YAAM,OAAO,aAAa,EAAE;AAC5B,aAAO,OAAO,gBAAgB,IAAI,MAAM,gBAAgB,MAAM,SAAS;AAAA,IACzE;AAAA,IACA,KAAK,WAAW;AAGd,YAAM,QAAQ,OAAO,KAAK,QAAQ,MAAM,KAAK,KAAK,QAAQ,IAAI,KAAK,EAAE;AACrE,aAAO,MAAM,WAAW,GAAG,IAAI,QAAQ,IAAI,KAAK;AAAA,IAClD;AAAA,IACA,KAAK;AACH,aAAO,OAAO,KAAK,QAAQ,MAAM,KAAK,KAAK,QAAQ,WAAW,KAAK,EAAE;AAAA,IACvE,KAAK;AACH,aAAO,OAAO,KAAK,QAAQ,WAAW,KAAK,EAAE;AAAA,IAC/C,KAAK;AACH,aAAO,IAAI,OAAO,KAAK,QAAQ,MAAM,KAAK,EAAE,EAAE,YAAY,CAAC;AAAA,IAC7D,KAAK;AACH,aAAO,OAAO,KAAK,QAAQ,KAAK,KAAK,EAAE;AAAA,IACzC;AACE,aAAO,SAAS,MAAM,MAAM;AAAA,EAChC;AACF;AAOO,IAAM,gBAAgB,CAC3B,KACA,UACW;AACX,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,WAAW;AACjB,iBAAe,UAAU,MAAM;AAC/B,MAAI;AACF,WAAO,OAAO,GAAc,EACzB,QAAQ,WAAW,MAAM,EACzB,KAAK;AAAA,EACV,UAAE;AACA,mBAAe;AAAA,EACjB;AACF;AAIA,IAAM,SAAS,CAAC,SAA4B;AAG1C,QAAM,UAAU;AAChB,QAAM,QAAmB,CAAC;AAC1B,MAAI,SAAS;AAEb,aAAW,SAAS,KAAK,SAAS,OAAO,GAAG;AAC1C,UAAM,KAAK,MAAM;AACjB,QAAI,KAAK;AACP,YAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,MAAM,QAAQ,EAAE,EAAE,CAAC;AAE3D,UAAM,QAAQ,MAAM,CAAC;AACrB,QAAI,MAAM,WAAW,GAAG,GAAG;AACzB,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,MAAM,MAAM,MAAM,GAAG,EAAE;AAAA,QACvB,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;AAAA,MAC1B,CAAC;AAAA,IACH,OAAO;AACL,YAAM,CAAC,EAAE,QAAQ,IAAI,OAAO,EAAE,IAC5B,MAAM,MAAM,yBAAyB,KAAK,CAAC;AAC7C,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO,CAAC,EAAE,MAAM,QAAQ,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,MAC3C,CAAC;AAAA,IACH;AACA,aAAS,KAAK,MAAM;AAAA,EACtB;AAEA,MAAI,SAAS,KAAK,OAAQ,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,EAAE,CAAC;AAC/E,SAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC;AAC3D;AAEA,IAAM,aAAa,CAAC,UAA2B;AAC7C,QAAM,QAAQ,MAAM,MAAM,8BAA8B;AACxD,MAAI;AACF,WAAO;AAAA,MACL,MAAM;AAAA,MACN,GAAI,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC;AAAA,MACpD,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,MAAM,CAAC,KAAK,GAAG,CAAC;AAAA,IAClD;AAEF,QAAM,UAAU,MAAM,MAAM,mBAAmB;AAC/C,MAAI;AACF,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO,EAAE,OAAO,QAAQ,CAAC,GAAG,UAAU,EAAE;AAAA,MACxC,SAAS,OAAO,QAAQ,CAAC,KAAK,EAAE;AAAA,IAClC;AAEF,QAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,MAAI,MAAM,MAAM,CAAC,MAAM,cAAc,KAAK,CAAC,CAAC;AAC1C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,MAAM,IAAI,CAAC,OAAO;AAAA,QACzB,MAAM;AAAA,QACN,SAAS;AAAA,UACP,EAAE,MAAM,aAAa,SAAS,OAAO,EAAE,QAAQ,eAAe,EAAE,CAAC,EAAE;AAAA,QACrE;AAAA,MACF,EAAE;AAAA,IACJ;AAEF,MAAI,MAAM,MAAM,CAAC,MAAM,iBAAiB,KAAK,CAAC,CAAC;AAC7C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,MAAM,IAAI,CAAC,OAAO;AAAA,QACzB,MAAM;AAAA,QACN,SAAS;AAAA,UACP,EAAE,MAAM,aAAa,SAAS,OAAO,EAAE,QAAQ,kBAAkB,EAAE,CAAC,EAAE;AAAA,QACxE;AAAA,MACF,EAAE;AAAA,IACJ;AAEF,SAAO,EAAE,MAAM,aAAa,SAAS,OAAO,KAAK,EAAE;AACrD;AAQO,IAAM,gBAAgB,CAAC,UAA0B;AAAA,EACtD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS,KACN,QAAQ,SAAS,IAAI,EACrB,MAAM,QAAQ,EACd,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO,EACd,IAAI,UAAU;AACnB;;;AChNA,IAAM,YAAY,CAAC,SAA8B;AAC/C,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO,CAAC;AAC/C,QAAM,IAAI;AACV,QAAM,OACJ,EAAE,SAAS,UACP;AAAA,IACE;AAAA,MACE,GAAI,OAAO,EAAE,QAAQ,IAAI,MAAM,WAC3B,EAAE,IAAI,EAAE,MAAM,IAAI,EAAY,IAC9B,CAAC;AAAA,MACL,GAAI,OAAO,EAAE,QAAQ,KAAK,MAAM,WAC5B,EAAE,UAAU,EAAE,MAAM,KAAK,EAAY,IACrC,CAAC;AAAA,IACP;AAAA,EACF,IACA,CAAC;AACP,SAAO,CAAC,GAAG,MAAM,IAAI,EAAE,WAAW,CAAC,GAAG,QAAQ,SAAS,CAAC;AAC1D;AAQO,IAAM,oBAAoB,CAAC,UAA0C;AAC1E,QAAM,cAAe,MAAM,OAAO,YAAY,KAA0B,CAAC;AACzE,QAAM,UAAU,oBAAI,IAAgC;AAEpD,QAAM,OAAO,CAAC,KAAe,WAAmC;AAC9D,UAAM,QAAQ,YAAY;AAAA,MACxB,CAAC,MACE,IAAI,aAAa,UAAa,EAAE,aAAa,IAAI,YACjD,IAAI,OAAO,UAAa,EAAE,OAAO,IAAI;AAAA,IAC1C;AACA,QAAI,CAAC,MAAO;AACZ,YAAQ,IAAI,MAAM,IAAI,CAAC,GAAI,QAAQ,IAAI,MAAM,EAAE,KAAK,CAAC,GAAI,MAAM,CAAC;AAAA,EAClE;AAEA,aAAW,OAAO,UAAU,MAAM,OAAO,WAAW;AAClD,SAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAEnC,aAAW,WAAW,MAAM,OAAO,SAAS,YAAY,CAAC;AACvD,eAAW,OAAO,UAAU,QAAQ,IAAI;AACtC,WAAK,KAAK;AAAA,QACR,MAAM;AAAA,QACN,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ,QAAQ;AAAA,MAC1B,CAAC;AAEL,SAAO,YAAY,IAAI,CAAC,OAAO;AAAA,IAC7B,GAAG;AAAA,IACH,SAAS,QAAQ,IAAI,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,QAAiB,CAAC;AAAA,EAC3D,EAAE;AACJ;AAEA,IAAM,UACJ;AAGF,IAAM,qBACJ;AAEK,IAAM,YAAY,CAAC,eACxB,QAAQ,KAAK,WAAW,QAAQ,KAChC,mBAAmB,KAAK,WAAW,QAAQ;AAQtC,IAAM,qBAAqB,OAChC,QACA,IACA,YAAqC,WAAW,UACY;AAC5D,QAAM,MAAM,MAAM,OAAO;AAAA,IACvB,kCAAkC,mBAAmB,EAAE,CAAC;AAAA,IACxD,EAAE,UAAU,UAAU,KAAK,KAAK;AAAA,EAClC;AAEA,QAAM,WAAW,IAAI,QAAQ,IAAI,UAAU;AAC3C,QAAM,QACJ,IAAI,UAAU,OAAO,IAAI,SAAS,OAAO,WACrC,MAAM,UAAU,QAAQ,IACxB;AAEN,MAAI,CAAC,MAAM,IAAI;AACb,UAAM,IAAI;AAAA,MACR,iCAAiC,EAAE,KAAK,MAAM,MAAM,IAAI,MAAM,UAAU;AAAA,IAC1E;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,IAAI,WAAW,MAAM,MAAM,YAAY,CAAC;AAAA,IAC/C,UAAU,MAAM,QAAQ,IAAI,cAAc;AAAA,EAC5C;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kud/jira",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Headless Jira client โ issues, comments, attachments, ADF conversion, agile boards and instance metadata, with no environment or process dependencies",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"build:watch": "tsup --watch",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"jira",
|
|
27
|
+
"atlassian",
|
|
28
|
+
"api",
|
|
29
|
+
"client",
|
|
30
|
+
"adf",
|
|
31
|
+
"jql"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://www.npmjs.com/package/@kud/jira",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/kud/jira.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": "https://github.com/kud/jira/issues",
|
|
39
|
+
"author": "kud",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.0.0"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "26.2.0",
|
|
50
|
+
"tsup": "8.5.1",
|
|
51
|
+
"typescript": "5.9.3",
|
|
52
|
+
"vitest": "4.1.10"
|
|
53
|
+
}
|
|
54
|
+
}
|