@knpkv/jira-cli 0.3.0 → 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 +20 -0
- package/README.md +9 -9
- 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/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 +4 -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 +5 -2
- package/skills/jira/SKILL.md +11 -11
- package/src/JiraCliError.ts +24 -0
- package/src/SyncWorkspace.ts +185 -0
- package/src/bin.ts +4 -6
- 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 +4 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/jira-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CLI tool to fetch Jira tickets and export to markdown",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -51,12 +51,14 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@effect/platform-node": "4.0.0-beta.87",
|
|
53
53
|
"gray-matter": "^4.0.3",
|
|
54
|
-
"
|
|
54
|
+
"js-yaml": "^4.1.0",
|
|
55
|
+
"@knpkv/agent-skills": "^0.2.1",
|
|
55
56
|
"@knpkv/atlassian-common": "0.3.0",
|
|
56
57
|
"@knpkv/jira-api-client": "0.3.0"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@effect/vitest": "4.0.0-beta.87",
|
|
61
|
+
"@types/js-yaml": "^4.0.9",
|
|
60
62
|
"@types/node": "latest",
|
|
61
63
|
"effect": "4.0.0-beta.87",
|
|
62
64
|
"typescript": "~6.0.3",
|
|
@@ -76,6 +78,7 @@
|
|
|
76
78
|
"build": "tsc",
|
|
77
79
|
"clean": "rimraf dist dist-test .tsbuildinfo",
|
|
78
80
|
"test": "vitest run",
|
|
81
|
+
"test:integration": "vitest run --config vitest.config.integration.ts",
|
|
79
82
|
"test:watch": "vitest",
|
|
80
83
|
"lint": "eslint src",
|
|
81
84
|
"lint:fix": "eslint src --fix",
|
package/skills/jira/SKILL.md
CHANGED
|
@@ -11,8 +11,8 @@ Use the `jira` binary for Jira Cloud issue export and release-version workflows.
|
|
|
11
11
|
|
|
12
12
|
- Authenticate first with `jira auth status`, `jira auth create`, `jira auth configure`, and `jira auth login`.
|
|
13
13
|
- Use `--json` on version commands when the agent needs structured data.
|
|
14
|
-
- Use numeric version ids for `jira version
|
|
15
|
-
- Confirm before
|
|
14
|
+
- Use numeric version ids for `jira version get`, `jira version update`, and `jira version related-work`.
|
|
15
|
+
- Confirm before remote write commands: `jira version update` and `jira version related-work add`.
|
|
16
16
|
|
|
17
17
|
## Authentication
|
|
18
18
|
|
|
@@ -31,21 +31,21 @@ OAuth scopes used by release workflows include `read:jira-work`, `write:jira-wor
|
|
|
31
31
|
Fetch one issue as markdown:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
jira get PROJ-123 --output-dir ./jira-tickets
|
|
34
|
+
jira issue get PROJ-123 --output-dir ./jira-tickets
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Search with JQL:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
jira search 'project = PROJ AND status = Done' --output-dir ./jira-tickets
|
|
41
|
-
jira search 'fixVersion = "1.0.0"' --format single --max-results 200
|
|
40
|
+
jira issue search 'project = PROJ AND status = Done' --output-dir ./jira-tickets
|
|
41
|
+
jira issue search 'fixVersion = "1.0.0"' --format single --max-results 200
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Search by fix version:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
jira search --by-version "1.0.0" --project PROJ
|
|
48
|
-
jira search --by-version "1.0.0" --project PROJ --format single
|
|
47
|
+
jira issue search --by-version "1.0.0" --project PROJ
|
|
48
|
+
jira issue search --by-version "1.0.0" --project PROJ --format single
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Output formats:
|
|
@@ -66,20 +66,20 @@ jira version list --project PROJ --custom-field "Security & Compliance Impact" -
|
|
|
66
66
|
View a version:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
jira version
|
|
69
|
+
jira version get 10042 --json
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Update a version description:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
jira version
|
|
75
|
+
jira version update 10042 --description "Q3 release"
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Manage related work links:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
jira version
|
|
82
|
-
jira version
|
|
81
|
+
jira version related-work list 10042 --json
|
|
82
|
+
jira version related-work add 10042 --title "Release notes" --url "https://example.atlassian.net/wiki/spaces/PROJ/pages/123" --category Communication
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
## Agent Workflow
|
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/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
|
*
|
|
@@ -147,6 +147,9 @@ export const MinimalLayer = DummyIssueServiceLayer.pipe(
|
|
|
147
147
|
*/
|
|
148
148
|
export const getLayerType = (args: ReadonlyArray<string>): "full" | "auth" | "minimal" => {
|
|
149
149
|
const cmd = args[0]
|
|
150
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
151
|
+
return "minimal"
|
|
152
|
+
}
|
|
150
153
|
if (cmd === "auth") {
|
|
151
154
|
return "auth"
|
|
152
155
|
}
|
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
|
)
|
|
@@ -9,8 +9,22 @@
|
|
|
9
9
|
* @internal
|
|
10
10
|
*/
|
|
11
11
|
import matter from "gray-matter"
|
|
12
|
+
import * as yaml from "js-yaml"
|
|
12
13
|
import type { Issue } from "../IssueService.js"
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* `gray-matter` ships a default YAML engine that calls `js-yaml`'s `safeDump`/
|
|
17
|
+
* `safeLoad`, both removed in `js-yaml` 4. The workspace pins `js-yaml` to 4.x
|
|
18
|
+
* (security override), so we supply an engine backed by the 4.x `dump`/`load`
|
|
19
|
+
* API, which is safe by default.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
const yamlEngine = {
|
|
24
|
+
parse: (str: string): object => (yaml.load(str) as object) ?? {},
|
|
25
|
+
stringify: (data: object): string => yaml.dump(data)
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
29
|
* Front-matter data for a Jira issue.
|
|
16
30
|
*
|
|
@@ -69,7 +83,7 @@ export const extractFrontMatter = (issue: Issue): IssueFrontMatter => ({
|
|
|
69
83
|
export const serializeIssue = (issue: Issue): string => {
|
|
70
84
|
const frontMatter = extractFrontMatter(issue)
|
|
71
85
|
const content = buildMarkdownContent(issue)
|
|
72
|
-
return matter.stringify(content, frontMatter)
|
|
86
|
+
return matter.stringify(content, frontMatter, { engines: { yaml: yamlEngine } })
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Baseline helpers.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import { SyncValidationError, SyncWorkspaceError } from "../../JiraCliError.js"
|
|
9
|
+
import { SyncBaselineSchema } from "./schemas.js"
|
|
10
|
+
import type { SyncBaseline } from "./types.js"
|
|
11
|
+
|
|
12
|
+
export const parseSyncBaseline = (
|
|
13
|
+
path: string,
|
|
14
|
+
content: string
|
|
15
|
+
): Effect.Effect<SyncBaseline, SyncWorkspaceError | SyncValidationError> =>
|
|
16
|
+
Effect.gen(function*() {
|
|
17
|
+
const raw = yield* Effect.try({
|
|
18
|
+
try: () => JSON.parse(content) as unknown,
|
|
19
|
+
catch: (cause) => new SyncWorkspaceError({ message: "Failed to parse Sync Baseline JSON", path, cause })
|
|
20
|
+
})
|
|
21
|
+
return yield* Schema.decodeUnknownEffect(SyncBaselineSchema)(raw).pipe(
|
|
22
|
+
Effect.map((baseline) => baseline as SyncBaseline),
|
|
23
|
+
Effect.mapError((cause) => new SyncValidationError({ message: "Invalid Sync Baseline", path, cause }))
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const serializeSyncBaseline = (baseline: SyncBaseline): string => `${JSON.stringify(baseline, null, 2)}\n`
|