@knpkv/jira-cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/dist/IssueService.d.ts +144 -0
  5. package/dist/IssueService.d.ts.map +1 -0
  6. package/dist/IssueService.js +250 -0
  7. package/dist/IssueService.js.map +1 -0
  8. package/dist/JiraAuth.d.ts +84 -0
  9. package/dist/JiraAuth.d.ts.map +1 -0
  10. package/dist/JiraAuth.js +246 -0
  11. package/dist/JiraAuth.js.map +1 -0
  12. package/dist/JiraCliError.d.ts +42 -0
  13. package/dist/JiraCliError.d.ts.map +1 -0
  14. package/dist/JiraCliError.js +35 -0
  15. package/dist/JiraCliError.js.map +1 -0
  16. package/dist/MarkdownWriter.d.ts +56 -0
  17. package/dist/MarkdownWriter.d.ts.map +1 -0
  18. package/dist/MarkdownWriter.js +66 -0
  19. package/dist/MarkdownWriter.js.map +1 -0
  20. package/dist/bin.d.ts +3 -0
  21. package/dist/bin.d.ts.map +1 -0
  22. package/dist/bin.js +39 -0
  23. package/dist/bin.js.map +1 -0
  24. package/dist/commands/auth.d.ts +22 -0
  25. package/dist/commands/auth.d.ts.map +1 -0
  26. package/dist/commands/auth.js +89 -0
  27. package/dist/commands/auth.js.map +1 -0
  28. package/dist/commands/errorHandler.d.ts +13 -0
  29. package/dist/commands/errorHandler.d.ts.map +1 -0
  30. package/dist/commands/errorHandler.js +13 -0
  31. package/dist/commands/errorHandler.js.map +1 -0
  32. package/dist/commands/get.d.ts +13 -0
  33. package/dist/commands/get.d.ts.map +1 -0
  34. package/dist/commands/get.js +25 -0
  35. package/dist/commands/get.js.map +1 -0
  36. package/dist/commands/index.d.ts +11 -0
  37. package/dist/commands/index.d.ts.map +1 -0
  38. package/dist/commands/index.js +11 -0
  39. package/dist/commands/index.js.map +1 -0
  40. package/dist/commands/layers.d.ts +44 -0
  41. package/dist/commands/layers.d.ts.map +1 -0
  42. package/dist/commands/layers.js +100 -0
  43. package/dist/commands/layers.js.map +1 -0
  44. package/dist/commands/search.d.ts +18 -0
  45. package/dist/commands/search.d.ts.map +1 -0
  46. package/dist/commands/search.js +64 -0
  47. package/dist/commands/search.js.map +1 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +10 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/internal/NodeLayers.d.ts +7 -0
  53. package/dist/internal/NodeLayers.d.ts.map +1 -0
  54. package/dist/internal/NodeLayers.js +15 -0
  55. package/dist/internal/NodeLayers.js.map +1 -0
  56. package/dist/internal/frontmatter.d.ts +60 -0
  57. package/dist/internal/frontmatter.d.ts.map +1 -0
  58. package/dist/internal/frontmatter.js +130 -0
  59. package/dist/internal/frontmatter.js.map +1 -0
  60. package/dist/internal/jqlBuilder.d.ts +39 -0
  61. package/dist/internal/jqlBuilder.d.ts.map +1 -0
  62. package/dist/internal/jqlBuilder.js +47 -0
  63. package/dist/internal/jqlBuilder.js.map +1 -0
  64. package/dist/internal/oauthServer.d.ts +55 -0
  65. package/dist/internal/oauthServer.d.ts.map +1 -0
  66. package/dist/internal/oauthServer.js +113 -0
  67. package/dist/internal/oauthServer.js.map +1 -0
  68. package/package.json +86 -0
  69. package/src/IssueService.ts +378 -0
  70. package/src/JiraAuth.ts +476 -0
  71. package/src/JiraCliError.ts +44 -0
  72. package/src/MarkdownWriter.ts +112 -0
  73. package/src/bin.ts +62 -0
  74. package/src/commands/auth.ts +124 -0
  75. package/src/commands/errorHandler.ts +14 -0
  76. package/src/commands/get.ts +42 -0
  77. package/src/commands/index.ts +11 -0
  78. package/src/commands/layers.ts +142 -0
  79. package/src/commands/search.ts +102 -0
  80. package/src/index.ts +26 -0
  81. package/src/internal/NodeLayers.ts +17 -0
  82. package/src/internal/frontmatter.ts +170 -0
  83. package/src/internal/jqlBuilder.ts +49 -0
  84. package/src/internal/oauthServer.ts +203 -0
  85. package/test/jqlBuilder.test.ts +45 -0
  86. package/tsconfig.json +32 -0
  87. package/vitest.config.ts +12 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Auth subcommands: create, configure, login, logout, status.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - Each command is a standalone `Command.make` that yields into `JiraAuth` service methods.
7
+ * - `create` opens the Atlassian Developer Console; `configure` prompts for client ID/secret.
8
+ *
9
+ * @internal
10
+ */
11
+ import { Command, Options, Prompt } from "@effect/cli"
12
+ import * as PlatformCommand from "@effect/platform/Command"
13
+ import * as Console from "effect/Console"
14
+ import * as Effect from "effect/Effect"
15
+ import * as Option from "effect/Option"
16
+ import { JiraAuth } from "../JiraAuth.js"
17
+
18
+ // === Auth create command ===
19
+ const createCommand = Command.make("create", {}, () =>
20
+ Effect.gen(function*() {
21
+ yield* Console.log(`
22
+ Creating OAuth app in Atlassian Developer Console...
23
+
24
+ 1. Browser will open to create a new OAuth 2.0 (3LO) app
25
+ 2. Enter app name (e.g., "Jira CLI")
26
+ 3. After creation, go to "Permissions" and add:
27
+
28
+ Jira API:
29
+ - read:jira-work Search and read issues, comments, attachments
30
+ - read:jira-user Read user info for assignee/reporter fields
31
+
32
+ User Identity API:
33
+ - read:me Get your account ID and email for auth
34
+
35
+ 4. Go to "Authorization" and set callback URL:
36
+ http://localhost:8585/callback
37
+ 5. Go to "Settings" and copy Client ID and Secret
38
+ 6. Run: jira auth configure --client-id <ID> --client-secret <SECRET>
39
+ `)
40
+ const url = "https://developer.atlassian.com/console/myapps/create-3lo-app/"
41
+ yield* PlatformCommand.make("open", url).pipe(
42
+ PlatformCommand.exitCode,
43
+ Effect.catchAll(() => PlatformCommand.make("xdg-open", url).pipe(PlatformCommand.exitCode)),
44
+ Effect.catchAll(() => PlatformCommand.make("cmd", "/c", "start", "", url).pipe(PlatformCommand.exitCode)),
45
+ Effect.asVoid,
46
+ Effect.catchAll(() => Effect.void)
47
+ )
48
+ })).pipe(Command.withDescription("Create OAuth app in Atlassian Developer Console"))
49
+
50
+ // === Auth configure command ===
51
+ const clientIdOption = Options.text("client-id").pipe(
52
+ Options.withDescription("OAuth client ID from Atlassian Developer Console"),
53
+ Options.optional
54
+ )
55
+ const clientSecretOption = Options.text("client-secret").pipe(
56
+ Options.withDescription("OAuth client secret"),
57
+ Options.optional
58
+ )
59
+
60
+ const configureCommand = Command.make(
61
+ "configure",
62
+ { clientId: clientIdOption, clientSecret: clientSecretOption },
63
+ ({ clientId, clientSecret }) =>
64
+ Effect.gen(function*() {
65
+ const auth = yield* JiraAuth
66
+
67
+ const rawClientId = Option.isSome(clientId)
68
+ ? clientId.value
69
+ : yield* Prompt.text({ message: "Enter OAuth client ID:" })
70
+ const rawClientSecret = Option.isSome(clientSecret)
71
+ ? clientSecret.value
72
+ : yield* Prompt.text({ message: "Enter OAuth client secret:" })
73
+
74
+ yield* auth.configure({ clientId: rawClientId, clientSecret: rawClientSecret })
75
+ yield* Console.log("OAuth configured. Run 'jira auth login' to authenticate.")
76
+ })
77
+ ).pipe(Command.withDescription("Configure OAuth client credentials"))
78
+
79
+ // === Auth login command ===
80
+ const siteOption = Options.text("site").pipe(
81
+ Options.withDescription("Jira site URL to use (for accounts with multiple sites)"),
82
+ Options.optional
83
+ )
84
+
85
+ const loginCommand = Command.make("login", { site: siteOption }, ({ site }) =>
86
+ Effect.gen(function*() {
87
+ const auth = yield* JiraAuth
88
+ const result = yield* auth.login(Option.isSome(site) ? { siteUrl: site.value } : undefined)
89
+ if (Array.isArray(result) && result.length > 0) {
90
+ yield* Console.log("\nRe-run with --site to select a specific site.")
91
+ }
92
+ })).pipe(Command.withDescription("Authenticate with Atlassian via OAuth"))
93
+
94
+ // === Auth logout command ===
95
+ const logoutCommand = Command.make("logout", {}, () =>
96
+ Effect.gen(function*() {
97
+ const auth = yield* JiraAuth
98
+ yield* auth.logout()
99
+ yield* Console.log("Logged out")
100
+ })).pipe(Command.withDescription("Remove stored authentication"))
101
+
102
+ // === Auth status command ===
103
+ const statusCommand = Command.make("status", {}, () =>
104
+ Effect.gen(function*() {
105
+ const auth = yield* JiraAuth
106
+ const user = yield* auth.getCurrentUser()
107
+ if (user) {
108
+ yield* Console.log(`Logged in as: ${user.name} (${user.email})`)
109
+ } else {
110
+ yield* Console.log("Not logged in. Use 'jira auth login' to authenticate.")
111
+ }
112
+ })).pipe(Command.withDescription("Show authentication status"))
113
+
114
+ // === Auth command group ===
115
+ export const authCommand = Command.make("auth").pipe(
116
+ Command.withDescription("Manage OAuth authentication"),
117
+ Command.withSubcommands([
118
+ createCommand,
119
+ configureCommand,
120
+ loginCommand,
121
+ logoutCommand,
122
+ statusCommand
123
+ ])
124
+ )
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Effect-idiomatic error handler — uses Cause.pretty for structured failure output.
3
+ *
4
+ * @internal
5
+ */
6
+ import * as Cause from "effect/Cause"
7
+ import * as Console from "effect/Console"
8
+ import type * as Effect from "effect/Effect"
9
+
10
+ /**
11
+ * Handle errors from CLI execution.
12
+ * Logs the pretty-printed cause to stderr via Console.error.
13
+ */
14
+ export const handleError = <E>(cause: Cause.Cause<E>): Effect.Effect<void> => Console.error(Cause.pretty(cause))
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `jira get <key>` command — fetches a single issue and writes to Markdown.
3
+ *
4
+ * @internal
5
+ */
6
+ import { Args, Command, Options } from "@effect/cli"
7
+ import * as Console from "effect/Console"
8
+ import * as Effect from "effect/Effect"
9
+ import { IssueService } from "../IssueService.js"
10
+ import { MarkdownWriter } from "../MarkdownWriter.js"
11
+
12
+ const keyArg = Args.text({ name: "key" }).pipe(
13
+ Args.withDescription("Issue key (e.g., PROJ-123)")
14
+ )
15
+
16
+ const outputDirOption = Options.directory("output-dir").pipe(
17
+ Options.withAlias("o"),
18
+ Options.withDescription("Output directory for markdown file"),
19
+ Options.withDefault("./jira-tickets")
20
+ )
21
+
22
+ export const getCommand = Command.make(
23
+ "get",
24
+ {
25
+ key: keyArg,
26
+ outputDir: outputDirOption
27
+ },
28
+ ({ key, outputDir }) =>
29
+ Effect.gen(function*() {
30
+ const issueService = yield* IssueService
31
+ const writer = yield* MarkdownWriter
32
+
33
+ yield* Console.log(`Fetching ${key}...`)
34
+
35
+ const issue = yield* issueService.getByKey(key)
36
+
37
+ yield* Console.log(`Writing to ${outputDir}/${key}.md...`)
38
+ yield* writer.writeMulti([issue], outputDir)
39
+
40
+ yield* Console.log(`Done.`)
41
+ })
42
+ ).pipe(Command.withDescription("Get a single Jira issue by key"))
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Barrel export for CLI commands and layer definitions.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export { authCommand } from "./auth.js"
8
+ export { handleError } from "./errorHandler.js"
9
+ export { getCommand } from "./get.js"
10
+ export { AppLayer, AuthOnlyLayer, getLayerType, MinimalLayer } from "./layers.js"
11
+ export { searchCommand } from "./search.js"
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Layer composition for CLI commands with three tiers: full, auth-only, minimal.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - **Lazy layer selection**: {@link getLayerType} inspects `process.argv[2]` to pick
7
+ * the smallest layer needed — `"minimal"` for help/version, `"auth"` for auth commands,
8
+ * `"full"` for search/get (which needs API client + issue service).
9
+ * - **Dummy services**: Auth-only and minimal layers provide `Effect.dieMessage` stubs
10
+ * for unused services to satisfy the type system without initialization cost.
11
+ *
12
+ * @internal
13
+ */
14
+ import * as NodeContext from "@effect/platform-node/NodeContext"
15
+ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
16
+ import { JiraApiClient, JiraApiConfig } from "@knpkv/jira-api-client"
17
+ import * as Effect from "effect/Effect"
18
+ import * as Layer from "effect/Layer"
19
+ import { IssueService, layer as IssueServiceLayer, SiteUrl } from "../IssueService.js"
20
+ import { JiraAuth, layer as JiraAuthLayer } from "../JiraAuth.js"
21
+ import { layer as MarkdownWriterLayer, MarkdownWriter } from "../MarkdownWriter.js"
22
+
23
+ // Dummy services for auth-only commands
24
+ const DummyIssueServiceLayer = Layer.succeed(
25
+ IssueService,
26
+ IssueService.of({
27
+ getByKey: () => Effect.dieMessage("Not configured - run 'jira auth login' first"),
28
+ search: () => Effect.dieMessage("Not configured - run 'jira auth login' first"),
29
+ searchAll: () => Effect.dieMessage("Not configured - run 'jira auth login' first")
30
+ })
31
+ )
32
+
33
+ const DummyMarkdownWriterLayer = Layer.succeed(
34
+ MarkdownWriter,
35
+ MarkdownWriter.of({
36
+ writeMulti: () => Effect.dieMessage("Not configured"),
37
+ writeSingle: () => Effect.dieMessage("Not configured")
38
+ })
39
+ )
40
+
41
+ const DummyJiraAuthLayer = Layer.succeed(
42
+ JiraAuth,
43
+ JiraAuth.of({
44
+ configure: () => Effect.dieMessage("Not configured"),
45
+ isConfigured: () => Effect.succeed(false),
46
+ login: () => Effect.dieMessage("Not configured"),
47
+ logout: () => Effect.dieMessage("Not configured"),
48
+ getAccessToken: () => Effect.dieMessage("Not configured"),
49
+ getCloudId: () => Effect.dieMessage("Not configured"),
50
+ getSiteUrl: () => Effect.dieMessage("Not configured"),
51
+ getCurrentUser: () => Effect.succeed(null),
52
+ isLoggedIn: () => Effect.succeed(false)
53
+ })
54
+ )
55
+
56
+ // Auth layer with HTTP client
57
+ const AuthLive = JiraAuthLayer.pipe(Layer.provide(NodeHttpClient.layer))
58
+
59
+ // Build Jira API config layer dynamically based on auth
60
+ const JiraConfigLive = Layer.unwrapEffect(
61
+ Effect.gen(function*() {
62
+ const auth = yield* JiraAuth
63
+ const accessToken = yield* auth.getAccessToken()
64
+ const cloudId = yield* auth.getCloudId()
65
+
66
+ return Layer.succeed(JiraApiConfig, {
67
+ baseUrl: "",
68
+ auth: {
69
+ type: "oauth2" as const,
70
+ accessToken,
71
+ cloudId
72
+ }
73
+ })
74
+ })
75
+ )
76
+
77
+ // Build Jira API client layer with config (no HttpClient needed — uses openapi-fetch)
78
+ const JiraClientLive = JiraApiClient.layer.pipe(
79
+ Layer.provide(JiraConfigLive)
80
+ )
81
+
82
+ // Build SiteUrl layer from auth
83
+ const SiteUrlLive = Layer.unwrapEffect(
84
+ Effect.gen(function*() {
85
+ const auth = yield* JiraAuth
86
+ const siteUrl = yield* auth.getSiteUrl()
87
+ return Layer.succeed(SiteUrl, siteUrl)
88
+ })
89
+ )
90
+
91
+ /**
92
+ * Full app layer with all services for search commands.
93
+ *
94
+ * @category Layers
95
+ */
96
+ export const AppLayer = MarkdownWriterLayer.pipe(
97
+ Layer.provideMerge(IssueServiceLayer),
98
+ Layer.provideMerge(SiteUrlLive),
99
+ Layer.provideMerge(JiraClientLive),
100
+ Layer.provideMerge(AuthLive),
101
+ Layer.provideMerge(NodeHttpClient.layer),
102
+ Layer.provideMerge(NodeContext.layer)
103
+ )
104
+
105
+ /**
106
+ * Auth-only layer for auth commands.
107
+ *
108
+ * @category Layers
109
+ */
110
+ export const AuthOnlyLayer = DummyIssueServiceLayer.pipe(
111
+ Layer.provideMerge(DummyMarkdownWriterLayer),
112
+ Layer.provideMerge(AuthLive),
113
+ Layer.provideMerge(NodeHttpClient.layer),
114
+ Layer.provideMerge(NodeContext.layer)
115
+ )
116
+
117
+ /**
118
+ * Minimal layer for help/version commands.
119
+ *
120
+ * @category Layers
121
+ */
122
+ export const MinimalLayer = DummyIssueServiceLayer.pipe(
123
+ Layer.provideMerge(DummyMarkdownWriterLayer),
124
+ Layer.provideMerge(DummyJiraAuthLayer),
125
+ Layer.provideMerge(NodeContext.layer)
126
+ )
127
+
128
+ /**
129
+ * Determine which layer to use based on command.
130
+ *
131
+ * @category Utilities
132
+ */
133
+ export const getLayerType = (argv: ReadonlyArray<string>): "full" | "auth" | "minimal" => {
134
+ const cmd = argv[2]
135
+ if (cmd === "auth") {
136
+ return "auth"
137
+ }
138
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "--version") {
139
+ return "minimal"
140
+ }
141
+ return "full"
142
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `jira search` command — JQL search with multi/single Markdown output.
3
+ *
4
+ * @internal
5
+ */
6
+ import { Args, Command, Options } from "@effect/cli"
7
+ import * as Console from "effect/Console"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Option from "effect/Option"
10
+ import { buildByVersionJql } from "../internal/jqlBuilder.js"
11
+ import { IssueService } from "../IssueService.js"
12
+ import { MarkdownWriter } from "../MarkdownWriter.js"
13
+
14
+ // === Options ===
15
+ const jqlArg = Args.text({ name: "jql" }).pipe(
16
+ Args.withDescription("JQL query to search for issues"),
17
+ Args.optional
18
+ )
19
+
20
+ const byVersionOption = Options.text("by-version").pipe(
21
+ Options.withAlias("v"),
22
+ Options.withDescription("Search by fix version (pre-defined query)"),
23
+ Options.optional
24
+ )
25
+
26
+ const projectOption = Options.text("project").pipe(
27
+ Options.withAlias("p"),
28
+ Options.withDescription("Filter by project key"),
29
+ Options.optional
30
+ )
31
+
32
+ const outputDirOption = Options.directory("output-dir").pipe(
33
+ Options.withAlias("o"),
34
+ Options.withDescription("Output directory for markdown files"),
35
+ Options.withDefault("./jira-tickets")
36
+ )
37
+
38
+ const formatOption = Options.choice("format", ["multi", "single"]).pipe(
39
+ Options.withAlias("f"),
40
+ Options.withDescription("Output format: multi (one file per issue) or single (combined file)"),
41
+ Options.withDefault("multi" as const)
42
+ )
43
+
44
+ const maxResultsOption = Options.integer("max-results").pipe(
45
+ Options.withAlias("m"),
46
+ Options.withDescription("Maximum number of results to fetch"),
47
+ Options.withDefault(100)
48
+ )
49
+
50
+ // === Search command ===
51
+ export const searchCommand = Command.make(
52
+ "search",
53
+ {
54
+ jql: jqlArg,
55
+ byVersion: byVersionOption,
56
+ project: projectOption,
57
+ outputDir: outputDirOption,
58
+ format: formatOption,
59
+ maxResults: maxResultsOption
60
+ },
61
+ ({ byVersion, format, jql, maxResults, outputDir, project }) =>
62
+ Effect.gen(function*() {
63
+ const issueService = yield* IssueService
64
+ const writer = yield* MarkdownWriter
65
+
66
+ // Build JQL query
67
+ let query: string
68
+
69
+ if (Option.isSome(byVersion)) {
70
+ const projectKey = Option.isSome(project) ? project.value : undefined
71
+ query = buildByVersionJql(byVersion.value, projectKey)
72
+ yield* Console.log(`Searching by fix version: ${byVersion.value}`)
73
+ } else if (Option.isSome(jql)) {
74
+ query = jql.value
75
+ } else {
76
+ yield* Console.log("Error: Either a JQL query or --by-version must be provided.")
77
+ yield* Console.log("Usage: jira search <jql>")
78
+ yield* Console.log(" jira search --by-version <version>")
79
+ return
80
+ }
81
+
82
+ yield* Console.log(`Query: ${query}`)
83
+ yield* Console.log("Fetching issues...")
84
+
85
+ const issues = yield* issueService.searchAll(query, { maxResults })
86
+
87
+ if (issues.length === 0) {
88
+ yield* Console.log("No issues found.")
89
+ return
90
+ }
91
+
92
+ yield* Console.log(`Found ${issues.length} issue(s). Writing to ${outputDir}...`)
93
+
94
+ if (format === "single") {
95
+ yield* writer.writeSingle(issues, outputDir, query)
96
+ yield* Console.log(`Exported to ${outputDir}/jira-export.md`)
97
+ } else {
98
+ yield* writer.writeMulti(issues, outputDir)
99
+ yield* Console.log(`Exported ${issues.length} file(s) to ${outputDir}/`)
100
+ }
101
+ })
102
+ ).pipe(Command.withDescription("Search Jira issues and export to markdown"))
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Root barrel export for `@knpkv/jira-cli`.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export {
8
+ type Attachment,
9
+ type Comment,
10
+ type Issue,
11
+ IssueService,
12
+ type IssueServiceShape,
13
+ layer as IssueServiceLayer,
14
+ type SearchOptions,
15
+ type SearchResult,
16
+ SiteUrl
17
+ } from "./IssueService.js"
18
+ export {
19
+ type AccessibleSite,
20
+ JiraAuth,
21
+ type JiraAuthService,
22
+ layer as JiraAuthLayer,
23
+ type LoginOptions
24
+ } from "./JiraAuth.js"
25
+ export * from "./JiraCliError.js"
26
+ export { layer as MarkdownWriterLayer, MarkdownWriter, type MarkdownWriterShape } from "./MarkdownWriter.js"
@@ -0,0 +1,17 @@
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
+ /**
11
+ * HTTP Server factory layer using Node.js http module.
12
+ *
13
+ * @category Layers
14
+ */
15
+ export const HttpServerFactoryLive = makeHttpServerFactory(
16
+ (port) => NodeHttpServer.layerServer(createServer, { port })
17
+ )
@@ -0,0 +1,170 @@
1
+ /**
2
+ * YAML frontmatter serialization for Jira issues using `gray-matter`.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - {@link serializeIssue} produces `---\nyaml\n---\nmarkdown` for a single issue.
7
+ * - {@link buildCombinedMarkdown} concatenates all issues with a JQL header for single-file export.
8
+ *
9
+ * @internal
10
+ */
11
+ import matter from "gray-matter"
12
+ import type { Issue } from "../IssueService.js"
13
+
14
+ /**
15
+ * Front-matter data for a Jira issue.
16
+ *
17
+ * @category Types
18
+ */
19
+ export interface IssueFrontMatter {
20
+ readonly key: string
21
+ readonly id: string
22
+ readonly summary: string
23
+ readonly status: string
24
+ readonly type: string
25
+ readonly priority: string | null
26
+ readonly assignee: string | null
27
+ readonly reporter: string | null
28
+ readonly created: string
29
+ readonly updated: string
30
+ readonly fixVersions: ReadonlyArray<string>
31
+ readonly labels: ReadonlyArray<string>
32
+ readonly components: ReadonlyArray<string>
33
+ readonly url: string
34
+ }
35
+
36
+ /**
37
+ * Extract front-matter data from an issue.
38
+ *
39
+ * @param issue - The issue to extract from
40
+ * @returns Front-matter object
41
+ *
42
+ * @category Utilities
43
+ */
44
+ export const extractFrontMatter = (issue: Issue): IssueFrontMatter => ({
45
+ key: issue.key,
46
+ id: issue.id,
47
+ summary: issue.summary,
48
+ status: issue.status,
49
+ type: issue.type,
50
+ priority: issue.priority,
51
+ assignee: issue.assignee,
52
+ reporter: issue.reporter,
53
+ created: issue.created.toISOString(),
54
+ updated: issue.updated.toISOString(),
55
+ fixVersions: issue.fixVersions,
56
+ labels: issue.labels,
57
+ components: issue.components,
58
+ url: issue.url
59
+ })
60
+
61
+ /**
62
+ * Serialize an issue to markdown with front-matter.
63
+ *
64
+ * @param issue - The issue to serialize
65
+ * @returns Markdown string with YAML front-matter
66
+ *
67
+ * @category Serialization
68
+ */
69
+ export const serializeIssue = (issue: Issue): string => {
70
+ const frontMatter = extractFrontMatter(issue)
71
+ const content = buildMarkdownContent(issue)
72
+ return matter.stringify(content, frontMatter)
73
+ }
74
+
75
+ /**
76
+ * Build markdown content for an issue (without front-matter).
77
+ *
78
+ * @param issue - The issue to build content for
79
+ * @returns Markdown content string
80
+ *
81
+ * @category Serialization
82
+ */
83
+ export const buildMarkdownContent = (issue: Issue): string => {
84
+ const parts: Array<string> = []
85
+
86
+ // Title
87
+ parts.push(`# ${issue.key}: ${issue.summary}`)
88
+ parts.push("")
89
+
90
+ // Description
91
+ if (issue.description) {
92
+ parts.push("## Description")
93
+ parts.push("")
94
+ parts.push(issue.description)
95
+ parts.push("")
96
+ }
97
+
98
+ // Attachments
99
+ if (issue.attachments.length > 0) {
100
+ parts.push("## Attachments")
101
+ parts.push("")
102
+ for (const attachment of issue.attachments) {
103
+ parts.push(`- [${attachment.filename}](${attachment.url})`)
104
+ }
105
+ parts.push("")
106
+ }
107
+
108
+ // Comments
109
+ if (issue.comments.length > 0) {
110
+ parts.push("## Comments")
111
+ parts.push("")
112
+ for (const comment of issue.comments) {
113
+ const date = comment.created.toISOString().split("T")[0]
114
+ parts.push(`### ${comment.author} (${date})`)
115
+ parts.push("")
116
+ parts.push(comment.body)
117
+ parts.push("")
118
+ }
119
+ }
120
+
121
+ return parts.join("\n")
122
+ }
123
+
124
+ /**
125
+ * Build a combined markdown file for multiple issues.
126
+ *
127
+ * @param issues - The issues to include
128
+ * @param jql - The JQL query used (for header)
129
+ * @returns Combined markdown string
130
+ *
131
+ * @category Serialization
132
+ */
133
+ export const buildCombinedMarkdown = (issues: ReadonlyArray<Issue>, jql: string): string => {
134
+ const parts: Array<string> = []
135
+
136
+ // Header
137
+ parts.push("# Jira Export")
138
+ parts.push("")
139
+ parts.push(`Query: \`${jql}\``)
140
+ parts.push(`Exported: ${new Date().toISOString()}`)
141
+ parts.push(`Total: ${issues.length} tickets`)
142
+ parts.push("")
143
+ parts.push("---")
144
+ parts.push("")
145
+
146
+ // Issues
147
+ for (const issue of issues) {
148
+ parts.push(`## ${issue.key}: ${issue.summary}`)
149
+ parts.push("")
150
+ parts.push(
151
+ `**Status:** ${issue.status} | **Type:** ${issue.type}${
152
+ issue.priority ? ` | **Priority:** ${issue.priority}` : ""
153
+ }`
154
+ )
155
+ if (issue.assignee) {
156
+ parts.push(`**Assignee:** ${issue.assignee}`)
157
+ }
158
+ parts.push("")
159
+
160
+ if (issue.description) {
161
+ parts.push(issue.description)
162
+ parts.push("")
163
+ }
164
+
165
+ parts.push("---")
166
+ parts.push("")
167
+ }
168
+
169
+ return parts.join("\n")
170
+ }