@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.
- package/CHANGELOG.md +37 -0
- package/README.md +23 -7
- package/dist/AttachmentService.d.ts +22 -0
- package/dist/AttachmentService.d.ts.map +1 -0
- package/dist/AttachmentService.js +67 -0
- package/dist/AttachmentService.js.map +1 -0
- package/dist/IssueService.d.ts +2 -20
- package/dist/IssueService.d.ts.map +1 -1
- package/dist/IssueService.js +47 -36
- package/dist/IssueService.js.map +1 -1
- package/dist/JiraAuth.d.ts +1 -1
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js.map +1 -1
- package/dist/VersionService.d.ts.map +1 -1
- package/dist/VersionService.js +39 -45
- package/dist/VersionService.js.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +2 -1
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/issueAttachment.d.ts +6 -0
- package/dist/commands/issueAttachment.d.ts.map +1 -0
- package/dist/commands/issueAttachment.js +88 -0
- package/dist/commands/issueAttachment.js.map +1 -0
- package/dist/commands/layers.d.ts +4 -3
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +12 -4
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/search.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/attachmentInsertion.d.ts +10 -0
- package/dist/internal/attachmentInsertion.d.ts.map +1 -0
- package/dist/internal/attachmentInsertion.js +22 -0
- package/dist/internal/attachmentInsertion.js.map +1 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +15 -3
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/sync/baseline.js +2 -1
- package/dist/internal/sync/baseline.js.map +1 -1
- package/dist/internal/sync/changes.js.map +1 -1
- package/dist/internal/sync/config.d.ts.map +1 -1
- package/dist/internal/sync/config.js +1 -1
- package/dist/internal/sync/config.js.map +1 -1
- package/dist/internal/sync/document.d.ts.map +1 -1
- package/dist/internal/sync/document.js +88 -29
- package/dist/internal/sync/document.js.map +1 -1
- package/dist/internal/sync/fieldValues.d.ts +0 -5
- package/dist/internal/sync/fieldValues.d.ts.map +1 -1
- package/dist/internal/sync/fieldValues.js +9 -4
- package/dist/internal/sync/fieldValues.js.map +1 -1
- package/dist/internal/sync/manifest.js.map +1 -1
- package/dist/internal/sync/schemas.d.ts +5 -5
- package/dist/internal/sync/schemas.d.ts.map +1 -1
- package/dist/internal/sync/schemas.js +2 -1
- package/dist/internal/sync/schemas.js.map +1 -1
- package/dist/internal/sync/types.d.ts +5 -2
- package/dist/internal/sync/types.d.ts.map +1 -1
- package/dist/internal/sync/types.js.map +1 -1
- package/package.json +13 -9
- package/src/AttachmentService.ts +106 -0
- package/src/IssueService.ts +81 -61
- package/src/JiraAuth.ts +7 -11
- package/src/VersionService.ts +142 -159
- package/src/bin.ts +2 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/issue.ts +3 -2
- package/src/commands/issueAttachment.ts +146 -0
- package/src/commands/layers.ts +20 -3
- package/src/commands/search.ts +1 -1
- package/src/index.ts +6 -0
- package/src/internal/attachmentInsertion.ts +44 -0
- package/src/internal/frontmatter.ts +17 -3
- package/src/internal/sync/baseline.ts +3 -3
- package/src/internal/sync/changes.ts +1 -1
- package/src/internal/sync/config.ts +1 -2
- package/src/internal/sync/document.ts +98 -29
- package/src/internal/sync/fieldValues.ts +4 -6
- package/src/internal/sync/manifest.ts +1 -1
- package/src/internal/sync/schemas.ts +3 -2
- package/src/internal/sync/types.ts +16 -4
- package/test/IssueService.test.ts +94 -0
- package/test/VersionService.test.ts +41 -20
- package/test/attachmentInsertion.test.ts +25 -0
- package/test/commandTree.test.ts +38 -0
- package/test/frontmatter.test.ts +20 -1
- package/test/integration.test.ts +94 -3
- package/test/layers.test.ts +12 -0
- package/test/syncDocument.test.ts +30 -1
- package/vitest.config.integration.ts +4 -0
- package/vitest.config.ts +4 -0
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
7
|
-
export
|
|
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":"AAAA;;;;GAIG;AAEH,
|
|
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;
|
|
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
|
|
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.
|
|
53
|
+
"effect": "4.0.0-beta.98"
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|
|
52
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
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/
|
|
56
|
-
"@knpkv/
|
|
57
|
-
"@knpkv/
|
|
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.
|
|
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.
|
|
67
|
+
"effect": "4.0.0-beta.98",
|
|
64
68
|
"typescript": "~6.0.3",
|
|
65
|
-
"vitest": "^4.1.
|
|
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)
|
package/src/IssueService.ts
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
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 (
|
|
176
|
-
|
|
177
|
-
if (typeof
|
|
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 (
|
|
192
|
-
|
|
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<
|
|
214
|
-
const fields = (bean
|
|
215
|
-
const renderedFields = (bean
|
|
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((
|
|
223
|
-
const
|
|
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
|
-
|
|
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"]
|
|
236
|
-
const commentList = (commentField
|
|
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
|
|
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
|
-
|
|
315
|
+
client.getIssue(key, {
|
|
295
316
|
params: {
|
|
296
|
-
|
|
297
|
-
|
|
317
|
+
fields: FIELDS,
|
|
318
|
+
expand: "renderedFields"
|
|
298
319
|
}
|
|
299
|
-
})
|
|
300
|
-
Effect.map((result) =>
|
|
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
|
-
|
|
330
|
+
client.searchIssuesUsingJql({
|
|
310
331
|
params: {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
expand: "renderedFields"
|
|
317
|
-
}
|
|
332
|
+
jql,
|
|
333
|
+
maxResults,
|
|
334
|
+
...(nextPageToken ? { nextPageToken } : {}),
|
|
335
|
+
fields: FIELDS,
|
|
336
|
+
expand: "renderedFields"
|
|
318
337
|
}
|
|
319
|
-
})
|
|
320
|
-
Effect.map(
|
|
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(
|
|
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> =>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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(
|
|
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
|
-
|
|
374
|
-
|
|
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]
|
|
432
|
-
: [deferred, Option.some(deferred)]
|
|
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) {
|