@knpkv/jira-cli 1.1.2 → 1.2.1

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +23 -7
  3. package/dist/AttachmentService.d.ts +22 -0
  4. package/dist/AttachmentService.d.ts.map +1 -0
  5. package/dist/AttachmentService.js +67 -0
  6. package/dist/AttachmentService.js.map +1 -0
  7. package/dist/IssueService.d.ts +2 -20
  8. package/dist/IssueService.d.ts.map +1 -1
  9. package/dist/IssueService.js +47 -36
  10. package/dist/IssueService.js.map +1 -1
  11. package/dist/JiraAuth.d.ts +1 -1
  12. package/dist/JiraAuth.d.ts.map +1 -1
  13. package/dist/JiraAuth.js.map +1 -1
  14. package/dist/VersionService.d.ts.map +1 -1
  15. package/dist/VersionService.js +39 -45
  16. package/dist/VersionService.js.map +1 -1
  17. package/dist/bin.js +2 -2
  18. package/dist/bin.js.map +1 -1
  19. package/dist/commands/auth.d.ts +1 -1
  20. package/dist/commands/index.d.ts +1 -0
  21. package/dist/commands/index.d.ts.map +1 -1
  22. package/dist/commands/index.js +1 -0
  23. package/dist/commands/index.js.map +1 -1
  24. package/dist/commands/issue.d.ts.map +1 -1
  25. package/dist/commands/issue.js +2 -1
  26. package/dist/commands/issue.js.map +1 -1
  27. package/dist/commands/issueAttachment.d.ts +6 -0
  28. package/dist/commands/issueAttachment.d.ts.map +1 -0
  29. package/dist/commands/issueAttachment.js +88 -0
  30. package/dist/commands/issueAttachment.js.map +1 -0
  31. package/dist/commands/layers.d.ts +4 -3
  32. package/dist/commands/layers.d.ts.map +1 -1
  33. package/dist/commands/layers.js +12 -4
  34. package/dist/commands/layers.js.map +1 -1
  35. package/dist/commands/search.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/attachmentInsertion.d.ts +10 -0
  41. package/dist/internal/attachmentInsertion.d.ts.map +1 -0
  42. package/dist/internal/attachmentInsertion.js +22 -0
  43. package/dist/internal/attachmentInsertion.js.map +1 -0
  44. package/dist/internal/frontmatter.d.ts.map +1 -1
  45. package/dist/internal/frontmatter.js +15 -3
  46. package/dist/internal/frontmatter.js.map +1 -1
  47. package/dist/internal/sync/baseline.js +2 -1
  48. package/dist/internal/sync/baseline.js.map +1 -1
  49. package/dist/internal/sync/changes.js.map +1 -1
  50. package/dist/internal/sync/config.d.ts.map +1 -1
  51. package/dist/internal/sync/config.js +1 -1
  52. package/dist/internal/sync/config.js.map +1 -1
  53. package/dist/internal/sync/document.d.ts.map +1 -1
  54. package/dist/internal/sync/document.js +88 -29
  55. package/dist/internal/sync/document.js.map +1 -1
  56. package/dist/internal/sync/fieldValues.d.ts +0 -5
  57. package/dist/internal/sync/fieldValues.d.ts.map +1 -1
  58. package/dist/internal/sync/fieldValues.js +9 -4
  59. package/dist/internal/sync/fieldValues.js.map +1 -1
  60. package/dist/internal/sync/manifest.js.map +1 -1
  61. package/dist/internal/sync/schemas.d.ts +5 -5
  62. package/dist/internal/sync/schemas.d.ts.map +1 -1
  63. package/dist/internal/sync/schemas.js +2 -1
  64. package/dist/internal/sync/schemas.js.map +1 -1
  65. package/dist/internal/sync/types.d.ts +5 -2
  66. package/dist/internal/sync/types.d.ts.map +1 -1
  67. package/dist/internal/sync/types.js.map +1 -1
  68. package/package.json +13 -9
  69. package/src/AttachmentService.ts +106 -0
  70. package/src/IssueService.ts +81 -61
  71. package/src/JiraAuth.ts +7 -11
  72. package/src/VersionService.ts +142 -159
  73. package/src/bin.ts +2 -2
  74. package/src/commands/index.ts +1 -0
  75. package/src/commands/issue.ts +3 -2
  76. package/src/commands/issueAttachment.ts +146 -0
  77. package/src/commands/layers.ts +20 -3
  78. package/src/commands/search.ts +1 -1
  79. package/src/index.ts +6 -0
  80. package/src/internal/attachmentInsertion.ts +44 -0
  81. package/src/internal/frontmatter.ts +17 -3
  82. package/src/internal/sync/baseline.ts +3 -3
  83. package/src/internal/sync/changes.ts +1 -1
  84. package/src/internal/sync/config.ts +1 -2
  85. package/src/internal/sync/document.ts +98 -29
  86. package/src/internal/sync/fieldValues.ts +4 -6
  87. package/src/internal/sync/manifest.ts +1 -1
  88. package/src/internal/sync/schemas.ts +3 -2
  89. package/src/internal/sync/types.ts +16 -4
  90. package/test/IssueService.test.ts +94 -0
  91. package/test/VersionService.test.ts +41 -20
  92. package/test/attachmentInsertion.test.ts +25 -0
  93. package/test/commandTree.test.ts +38 -0
  94. package/test/frontmatter.test.ts +20 -1
  95. package/test/integration.test.ts +94 -3
  96. package/test/layers.test.ts +12 -0
  97. package/test/syncDocument.test.ts +30 -1
  98. package/vitest.config.integration.ts +4 -0
  99. package/vitest.config.ts +4 -0
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * @internal
5
5
  */
6
- export declare const FIELD_SHAPES: readonly ["text", "multilineText", "number", "boolean", "date", "singleSelect", "multiSelect", "user", "cascadingSelect"];
7
- export type FieldShape = typeof FIELD_SHAPES[number];
6
+ export type FieldShape = "text" | "multilineText" | "number" | "boolean" | "date" | "singleSelect" | "multiSelect" | "user" | "cascadingSelect";
7
+ export declare const FIELD_SHAPES: ReadonlyArray<FieldShape>;
8
8
  export interface RequestedCustomField {
9
9
  readonly displayName: string;
10
10
  readonly fieldId?: string | undefined;
@@ -88,8 +88,11 @@ export interface AcceptedComment {
88
88
  readonly body: string;
89
89
  }
90
90
  export interface AttachmentReference {
91
+ readonly id: string;
91
92
  readonly filename: string;
92
93
  readonly url: string;
94
+ readonly mediaType: string | null;
95
+ readonly size: number | null;
93
96
  }
94
97
  export interface IssueDocument {
95
98
  readonly frontMatter: IssueDocumentFrontMatter;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/internal/sync/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/internal/sync/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/internal/sync/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,MAAM;IACN,eAAe;IACf,QAAQ;IACR,SAAS;IACT,MAAM;IACN,cAAc;IACd,aAAa;IACb,MAAM;IACN,iBAAiB;CACT,CAAA"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/internal/sync/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,MAAM,CAAC,MAAM,YAAY,GAA8B;IACrD,MAAM;IACN,eAAe;IACf,QAAQ;IACR,SAAS;IACT,MAAM;IACN,cAAc;IACd,aAAa;IACb,MAAM;IACN,iBAAiB;CAClB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knpkv/jira-cli",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "CLI tool to fetch Jira tickets and export to markdown",
5
5
  "license": "MIT",
6
6
  "author": "knpkv",
@@ -33,6 +33,10 @@
33
33
  "import": "./dist/IssueService.js",
34
34
  "types": "./dist/IssueService.d.ts"
35
35
  },
36
+ "./AttachmentService": {
37
+ "import": "./dist/AttachmentService.js",
38
+ "types": "./dist/AttachmentService.d.ts"
39
+ },
36
40
  "./MarkdownWriter": {
37
41
  "import": "./dist/MarkdownWriter.js",
38
42
  "types": "./dist/MarkdownWriter.d.ts"
@@ -46,23 +50,23 @@
46
50
  "access": "public"
47
51
  },
48
52
  "peerDependencies": {
49
- "effect": "4.0.0-beta.93"
53
+ "effect": "4.0.0-beta.98"
50
54
  },
51
55
  "dependencies": {
52
- "@effect/platform-node": "4.0.0-beta.93",
56
+ "@effect/platform-node": "4.0.0-beta.98",
53
57
  "gray-matter": "^4.0.3",
54
58
  "js-yaml": "5.2.1",
55
- "@knpkv/atlassian-common": "1.0.0",
56
- "@knpkv/jira-api-client": "0.3.1",
57
- "@knpkv/agent-skills": "^0.2.2"
59
+ "@knpkv/agent-skills": "^0.2.3",
60
+ "@knpkv/atlassian-common": "1.2.0",
61
+ "@knpkv/jira-api-client": "1.0.0"
58
62
  },
59
63
  "devDependencies": {
60
- "@effect/vitest": "4.0.0-beta.93",
64
+ "@effect/vitest": "4.0.0-beta.98",
61
65
  "@types/js-yaml": "^4.0.9",
62
66
  "@types/node": "latest",
63
- "effect": "4.0.0-beta.93",
67
+ "effect": "4.0.0-beta.98",
64
68
  "typescript": "~6.0.3",
65
- "vitest": "^4.1.9"
69
+ "vitest": "^4.1.10"
66
70
  },
67
71
  "keywords": [
68
72
  "effect",
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Explicit Jira Attachment Upload service.
3
+ *
4
+ * @module
5
+ */
6
+ import { normalizeAttachmentMediaType } from "@knpkv/atlassian-common/attachments"
7
+ import { JiraApiClient } from "@knpkv/jira-api-client"
8
+ import * as Context from "effect/Context"
9
+ import * as Effect from "effect/Effect"
10
+ import * as FileSystem from "effect/FileSystem"
11
+ import * as Layer from "effect/Layer"
12
+ import * as Path from "effect/Path"
13
+ import * as Schema from "effect/Schema"
14
+ import type { Attachment } from "./IssueService.js"
15
+ import { JiraApiError } from "./JiraCliError.js"
16
+
17
+ export interface UploadAttachmentInput {
18
+ readonly filePath: string
19
+ readonly filename?: string | undefined
20
+ readonly mediaType?: string | undefined
21
+ }
22
+
23
+ export interface AttachmentServiceShape {
24
+ readonly uploadToIssue: (
25
+ issueIdOrKey: string,
26
+ input: UploadAttachmentInput
27
+ ) => Effect.Effect<Attachment, JiraApiError>
28
+ }
29
+
30
+ export class AttachmentService extends Context.Service<
31
+ AttachmentService,
32
+ AttachmentServiceShape
33
+ >()("@knpkv/jira-cli/AttachmentService") {}
34
+
35
+ const UploadedAttachmentSchema = Schema.Struct({
36
+ id: Schema.String,
37
+ filename: Schema.String,
38
+ content: Schema.String,
39
+ mediaType: Schema.optional(Schema.NullOr(Schema.String)),
40
+ mimeType: Schema.optional(Schema.NullOr(Schema.String)),
41
+ size: Schema.optional(Schema.NullOr(Schema.Number))
42
+ })
43
+
44
+ const decodeUploadedAttachment = (raw: unknown): Effect.Effect<Attachment, JiraApiError> =>
45
+ Schema.decodeUnknownEffect(UploadedAttachmentSchema)(raw).pipe(
46
+ Effect.mapError((cause) => new JiraApiError({ message: "Jira returned an invalid attachment response", cause })),
47
+ Effect.flatMap((record) => {
48
+ if (record.id.length === 0 || record.filename.length === 0 || record.content.length === 0) {
49
+ return Effect.fail(
50
+ new JiraApiError({ message: "Jira returned an attachment without id, filename, or content URL" })
51
+ )
52
+ }
53
+ const mediaType = normalizeAttachmentMediaType(record.mediaType ?? null, record.mimeType ?? null)
54
+ return Effect.succeed({
55
+ id: record.id,
56
+ filename: record.filename,
57
+ url: record.content,
58
+ mediaType,
59
+ mimeType: mediaType ?? "",
60
+ size: record.size ?? 0
61
+ })
62
+ })
63
+ )
64
+
65
+ const decodeFirstUploadedAttachment = (raw: unknown): Effect.Effect<Attachment, JiraApiError> => {
66
+ if (!Array.isArray(raw)) {
67
+ return Effect.fail(new JiraApiError({ message: "Jira did not return an attachment array" }))
68
+ }
69
+ const first = raw[0]
70
+ if (first === undefined) {
71
+ return Effect.fail(new JiraApiError({ message: "Jira did not return an uploaded attachment" }))
72
+ }
73
+ return decodeUploadedAttachment(first)
74
+ }
75
+
76
+ const make = Effect.gen(function*() {
77
+ const client = yield* JiraApiClient
78
+ const fs = yield* FileSystem.FileSystem
79
+ const path = yield* Path.Path
80
+
81
+ const uploadToIssue: AttachmentServiceShape["uploadToIssue"] = (issueIdOrKey, input) =>
82
+ Effect.gen(function*() {
83
+ const bytes = yield* fs.readFile(input.filePath).pipe(
84
+ Effect.mapError((cause) =>
85
+ new JiraApiError({ message: `Failed to read attachment file ${input.filePath}`, cause })
86
+ )
87
+ )
88
+ const filename = input.filename ?? path.basename(input.filePath)
89
+ const result = yield* client.uploadAttachment(issueIdOrKey, {
90
+ bytes,
91
+ filename,
92
+ ...(input.mediaType === undefined ? {} : { mediaType: input.mediaType })
93
+ }).pipe(
94
+ Effect.mapError((cause) =>
95
+ new JiraApiError({ message: `Failed to upload attachment to ${issueIdOrKey}`, cause })
96
+ )
97
+ )
98
+
99
+ return yield* decodeFirstUploadedAttachment(result)
100
+ })
101
+
102
+ return AttachmentService.of({ uploadToIssue })
103
+ })
104
+
105
+ export const layer: Layer.Layer<AttachmentService, never, JiraApiClient | FileSystem.FileSystem | Path.Path> = Layer
106
+ .effect(AttachmentService, make)
@@ -18,10 +18,12 @@
18
18
  *
19
19
  * @module
20
20
  */
21
- import { JiraApiClient, toEffect } from "@knpkv/jira-api-client"
21
+ import { normalizeAttachmentMediaType } from "@knpkv/atlassian-common/attachments"
22
+ import { JiraApiClient } from "@knpkv/jira-api-client"
22
23
  import * as Context from "effect/Context"
23
24
  import * as Effect from "effect/Effect"
24
25
  import * as Layer from "effect/Layer"
26
+ import * as Predicate from "effect/Predicate"
25
27
  import { JiraApiError } from "./JiraCliError.js"
26
28
 
27
29
  /**
@@ -40,6 +42,8 @@ export interface Attachment {
40
42
  readonly id: string
41
43
  readonly filename: string
42
44
  readonly url: string
45
+ readonly mediaType: string | null
46
+ /** Backwards-compatible alias for consumers that still read Jira's MIME field name. */
43
47
  readonly mimeType: string
44
48
  readonly size: number
45
49
  }
@@ -110,6 +114,22 @@ interface SearchJqlResponse {
110
114
  readonly nextPageToken?: string
111
115
  }
112
116
 
117
+ const recordOrEmpty = (value: unknown): Readonly<Record<PropertyKey, unknown>> =>
118
+ Predicate.isReadonlyObject(value) ? value : {}
119
+
120
+ const recordArray = (value: unknown): ReadonlyArray<Readonly<Record<PropertyKey, unknown>>> =>
121
+ Array.isArray(value) ? value.filter(Predicate.isReadonlyObject) : []
122
+
123
+ const parseSearchJqlResponse = (value: unknown): SearchJqlResponse => {
124
+ const record = recordOrEmpty(value)
125
+ const nextPageToken = typeof record.nextPageToken === "string" ? record.nextPageToken : undefined
126
+ return {
127
+ issues: Array.isArray(record.issues) ? record.issues : [],
128
+ isLast: typeof record.isLast === "boolean" ? record.isLast : nextPageToken === undefined,
129
+ ...(nextPageToken !== undefined ? { nextPageToken } : {})
130
+ }
131
+ }
132
+
113
133
  /**
114
134
  * IssueService interface.
115
135
  *
@@ -172,10 +192,9 @@ const FIELDS = [
172
192
  const extractDisplayName = (field: unknown): string | null => {
173
193
  if (field === null || field === undefined) return null
174
194
  if (typeof field === "string") return field
175
- if (typeof field === "object") {
176
- const obj = field as Record<string, unknown>
177
- if (typeof obj["displayName"] === "string") return obj["displayName"]
178
- if (typeof obj["name"] === "string") return obj["name"]
195
+ if (Predicate.isReadonlyObject(field)) {
196
+ if (typeof field.displayName === "string") return field.displayName
197
+ if (typeof field.name === "string") return field.name
179
198
  }
180
199
  return null
181
200
  }
@@ -188,9 +207,8 @@ const extractNameArray = (field: unknown): ReadonlyArray<string> => {
188
207
  return field
189
208
  .map((item) => {
190
209
  if (typeof item === "string") return item
191
- if (typeof item === "object" && item !== null) {
192
- const obj = item as Record<string, unknown>
193
- if (typeof obj["name"] === "string") return obj["name"]
210
+ if (Predicate.isReadonlyObject(item)) {
211
+ if (typeof item.name === "string") return item.name
194
212
  }
195
213
  return null
196
214
  })
@@ -210,34 +228,35 @@ const parseDate = (val: unknown): Date => {
210
228
  /**
211
229
  * Map IssueBean from API to our Issue type.
212
230
  */
213
- const mapIssue = (bean: Record<string, unknown>, baseUrl: string): Issue => {
214
- const fields = (bean["fields"] ?? {}) as Record<string, unknown>
215
- const renderedFields = (bean["renderedFields"] ?? {}) as Record<string, unknown>
231
+ const mapIssue = (bean: Readonly<Record<PropertyKey, unknown>>, baseUrl: string): Issue => {
232
+ const fields = recordOrEmpty(bean.fields)
233
+ const renderedFields = recordOrEmpty(bean.renderedFields)
216
234
  const key = String(bean["key"] ?? "")
217
235
  const id = String(bean["id"] ?? "")
218
236
 
219
237
  // Extract attachments
220
238
  const attachmentField = fields["attachment"]
221
239
  const attachments: Array<Attachment> = Array.isArray(attachmentField)
222
- ? attachmentField.map((a) => {
223
- const att = a as Record<string, unknown>
240
+ ? recordArray(attachmentField).map((att) => {
241
+ const mediaType = normalizeAttachmentMediaType(
242
+ undefined,
243
+ typeof att["mimeType"] === "string" ? att["mimeType"] : null
244
+ )
224
245
  return {
225
246
  id: String(att["id"] ?? ""),
226
247
  filename: String(att["filename"] ?? ""),
227
248
  url: String(att["content"] ?? ""),
228
- mimeType: String(att["mimeType"] ?? ""),
249
+ mediaType,
250
+ mimeType: mediaType ?? "",
229
251
  size: Number(att["size"] ?? 0)
230
252
  }
231
253
  })
232
254
  : []
233
255
 
234
256
  // Extract comments
235
- const commentField = fields["comment"] as Record<string, unknown> | undefined
236
- const commentList = (commentField?.["comments"] ?? []) as Array<Record<string, unknown>>
237
- const renderedComments =
238
- ((renderedFields["comment"] as Record<string, unknown> | undefined)?.["comments"] ?? []) as Array<
239
- Record<string, unknown>
240
- >
257
+ const commentField = recordOrEmpty(fields["comment"])
258
+ const commentList = recordArray(commentField.comments)
259
+ const renderedComments = recordArray(recordOrEmpty(renderedFields.comment).comments)
241
260
 
242
261
  // Build map of rendered comments by ID for accurate matching
243
262
  const renderedMap = new Map<string, Record<string, unknown>>()
@@ -247,7 +266,7 @@ const mapIssue = (bean: Record<string, unknown>, baseUrl: string): Issue => {
247
266
  }
248
267
 
249
268
  const comments: Array<Comment> = commentList.map((c) => {
250
- const author = c["author"] as Record<string, unknown> | undefined
269
+ const author = recordOrEmpty(c.author)
251
270
  const commentId = String(c["id"] ?? "")
252
271
  const rendered = renderedMap.get(commentId)
253
272
  const renderedBody = rendered?.["body"]
@@ -286,18 +305,20 @@ const mapIssue = (bean: Record<string, unknown>, baseUrl: string): Issue => {
286
305
  }
287
306
  }
288
307
 
308
+ const mapIssueUnknown = (bean: unknown, baseUrl: string): Issue => mapIssue(recordOrEmpty(bean), baseUrl)
309
+
289
310
  const make = Effect.gen(function*() {
290
311
  const client = yield* JiraApiClient
291
312
  const siteUrl = yield* SiteUrl
292
313
 
293
314
  const getByKey = (key: string): Effect.Effect<Issue, JiraApiError> =>
294
- toEffect(client.v3.client.GET("/rest/api/3/issue/{issueIdOrKey}", {
315
+ client.getIssue(key, {
295
316
  params: {
296
- path: { issueIdOrKey: key },
297
- query: { fields: FIELDS, expand: "renderedFields" }
317
+ fields: FIELDS,
318
+ expand: "renderedFields"
298
319
  }
299
- })).pipe(
300
- Effect.map((result) => mapIssue(result as unknown as Record<string, unknown>, siteUrl)),
320
+ }).pipe(
321
+ Effect.map((result) => mapIssueUnknown(result, siteUrl)),
301
322
  Effect.mapError((cause) => new JiraApiError({ message: `Failed to get issue ${key}`, cause }))
302
323
  )
303
324
 
@@ -306,18 +327,16 @@ const make = Effect.gen(function*() {
306
327
  maxResults: number,
307
328
  nextPageToken?: string
308
329
  ): Effect.Effect<SearchJqlResponse, JiraApiError> =>
309
- toEffect(client.v3.client.GET("/rest/api/3/search/jql", {
330
+ client.searchIssuesUsingJql({
310
331
  params: {
311
- query: {
312
- jql,
313
- maxResults,
314
- ...(nextPageToken ? { nextPageToken } : {}),
315
- fields: FIELDS,
316
- expand: "renderedFields"
317
- }
332
+ jql,
333
+ maxResults,
334
+ ...(nextPageToken ? { nextPageToken } : {}),
335
+ fields: FIELDS,
336
+ expand: "renderedFields"
318
337
  }
319
- })).pipe(
320
- Effect.map((result) => result as SearchJqlResponse),
338
+ }).pipe(
339
+ Effect.map(parseSearchJqlResponse),
321
340
  Effect.mapError((cause) => new JiraApiError({ message: "Failed to search issues", cause }))
322
341
  )
323
342
 
@@ -327,7 +346,7 @@ const make = Effect.gen(function*() {
327
346
  const issues = result.issues ?? []
328
347
  const mappedIssues: Array<Issue> = []
329
348
  for (const bean of issues) {
330
- mappedIssues.push(mapIssue(bean as unknown as Record<string, unknown>, siteUrl))
349
+ mappedIssues.push(mapIssueUnknown(bean, siteUrl))
331
350
  }
332
351
  return {
333
352
  issues: mappedIssues,
@@ -343,35 +362,36 @@ const make = Effect.gen(function*() {
343
362
  const searchAll = (
344
363
  jql: string,
345
364
  options?: { readonly maxResults?: number }
346
- ): Effect.Effect<ReadonlyArray<Issue>, JiraApiError> => (Effect.gen(function*() {
347
- const allIssues: Array<Issue> = []
348
- const maxResults = options?.maxResults ?? 100
349
- let nextPageToken: string | undefined = undefined
350
- let pageCount = 0
351
-
352
- // Fetch first page
353
- let result = yield* searchJql(jql, maxResults, nextPageToken)
354
- let issues = result.issues ?? []
355
- pageCount++
356
-
357
- for (const bean of issues) {
358
- allIssues.push(mapIssue(bean as unknown as Record<string, unknown>, siteUrl))
359
- }
360
-
361
- // Fetch remaining pages with iteration guard
362
- while (!result.isLast && result.nextPageToken && pageCount < MAX_PAGES) {
363
- nextPageToken = result.nextPageToken
364
- result = yield* searchJql(jql, maxResults, nextPageToken)
365
- issues = result.issues ?? []
365
+ ): Effect.Effect<ReadonlyArray<Issue>, JiraApiError> =>
366
+ Effect.gen(function*() {
367
+ const allIssues: Array<Issue> = []
368
+ const maxResults = options?.maxResults ?? 100
369
+ let nextPageToken: string | undefined = undefined
370
+ let pageCount = 0
371
+
372
+ // Fetch first page
373
+ let result = yield* searchJql(jql, maxResults, nextPageToken)
374
+ let issues = result.issues ?? []
366
375
  pageCount++
367
376
 
368
377
  for (const bean of issues) {
369
- allIssues.push(mapIssue(bean as unknown as Record<string, unknown>, siteUrl))
378
+ allIssues.push(mapIssueUnknown(bean, siteUrl))
379
+ }
380
+
381
+ // Fetch remaining pages with iteration guard
382
+ while (!result.isLast && result.nextPageToken && pageCount < MAX_PAGES) {
383
+ nextPageToken = result.nextPageToken
384
+ result = yield* searchJql(jql, maxResults, nextPageToken)
385
+ issues = result.issues ?? []
386
+ pageCount++
387
+
388
+ for (const bean of issues) {
389
+ allIssues.push(mapIssueUnknown(bean, siteUrl))
390
+ }
370
391
  }
371
- }
372
392
 
373
- return allIssues
374
- }) as Effect.Effect<ReadonlyArray<Issue>, JiraApiError>)
393
+ return allIssues
394
+ })
375
395
 
376
396
  return IssueService.of({ getByKey, search, searchAll })
377
397
  })
package/src/JiraAuth.ts CHANGED
@@ -226,17 +226,16 @@ export class JiraAuth extends Context.Service<
226
226
  JiraAuthService
227
227
  >()("@knpkv/jira-cli/JiraAuth") {}
228
228
 
229
+ type RefreshError = OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
230
+ type RefreshDeferred = Deferred.Deferred<OAuthToken, RefreshError>
231
+
229
232
  const make = Effect.gen(function*() {
230
233
  const httpClient = yield* HttpClient.HttpClient
231
234
  const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner
232
235
  const cryptoService = yield* Crypto.Crypto
233
236
 
234
237
  // Ref to track ongoing refresh operation to prevent concurrent refreshes
235
- const refreshLock = yield* Ref.make<
236
- Option.Option<
237
- Deferred.Deferred<OAuthToken, OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
238
- >
239
- >(
238
+ const refreshLock = yield* Ref.make<Option.Option<RefreshDeferred>>(
240
239
  Option.none()
241
240
  )
242
241
 
@@ -422,14 +421,11 @@ const make = Effect.gen(function*() {
422
421
  }
423
422
 
424
423
  // Atomically check-then-set refresh lock to avoid TOCTOU race
425
- const deferred = yield* Deferred.make<
426
- OAuthToken,
427
- OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
428
- >()
424
+ const deferred = yield* Deferred.make<OAuthToken, RefreshError>()
429
425
  const existing = yield* Ref.modify(refreshLock, (current) =>
430
426
  Option.isSome(current)
431
- ? [current.value, current] as const
432
- : [deferred, Option.some(deferred)] as const)
427
+ ? ([current.value, current] satisfies readonly [RefreshDeferred, Option.Option<RefreshDeferred>])
428
+ : ([deferred, Option.some(deferred)] satisfies readonly [RefreshDeferred, Option.Option<RefreshDeferred>]))
433
429
 
434
430
  // Another fiber is already refreshing — just await its result
435
431
  if (existing !== deferred) {