@knpkv/jira-cli 1.3.1 → 1.4.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 (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +5 -5
  3. package/dist/JiraAuth.d.ts +2 -2
  4. package/dist/JiraAuth.d.ts.map +1 -1
  5. package/dist/JiraAuth.js +13 -13
  6. package/dist/JiraAuth.js.map +1 -1
  7. package/dist/bin.js +1 -1
  8. package/dist/bin.js.map +1 -1
  9. package/dist/commands/auth.d.ts +12 -2
  10. package/dist/commands/auth.d.ts.map +1 -1
  11. package/dist/commands/auth.js +7 -7
  12. package/dist/commands/auth.js.map +1 -1
  13. package/dist/commands/edit.d.ts +10 -1
  14. package/dist/commands/edit.d.ts.map +1 -1
  15. package/dist/commands/edit.js +9 -9
  16. package/dist/commands/edit.js.map +1 -1
  17. package/dist/commands/get.d.ts +6 -1
  18. package/dist/commands/get.d.ts.map +1 -1
  19. package/dist/commands/get.js +3 -3
  20. package/dist/commands/get.js.map +1 -1
  21. package/dist/commands/issue.d.ts +1 -1
  22. package/dist/commands/issue.d.ts.map +1 -1
  23. package/dist/commands/issue.js +1 -1
  24. package/dist/commands/issue.js.map +1 -1
  25. package/dist/commands/issueAttachment.d.ts +1 -1
  26. package/dist/commands/issueAttachment.d.ts.map +1 -1
  27. package/dist/commands/issueAttachment.js +13 -11
  28. package/dist/commands/issueAttachment.js.map +1 -1
  29. package/dist/commands/layers.d.ts +2 -2
  30. package/dist/commands/layers.d.ts.map +1 -1
  31. package/dist/commands/search.d.ts +6 -1
  32. package/dist/commands/search.d.ts.map +1 -1
  33. package/dist/commands/search.js +7 -7
  34. package/dist/commands/search.js.map +1 -1
  35. package/dist/commands/version.d.ts +9 -1
  36. package/dist/commands/version.d.ts.map +1 -1
  37. package/dist/commands/version.js +23 -23
  38. package/dist/commands/version.js.map +1 -1
  39. package/dist/internal/oauthServer.d.ts +2 -2
  40. package/dist/internal/oauthServer.d.ts.map +1 -1
  41. package/dist/internal/oauthServer.js +3 -2
  42. package/dist/internal/oauthServer.js.map +1 -1
  43. package/dist/internal/openBrowser.d.ts +1 -1
  44. package/dist/internal/openBrowser.d.ts.map +1 -1
  45. package/dist/internal/openBrowser.js +1 -1
  46. package/dist/internal/openBrowser.js.map +1 -1
  47. package/package.json +10 -10
  48. package/skills/jira/SKILL.md +4 -4
  49. package/src/JiraAuth.ts +23 -31
  50. package/src/bin.ts +1 -1
  51. package/src/commands/auth.ts +7 -7
  52. package/src/commands/edit.ts +9 -9
  53. package/src/commands/get.ts +3 -3
  54. package/src/commands/issue.ts +1 -1
  55. package/src/commands/issueAttachment.ts +21 -20
  56. package/src/commands/search.ts +7 -7
  57. package/src/commands/version.ts +26 -29
  58. package/src/internal/oauthServer.ts +4 -3
  59. package/src/internal/openBrowser.ts +1 -1
  60. package/test/IssueService.test.ts +2 -2
  61. package/test/JiraAuthRefresh.test.ts +1 -1
  62. package/test/VersionService.test.ts +2 -2
  63. package/test/commandTree.test.ts +1 -1
  64. package/test/integration.test.ts +7 -7
  65. package/test/oauthServer.test.ts +4 -3
  66. package/test/remoteWrites.test.ts +3 -3
package/src/JiraAuth.ts CHANGED
@@ -59,13 +59,13 @@ import * as Context from "effect/Context"
59
59
  import * as Crypto from "effect/Crypto"
60
60
  import * as Deferred from "effect/Deferred"
61
61
  import * as Effect from "effect/Effect"
62
+ import * as HttpClient from "effect/http/HttpClient"
62
63
  import * as Layer from "effect/Layer"
63
64
  import * as Option from "effect/Option"
64
65
  import type * as PlatformError from "effect/PlatformError"
66
+ import { ChildProcessSpawner } from "effect/process"
65
67
  import * as Redacted from "effect/Redacted"
66
68
  import * as Ref from "effect/Ref"
67
- import * as HttpClient from "effect/unstable/http/HttpClient"
68
- import { ChildProcessSpawner } from "effect/unstable/process"
69
69
  import { HttpServerFactoryLive } from "./internal/NodeLayers.js"
70
70
  import { callbackUrl, startCallbackServer } from "./internal/oauthServer.js"
71
71
  import { openBrowser } from "./internal/openBrowser.js"
@@ -254,12 +254,10 @@ const make = Effect.gen(function*() {
254
254
  Effect.gen(function*() {
255
255
  const config = yield* loadOAuthConfigOp()
256
256
  if (config === null) {
257
- return yield* Effect.fail(
258
- new OAuthError({
259
- step: "authorize",
260
- cause: "OAuth not configured. Run 'jira auth configure' first."
261
- })
262
- )
257
+ return yield* new OAuthError({
258
+ step: "authorize",
259
+ cause: "OAuth not configured. Run 'jira auth configure' first."
260
+ })
263
261
  }
264
262
  return config
265
263
  })
@@ -387,12 +385,10 @@ const make = Effect.gen(function*() {
387
385
  )
388
386
 
389
387
  if (sites.length === 0) {
390
- return yield* Effect.fail(
391
- new OAuthError({
392
- step: "authorize",
393
- cause: "No Jira sites found for this account"
394
- })
395
- )
388
+ return yield* new OAuthError({
389
+ step: "authorize",
390
+ cause: "No Jira sites found for this account"
391
+ })
396
392
  }
397
393
 
398
394
  let site: (typeof sites)[number]
@@ -402,12 +398,10 @@ const make = Effect.gen(function*() {
402
398
  const matched = sites.find((s) => s.url === options.siteUrl)
403
399
  if (!matched) {
404
400
  const available = sites.map((s) => ` - ${s.name}: ${s.url}`).join("\n")
405
- return yield* Effect.fail(
406
- new OAuthError({
407
- step: "authorize",
408
- cause: `Site '${options.siteUrl}' not found. Available sites:\n${available}`
409
- })
410
- )
401
+ return yield* new OAuthError({
402
+ step: "authorize",
403
+ cause: `Site '${options.siteUrl}' not found. Available sites:\n${available}`
404
+ })
411
405
  }
412
406
  site = matched
413
407
  } else {
@@ -464,7 +458,7 @@ const make = Effect.gen(function*() {
464
458
  Effect.gen(function*() {
465
459
  const token = yield* loadTokenOp()
466
460
  if (token === null) {
467
- return yield* Effect.fail(authMissing())
461
+ return yield* authMissing()
468
462
  }
469
463
 
470
464
  const nowMs = yield* Clock.currentTimeMillis
@@ -525,14 +519,12 @@ const make = Effect.gen(function*() {
525
519
  if (error.step === "refresh" && rejected) {
526
520
  return Effect.gen(function*() {
527
521
  yield* deleteTokenOp()
528
- return yield* Effect.fail(
529
- new OAuthError({
530
- step: "refresh",
531
- cause: "Refresh token expired. Please run 'jira auth login' to re-authenticate.",
532
- status,
533
- errorCode
534
- })
535
- )
522
+ return yield* new OAuthError({
523
+ step: "refresh",
524
+ cause: "Refresh token expired. Please run 'jira auth login' to re-authenticate.",
525
+ status,
526
+ errorCode
527
+ })
536
528
  })
537
529
  }
538
530
  return Effect.fail(error)
@@ -558,7 +550,7 @@ const make = Effect.gen(function*() {
558
550
  Effect.gen(function*() {
559
551
  const token = yield* loadTokenOp()
560
552
  if (token === null) {
561
- return yield* Effect.fail(authMissing())
553
+ return yield* authMissing()
562
554
  }
563
555
  return token.cloud_id
564
556
  })
@@ -570,7 +562,7 @@ const make = Effect.gen(function*() {
570
562
  Effect.gen(function*() {
571
563
  const token = yield* loadTokenOp()
572
564
  if (token === null) {
573
- return yield* Effect.fail(authMissing())
565
+ return yield* authMissing()
574
566
  }
575
567
  return token.site_url
576
568
  })
package/src/bin.ts CHANGED
@@ -8,10 +8,10 @@
8
8
  */
9
9
  import { NodeRuntime, NodeStdio } from "@effect/platform-node"
10
10
  import { makeInstallCommand } from "@knpkv/agent-skills"
11
+ import { Command } from "effect/cli"
11
12
  import * as Console from "effect/Console"
12
13
  import * as Effect from "effect/Effect"
13
14
  import * as Stdio from "effect/Stdio"
14
- import { Command } from "effect/unstable/cli"
15
15
  import pkg from "../package.json" with { type: "json" }
16
16
  import {
17
17
  AppLayer,
@@ -8,10 +8,10 @@
8
8
  *
9
9
  * @internal
10
10
  */
11
+ import { Argument as Args, Command, Flag as Options, Prompt } from "effect/cli"
11
12
  import * as Console from "effect/Console"
12
13
  import * as Effect from "effect/Effect"
13
14
  import * as Option from "effect/Option"
14
- import { Argument as Args, Command, Flag as Options, Prompt } from "effect/unstable/cli"
15
15
  import { openBrowser } from "../internal/openBrowser.js"
16
16
  import { JiraAuth } from "../JiraAuth.js"
17
17
 
@@ -42,11 +42,11 @@ Creating OAuth app in Atlassian Developer Console...
42
42
  })).pipe(Command.withDescription("Create OAuth app in Atlassian Developer Console"))
43
43
 
44
44
  // === Auth configure command ===
45
- const clientIdOption = Options.string("client-id").pipe(
45
+ const clientIdOption = Options.String("client-id").pipe(
46
46
  Options.withDescription("OAuth client ID from Atlassian Developer Console"),
47
47
  Options.optional
48
48
  )
49
- const clientSecretOption = Options.string("client-secret").pipe(
49
+ const clientSecretOption = Options.String("client-secret").pipe(
50
50
  Options.withDescription("OAuth client secret"),
51
51
  Options.optional
52
52
  )
@@ -60,10 +60,10 @@ const configureCommand = Command.make(
60
60
 
61
61
  const rawClientId = Option.isSome(clientId)
62
62
  ? clientId.value
63
- : yield* Prompt.text({ message: "Enter OAuth client ID:" })
63
+ : yield* Prompt.String({ message: "Enter OAuth client ID:" })
64
64
  const rawClientSecret = Option.isSome(clientSecret)
65
65
  ? clientSecret.value
66
- : yield* Prompt.text({ message: "Enter OAuth client secret:" })
66
+ : yield* Prompt.String({ message: "Enter OAuth client secret:" })
67
67
 
68
68
  yield* auth.configure({ clientId: rawClientId, clientSecret: rawClientSecret })
69
69
  yield* Console.log("OAuth configured. Run 'jira auth login' to authenticate.")
@@ -71,7 +71,7 @@ const configureCommand = Command.make(
71
71
  ).pipe(Command.withDescription("Configure OAuth client credentials"))
72
72
 
73
73
  // === Auth login command ===
74
- const siteOption = Options.string("site").pipe(
74
+ const siteOption = Options.String("site").pipe(
75
75
  Options.withDescription("Jira site URL to use (for accounts with multiple sites)"),
76
76
  Options.optional
77
77
  )
@@ -128,7 +128,7 @@ const profilesCommand = Command.make("profiles", {}, () =>
128
128
  }
129
129
  })).pipe(Command.withDescription("List stored auth profiles"))
130
130
 
131
- const profileArg = Args.string("profile").pipe(
131
+ const profileArg = Args.String("profile").pipe(
132
132
  Args.withDescription("Profile ID, name, site URL, cloud ID, or account ID")
133
133
  )
134
134
 
@@ -7,44 +7,44 @@
7
7
  *
8
8
  * @internal
9
9
  */
10
+ import { Argument as Args, Command, Flag as Options } from "effect/cli"
10
11
  import * as Console from "effect/Console"
11
12
  import * as Effect from "effect/Effect"
12
- import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"
13
13
  import { IssueService } from "../IssueService.js"
14
14
 
15
- const keyArg = Args.string("key").pipe(
15
+ const keyArg = Args.String("key").pipe(
16
16
  Args.withDescription("Issue key (e.g., PROJ-123)")
17
17
  )
18
18
 
19
- const addFixVersionOption = Options.string("add-fix-version").pipe(
19
+ const addFixVersionOption = Options.String("add-fix-version").pipe(
20
20
  Options.withDescription("Add a fix version by name, keeping existing ones (repeatable)"),
21
21
  Options.atLeast(0)
22
22
  )
23
- const removeFixVersionOption = Options.string("remove-fix-version").pipe(
23
+ const removeFixVersionOption = Options.String("remove-fix-version").pipe(
24
24
  Options.withDescription("Remove a fix version by name (repeatable)"),
25
25
  Options.atLeast(0)
26
26
  )
27
- const fixVersionOption = Options.string("fix-version").pipe(
27
+ const fixVersionOption = Options.String("fix-version").pipe(
28
28
  Options.withDescription(
29
29
  "Replace the fix versions with exactly these names (repeatable). Drops any not listed — prefer --add-fix-version"
30
30
  ),
31
31
  Options.atLeast(0)
32
32
  )
33
- const addLabelOption = Options.string("add-label").pipe(
33
+ const addLabelOption = Options.String("add-label").pipe(
34
34
  Options.withDescription("Add a label, keeping existing ones (repeatable)"),
35
35
  Options.atLeast(0)
36
36
  )
37
- const removeLabelOption = Options.string("remove-label").pipe(
37
+ const removeLabelOption = Options.String("remove-label").pipe(
38
38
  Options.withDescription("Remove a label (repeatable)"),
39
39
  Options.atLeast(0)
40
40
  )
41
- const labelOption = Options.string("label").pipe(
41
+ const labelOption = Options.String("label").pipe(
42
42
  Options.withDescription(
43
43
  "Replace the labels with exactly these (repeatable). Drops any not listed — prefer --add-label"
44
44
  ),
45
45
  Options.atLeast(0)
46
46
  )
47
- const jsonOption = Options.boolean("json").pipe(
47
+ const jsonOption = Options.Boolean("json").pipe(
48
48
  Options.withDescription("Output as JSON"),
49
49
  Options.withDefault(false)
50
50
  )
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * @internal
5
5
  */
6
+ import { Argument as Args, Command, Flag as Options } from "effect/cli"
6
7
  import * as Console from "effect/Console"
7
8
  import * as Effect from "effect/Effect"
8
- import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"
9
9
  import { IssueService } from "../IssueService.js"
10
10
  import { MarkdownWriter } from "../MarkdownWriter.js"
11
11
 
12
- const keyArg = Args.string("key").pipe(
12
+ const keyArg = Args.String("key").pipe(
13
13
  Args.withDescription("Issue key (e.g., PROJ-123)")
14
14
  )
15
15
 
16
- const outputDirOption = Options.directory("output-dir").pipe(
16
+ const outputDirOption = Options.Directory("output-dir").pipe(
17
17
  Options.withAlias("o"),
18
18
  Options.withDescription("Output directory for markdown file"),
19
19
  Options.withDefault("./jira-tickets")
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @internal
5
5
  */
6
- import { Command } from "effect/unstable/cli"
6
+ import { Command } from "effect/cli"
7
7
  import { editCommand } from "./edit.js"
8
8
  import { getCommand } from "./get.js"
9
9
  import { attachmentCommand } from "./issueAttachment.js"
@@ -4,39 +4,42 @@
4
4
  * @internal
5
5
  */
6
6
  import { renderAttachmentMarkdown } from "@knpkv/atlassian-common/attachments"
7
+ import { Argument as Args, Command, Flag as Options } from "effect/cli"
7
8
  import * as Console from "effect/Console"
8
9
  import * as Effect from "effect/Effect"
9
10
  import * as FileSystem from "effect/FileSystem"
10
11
  import * as Option from "effect/Option"
11
- import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"
12
12
  import { AttachmentService } from "../AttachmentService.js"
13
13
  import { insertJiraAttachmentReference } from "../internal/attachmentInsertion.js"
14
14
  import { JiraApiError, WriteError } from "../JiraCliError.js"
15
15
 
16
- const issueArg = Args.string("issue").pipe(
16
+ const issueArg = Args.String("issue").pipe(
17
17
  Args.withDescription("Issue key or id, for example PROJ-123")
18
18
  )
19
19
 
20
- const fileArg = Args.string("file").pipe(
20
+ const fileArg = Args.String("file").pipe(
21
21
  Args.withDescription("Local file to upload")
22
22
  )
23
23
 
24
- const documentOption = Options.file("document").pipe(
24
+ const documentOption = Options.File("document").pipe(
25
25
  Options.withDescription("Local Jira Markdown document containing the attachment placeholder"),
26
26
  Options.optional
27
27
  )
28
28
 
29
- const noInsertOption = Options.boolean("no-insert").pipe(
30
- Options.withDescription("Only upload and print the Attachment Reference")
29
+ const noInsertOption = Options.Boolean("no-insert").pipe(
30
+ Options.withDescription("Only upload and print the Attachment Reference"),
31
+ Options.withDefault(false)
31
32
  )
32
33
 
33
- const dryRunOption = Options.boolean("dry-run").pipe(
34
+ const dryRunOption = Options.Boolean("dry-run").pipe(
34
35
  Options.withAlias("n"),
35
- Options.withDescription("Validate local insertion input without uploading")
36
+ Options.withDescription("Validate local insertion input without uploading"),
37
+ Options.withDefault(false)
36
38
  )
37
39
 
38
- const jsonOption = Options.boolean("json").pipe(
39
- Options.withDescription("Write exactly one JSON value to stdout")
40
+ const jsonOption = Options.Boolean("json").pipe(
41
+ Options.withDescription("Write exactly one JSON value to stdout"),
42
+ Options.withDefault(false)
40
43
  )
41
44
 
42
45
  const readDocument = (documentPath: string) =>
@@ -88,16 +91,16 @@ const uploadCommand = Command.make(
88
91
  if (documentInput !== null) {
89
92
  const matches = countPlaceholderMatches(documentInput.content, file)
90
93
  if (matches !== 1) {
91
- return yield* Effect.fail(
92
- new JiraApiError({ message: `Expected exactly one attachment placeholder for ${file}, found ${matches}` })
93
- )
94
+ return yield* new JiraApiError({
95
+ message: `Expected exactly one attachment placeholder for ${file}, found ${matches}`
96
+ })
94
97
  }
95
98
  }
96
99
 
97
100
  if (dryRun) {
98
101
  const exists = yield* fileExists(file)
99
102
  if (!exists) {
100
- return yield* Effect.fail(new JiraApiError({ message: `Attachment file does not exist: ${file}` }))
103
+ return yield* new JiraApiError({ message: `Attachment file does not exist: ${file}` })
101
104
  }
102
105
  const result = { dryRun: true, issue, file, insert: shouldInsert }
103
106
  yield* Console.log(json ? JSON.stringify(result) : `Dry run: ${file} can be uploaded to ${issue}`)
@@ -111,12 +114,10 @@ const uploadCommand = Command.make(
111
114
  if (documentInput !== null) {
112
115
  const result = insertJiraAttachmentReference(documentInput.content, file, attachment)
113
116
  if (result.replacements !== 1) {
114
- return yield* Effect.fail(
115
- new JiraApiError({
116
- message:
117
- `Uploaded attachment ${attachment.id}, but expected exactly one local placeholder for ${file}; found ${result.replacements}`
118
- })
119
- )
117
+ return yield* new JiraApiError({
118
+ message:
119
+ `Uploaded attachment ${attachment.id}, but expected exactly one local placeholder for ${file}; found ${result.replacements}`
120
+ })
120
121
  }
121
122
  yield* writeDocument(documentInput.path, result.content)
122
123
  inserted = true
@@ -3,45 +3,45 @@
3
3
  *
4
4
  * @internal
5
5
  */
6
+ import { Argument as Args, Command, Flag as Options } from "effect/cli"
6
7
  import * as Console from "effect/Console"
7
8
  import * as Effect from "effect/Effect"
8
9
  import * as Option from "effect/Option"
9
- import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"
10
10
  import { buildByVersionJql } from "../internal/jqlBuilder.js"
11
11
  import { IssueService } from "../IssueService.js"
12
12
  import { MarkdownWriter } from "../MarkdownWriter.js"
13
13
 
14
14
  // === Options ===
15
- const jqlArg = Args.string("jql").pipe(
15
+ const jqlArg = Args.String("jql").pipe(
16
16
  Args.withDescription("JQL query to search for issues"),
17
17
  Args.optional
18
18
  )
19
19
 
20
- const byVersionOption = Options.string("by-version").pipe(
20
+ const byVersionOption = Options.String("by-version").pipe(
21
21
  Options.withAlias("v"),
22
22
  Options.withDescription("Search by fix version (pre-defined query)"),
23
23
  Options.optional
24
24
  )
25
25
 
26
- const projectOption = Options.string("project").pipe(
26
+ const projectOption = Options.String("project").pipe(
27
27
  Options.withAlias("p"),
28
28
  Options.withDescription("Filter by project key"),
29
29
  Options.optional
30
30
  )
31
31
 
32
- const outputDirOption = Options.directory("output-dir").pipe(
32
+ const outputDirOption = Options.Directory("output-dir").pipe(
33
33
  Options.withAlias("o"),
34
34
  Options.withDescription("Output directory for markdown files"),
35
35
  Options.withDefault("./jira-tickets")
36
36
  )
37
37
 
38
- const formatOption = Options.choice("format", ["multi", "single"]).pipe(
38
+ const formatOption = Options.Literals("format", ["multi", "single"]).pipe(
39
39
  Options.withAlias("f"),
40
40
  Options.withDescription("Output format: multi (one file per issue) or single (combined file)"),
41
41
  Options.withDefault("multi")
42
42
  )
43
43
 
44
- const maxResultsOption = Options.integer("max-results").pipe(
44
+ const maxResultsOption = Options.Int("max-results").pipe(
45
45
  Options.withAlias("m"),
46
46
  Options.withDescription("Maximum number of results to fetch"),
47
47
  Options.withDefault(100)
@@ -6,10 +6,10 @@
6
6
  *
7
7
  * @internal
8
8
  */
9
+ import { Argument as Args, Command, Flag as Options } from "effect/cli"
9
10
  import * as Console from "effect/Console"
10
11
  import * as Effect from "effect/Effect"
11
12
  import * as Option from "effect/Option"
12
- import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli"
13
13
  import { JiraApiError } from "../JiraCliError.js"
14
14
  import type { Person, Version } from "../VersionService.js"
15
15
  import { planRelatedWorkSync, VersionService } from "../VersionService.js"
@@ -48,27 +48,27 @@ const ensureNumericId = (id: string): Effect.Effect<void, JiraApiError> =>
48
48
  })
49
49
  )
50
50
 
51
- const projectOption = Options.string("project").pipe(
51
+ const projectOption = Options.String("project").pipe(
52
52
  Options.withAlias("p"),
53
53
  Options.withDescription("Jira project key (e.g. RPS)")
54
54
  )
55
- const releasedOption = Options.boolean("released").pipe(
55
+ const releasedOption = Options.Boolean("released").pipe(
56
56
  Options.withDescription("Only list released versions"),
57
57
  Options.withDefault(false)
58
58
  )
59
- const unreleasedOption = Options.boolean("unreleased").pipe(
59
+ const unreleasedOption = Options.Boolean("unreleased").pipe(
60
60
  Options.withDescription("Only list unreleased versions"),
61
61
  Options.withDefault(false)
62
62
  )
63
- const jsonOption = Options.boolean("json").pipe(
63
+ const jsonOption = Options.Boolean("json").pipe(
64
64
  Options.withDescription("Output as JSON"),
65
65
  Options.withDefault(false)
66
66
  )
67
- const emailsOption = Options.boolean("emails").pipe(
67
+ const emailsOption = Options.Boolean("emails").pipe(
68
68
  Options.withDescription("Include resolved user email addresses in --json output"),
69
69
  Options.withDefault(false)
70
70
  )
71
- const customFieldOption = Options.string("custom-field").pipe(
71
+ const customFieldOption = Options.String("custom-field").pipe(
72
72
  Options.withDescription(
73
73
  "Custom field display name to include on each ticket (repeatable, e.g. " +
74
74
  "--custom-field \"Security & Compliance Impact\"). Values are exposed in " +
@@ -76,13 +76,13 @@ const customFieldOption = Options.string("custom-field").pipe(
76
76
  ),
77
77
  Options.atLeast(0)
78
78
  )
79
- const maxOption = Options.integer("max").pipe(
79
+ const maxOption = Options.Int("max").pipe(
80
80
  Options.withAlias("m"),
81
81
  Options.withDescription("Maximum number of versions to fetch (default: all)"),
82
82
  Options.optional
83
83
  )
84
84
 
85
- const idArg = Args.string("id").pipe(Args.withDescription("Version id (numeric)"))
85
+ const idArg = Args.String("id").pipe(Args.withDescription("Version id (numeric)"))
86
86
 
87
87
  const listCommand = Command.make("list", {
88
88
  project: projectOption,
@@ -95,11 +95,9 @@ const listCommand = Command.make("list", {
95
95
  }, ({ customFields, emails, json, max, project, released, unreleased }) =>
96
96
  Effect.gen(function*() {
97
97
  if (released && unreleased) {
98
- return yield* Effect.fail(
99
- new JiraApiError({
100
- message: "--released and --unreleased are mutually exclusive; pass at most one (omit both to list all)."
101
- })
102
- )
98
+ return yield* new JiraApiError({
99
+ message: "--released and --unreleased are mutually exclusive; pass at most one (omit both to list all)."
100
+ })
103
101
  }
104
102
  const service = yield* VersionService
105
103
  const versions = yield* service.listProjectVersions(project, {
@@ -169,7 +167,7 @@ const formatTicketKeys = (tickets: Version["tickets"]): string => {
169
167
  return remaining > 0 ? `${shown} (+${remaining} more)` : shown
170
168
  }
171
169
 
172
- const descriptionOption = Options.string("description").pipe(
170
+ const descriptionOption = Options.String("description").pipe(
173
171
  Options.withAlias("d"),
174
172
  Options.withDescription("New version description")
175
173
  )
@@ -197,19 +195,19 @@ const ensureIsoDate = (flag: string, value: string): Effect.Effect<void, JiraApi
197
195
  // No `-n` alias: it means `--dry-run` everywhere else in these CLIs, and both
198
196
  // `getLayerType` implementations route on `argv.includes("-n")`. Reusing it for
199
197
  // a value on a remote-write command is the wrong default.
200
- const nameOption = Options.string("name").pipe(
198
+ const nameOption = Options.String("name").pipe(
201
199
  Options.withDescription("Version name (e.g. \"OOB 100\")")
202
200
  )
203
- const optionalDescriptionOption = Options.string("description").pipe(
201
+ const optionalDescriptionOption = Options.String("description").pipe(
204
202
  Options.withAlias("d"),
205
203
  Options.withDescription("Version description"),
206
204
  Options.optional
207
205
  )
208
- const startDateOption = Options.string("start-date").pipe(
206
+ const startDateOption = Options.String("start-date").pipe(
209
207
  Options.withDescription("Start date, ISO 8601 (yyyy-mm-dd)"),
210
208
  Options.optional
211
209
  )
212
- const releaseDateOption = Options.string("release-date").pipe(
210
+ const releaseDateOption = Options.String("release-date").pipe(
213
211
  Options.withDescription("Release date, ISO 8601 (yyyy-mm-dd)"),
214
212
  Options.optional
215
213
  )
@@ -269,15 +267,15 @@ const updateCommand = Command.make("update", { id: idArg, description: descripti
269
267
 
270
268
  // === related-work ===
271
269
 
272
- const titleOption = Options.string("title").pipe(
270
+ const titleOption = Options.String("title").pipe(
273
271
  Options.withAlias("t"),
274
272
  Options.withDescription("Related-work link title (e.g. \"Release notes\")")
275
273
  )
276
- const urlOption = Options.string("url").pipe(
274
+ const urlOption = Options.String("url").pipe(
277
275
  Options.withAlias("u"),
278
276
  Options.withDescription("Related-work link URL (e.g. a Confluence page)")
279
277
  )
280
- const categoryOption = Options.string("category").pipe(
278
+ const categoryOption = Options.String("category").pipe(
281
279
  Options.withAlias("c"),
282
280
  Options.withDescription("Related-work category (Jira groups by this; e.g. Communication, Testing, Design)"),
283
281
  Options.withDefault("Communication")
@@ -330,14 +328,15 @@ const relatedWorkAddCommand = Command.make("add", {
330
328
  )
331
329
  )
332
330
 
333
- const linkOption = Options.string("link").pipe(
331
+ const linkOption = Options.String("link").pipe(
334
332
  Options.withAlias("l"),
335
333
  Options.withDescription("Desired link as `title=url` (repeatable). Category comes from --category."),
336
334
  Options.atLeast(0)
337
335
  )
338
336
 
339
- const pruneOption = Options.boolean("prune").pipe(
340
- Options.withDescription("Also remove links in the category that are not in the desired set")
337
+ const pruneOption = Options.Boolean("prune").pipe(
338
+ Options.withDescription("Also remove links in the category that are not in the desired set"),
339
+ Options.withDefault(false)
341
340
  )
342
341
 
343
342
  /**
@@ -367,14 +366,12 @@ const relatedWorkSyncCommand = Command.make("sync", {
367
366
  const title = separator < 0 ? "" : raw.slice(0, separator).trim()
368
367
  const url = separator < 0 ? "" : raw.slice(separator + 1).trim()
369
368
  if (separator < 0 || title.length === 0 || url.length === 0) {
370
- return yield* Effect.fail(
371
- new JiraApiError({ message: `Invalid --link ${JSON.stringify(raw)}. Expected title=url.` })
372
- )
369
+ return yield* new JiraApiError({ message: `Invalid --link ${JSON.stringify(raw)}. Expected title=url.` })
373
370
  }
374
371
  desired.push({ title, url })
375
372
  }
376
373
  if (desired.length === 0) {
377
- return yield* Effect.fail(new JiraApiError({ message: "Pass at least one --link title=url." }))
374
+ return yield* new JiraApiError({ message: "Pass at least one --link title=url." })
378
375
  }
379
376
 
380
377
  const service = yield* VersionService
@@ -14,11 +14,12 @@ import { OAuthError } from "@knpkv/atlassian-common/auth"
14
14
  import * as Context from "effect/Context"
15
15
  import * as Deferred from "effect/Deferred"
16
16
  import * as Effect from "effect/Effect"
17
+ import { HttpRouter, HttpServer, HttpServerResponse } from "effect/http"
18
+ import type * as HttpServerError from "effect/http/HttpServerError"
17
19
  import * as Layer from "effect/Layer"
20
+ import { NetAddress } from "effect/net"
18
21
  import * as Schema from "effect/Schema"
19
22
  import type * as Scope from "effect/Scope"
20
- import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
21
- import type * as HttpServerError from "effect/unstable/http/HttpServerError"
22
23
 
23
24
  const DEFAULT_PORT = 8585
24
25
  const MAX_PORT = 8594
@@ -122,7 +123,7 @@ export const startCallbackServer = (
122
123
  )
123
124
  const { context: serverContext } = yield* buildServerContext(DEFAULT_PORT)
124
125
  const server: HttpServerInstance = Context.get(serverContext, HttpServer.HttpServer)
125
- const port = yield* (server.address._tag === "TcpAddress"
126
+ const port = yield* (NetAddress.isInetAddress(server.address)
126
127
  ? Effect.succeed(server.address.port)
127
128
  : Effect.fail(new OAuthError({ step: "authorize", cause: "OAuth callback server must listen on a TCP port" })))
128
129
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as Effect from "effect/Effect"
7
7
  import type * as PlatformError from "effect/PlatformError"
8
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
8
+ import { ChildProcess, ChildProcessSpawner } from "effect/process"
9
9
 
10
10
  const run = (
11
11
  command: string,
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
2
  import { JiraApiClient, make } from "@knpkv/jira-api-client"
3
3
  import * as Effect from "effect/Effect"
4
+ import * as HttpClient from "effect/http/HttpClient"
5
+ import * as HttpClientResponse from "effect/http/HttpClientResponse"
4
6
  import * as Layer from "effect/Layer"
5
7
  import * as Redacted from "effect/Redacted"
6
- import * as HttpClient from "effect/unstable/http/HttpClient"
7
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
8
8
  import { IssueService, layer as IssueServiceLayer, SiteUrl } from "../src/IssueService.js"
9
9
 
10
10
  const httpClient = HttpClient.make((request) =>
@@ -24,9 +24,9 @@ import * as Deferred from "effect/Deferred"
24
24
  import * as Effect from "effect/Effect"
25
25
  import * as Fiber from "effect/Fiber"
26
26
  import * as FileSystem from "effect/FileSystem"
27
+ import { HttpClient, HttpClientResponse } from "effect/http"
27
28
  import * as Layer from "effect/Layer"
28
29
  import { TestClock } from "effect/testing"
29
- import { HttpClient, HttpClientResponse } from "effect/unstable/http"
30
30
  import { JiraAuth, layer as jiraAuthLayer } from "../src/JiraAuth.js"
31
31
 
32
32
  const TOOL = "jira-cli"
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
2
  import { JiraApiClient, make } from "@knpkv/jira-api-client"
3
3
  import * as Effect from "effect/Effect"
4
+ import * as HttpClient from "effect/http/HttpClient"
5
+ import * as HttpClientResponse from "effect/http/HttpClientResponse"
4
6
  import * as Layer from "effect/Layer"
5
7
  import * as Redacted from "effect/Redacted"
6
- import * as HttpClient from "effect/unstable/http/HttpClient"
7
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
8
8
  import { stripEmails } from "../src/commands/version.js"
9
9
  import type { Version } from "../src/VersionService.js"
10
10
  import {