@kud/jira 0.2.0 → 0.3.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 CHANGED
@@ -189,6 +189,10 @@ Both stop at Markdown rather than emitting ANSI or HTML — rendering is the cal
189
189
 
190
190
  Turns a bare host (`myorg.atlassian.net`) into a full `https://` URL. A no-op on an already-complete URL.
191
191
 
192
+ ### `loadConfig(env?, path?)`
193
+
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.
195
+
192
196
  ## 🔧 Development
193
197
 
194
198
  ```
package/dist/index.d.ts CHANGED
@@ -11,6 +11,17 @@ type JiraStatus = {
11
11
  name: string;
12
12
  };
13
13
  };
14
+ type JiraIssueParent = {
15
+ id: string;
16
+ key: string;
17
+ fields?: {
18
+ summary?: string;
19
+ status?: JiraStatus;
20
+ issuetype?: {
21
+ name: string;
22
+ };
23
+ };
24
+ };
14
25
  type JiraIssueFields = {
15
26
  summary?: string;
16
27
  status?: JiraStatus;
@@ -27,6 +38,7 @@ type JiraIssueFields = {
27
38
  name: string;
28
39
  };
29
40
  labels?: string[];
41
+ parent?: JiraIssueParent;
30
42
  created?: string;
31
43
  updated?: string;
32
44
  description?: unknown;
@@ -358,4 +370,41 @@ declare const downloadAttachment: (client: JiraClient, id: string, fetchImpl?: t
358
370
  mimeType: string | null;
359
371
  }>;
360
372
 
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 };
373
+ type CustomFieldRef = {
374
+ id: string;
375
+ label: string;
376
+ };
377
+ type FileConfig = {
378
+ baseUrl?: string;
379
+ email?: string;
380
+ customFields?: CustomFieldRef[];
381
+ sprintField?: string;
382
+ defaultProject?: string;
383
+ defaultBoard?: number;
384
+ };
385
+ type Config = FileConfig & {
386
+ baseUrl: string;
387
+ email: string;
388
+ token: string;
389
+ };
390
+ declare const configPath: () => string;
391
+ /**
392
+ * Credentials come from the environment and never from the config file: the
393
+ * file is meant to be readable, diffable and shareable across a team, and a
394
+ * token in it would leak the moment anyone pasted theirs into a gist.
395
+ */
396
+ declare const readFileConfig: (path?: string) => FileConfig;
397
+ /**
398
+ * Only the token is a secret, so only the token is env-only. The instance URL
399
+ * and your email are settings, and asking someone to export three variables to
400
+ * run a CLI is friction that buys nothing. Environment still wins where both
401
+ * are present, so a second instance needs one inline override rather than a
402
+ * second config file.
403
+ */
404
+ declare const loadConfig: (env?: NodeJS.ProcessEnv, path?: string) => {
405
+ config: Config;
406
+ } | {
407
+ missing: string[];
408
+ };
409
+
410
+ export { type AttachmentOrigin, type Config, type CustomFieldRef, type FileConfig, 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 JiraIssueParent, 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, configPath, createJiraClient, downloadAttachment, isJiraApiError, isTextual, jiraApiError, loadConfig, locateAttachments, markdownToAdf, normalizeBaseUrl, readFileConfig };
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ var DEFAULT_FIELDS = [
17
17
  "priority",
18
18
  "project",
19
19
  "labels",
20
+ "parent",
20
21
  "updated"
21
22
  ];
22
23
  var createJiraClient = (options) => {
@@ -454,15 +455,64 @@ var downloadAttachment = async (client, id, fetchImpl = globalThis.fetch) => {
454
455
  mimeType: final.headers.get("content-type")
455
456
  };
456
457
  };
458
+
459
+ // src/config.ts
460
+ import { readFileSync } from "fs";
461
+ import { homedir } from "os";
462
+ import { join } from "path";
463
+ var configPath = () => process.env["JIRA_CONFIG_FILE"] || join(
464
+ process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config"),
465
+ "jira",
466
+ "config.json"
467
+ );
468
+ var readFileConfig = (path = configPath()) => {
469
+ let raw;
470
+ try {
471
+ raw = readFileSync(path, "utf8");
472
+ } catch {
473
+ return {};
474
+ }
475
+ const parsed = JSON.parse(raw);
476
+ if (parsed.customFields && !Array.isArray(parsed.customFields))
477
+ throw new Error(`${path}: customFields must be an array of {id,label}`);
478
+ if ("token" in parsed || "apiToken" in parsed)
479
+ throw new Error(
480
+ `${path} contains a token. Tokens belong in ATLASSIAN_API_TOKEN, not in a file that gets shared and backed up \u2014 remove it.`
481
+ );
482
+ return parsed;
483
+ };
484
+ var loadConfig = (env = process.env, path = configPath()) => {
485
+ const file = readFileConfig(path);
486
+ const baseUrl = env["ATLASSIAN_BASE_URL"] || file.baseUrl;
487
+ const email = env["ATLASSIAN_USER_EMAIL"] || file.email;
488
+ const token = env["ATLASSIAN_API_TOKEN"];
489
+ const missing = [
490
+ ["ATLASSIAN_BASE_URL", baseUrl],
491
+ ["ATLASSIAN_USER_EMAIL", email],
492
+ ["ATLASSIAN_API_TOKEN", token]
493
+ ].filter(([, v]) => !v).map(([k]) => k);
494
+ if (missing.length > 0) return { missing };
495
+ return {
496
+ config: {
497
+ ...file,
498
+ baseUrl: normalizeBaseUrl(baseUrl),
499
+ email,
500
+ token
501
+ }
502
+ };
503
+ };
457
504
  export {
458
505
  adfToMarkdown,
506
+ configPath,
459
507
  createJiraClient,
460
508
  downloadAttachment,
461
509
  isJiraApiError,
462
510
  isTextual,
463
511
  jiraApiError,
512
+ loadConfig,
464
513
  locateAttachments,
465
514
  markdownToAdf,
466
- normalizeBaseUrl
515
+ normalizeBaseUrl,
516
+ readFileConfig
467
517
  };
468
518
  //# 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"],"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 // `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"],"mappings":"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","names":[]}
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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/jira",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {