@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,49 @@
1
+ /**
2
+ * JQL query construction with proper value escaping.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - {@link buildByVersionJql} generates `fixVersion = "X"` queries with optional project filter.
7
+ * - {@link escapeJqlValue} handles backslashes, quotes, newlines, carriage returns.
8
+ *
9
+ * @internal
10
+ */
11
+
12
+ /**
13
+ * Build JQL query to find issues by fix version.
14
+ *
15
+ * @param version - The fix version to search for
16
+ * @param project - Optional project key to filter by
17
+ * @returns JQL query string
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * buildByVersionJql("1.0.0")
22
+ * // => 'fixVersion = "1.0.0" ORDER BY key ASC'
23
+ *
24
+ * buildByVersionJql("1.0.0", "PROJ")
25
+ * // => 'project = "PROJ" AND fixVersion = "1.0.0" ORDER BY key ASC'
26
+ * ```
27
+ *
28
+ * @category JQL Builders
29
+ */
30
+ export const buildByVersionJql = (version: string, project?: string): string => {
31
+ const escapedVersion = escapeJqlValue(version)
32
+ const projectClause = project !== undefined ? `project = "${escapeJqlValue(project)}" AND ` : ""
33
+ return `${projectClause}fixVersion = "${escapedVersion}" ORDER BY key ASC`
34
+ }
35
+
36
+ /**
37
+ * Escape a value for use in JQL queries.
38
+ *
39
+ * @param value - The value to escape
40
+ * @returns Escaped value safe for JQL
41
+ *
42
+ * @category Utilities
43
+ */
44
+ export const escapeJqlValue = (value: string): string =>
45
+ value
46
+ .replace(/\\/g, "\\\\")
47
+ .replace(/"/g, "\\\"")
48
+ .replace(/\n/g, "\\n")
49
+ .replace(/\r/g, "\\r")
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Local HTTP callback server for OAuth2 authorization code capture.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - **Deferred-coordinated lifecycle**: {@link startCallbackServer} returns a `codePromise`
7
+ * (Deferred) and a `shutdown` effect. The server validates the CSRF `state` parameter
8
+ * and resolves the Deferred with the authorization code.
9
+ * - **Port auto-discovery**: Tries default port 8585, increments on conflict.
10
+ *
11
+ * @internal
12
+ */
13
+ import * as HttpRouter from "@effect/platform/HttpRouter"
14
+ import * as HttpServer from "@effect/platform/HttpServer"
15
+ import type { ServeError } from "@effect/platform/HttpServerError"
16
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
17
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
18
+ import { OAuthError } from "@knpkv/atlassian-common/auth"
19
+ import * as Context from "effect/Context"
20
+ import * as Deferred from "effect/Deferred"
21
+ import * as Effect from "effect/Effect"
22
+ import * as Fiber from "effect/Fiber"
23
+ import * as Layer from "effect/Layer"
24
+
25
+ const DEFAULT_PORT = 8585
26
+ const MAX_PORT_ATTEMPTS = 10
27
+
28
+ /**
29
+ * Factory service for creating HTTP servers.
30
+ * This allows mocking the server creation in tests.
31
+ *
32
+ * @category Services
33
+ */
34
+ export interface HttpServerFactory {
35
+ readonly createServerLayer: (port: number) => Layer.Layer<
36
+ HttpServer.HttpServer,
37
+ ServeError,
38
+ never
39
+ >
40
+ }
41
+
42
+ /**
43
+ * Tag for the HttpServerFactory service.
44
+ *
45
+ * @category Services
46
+ */
47
+ export class HttpServerFactoryTag extends Context.Tag("@knpkv/jira-cli/HttpServerFactory")<
48
+ HttpServerFactoryTag,
49
+ HttpServerFactory
50
+ >() {}
51
+
52
+ /**
53
+ * Create a HttpServerFactory layer from a layer factory function.
54
+ * This allows injecting platform-specific implementations.
55
+ *
56
+ * @param createLayerFn - Function that creates HttpServer layer for a given port
57
+ * @returns Layer providing HttpServerFactory
58
+ *
59
+ * @category Layers
60
+ */
61
+ export const makeHttpServerFactory = (
62
+ createLayerFn: (port: number) => Layer.Layer<HttpServer.HttpServer, ServeError, never>
63
+ ): Layer.Layer<HttpServerFactoryTag> =>
64
+ Layer.succeed(HttpServerFactoryTag, {
65
+ createServerLayer: createLayerFn
66
+ })
67
+
68
+ /**
69
+ * Check if a port is available by attempting to start a server.
70
+ */
71
+ const isPortAvailable = (port: number): Effect.Effect<boolean, never, HttpServerFactoryTag> =>
72
+ Effect.gen(function*() {
73
+ const factory = yield* HttpServerFactoryTag
74
+ const serverLayer = factory.createServerLayer(port)
75
+
76
+ const result = yield* Layer.build(serverLayer).pipe(
77
+ Effect.scoped,
78
+ Effect.as(true),
79
+ Effect.catchAll(() => Effect.succeed(false))
80
+ )
81
+ return result
82
+ })
83
+
84
+ /**
85
+ * Find an available port starting from the default.
86
+ */
87
+ const findAvailablePort = (): Effect.Effect<number, OAuthError, HttpServerFactoryTag> =>
88
+ Effect.gen(function*() {
89
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
90
+ const port = DEFAULT_PORT + attempt
91
+ const available = yield* isPortAvailable(port)
92
+ if (available) {
93
+ return port
94
+ }
95
+ }
96
+ return yield* Effect.fail(
97
+ new OAuthError({
98
+ step: "authorize",
99
+ cause: `Could not find available port (tried ${DEFAULT_PORT}-${
100
+ DEFAULT_PORT + MAX_PORT_ATTEMPTS - 1
101
+ }). Close other applications using these ports.`
102
+ })
103
+ )
104
+ })
105
+
106
+ /**
107
+ * Result from the OAuth callback server.
108
+ */
109
+ export interface CallbackServerResult {
110
+ /** Promise that resolves with the authorization code */
111
+ readonly codePromise: Effect.Effect<string, OAuthError>
112
+ /** Shutdown the callback server */
113
+ readonly shutdown: Effect.Effect<void, never>
114
+ /** The port the server is listening on */
115
+ readonly port: number
116
+ }
117
+
118
+ /**
119
+ * Start a local HTTP server to receive OAuth callback.
120
+ *
121
+ * @param expectedState - The state parameter to verify against CSRF
122
+ * @returns Server control interface with code promise, shutdown, and port
123
+ *
124
+ * @category OAuth
125
+ */
126
+ export const startCallbackServer = (
127
+ expectedState: string
128
+ ): Effect.Effect<CallbackServerResult, OAuthError, HttpServerFactoryTag> =>
129
+ Effect.gen(function*() {
130
+ const factory = yield* HttpServerFactoryTag
131
+ const port = yield* findAvailablePort()
132
+ const deferred = yield* Deferred.make<string, OAuthError>()
133
+ const readyDeferred = yield* Deferred.make<void, OAuthError>()
134
+
135
+ const app = HttpRouter.empty.pipe(
136
+ HttpRouter.get(
137
+ "/callback",
138
+ Effect.gen(function*() {
139
+ const req = yield* HttpServerRequest.HttpServerRequest
140
+ const url = new URL(req.url, `http://localhost:${port}`)
141
+ const code = url.searchParams.get("code")
142
+ const state = url.searchParams.get("state")
143
+ const error = url.searchParams.get("error")
144
+ const errorDescription = url.searchParams.get("error_description")
145
+
146
+ if (error) {
147
+ yield* Deferred.fail(
148
+ deferred,
149
+ new OAuthError({ step: "authorize", cause: errorDescription ?? error })
150
+ )
151
+ return HttpServerResponse.html(
152
+ "<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>"
153
+ )
154
+ }
155
+
156
+ if (state !== expectedState) {
157
+ yield* Deferred.fail(
158
+ deferred,
159
+ new OAuthError({ step: "authorize", cause: "State mismatch - possible CSRF attack" })
160
+ )
161
+ return HttpServerResponse.html(
162
+ "<html><body><h1>Security Error</h1><p>State verification failed.</p></body></html>"
163
+ )
164
+ }
165
+
166
+ if (!code) {
167
+ yield* Deferred.fail(
168
+ deferred,
169
+ new OAuthError({ step: "authorize", cause: "No authorization code received" })
170
+ )
171
+ return HttpServerResponse.html(
172
+ "<html><body><h1>Error</h1><p>No authorization code received.</p></body></html>"
173
+ )
174
+ }
175
+
176
+ yield* Deferred.succeed(deferred, code)
177
+ return HttpServerResponse.html(
178
+ "<html><body><h1>Success!</h1><p>You can close this window and return to the terminal.</p></body></html>"
179
+ )
180
+ })
181
+ )
182
+ )
183
+
184
+ const serverLayer = factory.createServerLayer(port)
185
+
186
+ const serverFiber = yield* HttpServer.serve(app).pipe(
187
+ Layer.provide(serverLayer),
188
+ Layer.build,
189
+ Effect.tap(() => Deferred.succeed(readyDeferred, undefined)),
190
+ Effect.tapError((err) => Deferred.fail(readyDeferred, new OAuthError({ step: "authorize", cause: err }))),
191
+ Effect.flatMap(() => Effect.never),
192
+ Effect.scoped,
193
+ Effect.fork
194
+ )
195
+
196
+ yield* Deferred.await(readyDeferred)
197
+
198
+ return {
199
+ codePromise: Deferred.await(deferred),
200
+ shutdown: Fiber.interrupt(serverFiber).pipe(Effect.asVoid),
201
+ port
202
+ }
203
+ })
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { buildByVersionJql, escapeJqlValue } from "../src/internal/jqlBuilder.js"
3
+
4
+ describe("jqlBuilder", () => {
5
+ describe("buildByVersionJql", () => {
6
+ it("builds JQL for version only", () => {
7
+ const result = buildByVersionJql("1.0.0")
8
+ expect(result).toBe("fixVersion = \"1.0.0\" ORDER BY key ASC")
9
+ })
10
+
11
+ it("builds JQL with project filter", () => {
12
+ const result = buildByVersionJql("1.0.0", "PROJ")
13
+ expect(result).toBe("project = \"PROJ\" AND fixVersion = \"1.0.0\" ORDER BY key ASC")
14
+ })
15
+
16
+ it("handles version with special characters", () => {
17
+ const result = buildByVersionJql("1.0.0-beta.1")
18
+ expect(result).toBe("fixVersion = \"1.0.0-beta.1\" ORDER BY key ASC")
19
+ })
20
+
21
+ it("escapes quotes in version name", () => {
22
+ const result = buildByVersionJql("OOB 42 \"Nimble Needlefish\"")
23
+ expect(result).toBe("fixVersion = \"OOB 42 \\\"Nimble Needlefish\\\"\" ORDER BY key ASC")
24
+ })
25
+ })
26
+
27
+ describe("escapeJqlValue", () => {
28
+ it("escapes backslashes", () => {
29
+ expect(escapeJqlValue("path\\to\\file")).toBe("path\\\\to\\\\file")
30
+ })
31
+
32
+ it("escapes double quotes", () => {
33
+ expect(escapeJqlValue("say \"hello\"")).toBe("say \\\"hello\\\"")
34
+ })
35
+
36
+ it("leaves normal strings unchanged", () => {
37
+ expect(escapeJqlValue("normal string")).toBe("normal string")
38
+ })
39
+
40
+ it("escapes newlines and carriage returns", () => {
41
+ expect(escapeJqlValue("line1\nline2")).toBe("line1\\nline2")
42
+ expect(escapeJqlValue("line1\r\nline2")).toBe("line1\\r\\nline2")
43
+ })
44
+ })
45
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "strict": true,
10
+ "noImplicitAny": true,
11
+ "strictNullChecks": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "exactOptionalPropertyTypes": true,
14
+ "noImplicitReturns": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noUnusedLocals": false,
17
+ "noUnusedParameters": false,
18
+ "allowUnusedLabels": false,
19
+ "allowUnreachableCode": false,
20
+ "skipLibCheck": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "moduleResolution": "bundler",
23
+ "module": "ESNext",
24
+ "target": "ES2022",
25
+ "lib": ["ES2022"],
26
+ "types": ["node"],
27
+ "esModuleInterop": true,
28
+ "resolveJsonModule": true
29
+ },
30
+ "include": ["src/**/*"],
31
+ "exclude": ["node_modules", "dist", "test"]
32
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["test/**/*.test.ts"],
6
+ globals: true,
7
+ environment: "node",
8
+ testTimeout: 30000,
9
+ hookTimeout: 30000,
10
+ teardownTimeout: 30000
11
+ }
12
+ })