@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.
- package/CHANGELOG.md +20 -0
- package/README.md +5 -5
- package/dist/JiraAuth.d.ts +2 -2
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +13 -13
- package/dist/JiraAuth.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +12 -2
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +7 -7
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/edit.d.ts +10 -1
- package/dist/commands/edit.d.ts.map +1 -1
- package/dist/commands/edit.js +9 -9
- package/dist/commands/edit.js.map +1 -1
- package/dist/commands/get.d.ts +6 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +3 -3
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/issue.d.ts +1 -1
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +1 -1
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/issueAttachment.d.ts +1 -1
- package/dist/commands/issueAttachment.d.ts.map +1 -1
- package/dist/commands/issueAttachment.js +13 -11
- package/dist/commands/issueAttachment.js.map +1 -1
- package/dist/commands/layers.d.ts +2 -2
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/search.d.ts +6 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +7 -7
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/version.d.ts +9 -1
- package/dist/commands/version.d.ts.map +1 -1
- package/dist/commands/version.js +23 -23
- package/dist/commands/version.js.map +1 -1
- package/dist/internal/oauthServer.d.ts +2 -2
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +3 -2
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/openBrowser.d.ts +1 -1
- package/dist/internal/openBrowser.d.ts.map +1 -1
- package/dist/internal/openBrowser.js +1 -1
- package/dist/internal/openBrowser.js.map +1 -1
- package/package.json +10 -10
- package/skills/jira/SKILL.md +4 -4
- package/src/JiraAuth.ts +23 -31
- package/src/bin.ts +1 -1
- package/src/commands/auth.ts +7 -7
- package/src/commands/edit.ts +9 -9
- package/src/commands/get.ts +3 -3
- package/src/commands/issue.ts +1 -1
- package/src/commands/issueAttachment.ts +21 -20
- package/src/commands/search.ts +7 -7
- package/src/commands/version.ts +26 -29
- package/src/internal/oauthServer.ts +4 -3
- package/src/internal/openBrowser.ts +1 -1
- package/test/IssueService.test.ts +2 -2
- package/test/JiraAuthRefresh.test.ts +1 -1
- package/test/VersionService.test.ts +2 -2
- package/test/commandTree.test.ts +1 -1
- package/test/integration.test.ts +7 -7
- package/test/oauthServer.test.ts +4 -3
- 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*
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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*
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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*
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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*
|
|
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*
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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*
|
|
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*
|
|
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,
|
package/src/commands/auth.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
63
|
+
: yield* Prompt.String({ message: "Enter OAuth client ID:" })
|
|
64
64
|
const rawClientSecret = Option.isSome(clientSecret)
|
|
65
65
|
? clientSecret.value
|
|
66
|
-
: yield* Prompt.
|
|
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.
|
|
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.
|
|
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
|
|
package/src/commands/edit.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
47
|
+
const jsonOption = Options.Boolean("json").pipe(
|
|
48
48
|
Options.withDescription("Output as JSON"),
|
|
49
49
|
Options.withDefault(false)
|
|
50
50
|
)
|
package/src/commands/get.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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")
|
package/src/commands/issue.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
20
|
+
const fileArg = Args.String("file").pipe(
|
|
21
21
|
Args.withDescription("Local file to upload")
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
const documentOption = Options.
|
|
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.
|
|
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.
|
|
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.
|
|
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*
|
|
92
|
-
|
|
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*
|
|
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*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
package/src/commands/search.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
package/src/commands/version.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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*
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
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.
|
|
198
|
+
const nameOption = Options.String("name").pipe(
|
|
201
199
|
Options.withDescription("Version name (e.g. \"OOB 100\")")
|
|
202
200
|
)
|
|
203
|
-
const optionalDescriptionOption = Options.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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*
|
|
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*
|
|
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
|
|
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/
|
|
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 {
|