@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +9 -9
  3. package/dist/JiraCliError.d.ts +30 -0
  4. package/dist/JiraCliError.d.ts.map +1 -1
  5. package/dist/JiraCliError.js +14 -0
  6. package/dist/JiraCliError.js.map +1 -1
  7. package/dist/SyncWorkspace.d.ts +34 -0
  8. package/dist/SyncWorkspace.d.ts.map +1 -0
  9. package/dist/SyncWorkspace.js +105 -0
  10. package/dist/SyncWorkspace.js.map +1 -0
  11. package/dist/bin.js +4 -5
  12. package/dist/bin.js.map +1 -1
  13. package/dist/commands/get.d.ts.map +1 -1
  14. package/dist/commands/get.js +2 -2
  15. package/dist/commands/get.js.map +1 -1
  16. package/dist/commands/index.d.ts +1 -2
  17. package/dist/commands/index.d.ts.map +1 -1
  18. package/dist/commands/index.js +1 -2
  19. package/dist/commands/index.js.map +1 -1
  20. package/dist/commands/issue.d.ts +8 -0
  21. package/dist/commands/issue.d.ts.map +1 -0
  22. package/dist/commands/issue.js +10 -0
  23. package/dist/commands/issue.js.map +1 -0
  24. package/dist/commands/layers.d.ts.map +1 -1
  25. package/dist/commands/layers.js +4 -1
  26. package/dist/commands/layers.js.map +1 -1
  27. package/dist/commands/search.d.ts.map +1 -1
  28. package/dist/commands/search.js +4 -4
  29. package/dist/commands/search.js.map +1 -1
  30. package/dist/commands/version.js +13 -13
  31. package/dist/commands/version.js.map +1 -1
  32. package/dist/internal/frontmatter.d.ts.map +1 -1
  33. package/dist/internal/frontmatter.js +14 -1
  34. package/dist/internal/frontmatter.js.map +1 -1
  35. package/dist/internal/sync/baseline.d.ts +11 -0
  36. package/dist/internal/sync/baseline.d.ts.map +1 -0
  37. package/dist/internal/sync/baseline.js +18 -0
  38. package/dist/internal/sync/baseline.js.map +1 -0
  39. package/dist/internal/sync/changes.d.ts +15 -0
  40. package/dist/internal/sync/changes.d.ts.map +1 -0
  41. package/dist/internal/sync/changes.js +72 -0
  42. package/dist/internal/sync/changes.js.map +1 -0
  43. package/dist/internal/sync/config.d.ts +12 -0
  44. package/dist/internal/sync/config.d.ts.map +1 -0
  45. package/dist/internal/sync/config.js +53 -0
  46. package/dist/internal/sync/config.js.map +1 -0
  47. package/dist/internal/sync/document.d.ts +9 -0
  48. package/dist/internal/sync/document.d.ts.map +1 -0
  49. package/dist/internal/sync/document.js +173 -0
  50. package/dist/internal/sync/document.js.map +1 -0
  51. package/dist/internal/sync/fieldValues.d.ts +30 -0
  52. package/dist/internal/sync/fieldValues.d.ts.map +1 -0
  53. package/dist/internal/sync/fieldValues.js +91 -0
  54. package/dist/internal/sync/fieldValues.js.map +1 -0
  55. package/dist/internal/sync/manifest.d.ts +12 -0
  56. package/dist/internal/sync/manifest.d.ts.map +1 -0
  57. package/dist/internal/sync/manifest.js +23 -0
  58. package/dist/internal/sync/manifest.js.map +1 -0
  59. package/dist/internal/sync/paths.d.ts +26 -0
  60. package/dist/internal/sync/paths.d.ts.map +1 -0
  61. package/dist/internal/sync/paths.js +22 -0
  62. package/dist/internal/sync/paths.js.map +1 -0
  63. package/dist/internal/sync/schemas.d.ts +128 -0
  64. package/dist/internal/sync/schemas.d.ts.map +1 -0
  65. package/dist/internal/sync/schemas.js +82 -0
  66. package/dist/internal/sync/schemas.js.map +1 -0
  67. package/dist/internal/sync/types.d.ts +144 -0
  68. package/dist/internal/sync/types.d.ts.map +1 -0
  69. package/dist/internal/sync/types.js +17 -0
  70. package/dist/internal/sync/types.js.map +1 -0
  71. package/package.json +5 -2
  72. package/skills/jira/SKILL.md +11 -11
  73. package/src/JiraCliError.ts +24 -0
  74. package/src/SyncWorkspace.ts +185 -0
  75. package/src/bin.ts +4 -6
  76. package/src/commands/get.ts +2 -2
  77. package/src/commands/index.ts +1 -2
  78. package/src/commands/issue.ts +13 -0
  79. package/src/commands/layers.ts +4 -1
  80. package/src/commands/search.ts +4 -4
  81. package/src/commands/version.ts +15 -15
  82. package/src/internal/frontmatter.ts +15 -1
  83. package/src/internal/sync/baseline.ts +27 -0
  84. package/src/internal/sync/changes.ts +118 -0
  85. package/src/internal/sync/config.ts +76 -0
  86. package/src/internal/sync/document.ts +201 -0
  87. package/src/internal/sync/fieldValues.ts +145 -0
  88. package/src/internal/sync/manifest.ts +32 -0
  89. package/src/internal/sync/paths.ts +48 -0
  90. package/src/internal/sync/schemas.ts +103 -0
  91. package/src/internal/sync/types.ts +192 -0
  92. package/test/SyncWorkspace.test.ts +76 -0
  93. package/test/commandTree.test.ts +266 -0
  94. package/test/frontmatter.test.ts +69 -0
  95. package/test/integration.test.ts +187 -0
  96. package/test/syncChanges.test.ts +106 -0
  97. package/test/syncConfig.test.ts +138 -0
  98. package/test/syncDocument.test.ts +69 -0
  99. package/test/syncFieldValues.test.ts +101 -0
  100. package/vitest.config.integration.ts +17 -0
  101. 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.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
- "@knpkv/agent-skills": "^0.2.0",
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",
@@ -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 view`, `jira version set`, and `jira version relatedwork`.
15
- - Confirm before commands that mutate Jira: `jira version set` and `jira version relatedwork add`.
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 view 10042 --json
69
+ jira version get 10042 --json
70
70
  ```
71
71
 
72
72
  Update a version description:
73
73
 
74
74
  ```bash
75
- jira version set 10042 --description "Q3 release"
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 relatedwork list 10042 --json
82
- jira version relatedwork add 10042 --title "Release notes" --url "https://example.atlassian.net/wiki/spaces/PROJ/pages/123" --category Communication
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
@@ -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("Agent skill commands"),
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("Fetch Jira tickets and export to markdown"),
40
+ Command.withDescription("Jira CLI commands"),
42
41
  Command.withSubcommands([
43
42
  authCommand,
44
- getCommand,
45
- searchCommand,
43
+ issueCommand,
46
44
  skillsCommand,
47
45
  versionCommand
48
46
  ])
@@ -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("Get a single Jira issue by key"))
42
+ ).pipe(Command.withDescription("Read-only: get a single Jira issue by key"))
@@ -6,7 +6,6 @@
6
6
 
7
7
  export { authCommand } from "./auth.js"
8
8
  export { handleError } from "./errorHandler.js"
9
- export { getCommand } from "./get.js"
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
+ )
@@ -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 search/get (which needs API client + issue service).
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
  }
@@ -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("Search Jira issues and export to markdown"))
102
+ ).pipe(Command.withDescription("Read-only: search Jira issues and export to markdown"))
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `jira version` command — list / view Jira project versions (releases) with
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("List versions for a Jira project"))
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 `view` output. */
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 viewCommand = Command.make(
135
- "view",
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("Show a single Jira version"))
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 `view`: the first
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 setCommand = Command.make("set", { id: idArg, description: descriptionOption, json: jsonOption }, ({
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("Update a version's description (requires manage:jira-project scope)")
193
+ Command.withDescription("Remote write: update a version's description (requires manage:jira-project scope)")
194
194
  )
195
195
 
196
- // === relatedwork ===
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("List a version's related-work links"))
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
- "Attach a related-work link (e.g. a Confluence page) to a version (requires manage:jira-project scope)"
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("relatedwork").pipe(
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("List, view, or edit Jira project versions (releases)"),
266
- Command.withSubcommands([listCommand, viewCommand, setCommand, relatedWorkCommand])
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`