@knpkv/jira-cli 0.3.0 → 1.1.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 +38 -0
- package/README.md +24 -9
- package/dist/JiraAuth.d.ts +9 -1
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +26 -8
- 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/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/bin.js +4 -5
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +49 -5
- package/dist/commands/auth.js.map +1 -1
- 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 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -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.map +1 -1
- package/dist/commands/layers.js +8 -1
- package/dist/commands/layers.js.map +1 -1
- 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.js +13 -13
- package/dist/commands/version.js.map +1 -1
- 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/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 +7 -4
- package/skills/jira/SKILL.md +25 -13
- package/src/JiraAuth.ts +74 -14
- package/src/JiraCliError.ts +24 -0
- package/src/SyncWorkspace.ts +185 -0
- package/src/bin.ts +4 -6
- package/src/commands/auth.ts +59 -5
- package/src/commands/get.ts +2 -2
- package/src/commands/index.ts +1 -2
- package/src/commands/issue.ts +13 -0
- package/src/commands/layers.ts +8 -1
- package/src/commands/search.ts +4 -4
- package/src/commands/version.ts +15 -15
- package/src/internal/frontmatter.ts +15 -1
- 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/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 +1 -0
package/src/JiraAuth.ts
CHANGED
|
@@ -35,18 +35,23 @@ import {
|
|
|
35
35
|
revokeToken
|
|
36
36
|
} from "@knpkv/atlassian-common/auth"
|
|
37
37
|
import {
|
|
38
|
-
|
|
38
|
+
type AuthProfile,
|
|
39
|
+
deleteActiveProfile,
|
|
40
|
+
deleteProfileBySelector,
|
|
39
41
|
type FileSystemError,
|
|
40
42
|
type HomeDirectoryError,
|
|
41
43
|
HomeDirectoryLive,
|
|
42
44
|
isTokenExpired,
|
|
45
|
+
loadActiveProfile,
|
|
46
|
+
loadActiveProfileToken,
|
|
43
47
|
loadOAuthConfig,
|
|
44
|
-
|
|
48
|
+
loadProfiles,
|
|
45
49
|
type OAuthConfig,
|
|
46
50
|
type OAuthToken,
|
|
47
51
|
type OAuthUser,
|
|
48
52
|
saveOAuthConfig,
|
|
49
|
-
|
|
53
|
+
saveProfileToken,
|
|
54
|
+
setActiveProfileBySelector
|
|
50
55
|
} from "@knpkv/atlassian-common/config"
|
|
51
56
|
import * as Console from "effect/Console"
|
|
52
57
|
import * as Context from "effect/Context"
|
|
@@ -92,9 +97,16 @@ const TokenStorageLive = Layer.mergeAll(
|
|
|
92
97
|
)
|
|
93
98
|
|
|
94
99
|
// Wrap token storage operations with their required layers
|
|
95
|
-
const loadTokenOp = () =>
|
|
96
|
-
const saveTokenOp = (token: OAuthToken) =>
|
|
97
|
-
const deleteTokenOp = () =>
|
|
100
|
+
const loadTokenOp = () => loadActiveProfileToken(TOOL_NAME).pipe(Effect.provide(TokenStorageLive))
|
|
101
|
+
const saveTokenOp = (token: OAuthToken) => saveProfileToken(TOOL_NAME, token).pipe(Effect.provide(TokenStorageLive))
|
|
102
|
+
const deleteTokenOp = () => deleteActiveProfile(TOOL_NAME).pipe(Effect.provide(TokenStorageLive))
|
|
103
|
+
const listProfilesOp = () =>
|
|
104
|
+
loadProfiles(TOOL_NAME).pipe(Effect.map((store) => store.profiles), Effect.provide(TokenStorageLive))
|
|
105
|
+
const loadActiveProfileOp = () => loadActiveProfile(TOOL_NAME).pipe(Effect.provide(TokenStorageLive))
|
|
106
|
+
const switchProfileOp = (selector: string) =>
|
|
107
|
+
setActiveProfileBySelector(TOOL_NAME, selector).pipe(Effect.provide(TokenStorageLive))
|
|
108
|
+
const removeProfileOp = (selector: string) =>
|
|
109
|
+
deleteProfileBySelector(TOOL_NAME, selector).pipe(Effect.provide(TokenStorageLive))
|
|
98
110
|
const loadOAuthConfigOp = () => loadOAuthConfig(TOOL_NAME).pipe(Effect.provide(TokenStorageLive))
|
|
99
111
|
const saveOAuthConfigOp = (config: OAuthConfig) =>
|
|
100
112
|
saveOAuthConfig(TOOL_NAME, config).pipe(Effect.provide(TokenStorageLive))
|
|
@@ -167,6 +179,24 @@ export interface JiraAuthService {
|
|
|
167
179
|
OAuthUser | null,
|
|
168
180
|
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
169
181
|
>
|
|
182
|
+
/** Get active auth profile */
|
|
183
|
+
readonly getActiveProfile: () => Effect.Effect<
|
|
184
|
+
AuthProfile | null,
|
|
185
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
186
|
+
>
|
|
187
|
+
/** List stored auth profiles */
|
|
188
|
+
readonly listProfiles: () => Effect.Effect<
|
|
189
|
+
ReadonlyArray<AuthProfile>,
|
|
190
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
191
|
+
>
|
|
192
|
+
/** Switch active profile by ID, name, site URL, cloud ID, or account ID */
|
|
193
|
+
readonly switchProfile: (
|
|
194
|
+
selector: string
|
|
195
|
+
) => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
196
|
+
/** Remove stored profile by ID, name, site URL, cloud ID, or account ID */
|
|
197
|
+
readonly removeProfile: (
|
|
198
|
+
selector: string
|
|
199
|
+
) => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
170
200
|
/** Check if user is logged in */
|
|
171
201
|
readonly isLoggedIn: () => Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
172
202
|
}
|
|
@@ -403,14 +433,11 @@ const make = Effect.gen(function*() {
|
|
|
403
433
|
return Redacted.make(refreshed.access_token)
|
|
404
434
|
}
|
|
405
435
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
Effect.tap((refreshed) => Deferred.succeed(deferred, refreshed)),
|
|
412
|
-
Effect.tapError((error) => Deferred.fail(deferred, error)),
|
|
413
|
-
Effect.ensuring(Ref.set(refreshLock, Option.none())),
|
|
436
|
+
const refresh = Effect.gen(function*() {
|
|
437
|
+
const config = yield* getConfig()
|
|
438
|
+
yield* Console.log("Token expired, refreshing...")
|
|
439
|
+
return yield* refreshTokenImpl(token, config)
|
|
440
|
+
}).pipe(
|
|
414
441
|
Effect.catchTag("OAuthError", (error) => {
|
|
415
442
|
if (error.step === "refresh") {
|
|
416
443
|
return Effect.gen(function*() {
|
|
@@ -427,6 +454,15 @@ const make = Effect.gen(function*() {
|
|
|
427
454
|
})
|
|
428
455
|
)
|
|
429
456
|
|
|
457
|
+
// This fiber owns the refresh. Complete the shared Deferred with the final
|
|
458
|
+
// transformed exit so waiters observe the same success or failure.
|
|
459
|
+
const exit = yield* refresh.pipe(
|
|
460
|
+
Effect.exit,
|
|
461
|
+
Effect.ensuring(Ref.set(refreshLock, Option.none()))
|
|
462
|
+
)
|
|
463
|
+
yield* Deferred.done(deferred, exit)
|
|
464
|
+
const result = yield* Deferred.await(deferred)
|
|
465
|
+
|
|
430
466
|
return Redacted.make(result.access_token)
|
|
431
467
|
})
|
|
432
468
|
|
|
@@ -463,6 +499,26 @@ const make = Effect.gen(function*() {
|
|
|
463
499
|
return token?.user ?? null
|
|
464
500
|
})
|
|
465
501
|
|
|
502
|
+
const getActiveProfile = (): Effect.Effect<
|
|
503
|
+
AuthProfile | null,
|
|
504
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
505
|
+
> => loadActiveProfileOp()
|
|
506
|
+
|
|
507
|
+
const listProfiles = (): Effect.Effect<
|
|
508
|
+
ReadonlyArray<AuthProfile>,
|
|
509
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
510
|
+
> => listProfilesOp()
|
|
511
|
+
|
|
512
|
+
const switchProfile = (
|
|
513
|
+
selector: string
|
|
514
|
+
): Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
515
|
+
switchProfileOp(selector)
|
|
516
|
+
|
|
517
|
+
const removeProfile = (
|
|
518
|
+
selector: string
|
|
519
|
+
): Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
520
|
+
removeProfileOp(selector)
|
|
521
|
+
|
|
466
522
|
const isLoggedIn = (): Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError> =>
|
|
467
523
|
Effect.gen(function*() {
|
|
468
524
|
const token = yield* loadTokenOp()
|
|
@@ -478,6 +534,10 @@ const make = Effect.gen(function*() {
|
|
|
478
534
|
getCloudId,
|
|
479
535
|
getSiteUrl,
|
|
480
536
|
getCurrentUser,
|
|
537
|
+
getActiveProfile,
|
|
538
|
+
listProfiles,
|
|
539
|
+
switchProfile,
|
|
540
|
+
removeProfile,
|
|
481
541
|
isLoggedIn
|
|
482
542
|
})
|
|
483
543
|
})
|
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
|
+
}> {}
|
|
@@ -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
|
+
)
|
package/src/bin.ts
CHANGED
|
@@ -17,11 +17,10 @@ import {
|
|
|
17
17
|
AppLayer,
|
|
18
18
|
authCommand,
|
|
19
19
|
AuthOnlyLayer,
|
|
20
|
-
getCommand,
|
|
21
20
|
getLayerType,
|
|
22
21
|
handleError,
|
|
22
|
+
issueCommand,
|
|
23
23
|
MinimalLayer,
|
|
24
|
-
searchCommand,
|
|
25
24
|
versionCommand
|
|
26
25
|
} from "./commands/index.js"
|
|
27
26
|
|
|
@@ -32,17 +31,16 @@ const skillsInstall = makeInstallCommand({
|
|
|
32
31
|
})
|
|
33
32
|
|
|
34
33
|
const skillsCommand = Command.make("skills", {}, () => Console.log("Usage: jira skills install")).pipe(
|
|
35
|
-
Command.withDescription("
|
|
34
|
+
Command.withDescription("Local write agent skill commands"),
|
|
36
35
|
Command.withSubcommands([skillsInstall])
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
// === Main command ===
|
|
40
39
|
const jira = Command.make("jira").pipe(
|
|
41
|
-
Command.withDescription("
|
|
40
|
+
Command.withDescription("Jira CLI commands"),
|
|
42
41
|
Command.withSubcommands([
|
|
43
42
|
authCommand,
|
|
44
|
-
|
|
45
|
-
searchCommand,
|
|
43
|
+
issueCommand,
|
|
46
44
|
skillsCommand,
|
|
47
45
|
versionCommand
|
|
48
46
|
])
|
package/src/commands/auth.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import * as Console from "effect/Console"
|
|
12
12
|
import * as Effect from "effect/Effect"
|
|
13
13
|
import * as Option from "effect/Option"
|
|
14
|
-
import { Command, Flag as Options, Prompt } from "effect/unstable/cli"
|
|
14
|
+
import { Argument as Args, Command, Flag as Options, Prompt } from "effect/unstable/cli"
|
|
15
15
|
import { openBrowser } from "../internal/openBrowser.js"
|
|
16
16
|
import { JiraAuth } from "../JiraAuth.js"
|
|
17
17
|
|
|
@@ -97,14 +97,65 @@ const logoutCommand = Command.make("logout", {}, () =>
|
|
|
97
97
|
const statusCommand = Command.make("status", {}, () =>
|
|
98
98
|
Effect.gen(function*() {
|
|
99
99
|
const auth = yield* JiraAuth
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
100
|
+
const profile = yield* auth.getActiveProfile()
|
|
101
|
+
if (profile) {
|
|
102
|
+
const user = profile.token.user
|
|
103
|
+
const account = user ? `${user.name} (${user.email})` : "unknown user"
|
|
104
|
+
yield* Console.log(`Active profile: ${profile.name}`)
|
|
105
|
+
yield* Console.log(`Account: ${account}`)
|
|
106
|
+
yield* Console.log(`Site: ${profile.token.site_url}`)
|
|
107
|
+
yield* Console.log(`Profile ID: ${profile.id}`)
|
|
103
108
|
} else {
|
|
104
109
|
yield* Console.log("Not logged in. Use 'jira auth login' to authenticate.")
|
|
105
110
|
}
|
|
106
111
|
})).pipe(Command.withDescription("Show authentication status"))
|
|
107
112
|
|
|
113
|
+
// === Auth profiles command ===
|
|
114
|
+
const profilesCommand = Command.make("profiles", {}, () =>
|
|
115
|
+
Effect.gen(function*() {
|
|
116
|
+
const auth = yield* JiraAuth
|
|
117
|
+
const [profiles, active] = yield* Effect.all([auth.listProfiles(), auth.getActiveProfile()])
|
|
118
|
+
if (profiles.length === 0) {
|
|
119
|
+
yield* Console.log("No auth profiles. Use 'jira auth login' to authenticate.")
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const profile of profiles) {
|
|
124
|
+
const marker = active?.id === profile.id ? "*" : " "
|
|
125
|
+
yield* Console.log(`${marker} ${profile.id}`)
|
|
126
|
+
yield* Console.log(` ${profile.name}`)
|
|
127
|
+
yield* Console.log(` ${profile.token.site_url}`)
|
|
128
|
+
}
|
|
129
|
+
})).pipe(Command.withDescription("List stored auth profiles"))
|
|
130
|
+
|
|
131
|
+
const profileArg = Args.string("profile").pipe(
|
|
132
|
+
Args.withDescription("Profile ID, name, site URL, cloud ID, or account ID")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// === Auth use command ===
|
|
136
|
+
const useCommand = Command.make("use", { profile: profileArg }, ({ profile }) =>
|
|
137
|
+
Effect.gen(function*() {
|
|
138
|
+
const auth = yield* JiraAuth
|
|
139
|
+
const selected = yield* auth.switchProfile(profile)
|
|
140
|
+
if (!selected) {
|
|
141
|
+
yield* Console.log(`Profile not found: ${profile}`)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
yield* Console.log(`Active profile: ${selected.name}`)
|
|
145
|
+
})).pipe(Command.withDescription("Switch active auth profile"))
|
|
146
|
+
|
|
147
|
+
// === Auth remove command ===
|
|
148
|
+
const removeCommand = Command.make("remove", { profile: profileArg }, ({ profile }) =>
|
|
149
|
+
Effect.gen(function*() {
|
|
150
|
+
const auth = yield* JiraAuth
|
|
151
|
+
const removed = yield* auth.removeProfile(profile)
|
|
152
|
+
if (!removed) {
|
|
153
|
+
yield* Console.log(`Profile not found: ${profile}`)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
yield* Console.log(`Removed profile: ${removed.name}`)
|
|
157
|
+
})).pipe(Command.withDescription("Remove stored auth profile"))
|
|
158
|
+
|
|
108
159
|
// === Auth command group ===
|
|
109
160
|
export const authCommand = Command.make("auth").pipe(
|
|
110
161
|
Command.withDescription("Manage OAuth authentication"),
|
|
@@ -113,6 +164,9 @@ export const authCommand = Command.make("auth").pipe(
|
|
|
113
164
|
configureCommand,
|
|
114
165
|
loginCommand,
|
|
115
166
|
logoutCommand,
|
|
116
|
-
statusCommand
|
|
167
|
+
statusCommand,
|
|
168
|
+
profilesCommand,
|
|
169
|
+
useCommand,
|
|
170
|
+
removeCommand
|
|
117
171
|
])
|
|
118
172
|
)
|
package/src/commands/get.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `jira get <key>` command — fetches a single issue and writes to Markdown.
|
|
2
|
+
* `jira issue get <key>` command — fetches a single issue and writes to Markdown.
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
@@ -39,4 +39,4 @@ export const getCommand = Command.make(
|
|
|
39
39
|
|
|
40
40
|
yield* Console.log(`Done.`)
|
|
41
41
|
})
|
|
42
|
-
).pipe(Command.withDescription("
|
|
42
|
+
).pipe(Command.withDescription("Read-only: get a single Jira issue by key"))
|
package/src/commands/index.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
export { authCommand } from "./auth.js"
|
|
8
8
|
export { handleError } from "./errorHandler.js"
|
|
9
|
-
export {
|
|
9
|
+
export { issueCommand } from "./issue.js"
|
|
10
10
|
export { AppLayer, AuthOnlyLayer, getLayerType, MinimalLayer } from "./layers.js"
|
|
11
|
-
export { searchCommand } from "./search.js"
|
|
12
11
|
export { versionCommand } from "./version.js"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jira issue` command namespace.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from "effect/unstable/cli"
|
|
7
|
+
import { getCommand } from "./get.js"
|
|
8
|
+
import { searchCommand } from "./search.js"
|
|
9
|
+
|
|
10
|
+
export const issueCommand = Command.make("issue").pipe(
|
|
11
|
+
Command.withDescription("Read-only Jira issue commands"),
|
|
12
|
+
Command.withSubcommands([getCommand, searchCommand])
|
|
13
|
+
)
|
package/src/commands/layers.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - **Lazy layer selection**: {@link getLayerType} inspects CLI arguments from `Stdio` to pick
|
|
7
7
|
* the smallest layer needed — `"minimal"` for help/version, `"auth"` for auth commands,
|
|
8
|
-
* `"full"` for
|
|
8
|
+
* `"full"` for issue/version reads and writes.
|
|
9
9
|
* - **Dummy services**: Auth-only and minimal layers provide dying stubs
|
|
10
10
|
* for unused services to satisfy the type system without initialization cost.
|
|
11
11
|
*
|
|
@@ -61,6 +61,10 @@ const DummyJiraAuthLayer = Layer.succeed(
|
|
|
61
61
|
getCloudId: () => Effect.die(new Error("Not configured")),
|
|
62
62
|
getSiteUrl: () => Effect.die(new Error("Not configured")),
|
|
63
63
|
getCurrentUser: () => Effect.succeed(null),
|
|
64
|
+
getActiveProfile: () => Effect.succeed(null),
|
|
65
|
+
listProfiles: () => Effect.succeed([]),
|
|
66
|
+
switchProfile: () => Effect.succeed(null),
|
|
67
|
+
removeProfile: () => Effect.succeed(null),
|
|
64
68
|
isLoggedIn: () => Effect.succeed(false)
|
|
65
69
|
})
|
|
66
70
|
)
|
|
@@ -147,6 +151,9 @@ export const MinimalLayer = DummyIssueServiceLayer.pipe(
|
|
|
147
151
|
*/
|
|
148
152
|
export const getLayerType = (args: ReadonlyArray<string>): "full" | "auth" | "minimal" => {
|
|
149
153
|
const cmd = args[0]
|
|
154
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
155
|
+
return "minimal"
|
|
156
|
+
}
|
|
150
157
|
if (cmd === "auth") {
|
|
151
158
|
return "auth"
|
|
152
159
|
}
|
package/src/commands/search.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `jira search` command — JQL search with multi/single Markdown output.
|
|
2
|
+
* `jira issue search` command — JQL search with multi/single Markdown output.
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
@@ -74,8 +74,8 @@ export const searchCommand = Command.make(
|
|
|
74
74
|
query = jql.value
|
|
75
75
|
} else {
|
|
76
76
|
yield* Console.log("Error: Either a JQL query or --by-version must be provided.")
|
|
77
|
-
yield* Console.log("Usage: jira search <jql>")
|
|
78
|
-
yield* Console.log(" jira search --by-version <version>")
|
|
77
|
+
yield* Console.log("Usage: jira issue search <jql>")
|
|
78
|
+
yield* Console.log(" jira issue search --by-version <version>")
|
|
79
79
|
return
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -99,4 +99,4 @@ export const searchCommand = Command.make(
|
|
|
99
99
|
yield* Console.log(`Exported ${issues.length} file(s) to ${outputDir}/`)
|
|
100
100
|
}
|
|
101
101
|
})
|
|
102
|
-
).pipe(Command.withDescription("
|
|
102
|
+
).pipe(Command.withDescription("Read-only: search Jira issues and export to markdown"))
|
package/src/commands/version.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `jira version` command — list /
|
|
2
|
+
* `jira version` command — list / get Jira project versions (releases) with
|
|
3
3
|
* Driver, Contributors and Approver fields resolved to display names, plus
|
|
4
4
|
* mutations: edit the description and manage "Related work" links (the
|
|
5
5
|
* Confluence pages surfaced on a release report).
|
|
@@ -126,13 +126,13 @@ const listCommand = Command.make("list", {
|
|
|
126
126
|
v.approvers.map((a) => `${a.person.displayName}:${a.status}`).join("|") || "-"
|
|
127
127
|
].join(sep))
|
|
128
128
|
}
|
|
129
|
-
})).pipe(Command.withDescription("
|
|
129
|
+
})).pipe(Command.withDescription("Read-only: list versions for a Jira project"))
|
|
130
130
|
|
|
131
|
-
/** Cap on the number of ticket keys listed in the human `
|
|
131
|
+
/** Cap on the number of ticket keys listed in the human `get` output. */
|
|
132
132
|
const TICKET_KEYS_LIMIT = 20
|
|
133
133
|
|
|
134
|
-
const
|
|
135
|
-
"
|
|
134
|
+
const getCommand = Command.make(
|
|
135
|
+
"get",
|
|
136
136
|
{ id: idArg, json: jsonOption, emails: emailsOption },
|
|
137
137
|
({ emails, id, json }) =>
|
|
138
138
|
Effect.gen(function*() {
|
|
@@ -154,10 +154,10 @@ const viewCommand = Command.make(
|
|
|
154
154
|
)
|
|
155
155
|
yield* Console.log(`tickets (${version.tickets.length}): ${formatTicketKeys(version.tickets)}`)
|
|
156
156
|
})
|
|
157
|
-
).pipe(Command.withDescription("
|
|
157
|
+
).pipe(Command.withDescription("Read-only: get a single Jira version"))
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* Render a version's ticket keys for the human `
|
|
160
|
+
* Render a version's ticket keys for the human `get`: the first
|
|
161
161
|
* {@link TICKET_KEYS_LIMIT} keys, with a `(+M more)` suffix when truncated, or
|
|
162
162
|
* `-` when there are none.
|
|
163
163
|
*/
|
|
@@ -174,7 +174,7 @@ const descriptionOption = Options.string("description").pipe(
|
|
|
174
174
|
Options.withDescription("New version description")
|
|
175
175
|
)
|
|
176
176
|
|
|
177
|
-
const
|
|
177
|
+
const updateCommand = Command.make("update", { id: idArg, description: descriptionOption, json: jsonOption }, ({
|
|
178
178
|
description,
|
|
179
179
|
id,
|
|
180
180
|
json
|
|
@@ -190,10 +190,10 @@ const setCommand = Command.make("set", { id: idArg, description: descriptionOpti
|
|
|
190
190
|
yield* Console.log(`Updated version ${version.name} (${version.id})`)
|
|
191
191
|
yield* Console.log(`description: ${version.description ?? "-"}`)
|
|
192
192
|
})).pipe(
|
|
193
|
-
Command.withDescription("
|
|
193
|
+
Command.withDescription("Remote write: update a version's description (requires manage:jira-project scope)")
|
|
194
194
|
)
|
|
195
195
|
|
|
196
|
-
// ===
|
|
196
|
+
// === related-work ===
|
|
197
197
|
|
|
198
198
|
const titleOption = Options.string("title").pipe(
|
|
199
199
|
Options.withAlias("t"),
|
|
@@ -231,7 +231,7 @@ const relatedWorkListCommand = Command.make(
|
|
|
231
231
|
yield* Console.log([w.category || "-", w.title ?? "-", w.url ?? "-"].join(sep))
|
|
232
232
|
}
|
|
233
233
|
})
|
|
234
|
-
).pipe(Command.withDescription("
|
|
234
|
+
).pipe(Command.withDescription("Read-only: list a version's related-work links"))
|
|
235
235
|
|
|
236
236
|
const relatedWorkAddCommand = Command.make("add", {
|
|
237
237
|
id: idArg,
|
|
@@ -252,16 +252,16 @@ const relatedWorkAddCommand = Command.make("add", {
|
|
|
252
252
|
yield* Console.log(`url: ${created.url ?? url}`)
|
|
253
253
|
})).pipe(
|
|
254
254
|
Command.withDescription(
|
|
255
|
-
"
|
|
255
|
+
"Remote write: attach a related-work link (e.g. a Confluence page) to a version (requires manage:jira-project scope)"
|
|
256
256
|
)
|
|
257
257
|
)
|
|
258
258
|
|
|
259
|
-
const relatedWorkCommand = Command.make("
|
|
259
|
+
const relatedWorkCommand = Command.make("related-work").pipe(
|
|
260
260
|
Command.withDescription("List or attach version related-work links (Confluence pages on the release report)"),
|
|
261
261
|
Command.withSubcommands([relatedWorkListCommand, relatedWorkAddCommand])
|
|
262
262
|
)
|
|
263
263
|
|
|
264
264
|
export const versionCommand = Command.make("version").pipe(
|
|
265
|
-
Command.withDescription("
|
|
266
|
-
Command.withSubcommands([listCommand,
|
|
265
|
+
Command.withDescription("Jira version commands"),
|
|
266
|
+
Command.withSubcommands([listCommand, getCommand, updateCommand, relatedWorkCommand])
|
|
267
267
|
)
|