@knpkv/jira-cli 1.3.1 → 1.4.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.
- package/CHANGELOG.md +31 -0
- package/README.md +5 -5
- package/dist/JiraAuth.d.ts +23 -8
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +8 -303
- 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 +2 -2
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +8 -8
- 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/package.json +10 -10
- package/skills/jira/SKILL.md +4 -4
- package/src/JiraAuth.ts +24 -477
- package/src/bin.ts +1 -1
- package/src/commands/auth.ts +8 -8
- 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/test/IssueService.test.ts +2 -2
- package/test/JiraAuth.test.ts +79 -0
- package/test/VersionService.test.ts +2 -2
- package/test/commandTree.test.ts +1 -1
- package/test/integration.test.ts +7 -7
- package/test/remoteWrites.test.ts +3 -3
- package/vitest.config.integration.ts +4 -1
- package/vitest.config.ts +4 -1
- package/dist/internal/NodeLayers.d.ts +0 -7
- package/dist/internal/NodeLayers.d.ts.map +0 -1
- package/dist/internal/NodeLayers.js +0 -15
- package/dist/internal/NodeLayers.js.map +0 -1
- package/dist/internal/oauthServer.d.ts +0 -72
- package/dist/internal/oauthServer.d.ts.map +0 -1
- package/dist/internal/oauthServer.js +0 -96
- package/dist/internal/oauthServer.js.map +0 -1
- package/dist/internal/openBrowser.d.ts +0 -10
- package/dist/internal/openBrowser.d.ts.map +0 -1
- package/dist/internal/openBrowser.js +0 -17
- package/dist/internal/openBrowser.js.map +0 -1
- package/src/internal/NodeLayers.ts +0 -17
- package/src/internal/oauthServer.ts +0 -186
- package/src/internal/openBrowser.ts +0 -31
- package/test/JiraAuthRefresh.test.ts +0 -269
- package/test/oauthServer.test.ts +0 -171
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
|
|
@@ -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) =>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** @effect-diagnostics strictEffectProvide:skip-file */
|
|
2
|
+
/**
|
|
3
|
+
* JiraAuth is the shared Atlassian CLI auth bound to Jira. These pin the binding:
|
|
4
|
+
* the `jira-cli` storage namespace, Jira's own "not logged in" error, the `jira`
|
|
5
|
+
* command in re-login hints, and a redacted token. The refresh rules themselves
|
|
6
|
+
* are covered once in `@knpkv/atlassian-common`'s cli-auth tests.
|
|
7
|
+
*/
|
|
8
|
+
import { NodeFileSystem, NodePath, NodeServices } from "@effect/platform-node"
|
|
9
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
10
|
+
import { HomeDirectoryLive, saveOAuthConfig, saveProfileToken } from "@knpkv/atlassian-common/config"
|
|
11
|
+
import { ConfigProvider } from "effect"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import * as FileSystem from "effect/FileSystem"
|
|
14
|
+
import { HttpClient, HttpClientResponse } from "effect/http"
|
|
15
|
+
import * as Layer from "effect/Layer"
|
|
16
|
+
import * as Redacted from "effect/Redacted"
|
|
17
|
+
import { JiraAuth, layer as jiraAuthLayer } from "../src/JiraAuth.js"
|
|
18
|
+
|
|
19
|
+
const storage = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, HomeDirectoryLive)
|
|
20
|
+
|
|
21
|
+
const withHome = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
22
|
+
Effect.gen(function*() {
|
|
23
|
+
const fs = yield* FileSystem.FileSystem
|
|
24
|
+
const home = yield* fs.makeTempDirectoryScoped({ prefix: "jira-auth-" })
|
|
25
|
+
return yield* effect.pipe(
|
|
26
|
+
Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: home } })))
|
|
27
|
+
)
|
|
28
|
+
}).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer))
|
|
29
|
+
|
|
30
|
+
const rejectingClient = Layer.succeed(
|
|
31
|
+
HttpClient.HttpClient,
|
|
32
|
+
HttpClient.make((request) =>
|
|
33
|
+
Effect.succeed(
|
|
34
|
+
HttpClientResponse.fromWeb(request, new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400 }))
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const withAuth = <A, E>(use: (auth: JiraAuth["Service"]) => Effect.Effect<A, E>) =>
|
|
40
|
+
JiraAuth.pipe(
|
|
41
|
+
Effect.flatMap(use),
|
|
42
|
+
Effect.provide(jiraAuthLayer.pipe(Layer.provide(Layer.mergeAll(rejectingClient, NodeServices.layer))))
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const token = (expiresAt: number) => ({
|
|
46
|
+
access_token: "access-1",
|
|
47
|
+
refresh_token: "refresh-1",
|
|
48
|
+
expires_at: expiresAt,
|
|
49
|
+
token_type: "Bearer",
|
|
50
|
+
scope: "offline_access",
|
|
51
|
+
cloud_id: "cloud-1",
|
|
52
|
+
site_url: "https://example.atlassian.net"
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("JiraAuth", () => {
|
|
56
|
+
it.effect("fails with Jira's own error when nobody is logged in", () =>
|
|
57
|
+
withHome(Effect.gen(function*() {
|
|
58
|
+
const error = yield* withAuth((auth) => auth.getAccessToken()).pipe(Effect.flip)
|
|
59
|
+
expect(error).toMatchObject({ _tag: "AuthMissingError", message: "Not logged in. Run 'jira auth login' first." })
|
|
60
|
+
})))
|
|
61
|
+
|
|
62
|
+
it.effect("reads the jira-cli profile and returns a redacted token", () =>
|
|
63
|
+
withHome(Effect.gen(function*() {
|
|
64
|
+
yield* saveProfileToken("jira-cli", token(Number.MAX_SAFE_INTEGER)).pipe(Effect.provide(storage))
|
|
65
|
+
const accessToken = yield* withAuth((auth) => auth.getAccessToken())
|
|
66
|
+
expect(Redacted.value(accessToken)).toBe("access-1")
|
|
67
|
+
expect(yield* withAuth((auth) => auth.getSiteUrl())).toBe("https://example.atlassian.net")
|
|
68
|
+
})))
|
|
69
|
+
|
|
70
|
+
it.effect("names the jira command when the stored token is spent", () =>
|
|
71
|
+
withHome(Effect.gen(function*() {
|
|
72
|
+
yield* saveOAuthConfig("jira-cli", { clientId: "client-1", clientSecret: "secret-1" }).pipe(
|
|
73
|
+
Effect.provide(storage)
|
|
74
|
+
)
|
|
75
|
+
yield* saveProfileToken("jira-cli", token(0)).pipe(Effect.provide(storage))
|
|
76
|
+
const error = yield* withAuth((auth) => auth.getAccessToken()).pipe(Effect.flip)
|
|
77
|
+
expect(error.message).toContain("Please run 'jira auth login'")
|
|
78
|
+
})))
|
|
79
|
+
})
|
|
@@ -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 {
|
package/test/commandTree.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import { Command } from "effect/cli"
|
|
2
3
|
import * as Effect from "effect/Effect"
|
|
3
4
|
import * as Layer from "effect/Layer"
|
|
4
5
|
import * as Ref from "effect/Ref"
|
|
5
6
|
import * as Terminal from "effect/Terminal"
|
|
6
|
-
import { Command } from "effect/unstable/cli"
|
|
7
7
|
import { AttachmentService } from "../src/AttachmentService.js"
|
|
8
8
|
import { issueCommand } from "../src/commands/issue.js"
|
|
9
9
|
import { versionCommand } from "../src/commands/version.js"
|
package/test/integration.test.ts
CHANGED
|
@@ -55,15 +55,15 @@ const requireNonEmptyRedacted = (
|
|
|
55
55
|
Redacted.value(value).trim().length > 0 ? Effect.succeed(value) : Effect.fail(new Error(`${name} must be set`))
|
|
56
56
|
|
|
57
57
|
const SHOULD_RUN_INTEGRATION = Effect.runSync(
|
|
58
|
-
Config.option(Config.
|
|
58
|
+
Config.option(Config.String("JIRA_INTEGRATION")).pipe(
|
|
59
59
|
Effect.map((enabled) => Option.isSome(enabled) && envFlagEnabled(enabled.value))
|
|
60
60
|
)
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
const SHOULD_RUN_ATTACHMENT_INTEGRATION = Effect.runSync(
|
|
64
64
|
Effect.all([
|
|
65
|
-
Config.option(Config.
|
|
66
|
-
Config.option(Config.
|
|
65
|
+
Config.option(Config.String("JIRA_INTEGRATION")),
|
|
66
|
+
Config.option(Config.String("JIRA_ATTACHMENT_INTEGRATION"))
|
|
67
67
|
]).pipe(
|
|
68
68
|
Effect.map(([integration, attachment]) =>
|
|
69
69
|
Option.isSome(integration) &&
|
|
@@ -75,10 +75,10 @@ const SHOULD_RUN_ATTACHMENT_INTEGRATION = Effect.runSync(
|
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
const readIntegrationConfig = Effect.gen(function*() {
|
|
78
|
-
const baseUrl = yield* Config.option(Config.
|
|
79
|
-
const issueKey = yield* Config.option(Config.
|
|
80
|
-
const email = yield* Config.
|
|
81
|
-
const apiKey = yield* Config.
|
|
78
|
+
const baseUrl = yield* Config.option(Config.String("JIRA_BASE_URL"))
|
|
79
|
+
const issueKey = yield* Config.option(Config.String("JIRA_ISSUE_KEY"))
|
|
80
|
+
const email = yield* Config.String("JIRA_EMAIL")
|
|
81
|
+
const apiKey = yield* Config.Redacted("JIRA_API_KEY")
|
|
82
82
|
|
|
83
83
|
return {
|
|
84
84
|
baseUrl: nonEmptyOrDefault(baseUrl, DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
@@ -1,12 +1,12 @@
|
|
|
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 type * as HttpClientRequest from "effect/http/HttpClientRequest"
|
|
6
|
+
import * as HttpClientResponse from "effect/http/HttpClientResponse"
|
|
4
7
|
import * as Layer from "effect/Layer"
|
|
5
8
|
import * as Redacted from "effect/Redacted"
|
|
6
9
|
import * as Schema from "effect/Schema"
|
|
7
|
-
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
8
|
-
import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
9
|
-
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
10
10
|
import { IssueService, layer as IssueServiceLayer, SiteUrl } from "../src/IssueService.js"
|
|
11
11
|
import { layer as VersionServiceLayer, VersionService } from "../src/VersionService.js"
|
|
12
12
|
|
|
@@ -3,8 +3,11 @@ import { defineConfig } from "vitest/config"
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
resolve: {
|
|
5
5
|
alias: {
|
|
6
|
-
"@knpkv/atlassian-common/attachments":
|
|
6
|
+
"@knpkv/atlassian-common/attachments":
|
|
7
|
+
new URL("../atlassian-common/src/attachments.ts", import.meta.url).pathname,
|
|
7
8
|
"@knpkv/atlassian-common/auth": new URL("../atlassian-common/src/auth/index.ts", import.meta.url).pathname,
|
|
9
|
+
"@knpkv/atlassian-common/cli-auth":
|
|
10
|
+
new URL("../atlassian-common/src/cli-auth/index.ts", import.meta.url).pathname,
|
|
8
11
|
"@knpkv/atlassian-common/config": new URL("../atlassian-common/src/config/index.ts", import.meta.url).pathname,
|
|
9
12
|
"@knpkv/agent-skills": new URL("../agent-skills/src/index.ts", import.meta.url).pathname,
|
|
10
13
|
"@knpkv/jira-api-client": new URL("../jira-api-client/src/index.ts", import.meta.url).pathname
|
package/vitest.config.ts
CHANGED
|
@@ -3,8 +3,11 @@ import { defineConfig } from "vitest/config"
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
resolve: {
|
|
5
5
|
alias: {
|
|
6
|
-
"@knpkv/atlassian-common/attachments":
|
|
6
|
+
"@knpkv/atlassian-common/attachments":
|
|
7
|
+
new URL("../atlassian-common/src/attachments.ts", import.meta.url).pathname,
|
|
7
8
|
"@knpkv/atlassian-common/auth": new URL("../atlassian-common/src/auth/index.ts", import.meta.url).pathname,
|
|
9
|
+
"@knpkv/atlassian-common/cli-auth":
|
|
10
|
+
new URL("../atlassian-common/src/cli-auth/index.ts", import.meta.url).pathname,
|
|
8
11
|
"@knpkv/atlassian-common/config": new URL("../atlassian-common/src/config/index.ts", import.meta.url).pathname,
|
|
9
12
|
"@knpkv/agent-skills": new URL("../agent-skills/src/index.ts", import.meta.url).pathname,
|
|
10
13
|
"@knpkv/jira-api-client": new URL("../jira-api-client/src/index.ts", import.meta.url).pathname
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP Server factory layer using Node.js http module.
|
|
3
|
-
*
|
|
4
|
-
* @category Layers
|
|
5
|
-
*/
|
|
6
|
-
export declare const HttpServerFactoryLive: import("effect/Layer").Layer<import("./oauthServer.js").HttpServerFactoryTag, never, never>;
|
|
7
|
-
//# sourceMappingURL=NodeLayers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"NodeLayers.d.ts","sourceRoot":"","sources":["../../src/internal/NodeLayers.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,6FAEjC,CAAA"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node.js-specific HTTP server factory — the only file importing `@effect/platform-node` server.
|
|
3
|
-
*
|
|
4
|
-
* @internal
|
|
5
|
-
*/
|
|
6
|
-
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
7
|
-
import { createServer } from "node:http";
|
|
8
|
-
import { makeHttpServerFactory } from "./oauthServer.js";
|
|
9
|
-
/**
|
|
10
|
-
* HTTP Server factory layer using Node.js http module.
|
|
11
|
-
*
|
|
12
|
-
* @category Layers
|
|
13
|
-
*/
|
|
14
|
-
export const HttpServerFactoryLive = makeHttpServerFactory((options) => NodeHttpServer.layerServer(createServer, options));
|
|
15
|
-
//# sourceMappingURL=NodeLayers.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"NodeLayers.js","sourceRoot":"","sources":["../../src/internal/NodeLayers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,cAAc,MAAM,sCAAsC,CAAA;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CACxD,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,WAAW,CAAC,YAAY,EAAE,OAAO,CAAC,CAC/D,CAAA"}
|