@knpkv/jira-cli 0.1.1 → 1.0.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 +67 -0
- package/README.md +66 -4
- 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/JiraCliError.d.ts +30 -0
- package/dist/JiraCliError.d.ts.map +1 -1
- package/dist/JiraCliError.js +14 -0
- package/dist/JiraCliError.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/SyncWorkspace.d.ts +34 -0
- package/dist/SyncWorkspace.d.ts.map +1 -0
- package/dist/SyncWorkspace.js +105 -0
- package/dist/SyncWorkspace.js.map +1 -0
- 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 +29 -22
- 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 +4 -4
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -2
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/issue.d.ts +8 -0
- package/dist/commands/issue.d.ts.map +1 -0
- package/dist/commands/issue.js +10 -0
- package/dist/commands/issue.js.map +1 -0
- package/dist/commands/layers.d.ts +6 -18
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +35 -24
- 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 +8 -8
- 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/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +14 -1
- package/dist/internal/frontmatter.js.map +1 -1
- 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/dist/internal/sync/baseline.d.ts +11 -0
- package/dist/internal/sync/baseline.d.ts.map +1 -0
- package/dist/internal/sync/baseline.js +18 -0
- package/dist/internal/sync/baseline.js.map +1 -0
- package/dist/internal/sync/changes.d.ts +15 -0
- package/dist/internal/sync/changes.d.ts.map +1 -0
- package/dist/internal/sync/changes.js +72 -0
- package/dist/internal/sync/changes.js.map +1 -0
- package/dist/internal/sync/config.d.ts +12 -0
- package/dist/internal/sync/config.d.ts.map +1 -0
- package/dist/internal/sync/config.js +53 -0
- package/dist/internal/sync/config.js.map +1 -0
- package/dist/internal/sync/document.d.ts +9 -0
- package/dist/internal/sync/document.d.ts.map +1 -0
- package/dist/internal/sync/document.js +173 -0
- package/dist/internal/sync/document.js.map +1 -0
- package/dist/internal/sync/fieldValues.d.ts +30 -0
- package/dist/internal/sync/fieldValues.d.ts.map +1 -0
- package/dist/internal/sync/fieldValues.js +91 -0
- package/dist/internal/sync/fieldValues.js.map +1 -0
- package/dist/internal/sync/manifest.d.ts +12 -0
- package/dist/internal/sync/manifest.d.ts.map +1 -0
- package/dist/internal/sync/manifest.js +23 -0
- package/dist/internal/sync/manifest.js.map +1 -0
- package/dist/internal/sync/paths.d.ts +26 -0
- package/dist/internal/sync/paths.d.ts.map +1 -0
- package/dist/internal/sync/paths.js +22 -0
- package/dist/internal/sync/paths.js.map +1 -0
- package/dist/internal/sync/schemas.d.ts +128 -0
- package/dist/internal/sync/schemas.d.ts.map +1 -0
- package/dist/internal/sync/schemas.js +82 -0
- package/dist/internal/sync/schemas.js.map +1 -0
- package/dist/internal/sync/types.d.ts +144 -0
- package/dist/internal/sync/types.d.ts.map +1 -0
- package/dist/internal/sync/types.js +17 -0
- package/dist/internal/sync/types.js.map +1 -0
- package/package.json +13 -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/JiraCliError.ts +24 -0
- package/src/MarkdownWriter.ts +7 -11
- package/src/SyncWorkspace.ts +185 -0
- package/src/VersionService.ts +647 -0
- package/src/bin.ts +39 -29
- package/src/commands/auth.ts +6 -12
- package/src/commands/get.ts +4 -4
- package/src/commands/index.ts +2 -2
- package/src/commands/issue.ts +13 -0
- package/src/commands/layers.ts +44 -26
- package/src/commands/search.ts +8 -8
- package/src/commands/version.ts +267 -0
- package/src/internal/frontmatter.ts +15 -1
- package/src/internal/oauthServer.ts +43 -70
- package/src/internal/openBrowser.ts +31 -0
- package/src/internal/sync/baseline.ts +27 -0
- package/src/internal/sync/changes.ts +118 -0
- package/src/internal/sync/config.ts +76 -0
- package/src/internal/sync/document.ts +201 -0
- package/src/internal/sync/fieldValues.ts +145 -0
- package/src/internal/sync/manifest.ts +32 -0
- package/src/internal/sync/paths.ts +48 -0
- package/src/internal/sync/schemas.ts +103 -0
- package/src/internal/sync/types.ts +192 -0
- package/test/SyncWorkspace.test.ts +76 -0
- package/test/VersionService.test.ts +266 -0
- package/test/commandTree.test.ts +266 -0
- package/test/frontmatter.test.ts +69 -0
- package/test/integration.test.ts +187 -0
- package/test/syncChanges.test.ts +106 -0
- package/test/syncConfig.test.ts +138 -0
- package/test/syncDocument.test.ts +69 -0
- package/test/syncFieldValues.test.ts +101 -0
- package/vitest.config.integration.ts +17 -0
- package/vitest.config.ts +6 -0
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/JiraCliError.ts
CHANGED
|
@@ -42,3 +42,27 @@ export class WriteError extends Data.TaggedError("WriteError")<{
|
|
|
42
42
|
readonly message: string
|
|
43
43
|
readonly cause?: unknown
|
|
44
44
|
}> {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Error when reading or writing Jira Markdown Sync workspace state.
|
|
48
|
+
*
|
|
49
|
+
* @category Errors
|
|
50
|
+
*/
|
|
51
|
+
export class SyncWorkspaceError extends Data.TaggedError("SyncWorkspaceError")<{
|
|
52
|
+
readonly message: string
|
|
53
|
+
readonly path?: string | undefined
|
|
54
|
+
readonly cause?: unknown
|
|
55
|
+
}> {}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error when local Jira Markdown Sync configuration or data fails validation.
|
|
59
|
+
*
|
|
60
|
+
* @category Errors
|
|
61
|
+
*/
|
|
62
|
+
export class SyncValidationError extends Data.TaggedError("SyncValidationError")<{
|
|
63
|
+
readonly message: string
|
|
64
|
+
readonly issueKey?: string | undefined
|
|
65
|
+
readonly field?: string | undefined
|
|
66
|
+
readonly path?: string | undefined
|
|
67
|
+
readonly cause?: unknown
|
|
68
|
+
}> {}
|
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) =>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local workspace I/O for Jira Markdown Sync.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import * as Context from "effect/Context"
|
|
7
|
+
import * as Effect from "effect/Effect"
|
|
8
|
+
import * as FileSystem from "effect/FileSystem"
|
|
9
|
+
import * as Layer from "effect/Layer"
|
|
10
|
+
import * as Path from "effect/Path"
|
|
11
|
+
import { parseSyncBaseline, serializeSyncBaseline } from "./internal/sync/baseline.js"
|
|
12
|
+
import { makeDefaultWorkspaceConfig, parseWorkspaceConfig, serializeWorkspaceConfig } from "./internal/sync/config.js"
|
|
13
|
+
import { makeEmptyManifest, parseSyncManifest, serializeSyncManifest } from "./internal/sync/manifest.js"
|
|
14
|
+
import {
|
|
15
|
+
baselineFilePath,
|
|
16
|
+
conventionDocumentPath,
|
|
17
|
+
resolveWorkspacePaths,
|
|
18
|
+
type SyncWorkspacePaths
|
|
19
|
+
} from "./internal/sync/paths.js"
|
|
20
|
+
import type { SyncBaseline, SyncManifest, WorkspaceConfig } from "./internal/sync/types.js"
|
|
21
|
+
import { SyncWorkspaceError } from "./JiraCliError.js"
|
|
22
|
+
|
|
23
|
+
export interface InitWorkspaceInput {
|
|
24
|
+
readonly root: string
|
|
25
|
+
readonly siteUrl: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SyncWorkspaceShape {
|
|
29
|
+
readonly init: (input: InitWorkspaceInput) => Effect.Effect<SyncWorkspacePaths, SyncWorkspaceError>
|
|
30
|
+
readonly paths: (root: string, config?: Pick<WorkspaceConfig, "documentsDir">) => Effect.Effect<SyncWorkspacePaths>
|
|
31
|
+
readonly readConfig: (root: string) => Effect.Effect<WorkspaceConfig, SyncWorkspaceError>
|
|
32
|
+
readonly writeConfig: (root: string, config: WorkspaceConfig) => Effect.Effect<void, SyncWorkspaceError>
|
|
33
|
+
readonly readManifest: (root: string) => Effect.Effect<SyncManifest, SyncWorkspaceError>
|
|
34
|
+
readonly writeManifest: (root: string, manifest: SyncManifest) => Effect.Effect<void, SyncWorkspaceError>
|
|
35
|
+
readonly readBaseline: (root: string, issueId: string) => Effect.Effect<SyncBaseline, SyncWorkspaceError>
|
|
36
|
+
readonly writeBaseline: (
|
|
37
|
+
root: string,
|
|
38
|
+
baseline: SyncBaseline
|
|
39
|
+
) => Effect.Effect<void, SyncWorkspaceError>
|
|
40
|
+
readonly conventionDocumentPath: (root: string, issueKey: string) => Effect.Effect<string, SyncWorkspaceError>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class SyncWorkspace extends Context.Service<
|
|
44
|
+
SyncWorkspace,
|
|
45
|
+
SyncWorkspaceShape
|
|
46
|
+
>()("@knpkv/jira-cli/SyncWorkspace") {}
|
|
47
|
+
|
|
48
|
+
const mapWorkspaceError = (message: string, path?: string | undefined) => (cause: unknown) =>
|
|
49
|
+
new SyncWorkspaceError({ message, path, cause })
|
|
50
|
+
|
|
51
|
+
const make = Effect.gen(function*() {
|
|
52
|
+
const fs = yield* FileSystem.FileSystem
|
|
53
|
+
const path = yield* Path.Path
|
|
54
|
+
|
|
55
|
+
const paths: SyncWorkspaceShape["paths"] = (root, config) => Effect.succeed(resolveWorkspacePaths(path, root, config))
|
|
56
|
+
|
|
57
|
+
const ensureDir = (dir: string): Effect.Effect<void, SyncWorkspaceError> =>
|
|
58
|
+
fs.makeDirectory(dir, { recursive: true }).pipe(
|
|
59
|
+
Effect.mapError(mapWorkspaceError("Failed to create sync workspace directory", dir)),
|
|
60
|
+
Effect.asVoid
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const writeFile = (filePath: string, content: string): Effect.Effect<void, SyncWorkspaceError> =>
|
|
64
|
+
Effect.gen(function*() {
|
|
65
|
+
const dir = path.dirname(filePath)
|
|
66
|
+
yield* ensureDir(dir)
|
|
67
|
+
const tempPath = `${filePath}.tmp.${Date.now()}`
|
|
68
|
+
yield* fs.writeFileString(tempPath, content).pipe(
|
|
69
|
+
Effect.mapError(mapWorkspaceError("Failed to write sync workspace file", filePath))
|
|
70
|
+
)
|
|
71
|
+
yield* fs.rename(tempPath, filePath).pipe(
|
|
72
|
+
Effect.mapError(mapWorkspaceError("Failed to replace sync workspace file", filePath))
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const readFile = (filePath: string): Effect.Effect<string, SyncWorkspaceError> =>
|
|
77
|
+
fs.readFileString(filePath).pipe(
|
|
78
|
+
Effect.mapError(mapWorkspaceError("Failed to read sync workspace file", filePath))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const init: SyncWorkspaceShape["init"] = ({ root, siteUrl }) =>
|
|
82
|
+
Effect.gen(function*() {
|
|
83
|
+
const config = makeDefaultWorkspaceConfig(siteUrl)
|
|
84
|
+
const workspacePaths = resolveWorkspacePaths(path, root, config)
|
|
85
|
+
yield* ensureDir(workspacePaths.documentsDir)
|
|
86
|
+
yield* ensureDir(workspacePaths.metadataDir)
|
|
87
|
+
yield* ensureDir(workspacePaths.baselinesDir)
|
|
88
|
+
yield* ensureDir(workspacePaths.historyDir)
|
|
89
|
+
yield* writeFile(workspacePaths.configFile, serializeWorkspaceConfig(config))
|
|
90
|
+
yield* writeFile(workspacePaths.manifestFile, serializeSyncManifest(makeEmptyManifest(siteUrl)))
|
|
91
|
+
return workspacePaths
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const readConfig: SyncWorkspaceShape["readConfig"] = (root) =>
|
|
95
|
+
Effect.gen(function*() {
|
|
96
|
+
const workspacePaths = resolveWorkspacePaths(path, root)
|
|
97
|
+
const content = yield* readFile(workspacePaths.configFile)
|
|
98
|
+
return yield* parseWorkspaceConfig(workspacePaths.configFile, content).pipe(
|
|
99
|
+
Effect.mapError((cause) =>
|
|
100
|
+
new SyncWorkspaceError({
|
|
101
|
+
message: cause.message,
|
|
102
|
+
path: "path" in cause ? cause.path : workspacePaths.configFile,
|
|
103
|
+
cause
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const writeConfig: SyncWorkspaceShape["writeConfig"] = (root, config) =>
|
|
110
|
+
Effect.gen(function*() {
|
|
111
|
+
const workspacePaths = resolveWorkspacePaths(path, root, config)
|
|
112
|
+
yield* ensureDir(workspacePaths.metadataDir)
|
|
113
|
+
yield* writeFile(workspacePaths.configFile, serializeWorkspaceConfig(config))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const readManifest: SyncWorkspaceShape["readManifest"] = (root) =>
|
|
117
|
+
Effect.gen(function*() {
|
|
118
|
+
const workspacePaths = resolveWorkspacePaths(path, root)
|
|
119
|
+
const content = yield* readFile(workspacePaths.manifestFile)
|
|
120
|
+
return yield* parseSyncManifest(workspacePaths.manifestFile, content).pipe(
|
|
121
|
+
Effect.mapError((cause) =>
|
|
122
|
+
new SyncWorkspaceError({
|
|
123
|
+
message: cause.message,
|
|
124
|
+
path: "path" in cause ? cause.path : workspacePaths.manifestFile,
|
|
125
|
+
cause
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const writeManifest: SyncWorkspaceShape["writeManifest"] = (root, manifest) =>
|
|
132
|
+
Effect.gen(function*() {
|
|
133
|
+
const workspacePaths = resolveWorkspacePaths(path, root)
|
|
134
|
+
yield* ensureDir(workspacePaths.metadataDir)
|
|
135
|
+
yield* writeFile(workspacePaths.manifestFile, serializeSyncManifest(manifest))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const readBaseline: SyncWorkspaceShape["readBaseline"] = (root, issueId) =>
|
|
139
|
+
Effect.gen(function*() {
|
|
140
|
+
const workspacePaths = resolveWorkspacePaths(path, root)
|
|
141
|
+
const filePath = baselineFilePath(path, workspacePaths, issueId)
|
|
142
|
+
const content = yield* readFile(filePath)
|
|
143
|
+
return yield* parseSyncBaseline(filePath, content).pipe(
|
|
144
|
+
Effect.mapError((cause) =>
|
|
145
|
+
new SyncWorkspaceError({
|
|
146
|
+
message: cause.message,
|
|
147
|
+
path: "path" in cause ? cause.path : filePath,
|
|
148
|
+
cause
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const writeBaseline: SyncWorkspaceShape["writeBaseline"] = (root, baseline) =>
|
|
155
|
+
Effect.gen(function*() {
|
|
156
|
+
const workspacePaths = resolveWorkspacePaths(path, root)
|
|
157
|
+
const filePath = baselineFilePath(path, workspacePaths, baseline.issueId)
|
|
158
|
+
yield* ensureDir(workspacePaths.baselinesDir)
|
|
159
|
+
yield* writeFile(filePath, serializeSyncBaseline(baseline))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const getConventionDocumentPath: SyncWorkspaceShape["conventionDocumentPath"] = (root, issueKey) =>
|
|
163
|
+
Effect.gen(function*() {
|
|
164
|
+
const config = yield* readConfig(root)
|
|
165
|
+
const workspacePaths = resolveWorkspacePaths(path, root, config)
|
|
166
|
+
return conventionDocumentPath(path, workspacePaths, issueKey)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return SyncWorkspace.of({
|
|
170
|
+
init,
|
|
171
|
+
paths,
|
|
172
|
+
readConfig,
|
|
173
|
+
writeConfig,
|
|
174
|
+
readManifest,
|
|
175
|
+
writeManifest,
|
|
176
|
+
readBaseline,
|
|
177
|
+
writeBaseline,
|
|
178
|
+
conventionDocumentPath: getConventionDocumentPath
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
export const layer: Layer.Layer<SyncWorkspace, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
|
|
183
|
+
SyncWorkspace,
|
|
184
|
+
make
|
|
185
|
+
)
|