@knpkv/jira-cli 1.2.2 → 1.3.1

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 (121) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +64 -0
  3. package/dist/AttachmentService.d.ts +2 -2
  4. package/dist/AttachmentService.d.ts.map +1 -1
  5. package/dist/AttachmentService.js +1 -1
  6. package/dist/AttachmentService.js.map +1 -1
  7. package/dist/IssueService.d.ts +82 -3
  8. package/dist/IssueService.d.ts.map +1 -1
  9. package/dist/IssueService.js +111 -16
  10. package/dist/IssueService.js.map +1 -1
  11. package/dist/JiraAuth.d.ts +1 -1
  12. package/dist/JiraAuth.d.ts.map +1 -1
  13. package/dist/JiraAuth.js +97 -21
  14. package/dist/JiraAuth.js.map +1 -1
  15. package/dist/MarkdownWriter.d.ts +4 -4
  16. package/dist/MarkdownWriter.d.ts.map +1 -1
  17. package/dist/MarkdownWriter.js +2 -2
  18. package/dist/MarkdownWriter.js.map +1 -1
  19. package/dist/SyncWorkspace.d.ts +2 -2
  20. package/dist/SyncWorkspace.d.ts.map +1 -1
  21. package/dist/SyncWorkspace.js.map +1 -1
  22. package/dist/VersionService.d.ts +81 -9
  23. package/dist/VersionService.d.ts.map +1 -1
  24. package/dist/VersionService.js +113 -17
  25. package/dist/VersionService.js.map +1 -1
  26. package/dist/commands/auth.d.ts +3 -2
  27. package/dist/commands/auth.d.ts.map +1 -1
  28. package/dist/commands/edit.d.ts +13 -0
  29. package/dist/commands/edit.d.ts.map +1 -0
  30. package/dist/commands/edit.js +49 -0
  31. package/dist/commands/edit.js.map +1 -0
  32. package/dist/commands/errorHandler.d.ts.map +1 -1
  33. package/dist/commands/issue.d.ts +1 -1
  34. package/dist/commands/issue.d.ts.map +1 -1
  35. package/dist/commands/issue.js +2 -1
  36. package/dist/commands/issue.js.map +1 -1
  37. package/dist/commands/layers.d.ts +3 -3
  38. package/dist/commands/layers.d.ts.map +1 -1
  39. package/dist/commands/layers.js +5 -2
  40. package/dist/commands/layers.js.map +1 -1
  41. package/dist/commands/version.d.ts +8 -1
  42. package/dist/commands/version.d.ts.map +1 -1
  43. package/dist/commands/version.js +116 -4
  44. package/dist/commands/version.js.map +1 -1
  45. package/dist/index.d.ts +4 -3
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal/NodeLayers.js +1 -1
  50. package/dist/internal/NodeLayers.js.map +1 -1
  51. package/dist/internal/attachmentInsertion.d.ts.map +1 -1
  52. package/dist/internal/frontmatter.d.ts.map +1 -1
  53. package/dist/internal/frontmatter.js +2 -1
  54. package/dist/internal/frontmatter.js.map +1 -1
  55. package/dist/internal/jqlBuilder.d.ts.map +1 -1
  56. package/dist/internal/oauthServer.d.ts +14 -9
  57. package/dist/internal/oauthServer.d.ts.map +1 -1
  58. package/dist/internal/oauthServer.js +21 -21
  59. package/dist/internal/oauthServer.js.map +1 -1
  60. package/dist/internal/openBrowser.d.ts.map +1 -1
  61. package/dist/internal/sync/baseline.d.ts.map +1 -1
  62. package/dist/internal/sync/baseline.js +2 -2
  63. package/dist/internal/sync/baseline.js.map +1 -1
  64. package/dist/internal/sync/changes.d.ts.map +1 -1
  65. package/dist/internal/sync/config.d.ts.map +1 -1
  66. package/dist/internal/sync/config.js +1 -1
  67. package/dist/internal/sync/config.js.map +1 -1
  68. package/dist/internal/sync/document.d.ts.map +1 -1
  69. package/dist/internal/sync/document.js +8 -8
  70. package/dist/internal/sync/document.js.map +1 -1
  71. package/dist/internal/sync/fieldValues.d.ts +9 -9
  72. package/dist/internal/sync/fieldValues.d.ts.map +1 -1
  73. package/dist/internal/sync/fieldValues.js +12 -16
  74. package/dist/internal/sync/fieldValues.js.map +1 -1
  75. package/dist/internal/sync/manifest.d.ts.map +1 -1
  76. package/dist/internal/sync/manifest.js +1 -1
  77. package/dist/internal/sync/manifest.js.map +1 -1
  78. package/dist/internal/sync/paths.d.ts.map +1 -1
  79. package/dist/internal/sync/schemas.d.ts +17 -9
  80. package/dist/internal/sync/schemas.d.ts.map +1 -1
  81. package/dist/internal/sync/schemas.js +6 -6
  82. package/dist/internal/sync/schemas.js.map +1 -1
  83. package/dist/internal/sync/types.d.ts +4 -4
  84. package/dist/internal/sync/types.d.ts.map +1 -1
  85. package/dist/internal/sync/types.js +1 -1
  86. package/dist/internal/sync/types.js.map +1 -1
  87. package/package.json +14 -10
  88. package/skills/jira/SKILL.md +17 -1
  89. package/src/AttachmentService.ts +6 -6
  90. package/src/IssueService.ts +214 -27
  91. package/src/JiraAuth.ts +120 -35
  92. package/src/MarkdownWriter.ts +6 -6
  93. package/src/SyncWorkspace.ts +12 -11
  94. package/src/VersionService.ts +199 -27
  95. package/src/commands/edit.ts +88 -0
  96. package/src/commands/issue.ts +2 -1
  97. package/src/commands/layers.ts +5 -2
  98. package/src/commands/version.ts +161 -5
  99. package/src/index.ts +13 -3
  100. package/src/internal/NodeLayers.ts +1 -1
  101. package/src/internal/frontmatter.ts +3 -2
  102. package/src/internal/oauthServer.ts +46 -36
  103. package/src/internal/sync/baseline.ts +3 -2
  104. package/src/internal/sync/config.ts +2 -1
  105. package/src/internal/sync/document.ts +15 -14
  106. package/src/internal/sync/fieldValues.ts +21 -22
  107. package/src/internal/sync/manifest.ts +1 -1
  108. package/src/internal/sync/schemas.ts +6 -6
  109. package/src/internal/sync/types.ts +4 -4
  110. package/test/JiraAuthRefresh.test.ts +269 -0
  111. package/test/commandTree.test.ts +157 -1
  112. package/test/editIssuePayload.test.ts +103 -0
  113. package/test/frontmatter.test.ts +4 -2
  114. package/test/integration.test.ts +5 -3
  115. package/test/oauthServer.test.ts +171 -0
  116. package/test/publicApi.test.ts +34 -0
  117. package/test/relatedWorkSync.test.ts +179 -0
  118. package/test/remoteWrites.test.ts +220 -0
  119. package/test/syncChanges.test.ts +2 -2
  120. package/test/syncConfig.test.ts +2 -2
  121. package/tsconfig.json +3 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,129 @@
1
1
  # @knpkv/jira-cli
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#361](https://github.com/knpkv/npm/pull/361) [`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2) Thanks [@konopkov](https://github.com/konopkov)! - Upgrade the workspace to Effect 4.0.0-rc.109, pin the vendored Effect reference to that exact upstream release, guard source/package alignment, and bound Control Center test concurrency for reliable CI execution.
8
+ - Updated dependencies [[`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2)]:
9
+ - @knpkv/agent-skills@0.3.1
10
+ - @knpkv/atlassian-common@1.4.1
11
+ - @knpkv/jira-api-client@1.1.1
12
+
13
+ ## 1.3.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#366](https://github.com/knpkv/npm/pull/366) [`b08ca20`](https://github.com/knpkv/npm/commit/b08ca2004b3efcd72a695b44c72b56dae20afdfd) Thanks [@konopkov](https://github.com/konopkov)! - Add `jira version create` and `jira issue edit`, closing the two gaps that forced release scaffolding out of the CLI.
18
+
19
+ `jira version create --project <KEY> --name <NAME>` opens a new unreleased version, optionally with `--description`, `--start-date` and `--release-date`. The project key is resolved to the numeric `projectId` the endpoint requires, and dates are validated as ISO 8601 locally so a bad one names its own flag instead of returning an unattributed 400.
20
+
21
+ `jira issue edit <KEY>` edits fix versions and labels. Both fields are sets, so the incremental flags — `--add-fix-version`, `--remove-fix-version`, `--add-label`, `--remove-label` — are the ones to reach for: they go through Jira's `update` verb, which applies server-side and cannot clobber a concurrent edit. The replacing forms (`--fix-version`, `--label`) are still available and say in their help that they drop anything not listed. Passing both forms for one field is refused up front, because Jira's own error for that case does not name the offending field.
22
+
23
+ - [#354](https://github.com/knpkv/npm/pull/354) [`2e26e30`](https://github.com/knpkv/npm/commit/2e26e3032ce527260a4e4d9fca8af43039f762d6) Thanks [@konopkov](https://github.com/konopkov)! - Add `jira version related-work sync` — reconcile a version's related-work links
24
+ against the given set instead of blindly appending.
25
+
26
+ Repeated `related-work add` calls pile up duplicate "Release notes" links every
27
+ time a release is re-scaffolded. `sync` takes the desired set as repeatable
28
+ `--link title=url` flags and adds only what is missing, matching on URL (the
29
+ only stable identity a link has — Jira assigns the id and the title is
30
+ editable, so a link retitled by hand is still recognised). Scoped to one
31
+ `--category` so reconciling `Communication` cannot disturb `Testing` links.
32
+ `--prune` opts into removing extras, which is off by default because links
33
+ added by hand in the Jira UI are legitimate; it removes surplus copies of a
34
+ desired URL too, since an existing pile-up is the case it exists to clean up.
35
+ Repeated `--link` flags for one URL collapse to a single link.
36
+
37
+ The planning step is exposed as the pure `planRelatedWorkSync` and covered by
38
+ tests, including that a second run is a no-op.
39
+
40
+ - [#370](https://github.com/knpkv/npm/pull/370) [`27d2ca1`](https://github.com/knpkv/npm/commit/27d2ca18b0c0b0f8a252d461c0aaf10eb92e9ffc) Thanks [@konopkov](https://github.com/konopkov)! - Enforce the complete anti-slop rule set with zero accepted diagnostics and update affected APIs and implementations to satisfy the required contracts.
41
+
42
+ ### Patch Changes
43
+
44
+ - [#358](https://github.com/knpkv/npm/pull/358) [`503d345`](https://github.com/knpkv/npm/commit/503d3459b419a3c9fd366715d5916e41086f493d) Thanks [@konopkov](https://github.com/konopkov)! - Make the OAuth refresh-token rotation atomic so an interrupted CLI cannot log
45
+ the user out.
46
+
47
+ Atlassian rotates refresh tokens: the refresh response carries a replacement and
48
+ the token that was sent is consumed server-side. `refreshTokenImpl` performed
49
+ the grant and then persisted the result in a separate, interruptible step, so a
50
+ fiber interrupt landing between the two destroyed the credential — the stored
51
+ refresh token was already spent, the replacement was never written, and the next
52
+ refresh failed with a 4xx. `getAccessToken` treats that failure as an expired
53
+ refresh token and deletes the token file, so the user was silently logged out
54
+ and had to run `jira auth login` again.
55
+
56
+ Nothing interrupted it before, which is why this had not surfaced. It becomes
57
+ reachable as soon as a caller kills the process: this runs during layer
58
+ construction on every CLI invocation, and `@knpkv/jira-clockify`'s nvim
59
+ statusline terminates the poll process when the editor closes. The grant and
60
+ the persist now share one `Effect.uninterruptible` region.
61
+
62
+ That region carries its own 30s deadline rather than relying on a caller's, for
63
+ two reasons. A caller's `Effect.timeout` would be inert — `timeout` is a race,
64
+ and racing an uninterruptible loser means waiting for it anyway. And an
65
+ uninterruptible region with no deadline of its own absorbs SIGINT/SIGTERM
66
+ entirely, since `NodeRuntime.runMain`'s signal handlers do nothing but interrupt
67
+ the main fiber — a hung `jira` command would stop responding to Ctrl-C. The
68
+ deadline forked inside the region is itself interruptible, so it does bound it.
69
+
70
+ Abandoning the round-trip still cannot prove the grant did not land — Atlassian
71
+ may consume and rotate the token after we stop listening, and no client-side
72
+ deadline changes that. So the deadline is paired with a second rule:
73
+ `getAccessToken` no longer deletes the stored token on any `step: "refresh"`
74
+ failure. It deletes only on the answers that actually mean the grant is dead —
75
+ the provider explicitly reporting `invalid_grant`, on a `400` or a `403` — using
76
+ the new `OAuthError.status` and `OAuthError.errorCode` from
77
+ `@knpkv/atlassian-common`. Previously a transport error or timeout deleted
78
+ the active profile outright, which turned a bad network window into an
79
+ unattended silent logout: `JiraApiConfigLive` builds on every CLI invocation,
80
+ and jcf's statusline runs one every 30 seconds.
81
+
82
+ The statuses deliberately left alone matter as much. `429` is the one this most
83
+ needs to survive — several `jira`/`jcf` processes on one expired token hit the
84
+ endpoint together, one wins the rotation and the rest are rate-limited. `408`
85
+ and `425` restate the timeout case. `407` and other middlebox replies never came
86
+ from Atlassian at all. `401` and `400 invalid_client` mean the client secret is
87
+ wrong, where the fix is `jira auth configure`, not a re-login that would fail
88
+ the same way. A bare `403` is left alone too: it is as likely to be a proxy or
89
+ WAF as Atlassian revoking anything. An unparseable body is no verdict at all.
90
+
91
+ Now an incomplete refresh normally costs a retry rather than the session. This
92
+ narrows the window rather than closing it: the atomicity is fiber-level, so a
93
+ SIGKILL after Atlassian has already rotated the token still loses the
94
+ replacement, and the next refresh then legitimately reports `invalid_grant`. No
95
+ client-side design can close that window against a hard kill.
96
+
97
+ Covered by tests: a refresh that never answers and one that is rate-limited both
98
+ leave the profile on disk, a `400` removes it, and an interrupt mid-rotation
99
+ still persists the replacement token.
100
+
101
+ - [#361](https://github.com/knpkv/npm/pull/361) [`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2) Thanks [@konopkov](https://github.com/konopkov)! - Update Effect and effect-qb, migrate schema-tagged errors to the current Effect API, and adopt the dialect-scoped SQLite function and type APIs introduced by effect-qb 0.22.
102
+
103
+ - [#357](https://github.com/knpkv/npm/pull/357) [`77e3257`](https://github.com/knpkv/npm/commit/77e3257743aacfaf9e11e016a60206f416c5fe79) Thanks [@konopkov](https://github.com/konopkov)! - Secure local control planes and CI credential boundaries. CodeCommit web now
104
+ uses a process-scoped owner session with CSRF protection and loopback-only
105
+ listeners; review sandboxes use authenticated loopback code-server instances,
106
+ digest-pinned images, constrained mounts, non-root execution, and dropped
107
+ capabilities. OAuth callback listeners validate state before accepting terminal
108
+ outcomes and bind explicitly to loopback. GitHub workflows pin external actions
109
+ to immutable commits and keep long-lived Atlassian credentials out of pull
110
+ request execution.
111
+ - Updated dependencies [[`503d345`](https://github.com/knpkv/npm/commit/503d3459b419a3c9fd366715d5916e41086f493d), [`b08ca20`](https://github.com/knpkv/npm/commit/b08ca2004b3efcd72a695b44c72b56dae20afdfd), [`676419e`](https://github.com/knpkv/npm/commit/676419e39c395dd4cfea6d9ffaee7d002a3f75e2), [`b08ca20`](https://github.com/knpkv/npm/commit/b08ca2004b3efcd72a695b44c72b56dae20afdfd), [`27d2ca1`](https://github.com/knpkv/npm/commit/27d2ca18b0c0b0f8a252d461c0aaf10eb92e9ffc)]:
112
+ - @knpkv/atlassian-common@1.4.0
113
+ - @knpkv/agent-skills@0.3.0
114
+ - @knpkv/jira-api-client@1.1.0
115
+
116
+ ## 1.2.3
117
+
118
+ ### Patch Changes
119
+
120
+ - [#343](https://github.com/knpkv/npm/pull/343) [`4def7db`](https://github.com/knpkv/npm/commit/4def7db2f400cf68218262994d67ed90a7154bf1) Thanks [@konopkov](https://github.com/konopkov)! - Align runtime ownership, cancellation, caching, time, failure handling, polling,
121
+ decoding, and executable entrypoints with Effect v4 idioms. Expose clock-injected
122
+ Atlassian token construction and expiry helpers, and enable workspace-wide
123
+ Effect diagnostics and prevention checks.
124
+ - Updated dependencies [[`4def7db`](https://github.com/knpkv/npm/commit/4def7db2f400cf68218262994d67ed90a7154bf1), [`a9d5408`](https://github.com/knpkv/npm/commit/a9d54085f6fc25cde1d5b298f50cb6e06e2bc93f)]:
125
+ - @knpkv/atlassian-common@1.3.0
126
+
3
127
  ## 1.2.2
4
128
 
5
129
  ### Patch Changes
package/README.md CHANGED
@@ -43,6 +43,12 @@ Opens Atlassian Developer Console. Create a new OAuth 2.0 (3LO) app with:
43
43
  http://localhost:8585/callback
44
44
  ```
45
45
 
46
+ The provider-facing callback URL and callback listener both use `localhost`, so
47
+ the browser and listener select the same loopback family on IPv4-only and
48
+ IPv6-only hosts. Requests with a missing
49
+ or mismatched OAuth state are rejected without consuming the pending login, so
50
+ only the provider callback carrying the expected state can complete or fail it.
51
+
46
52
  ### 2. Configure Credentials
47
53
 
48
54
  ```bash
@@ -112,6 +118,30 @@ jira issue attachment upload PROJ-123 ./evidence.svg --document ./PROJ-123.md
112
118
 
113
119
  The command uploads the file, replaces exactly one matching placeholder with the remote attachment reference, and includes hidden `jiraAttachment` metadata so later parses keep the Jira attachment identity.
114
120
 
121
+ ### Edit an issue
122
+
123
+ Edit an issue's list-valued fields (fix versions and labels).
124
+
125
+ ```bash
126
+ jira issue edit PROJ-123 --add-fix-version "OOB 100" --add-label release-notes
127
+ jira issue edit PROJ-123 --remove-fix-version "OOB 99" --json
128
+ ```
129
+
130
+ | Option | Description | Default |
131
+ | ---------------------- | -------------------------------------------------------------- | ------- |
132
+ | `--add-fix-version` | Add a fix version by name, keeping existing ones (repeatable) | - |
133
+ | `--remove-fix-version` | Remove a fix version by name (repeatable) | - |
134
+ | `--fix-version` | Replace the fix versions with exactly these names (repeatable) | - |
135
+ | `--add-label` | Add a label, keeping existing ones (repeatable) | - |
136
+ | `--remove-label` | Remove a label (repeatable) | - |
137
+ | `--label` | Replace the labels with exactly these (repeatable) | - |
138
+ | `--json` | Output as JSON | `false` |
139
+
140
+ Both fields are sets, so prefer the incremental flags: the replacing forms
141
+ (`--fix-version`, `--label`) silently drop every value not listed. A replacing
142
+ flag cannot be combined with its own incremental flags. Requires the
143
+ `write:jira-work` scope.
144
+
115
145
  ### Output Formats
116
146
 
117
147
  **Multi (default):** One markdown file per issue with YAML front-matter.
@@ -162,6 +192,28 @@ jira version get 10042 --json
162
192
 
163
193
  The version id is the **numeric** id (e.g. `10042`); use `version list` to find it.
164
194
 
195
+ ### Create a version
196
+
197
+ ```bash
198
+ jira version create --project PROJ --name "OOB 100"
199
+ jira version create --project PROJ --name "OOB 100" \
200
+ --description "Q3 release" \
201
+ --start-date 2026-01-05 \
202
+ --release-date 2026-01-19 \
203
+ --json
204
+ ```
205
+
206
+ | Option | Alias | Description | Default |
207
+ | ---------------- | ----- | ------------------------------------- | ------- |
208
+ | `--project` | `-p` | Jira project key (e.g. `PROJ`) | - |
209
+ | `--name` | | Version name (e.g. `"OOB 100"`) | - |
210
+ | `--description` | `-d` | Version description | - |
211
+ | `--start-date` | | Start date, ISO 8601 (`yyyy-mm-dd`) | - |
212
+ | `--release-date` | | Release date, ISO 8601 (`yyyy-mm-dd`) | - |
213
+ | `--json` | | Output as JSON | `false` |
214
+
215
+ The version is created unreleased. Requires the `manage:jira-project` scope.
216
+
165
217
  ### Set the description
166
218
 
167
219
  ```bash
@@ -180,10 +232,22 @@ jira version related-work add 10042 \
180
232
  --title "Release notes" \
181
233
  --url "https://example.atlassian.net/wiki/spaces/PROJ/pages/123" \
182
234
  --category Communication
235
+
236
+ # Reconcile the category to exactly this set — idempotent, safe to re-run
237
+ jira version related-work sync 10042 \
238
+ --link "Release notes=https://example.atlassian.net/wiki/spaces/PROJ/pages/123" \
239
+ --link "Test report=https://example.atlassian.net/wiki/spaces/PROJ/pages/124" \
240
+ --category Communication \
241
+ --prune
183
242
  ```
184
243
 
185
244
  `related-work add` requires the `write:jira-work` scope.
186
245
 
246
+ `related-work sync` is what a release scaffold should use: repeated `add` calls pile up duplicate links, while
247
+ `sync` matches on URL (the only stable identity a link has, since Jira assigns the id and the title is editable)
248
+ and adds only what is missing. `--prune` additionally removes links in the category that are not in the desired
249
+ set, including surplus copies of a URL that _is_ desired. Other categories are never touched.
250
+
187
251
  ## Auth Commands
188
252
 
189
253
  ```bash
@@ -11,10 +11,10 @@ export interface UploadAttachmentInput {
11
11
  readonly filename?: string | undefined;
12
12
  readonly mediaType?: string | undefined;
13
13
  }
14
- export interface AttachmentServiceShape {
14
+ export interface AttachmentServiceContract {
15
15
  readonly uploadToIssue: (issueIdOrKey: string, input: UploadAttachmentInput) => Effect.Effect<Attachment, JiraApiError>;
16
16
  }
17
- declare const AttachmentService_base: Context.ServiceClass<AttachmentService, "@knpkv/jira-cli/AttachmentService", AttachmentServiceShape>;
17
+ declare const AttachmentService_base: Context.ServiceClass<AttachmentService, "@knpkv/jira-cli/AttachmentService", AttachmentServiceContract>;
18
18
  export declare class AttachmentService extends AttachmentService_base {
19
19
  }
20
20
  export declare const layer: Layer.Layer<AttachmentService, never, JiraApiClient | FileSystem.FileSystem | Path.Path>;
@@ -1 +1 @@
1
- {"version":3,"file":"AttachmentService.d.ts","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,aAAa,EAAE,CACtB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,qBAAqB,KACzB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;CAC7C;;AAED,qBAAa,iBAAkB,SAAQ,sBAGC;CAAG;AAwE3C,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,EAAE,aAAa,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CACzE,CAAA"}
1
+ {"version":3,"file":"AttachmentService.d.ts","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,aAAa,EAAE,CACtB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,qBAAqB,KACzB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;CAC7C;;AAED,qBAAa,iBAAkB,SAAQ,sBAGC;CAAG;AAwE3C,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,EAAE,aAAa,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CACzE,CAAA"}
@@ -56,7 +56,7 @@ const make = Effect.gen(function* () {
56
56
  const result = yield* client.uploadAttachment(issueIdOrKey, {
57
57
  bytes,
58
58
  filename,
59
- ...(input.mediaType === undefined ? {} : { mediaType: input.mediaType })
59
+ ...(!(input.mediaType === undefined) && { mediaType: input.mediaType })
60
60
  }).pipe(Effect.mapError((cause) => new JiraApiError({ message: `Failed to upload attachment to ${issueIdOrKey}`, cause })));
61
61
  return yield* decodeFirstUploadedAttachment(result);
62
62
  });
@@ -1 +1 @@
1
- {"version":3,"file":"AttachmentService.js","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AttachmentService.js","sourceRoot":"","sources":["../src/AttachmentService.ts"],"names":[],"mappings":"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"}
@@ -81,12 +81,86 @@ export interface SearchResult {
81
81
  readonly nextPageToken: string | null;
82
82
  readonly maxResults: number;
83
83
  }
84
+ /**
85
+ * Edits to apply to an issue's list-valued fields.
86
+ *
87
+ * `add`/`remove` are incremental and `set` replaces. Prefer the incremental pair:
88
+ * `fixVersions` and `labels` are **sets**, so a `set` that forgets an existing
89
+ * value silently drops it — the failure mode when scripting a release scope.
90
+ *
91
+ * An empty array reads as "not supplied", not as "clear the field". The CLI's
92
+ * repeatable flags use `Options.atLeast(0)`, which reports an absent flag as
93
+ * `[]`, so treating `[]` as a replacement would turn every unpassed flag into a
94
+ * destructive clear. The consequence is that clearing a field outright is not
95
+ * currently expressible; it needs an explicit `--clear-*` flag (or a
96
+ * presence-aware option type) rather than a reinterpretation of `[]`.
97
+ *
98
+ * @category Types
99
+ */
100
+ export interface EditIssueInput {
101
+ readonly addFixVersions?: ReadonlyArray<string>;
102
+ readonly removeFixVersions?: ReadonlyArray<string>;
103
+ readonly setFixVersions?: ReadonlyArray<string>;
104
+ readonly addLabels?: ReadonlyArray<string>;
105
+ readonly removeLabels?: ReadonlyArray<string>;
106
+ readonly setLabels?: ReadonlyArray<string>;
107
+ }
108
+ /** Any JSON value, matching what the generated client accepts for field values. */
109
+ type JsonValue = string | number | boolean | null | ReadonlyArray<JsonValue> | {
110
+ readonly [key: string]: JsonValue;
111
+ };
112
+ /** One entry in Jira's `update` map. */
113
+ interface FieldOperation {
114
+ readonly add?: JsonValue;
115
+ readonly remove?: JsonValue;
116
+ }
117
+ /** A `PUT /issue/{key}` body: `fields` replaces, `update` applies operations. */
118
+ interface EditIssuePayload {
119
+ readonly fields?: {
120
+ readonly [field: string]: JsonValue;
121
+ };
122
+ readonly update?: {
123
+ readonly [field: string]: ReadonlyArray<FieldOperation>;
124
+ };
125
+ }
126
+ /**
127
+ * Outcome of {@link buildEditIssuePayload}: either a body to send, or the
128
+ * reason the requested edit is not expressible as one.
129
+ *
130
+ * Tagged rather than discriminated by property presence, so a caller matches on
131
+ * `_tag` and a future third outcome cannot narrow silently.
132
+ *
133
+ * @category Types
134
+ */
135
+ export type EditIssuePayloadResult = {
136
+ readonly _tag: "Payload";
137
+ readonly payload: EditIssuePayload;
138
+ } | {
139
+ readonly _tag: "Invalid";
140
+ readonly reason: string;
141
+ };
142
+ /**
143
+ * Build the `PUT /issue/{key}` body for an {@link EditIssueInput}.
144
+ *
145
+ * Incremental edits go through Jira's `update` verb rather than a
146
+ * read-modify-write on `fields`: the server applies them atomically, so a
147
+ * concurrent edit cannot be clobbered and no extra GET is needed.
148
+ *
149
+ * Jira rejects a field that appears in both `fields` and `update`, and the error
150
+ * it returns does not name the field — so that combination is caught here with a
151
+ * message that does.
152
+ *
153
+ * Pure so the payload shape can be tested without the API.
154
+ *
155
+ * @category Utilities
156
+ */
157
+ export declare const buildEditIssuePayload: (input: EditIssueInput) => EditIssuePayloadResult;
84
158
  /**
85
159
  * IssueService interface.
86
160
  *
87
161
  * @category Services
88
162
  */
89
- export interface IssueServiceShape {
163
+ export interface IssueServiceContract {
90
164
  /** Get a single issue by key */
91
165
  readonly getByKey: (key: string) => Effect.Effect<Issue, JiraApiError>;
92
166
  /** Search issues by JQL query */
@@ -95,8 +169,13 @@ export interface IssueServiceShape {
95
169
  readonly searchAll: (jql: string, options?: {
96
170
  readonly maxResults?: number;
97
171
  }) => Effect.Effect<ReadonlyArray<Issue>, JiraApiError>;
172
+ /**
173
+ * Edit an issue's list-valued fields (fixVersions, labels) and return the
174
+ * issue as it stands afterwards. Needs `write:jira-work`.
175
+ */
176
+ readonly edit: (key: string, input: EditIssueInput) => Effect.Effect<Issue, JiraApiError>;
98
177
  }
99
- declare const IssueService_base: Context.ServiceClass<IssueService, "@knpkv/jira-cli/IssueService", IssueServiceShape>;
178
+ declare const IssueService_base: Context.ServiceClass<IssueService, "@knpkv/jira-cli/IssueService", IssueServiceContract>;
100
179
  /**
101
180
  * IssueService tag.
102
181
  *
@@ -121,6 +200,6 @@ export declare class IssueService extends IssueService_base {
121
200
  *
122
201
  * @category Layers
123
202
  */
124
- export declare const layer: Layer.Layer<IssueService, never, SiteUrl | JiraApiClient>;
203
+ export declare const layer: Layer.Layer<IssueService, never, JiraApiClient | SiteUrl>;
125
204
  export {};
126
205
  //# sourceMappingURL=IssueService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"IssueService.d.ts","sourceRoot":"","sources":["../src/IssueService.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;;AAEhD;;;;GAIG;AACH,qBAAa,OAAQ,SAAQ,YAA6D;CAAG;AAE7F;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACtC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;IAC/C,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAA;IACrC,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAwBD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;IACtE,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,CAAA;IACpG,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,CAClB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,KACvC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC,CAAA;CACvD;;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,YAAa,SAAQ,iBAGC;CAAG;AAqOtC;;;;GAIG;AACH,eAAO,MAAM,KAAK,2DAAmC,CAAA"}
1
+ {"version":3,"file":"IssueService.d.ts","sourceRoot":"","sources":["../src/IssueService.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  * like `extractDisplayName` and `extractNameArray`.
9
9
  * - **Rendered fields**: Requests include `expand: "renderedFields"` to get HTML-rendered
10
10
  * descriptions and comments, falling back to plain text.
11
- * - **Pagination guard**: {@link IssueServiceShape.searchAll} iterates pages using
11
+ * - **Pagination guard**: {@link IssueServiceContract.searchAll} iterates pages using
12
12
  * `nextPageToken` with a MAX_PAGES (1000) safety limit.
13
13
  *
14
14
  * **Common tasks**
@@ -24,6 +24,7 @@ import * as Context from "effect/Context";
24
24
  import * as Effect from "effect/Effect";
25
25
  import * as Layer from "effect/Layer";
26
26
  import * as Predicate from "effect/Predicate";
27
+ import * as Schema from "effect/Schema";
27
28
  import { JiraApiError } from "./JiraCliError.js";
28
29
  /**
29
30
  * Site URL configuration for issue links.
@@ -32,15 +33,91 @@ import { JiraApiError } from "./JiraCliError.js";
32
33
  */
33
34
  export class SiteUrl extends Context.Service()("@knpkv/jira-cli/SiteUrl") {
34
35
  }
35
- const recordOrEmpty = (value) => Predicate.isReadonlyObject(value) ? value : {};
36
- const recordArray = (value) => Array.isArray(value) ? value.filter(Predicate.isReadonlyObject) : [];
36
+ const JsonObject = Schema.Record(Schema.String, Schema.Json);
37
+ const isJsonObject = Schema.is(JsonObject);
38
+ const recordOrEmpty = (value) => isJsonObject(value) ? value : {};
39
+ const recordArray = (value) => Array.isArray(value) ? value.filter(isJsonObject) : [];
37
40
  const parseSearchJqlResponse = (value) => {
38
41
  const record = recordOrEmpty(value);
39
- const nextPageToken = typeof record.nextPageToken === "string" ? record.nextPageToken : undefined;
42
+ const nextPageToken = Predicate.isString(record.nextPageToken) ? record.nextPageToken : undefined;
40
43
  return {
41
44
  issues: Array.isArray(record.issues) ? record.issues : [],
42
- isLast: typeof record.isLast === "boolean" ? record.isLast : nextPageToken === undefined,
43
- ...(nextPageToken !== undefined ? { nextPageToken } : {})
45
+ isLast: Predicate.isBoolean(record.isLast) ? record.isLast : nextPageToken === undefined,
46
+ ...((nextPageToken !== undefined) && { nextPageToken })
47
+ };
48
+ };
49
+ /**
50
+ * Build the `PUT /issue/{key}` body for an {@link EditIssueInput}.
51
+ *
52
+ * Incremental edits go through Jira's `update` verb rather than a
53
+ * read-modify-write on `fields`: the server applies them atomically, so a
54
+ * concurrent edit cannot be clobbered and no extra GET is needed.
55
+ *
56
+ * Jira rejects a field that appears in both `fields` and `update`, and the error
57
+ * it returns does not name the field — so that combination is caught here with a
58
+ * message that does.
59
+ *
60
+ * Pure so the payload shape can be tested without the API.
61
+ *
62
+ * @category Utilities
63
+ */
64
+ export const buildEditIssuePayload = (input) => {
65
+ const fields = {};
66
+ const update = {};
67
+ const specs = [
68
+ {
69
+ field: "fixVersions",
70
+ flag: "fix-version",
71
+ set: input.setFixVersions,
72
+ add: input.addFixVersions,
73
+ remove: input.removeFixVersions,
74
+ // fixVersions entries are objects keyed by name.
75
+ wrap: (name) => ({ name })
76
+ },
77
+ {
78
+ field: "labels",
79
+ flag: "label",
80
+ set: input.setLabels,
81
+ add: input.addLabels,
82
+ remove: input.removeLabels,
83
+ // Labels are bare strings.
84
+ wrap: (label) => label
85
+ }
86
+ ];
87
+ for (const spec of specs) {
88
+ // An empty repeatable flag arrives as `[]`, which means "not passed" rather
89
+ // than "replace with nothing" — treating it as a set would clear the field.
90
+ const set = spec.set !== undefined && spec.set.length > 0 ? spec.set : undefined;
91
+ const increments = [
92
+ ...(spec.add ?? []).map((value) => ({ add: spec.wrap(value) })),
93
+ ...(spec.remove ?? []).map((value) => ({ remove: spec.wrap(value) }))
94
+ ];
95
+ if (set !== undefined && increments.length > 0) {
96
+ return {
97
+ _tag: "Invalid",
98
+ reason: `--${spec.flag} cannot be combined with --add-${spec.flag}/--remove-${spec.flag}; ` +
99
+ `--${spec.flag} replaces the whole list.`
100
+ };
101
+ }
102
+ if (set !== undefined)
103
+ fields[spec.field] = set.map(spec.wrap);
104
+ else if (increments.length > 0)
105
+ update[spec.field] = increments;
106
+ }
107
+ const hasFields = Object.keys(fields).length > 0;
108
+ const hasUpdate = Object.keys(update).length > 0;
109
+ if (!hasFields && !hasUpdate) {
110
+ return {
111
+ _tag: "Invalid",
112
+ reason: "Nothing to edit. Pass at least one of --add-fix-version, --remove-fix-version, --fix-version, --add-label, --remove-label or --label."
113
+ };
114
+ }
115
+ return {
116
+ _tag: "Payload",
117
+ payload: {
118
+ ...(hasFields && { fields }),
119
+ ...(hasUpdate && { update })
120
+ }
44
121
  };
45
122
  };
46
123
  /**
@@ -84,12 +161,12 @@ const FIELDS = [
84
161
  const extractDisplayName = (field) => {
85
162
  if (field === null || field === undefined)
86
163
  return null;
87
- if (typeof field === "string")
164
+ if (Predicate.isString(field))
88
165
  return field;
89
166
  if (Predicate.isReadonlyObject(field)) {
90
- if (typeof field.displayName === "string")
167
+ if (Predicate.isString(field.displayName))
91
168
  return field.displayName;
92
- if (typeof field.name === "string")
169
+ if (Predicate.isString(field.name))
93
170
  return field.name;
94
171
  }
95
172
  return null;
@@ -102,10 +179,10 @@ const extractNameArray = (field) => {
102
179
  return [];
103
180
  return field
104
181
  .map((item) => {
105
- if (typeof item === "string")
182
+ if (Predicate.isString(item))
106
183
  return item;
107
184
  if (Predicate.isReadonlyObject(item)) {
108
- if (typeof item.name === "string")
185
+ if (Predicate.isString(item.name))
109
186
  return item.name;
110
187
  }
111
188
  return null;
@@ -134,7 +211,7 @@ const mapIssue = (bean, baseUrl) => {
134
211
  const attachmentField = fields["attachment"];
135
212
  const attachments = Array.isArray(attachmentField)
136
213
  ? recordArray(attachmentField).map((att) => {
137
- const mediaType = normalizeAttachmentMediaType(undefined, typeof att["mimeType"] === "string" ? att["mimeType"] : null);
214
+ const mediaType = normalizeAttachmentMediaType(undefined, Predicate.isString(att["mimeType"]) ? att["mimeType"] : null);
138
215
  return {
139
216
  id: String(att["id"] ?? ""),
140
217
  filename: String(att["filename"] ?? ""),
@@ -164,13 +241,13 @@ const mapIssue = (bean, baseUrl) => {
164
241
  return {
165
242
  id: commentId,
166
243
  author: extractDisplayName(author) ?? "Unknown",
167
- body: typeof renderedBody === "string" ? renderedBody : String(c["body"] ?? ""),
244
+ body: Predicate.isString(renderedBody) ? renderedBody : String(c["body"] ?? ""),
168
245
  created: parseDate(c["created"]),
169
246
  updated: parseDate(c["updated"])
170
247
  };
171
248
  });
172
249
  // Use rendered description (HTML) if available
173
- const description = typeof renderedFields["description"] === "string"
250
+ const description = Predicate.isString(renderedFields["description"])
174
251
  ? renderedFields["description"]
175
252
  : String(fields["description"] ?? "");
176
253
  return {
@@ -207,7 +284,7 @@ const make = Effect.gen(function* () {
207
284
  params: {
208
285
  jql,
209
286
  maxResults,
210
- ...(nextPageToken ? { nextPageToken } : {}),
287
+ ...(nextPageToken && { nextPageToken }),
211
288
  fields: FIELDS,
212
289
  expand: "renderedFields"
213
290
  }
@@ -250,7 +327,25 @@ const make = Effect.gen(function* () {
250
327
  }
251
328
  return allIssues;
252
329
  });
253
- return IssueService.of({ getByKey, search, searchAll });
330
+ const edit = (key, input) => Effect.gen(function* () {
331
+ const built = buildEditIssuePayload(input);
332
+ if (built._tag === "Invalid")
333
+ return yield* Effect.fail(new JiraApiError({ message: built.reason }));
334
+ yield* client.editIssue(key, { payload: built.payload }).pipe(Effect.mapError((cause) => new JiraApiError({ message: `Failed to edit issue ${key}`, cause })));
335
+ // The 204 carries no body, so re-read to report the resulting state.
336
+ //
337
+ // Past this point the edit is already committed, so a failed read-back must
338
+ // not be reported as a failed edit: a caller told "edit failed" may redo a
339
+ // change that already landed. `returnIssue` would close the window, but the
340
+ // endpoint takes no `fields`, so the returned issue can silently omit what
341
+ // `getByKey` asks for — quietly wrong data in place of a loud error.
342
+ return yield* getByKey(key).pipe(Effect.mapError((cause) => new JiraApiError({
343
+ message: `Edited issue ${key}, but reading it back failed, so the result below is unverified. ` +
344
+ `The edit was applied — do not repeat it.`,
345
+ cause
346
+ })));
347
+ });
348
+ return IssueService.of({ getByKey, search, searchAll, edit });
254
349
  });
255
350
  /**
256
351
  * Layer for IssueService.