@knpkv/jira-cli 0.1.1 → 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/CHANGELOG.md +47 -0
- package/README.md +63 -1
- package/dist/IssueService.d.ts +2 -2
- package/dist/IssueService.d.ts.map +1 -1
- package/dist/IssueService.js +3 -3
- package/dist/IssueService.js.map +1 -1
- package/dist/JiraAuth.d.ts +14 -14
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +18 -10
- package/dist/JiraAuth.js.map +1 -1
- package/dist/MarkdownWriter.d.ts +4 -4
- package/dist/MarkdownWriter.d.ts.map +1 -1
- package/dist/MarkdownWriter.js +6 -6
- package/dist/MarkdownWriter.js.map +1 -1
- package/dist/VersionService.d.ts +206 -0
- package/dist/VersionService.d.ts.map +1 -0
- package/dist/VersionService.js +426 -0
- package/dist/VersionService.js.map +1 -0
- package/dist/bin.js +28 -20
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +2 -21
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +6 -6
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/get.d.ts +3 -8
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +2 -2
- package/dist/commands/get.js.map +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/layers.d.ts +6 -18
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +31 -23
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/search.d.ts +3 -8
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +4 -4
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +179 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/internal/oauthServer.d.ts +17 -5
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +23 -40
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/openBrowser.d.ts +10 -0
- package/dist/internal/openBrowser.d.ts.map +1 -0
- package/dist/internal/openBrowser.js +17 -0
- package/dist/internal/openBrowser.js.map +1 -0
- package/package.json +10 -12
- package/skills/jira/SKILL.md +90 -0
- package/skills/jira/agents/openai.yaml +4 -0
- package/src/IssueService.ts +34 -28
- package/src/JiraAuth.ts +53 -39
- package/src/MarkdownWriter.ts +7 -11
- package/src/VersionService.ts +647 -0
- package/src/bin.ts +38 -26
- package/src/commands/auth.ts +6 -12
- package/src/commands/get.ts +2 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/layers.ts +40 -25
- package/src/commands/search.ts +4 -4
- package/src/commands/version.ts +267 -0
- package/src/internal/oauthServer.ts +43 -70
- package/src/internal/openBrowser.ts +31 -0
- package/test/VersionService.test.ts +266 -0
- package/vitest.config.ts +5 -0
package/src/IssueService.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { JiraApiError } from "./JiraCliError.js"
|
|
|
29
29
|
*
|
|
30
30
|
* @category Config
|
|
31
31
|
*/
|
|
32
|
-
export class SiteUrl extends Context.
|
|
32
|
+
export class SiteUrl extends Context.Service<SiteUrl, string>()("@knpkv/jira-cli/SiteUrl") {}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Attachment metadata.
|
|
@@ -104,6 +104,12 @@ export interface SearchResult {
|
|
|
104
104
|
readonly maxResults: number
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
interface SearchJqlResponse {
|
|
108
|
+
readonly issues?: ReadonlyArray<unknown>
|
|
109
|
+
readonly isLast?: boolean
|
|
110
|
+
readonly nextPageToken?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
/**
|
|
108
114
|
* IssueService interface.
|
|
109
115
|
*
|
|
@@ -138,10 +144,10 @@ export interface IssueServiceShape {
|
|
|
138
144
|
*
|
|
139
145
|
* @category Services
|
|
140
146
|
*/
|
|
141
|
-
export class IssueService extends Context.
|
|
147
|
+
export class IssueService extends Context.Service<
|
|
142
148
|
IssueService,
|
|
143
149
|
IssueServiceShape
|
|
144
|
-
>() {}
|
|
150
|
+
>()("@knpkv/jira-cli/IssueService") {}
|
|
145
151
|
|
|
146
152
|
const FIELDS = [
|
|
147
153
|
"summary",
|
|
@@ -299,7 +305,7 @@ const make = Effect.gen(function*() {
|
|
|
299
305
|
jql: string,
|
|
300
306
|
maxResults: number,
|
|
301
307
|
nextPageToken?: string
|
|
302
|
-
) =>
|
|
308
|
+
): Effect.Effect<SearchJqlResponse, JiraApiError> =>
|
|
303
309
|
toEffect(client.v3.client.GET("/rest/api/3/search/jql", {
|
|
304
310
|
params: {
|
|
305
311
|
query: {
|
|
@@ -311,6 +317,7 @@ const make = Effect.gen(function*() {
|
|
|
311
317
|
}
|
|
312
318
|
}
|
|
313
319
|
})).pipe(
|
|
320
|
+
Effect.map((result) => result as SearchJqlResponse),
|
|
314
321
|
Effect.mapError((cause) => new JiraApiError({ message: "Failed to search issues", cause }))
|
|
315
322
|
)
|
|
316
323
|
|
|
@@ -336,36 +343,35 @@ const make = Effect.gen(function*() {
|
|
|
336
343
|
const searchAll = (
|
|
337
344
|
jql: string,
|
|
338
345
|
options?: { readonly maxResults?: number }
|
|
339
|
-
): Effect.Effect<ReadonlyArray<Issue>, JiraApiError> =>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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 ?? []
|
|
349
366
|
pageCount++
|
|
350
367
|
|
|
351
368
|
for (const bean of issues) {
|
|
352
369
|
allIssues.push(mapIssue(bean as unknown as Record<string, unknown>, siteUrl))
|
|
353
370
|
}
|
|
371
|
+
}
|
|
354
372
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
nextPageToken = result.nextPageToken
|
|
358
|
-
result = yield* searchJql(jql, maxResults, nextPageToken)
|
|
359
|
-
issues = result.issues ?? []
|
|
360
|
-
pageCount++
|
|
361
|
-
|
|
362
|
-
for (const bean of issues) {
|
|
363
|
-
allIssues.push(mapIssue(bean as unknown as Record<string, unknown>, siteUrl))
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return allIssues
|
|
368
|
-
})
|
|
373
|
+
return allIssues
|
|
374
|
+
}) as Effect.Effect<ReadonlyArray<Issue>, JiraApiError>)
|
|
369
375
|
|
|
370
376
|
return IssueService.of({ getByKey, search, searchAll })
|
|
371
377
|
})
|
package/src/JiraAuth.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* **Mental model**
|
|
5
5
|
*
|
|
6
6
|
* - **Service pattern**: {@link JiraAuth} is a `Context.Tag` whose layer requires `HttpClient`
|
|
7
|
-
* and `
|
|
7
|
+
* and `ChildProcessSpawner`. All token storage operations are pre-bound to
|
|
8
8
|
* `@knpkv/atlassian-common/config` with a `"jira-cli"` tool name.
|
|
9
9
|
* - **Refresh lock**: A `Ref<Option<Deferred>>` prevents concurrent token refreshes — the
|
|
10
10
|
* first caller refreshes, others await the same Deferred.
|
|
@@ -21,10 +21,6 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
|
|
23
23
|
import * as NodePath from "@effect/platform-node/NodePath"
|
|
24
|
-
import * as Command from "@effect/platform/Command"
|
|
25
|
-
import * as CommandExecutor from "@effect/platform/CommandExecutor"
|
|
26
|
-
import type * as Error from "@effect/platform/Error"
|
|
27
|
-
import * as HttpClient from "@effect/platform/HttpClient"
|
|
28
24
|
import {
|
|
29
25
|
buildAuthUrl,
|
|
30
26
|
buildOAuthToken,
|
|
@@ -58,19 +54,31 @@ import * as Deferred from "effect/Deferred"
|
|
|
58
54
|
import * as Effect from "effect/Effect"
|
|
59
55
|
import * as Layer from "effect/Layer"
|
|
60
56
|
import * as Option from "effect/Option"
|
|
57
|
+
import type * as PlatformError from "effect/PlatformError"
|
|
61
58
|
import * as Redacted from "effect/Redacted"
|
|
62
59
|
import * as Ref from "effect/Ref"
|
|
60
|
+
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
61
|
+
import { ChildProcessSpawner } from "effect/unstable/process"
|
|
63
62
|
import { HttpServerFactoryLive } from "./internal/NodeLayers.js"
|
|
64
63
|
import { startCallbackServer } from "./internal/oauthServer.js"
|
|
64
|
+
import { openBrowser } from "./internal/openBrowser.js"
|
|
65
65
|
import type { AuthMissingError } from "./JiraCliError.js"
|
|
66
66
|
import { authMissing } from "./JiraCliError.js"
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/** OAuth scopes for the Jira CLI. Granular scopes must also be enabled on the app in the developer console. */
|
|
69
69
|
const JIRA_CLI_SCOPES = [
|
|
70
|
+
// Read issues, search, and read versions.
|
|
70
71
|
"read:jira-work",
|
|
72
|
+
// Edit issues and write worklogs.
|
|
71
73
|
"write:jira-work",
|
|
74
|
+
// Edit a version (e.g. its description) via `PUT /rest/api/3/version/{id}` and
|
|
75
|
+
// manage version "Related work" links (`/rest/api/3/version/{id}/relatedwork`).
|
|
76
|
+
"manage:jira-project",
|
|
77
|
+
// Resolve account IDs to display names for Driver/Contributors/Approvers.
|
|
72
78
|
"read:jira-user",
|
|
79
|
+
// Read the authenticated user's own profile (`/rest/api/3/myself`).
|
|
73
80
|
"read:me",
|
|
81
|
+
// Issue a refresh token so the CLI stays logged in across runs.
|
|
74
82
|
"offline_access"
|
|
75
83
|
]
|
|
76
84
|
|
|
@@ -121,40 +129,46 @@ export interface JiraAuthService {
|
|
|
121
129
|
/** Configure OAuth client credentials */
|
|
122
130
|
readonly configure: (
|
|
123
131
|
config: OAuthConfig
|
|
124
|
-
) => Effect.Effect<void, FileSystemError | HomeDirectoryError |
|
|
132
|
+
) => Effect.Effect<void, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
125
133
|
/** Check if OAuth is configured */
|
|
126
|
-
readonly isConfigured: () => Effect.Effect<
|
|
134
|
+
readonly isConfigured: () => Effect.Effect<
|
|
135
|
+
boolean,
|
|
136
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
137
|
+
>
|
|
127
138
|
/** Start OAuth login flow. Returns list of sites if multiple are available. */
|
|
128
139
|
readonly login: (
|
|
129
140
|
options?: LoginOptions
|
|
130
141
|
) => Effect.Effect<
|
|
131
142
|
ReadonlyArray<AccessibleSite> | void,
|
|
132
|
-
OAuthError | FileSystemError | HomeDirectoryError |
|
|
143
|
+
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
133
144
|
>
|
|
134
145
|
/** Remove stored authentication */
|
|
135
|
-
readonly logout: () => Effect.Effect<
|
|
146
|
+
readonly logout: () => Effect.Effect<
|
|
147
|
+
void,
|
|
148
|
+
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
149
|
+
>
|
|
136
150
|
/** Get access token, refreshing if needed */
|
|
137
151
|
readonly getAccessToken: () => Effect.Effect<
|
|
138
152
|
Redacted.Redacted<string>,
|
|
139
|
-
AuthMissingError | OAuthError | FileSystemError | HomeDirectoryError |
|
|
153
|
+
AuthMissingError | OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
140
154
|
>
|
|
141
155
|
/** Get cloud ID from stored token */
|
|
142
156
|
readonly getCloudId: () => Effect.Effect<
|
|
143
157
|
string,
|
|
144
|
-
AuthMissingError | FileSystemError | HomeDirectoryError |
|
|
158
|
+
AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
145
159
|
>
|
|
146
160
|
/** Get site URL from stored token */
|
|
147
161
|
readonly getSiteUrl: () => Effect.Effect<
|
|
148
162
|
string,
|
|
149
|
-
AuthMissingError | FileSystemError | HomeDirectoryError |
|
|
163
|
+
AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
150
164
|
>
|
|
151
165
|
/** Get current user info from stored token */
|
|
152
166
|
readonly getCurrentUser: () => Effect.Effect<
|
|
153
167
|
OAuthUser | null,
|
|
154
|
-
FileSystemError | HomeDirectoryError |
|
|
168
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
155
169
|
>
|
|
156
170
|
/** Check if user is logged in */
|
|
157
|
-
readonly isLoggedIn: () => Effect.Effect<boolean, FileSystemError | HomeDirectoryError |
|
|
171
|
+
readonly isLoggedIn: () => Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
/**
|
|
@@ -176,37 +190,33 @@ export interface JiraAuthService {
|
|
|
176
190
|
*
|
|
177
191
|
* @category Services
|
|
178
192
|
*/
|
|
179
|
-
export class JiraAuth extends Context.
|
|
193
|
+
export class JiraAuth extends Context.Service<
|
|
180
194
|
JiraAuth,
|
|
181
195
|
JiraAuthService
|
|
182
|
-
>() {}
|
|
196
|
+
>()("@knpkv/jira-cli/JiraAuth") {}
|
|
183
197
|
|
|
184
198
|
const make = Effect.gen(function*() {
|
|
185
199
|
const httpClient = yield* HttpClient.HttpClient
|
|
186
|
-
const
|
|
200
|
+
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
187
201
|
|
|
188
202
|
// Ref to track ongoing refresh operation to prevent concurrent refreshes
|
|
189
203
|
const refreshLock = yield* Ref.make<
|
|
190
204
|
Option.Option<
|
|
191
|
-
Deferred.Deferred<OAuthToken, OAuthError | FileSystemError | HomeDirectoryError |
|
|
205
|
+
Deferred.Deferred<OAuthToken, OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
192
206
|
>
|
|
193
207
|
>(
|
|
194
208
|
Option.none()
|
|
195
209
|
)
|
|
196
210
|
|
|
197
211
|
const openBrowserImpl = (url: string): Effect.Effect<void, OAuthError> =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
Effect.catchAll(() => Command.make("xdg-open", url).pipe(Command.exitCode)),
|
|
201
|
-
Effect.catchAll(() => Command.make("cmd", "/c", "start", "", url).pipe(Command.exitCode)),
|
|
202
|
-
Effect.provide(Layer.succeed(CommandExecutor.CommandExecutor, commandExecutor)),
|
|
203
|
-
Effect.asVoid,
|
|
212
|
+
openBrowser(url).pipe(
|
|
213
|
+
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
|
|
204
214
|
Effect.mapError((cause) => new OAuthError({ step: "authorize", cause }))
|
|
205
215
|
)
|
|
206
216
|
|
|
207
217
|
const getConfig = (): Effect.Effect<
|
|
208
218
|
OAuthConfig,
|
|
209
|
-
OAuthError | FileSystemError | HomeDirectoryError |
|
|
219
|
+
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
210
220
|
> =>
|
|
211
221
|
Effect.gen(function*() {
|
|
212
222
|
const config = yield* loadOAuthConfigOp()
|
|
@@ -224,7 +234,7 @@ const make = Effect.gen(function*() {
|
|
|
224
234
|
const refreshTokenImpl = (
|
|
225
235
|
token: OAuthToken,
|
|
226
236
|
config: OAuthConfig
|
|
227
|
-
): Effect.Effect<OAuthToken, OAuthError | FileSystemError | HomeDirectoryError |
|
|
237
|
+
): Effect.Effect<OAuthToken, OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
228
238
|
Effect.gen(function*() {
|
|
229
239
|
const updated = yield* refreshToken(token, config).pipe(
|
|
230
240
|
Effect.provide(Layer.succeed(HttpClient.HttpClient, httpClient))
|
|
@@ -243,9 +253,10 @@ const make = Effect.gen(function*() {
|
|
|
243
253
|
|
|
244
254
|
const configure = (
|
|
245
255
|
config: OAuthConfig
|
|
246
|
-
): Effect.Effect<void, FileSystemError | HomeDirectoryError |
|
|
256
|
+
): Effect.Effect<void, FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
257
|
+
saveOAuthConfigOp(config)
|
|
247
258
|
|
|
248
|
-
const isConfigured = (): Effect.Effect<boolean, FileSystemError | HomeDirectoryError |
|
|
259
|
+
const isConfigured = (): Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
249
260
|
Effect.gen(function*() {
|
|
250
261
|
const config = yield* loadOAuthConfigOp()
|
|
251
262
|
return config !== null
|
|
@@ -255,7 +266,7 @@ const make = Effect.gen(function*() {
|
|
|
255
266
|
options?: LoginOptions
|
|
256
267
|
): Effect.Effect<
|
|
257
268
|
ReadonlyArray<AccessibleSite> | void,
|
|
258
|
-
OAuthError | FileSystemError | HomeDirectoryError |
|
|
269
|
+
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
259
270
|
> =>
|
|
260
271
|
Effect.gen(function*() {
|
|
261
272
|
const config = yield* getConfig()
|
|
@@ -276,7 +287,7 @@ const make = Effect.gen(function*() {
|
|
|
276
287
|
const code = yield* codePromise.pipe(
|
|
277
288
|
Effect.timeout("5 minutes"),
|
|
278
289
|
Effect.catchTag(
|
|
279
|
-
"
|
|
290
|
+
"TimeoutError",
|
|
280
291
|
() => Effect.fail(new OAuthError({ step: "authorize", cause: "Authorization timed out" }))
|
|
281
292
|
),
|
|
282
293
|
Effect.ensuring(shutdown)
|
|
@@ -340,7 +351,10 @@ const make = Effect.gen(function*() {
|
|
|
340
351
|
return undefined
|
|
341
352
|
})
|
|
342
353
|
|
|
343
|
-
const logout = (): Effect.Effect<
|
|
354
|
+
const logout = (): Effect.Effect<
|
|
355
|
+
void,
|
|
356
|
+
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
357
|
+
> =>
|
|
344
358
|
Effect.gen(function*() {
|
|
345
359
|
const token = yield* loadTokenOp()
|
|
346
360
|
if (token === null) {
|
|
@@ -352,7 +366,7 @@ const make = Effect.gen(function*() {
|
|
|
352
366
|
if (config !== null) {
|
|
353
367
|
yield* revokeTokenImpl(token, config).pipe(
|
|
354
368
|
Effect.tap(() => Effect.log("Token revoked with Atlassian")),
|
|
355
|
-
Effect.
|
|
369
|
+
Effect.catch((error) => Effect.log(`Warning: Failed to revoke token: ${error.message}`))
|
|
356
370
|
)
|
|
357
371
|
}
|
|
358
372
|
|
|
@@ -361,7 +375,7 @@ const make = Effect.gen(function*() {
|
|
|
361
375
|
|
|
362
376
|
const getAccessToken = (): Effect.Effect<
|
|
363
377
|
Redacted.Redacted<string>,
|
|
364
|
-
AuthMissingError | OAuthError | FileSystemError | HomeDirectoryError |
|
|
378
|
+
AuthMissingError | OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
365
379
|
> =>
|
|
366
380
|
Effect.gen(function*() {
|
|
367
381
|
const token = yield* loadTokenOp()
|
|
@@ -376,7 +390,7 @@ const make = Effect.gen(function*() {
|
|
|
376
390
|
// Atomically check-then-set refresh lock to avoid TOCTOU race
|
|
377
391
|
const deferred = yield* Deferred.make<
|
|
378
392
|
OAuthToken,
|
|
379
|
-
OAuthError | FileSystemError | HomeDirectoryError |
|
|
393
|
+
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
380
394
|
>()
|
|
381
395
|
const existing = yield* Ref.modify(refreshLock, (current) =>
|
|
382
396
|
Option.isSome(current)
|
|
@@ -418,7 +432,7 @@ const make = Effect.gen(function*() {
|
|
|
418
432
|
|
|
419
433
|
const getCloudId = (): Effect.Effect<
|
|
420
434
|
string,
|
|
421
|
-
AuthMissingError | FileSystemError | HomeDirectoryError |
|
|
435
|
+
AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
422
436
|
> =>
|
|
423
437
|
Effect.gen(function*() {
|
|
424
438
|
const token = yield* loadTokenOp()
|
|
@@ -430,7 +444,7 @@ const make = Effect.gen(function*() {
|
|
|
430
444
|
|
|
431
445
|
const getSiteUrl = (): Effect.Effect<
|
|
432
446
|
string,
|
|
433
|
-
AuthMissingError | FileSystemError | HomeDirectoryError |
|
|
447
|
+
AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
434
448
|
> =>
|
|
435
449
|
Effect.gen(function*() {
|
|
436
450
|
const token = yield* loadTokenOp()
|
|
@@ -442,14 +456,14 @@ const make = Effect.gen(function*() {
|
|
|
442
456
|
|
|
443
457
|
const getCurrentUser = (): Effect.Effect<
|
|
444
458
|
OAuthUser | null,
|
|
445
|
-
FileSystemError | HomeDirectoryError |
|
|
459
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
446
460
|
> =>
|
|
447
461
|
Effect.gen(function*() {
|
|
448
462
|
const token = yield* loadTokenOp()
|
|
449
463
|
return token?.user ?? null
|
|
450
464
|
})
|
|
451
465
|
|
|
452
|
-
const isLoggedIn = (): Effect.Effect<boolean, FileSystemError | HomeDirectoryError |
|
|
466
|
+
const isLoggedIn = (): Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
453
467
|
Effect.gen(function*() {
|
|
454
468
|
const token = yield* loadTokenOp()
|
|
455
469
|
return token !== null
|
package/src/MarkdownWriter.ts
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - **Two output modes**: {@link MarkdownWriterShape.writeMulti} creates one `.md` per issue;
|
|
7
7
|
* {@link MarkdownWriterShape.writeSingle} combines all into `jira-export.md`.
|
|
8
|
-
* - **Effect service**: Requires `FileSystem` and `Path` from
|
|
8
|
+
* - **Effect service**: Requires `FileSystem` and `Path` from `effect`.
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
13
|
-
import * as Path from "@effect/platform/Path"
|
|
14
12
|
import * as Context from "effect/Context"
|
|
15
13
|
import * as Effect from "effect/Effect"
|
|
14
|
+
import * as FileSystem from "effect/FileSystem"
|
|
16
15
|
import * as Layer from "effect/Layer"
|
|
16
|
+
import * as Path from "effect/Path"
|
|
17
17
|
import { buildCombinedMarkdown, serializeIssue } from "./internal/frontmatter.js"
|
|
18
18
|
import type { Issue } from "./IssueService.js"
|
|
19
19
|
import { WriteError } from "./JiraCliError.js"
|
|
@@ -53,10 +53,10 @@ export interface MarkdownWriterShape {
|
|
|
53
53
|
*
|
|
54
54
|
* @category Services
|
|
55
55
|
*/
|
|
56
|
-
export class MarkdownWriter extends Context.
|
|
56
|
+
export class MarkdownWriter extends Context.Service<
|
|
57
57
|
MarkdownWriter,
|
|
58
58
|
MarkdownWriterShape
|
|
59
|
-
>() {}
|
|
59
|
+
>()("@knpkv/jira-cli/MarkdownWriter") {}
|
|
60
60
|
|
|
61
61
|
const make = Effect.gen(function*() {
|
|
62
62
|
const fs = yield* FileSystem.FileSystem
|
|
@@ -64,16 +64,12 @@ const make = Effect.gen(function*() {
|
|
|
64
64
|
|
|
65
65
|
const ensureDir = (dir: string): Effect.Effect<void, WriteError> =>
|
|
66
66
|
fs.makeDirectory(dir, { recursive: true }).pipe(
|
|
67
|
-
Effect.
|
|
68
|
-
Effect.fail(new WriteError({ path: dir, message: "Failed to create directory", cause }))
|
|
69
|
-
)
|
|
67
|
+
Effect.catch((cause) => Effect.fail(new WriteError({ path: dir, message: "Failed to create directory", cause })))
|
|
70
68
|
)
|
|
71
69
|
|
|
72
70
|
const writeFile = (filePath: string, content: string): Effect.Effect<void, WriteError> =>
|
|
73
71
|
fs.writeFileString(filePath, content).pipe(
|
|
74
|
-
Effect.
|
|
75
|
-
Effect.fail(new WriteError({ path: filePath, message: "Failed to write file", cause }))
|
|
76
|
-
)
|
|
72
|
+
Effect.catch((cause) => Effect.fail(new WriteError({ path: filePath, message: "Failed to write file", cause })))
|
|
77
73
|
)
|
|
78
74
|
|
|
79
75
|
const writeMulti: MarkdownWriterShape["writeMulti"] = (issues, outputDir) =>
|