@knpkv/jira-cli 1.4.1 → 1.5.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 +28 -0
- package/README.md +4 -1
- package/dist/JiraAuth.d.ts +7 -53
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +25 -3
- package/dist/JiraAuth.js.map +1 -1
- package/dist/commands/auth.d.ts +1 -2
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +4 -128
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/layers.d.ts +1 -1
- package/dist/commands/layers.d.ts.map +1 -1
- package/package.json +3 -3
- package/skills/jira/SKILL.md +1 -0
- package/src/JiraAuth.ts +18 -99
- package/src/commands/auth.ts +4 -167
- package/test/authCommand.test.ts +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @knpkv/jira-cli
|
|
2
2
|
|
|
3
|
+
## 1.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#477](https://github.com/knpkv/npm/pull/477) [`b4a69a0`](https://github.com/knpkv/npm/commit/b4a69a056ce3e2575df70d7aaea1a202f49b6a56) Thanks [@konopkov](https://github.com/konopkov)! - `JiraAuthService` and `ConfluenceAuthService` are now derived from atlassian-common's `AtlassianCliAuth` instead of re-spelling it, and `LoginOptions` / `AccessibleSite` are re-exported from there. The types are unchanged: a type-equality check against the previous hand-written interfaces passed before the switch. `ConfluenceAuthService` keeps its one real difference, a string access token and no `getSiteUrl`.
|
|
8
|
+
|
|
9
|
+
- [#475](https://github.com/knpkv/npm/pull/475) [`08c62b7`](https://github.com/knpkv/npm/commit/08c62b787c649d8f78753515be6faf317791d62b) Thanks [@konopkov](https://github.com/konopkov)! - `auth use <profile>` and `auth remove <profile>` now fail with a typed `ProfileNotFoundError` (exported from `@knpkv/atlassian-common/cli-auth`) and exit non-zero when no stored profile matches. The failure names the profile (`Profile not found: <profile>`, on stderr through each CLI's error handler); previously the line went to stdout and the command exited 0, so a script could not tell a typo from a switch.
|
|
10
|
+
- Updated dependencies [[`08c62b7`](https://github.com/knpkv/npm/commit/08c62b787c649d8f78753515be6faf317791d62b)]:
|
|
11
|
+
- @knpkv/atlassian-common@1.8.0
|
|
12
|
+
|
|
13
|
+
## 1.5.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- [#472](https://github.com/knpkv/npm/pull/472) [`d12c336`](https://github.com/knpkv/npm/commit/d12c3364e56abb7cfd32aa693c5406788a67e3f5) Thanks [@konopkov](https://github.com/konopkov)! - One shared `auth` command group for jira and confluence: `makeAuthCommand(service, descriptor, options)` in `@knpkv/atlassian-common/cli-auth`. Each CLI exports one descriptor (`jiraCliDescriptor`, `confluenceCliDescriptor`: command name, product name, login scopes) that feeds both its auth and its `auth` commands.
|
|
18
|
+
|
|
19
|
+
- `jira auth create` lists every scope `jira auth login` requests. It previously listed three of five (missing `write:jira-work` and `manage:jira-project`), so an app set up from its instructions could not log in.
|
|
20
|
+
- `jira auth manage` is new: it opens the developer console with the scope and callback checklist.
|
|
21
|
+
- `confluence auth profiles`, `auth use <profile>` and `auth remove <profile>` are new; the docs already described them.
|
|
22
|
+
- `confluence auth status` prints the active profile, account, site and profile ID.
|
|
23
|
+
- `auth create` and `auth manage` print the URL first and exit non-zero when no browser opens (jira's `create` used to ignore that).
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [[`d12c336`](https://github.com/knpkv/npm/commit/d12c3364e56abb7cfd32aa693c5406788a67e3f5)]:
|
|
28
|
+
- @knpkv/atlassian-common@1.7.0
|
|
29
|
+
- @knpkv/agent-skills@0.3.3
|
|
30
|
+
|
|
3
31
|
## 1.4.1
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -22,7 +22,9 @@ cd packages/jira-cli && pnpm link --global
|
|
|
22
22
|
jira auth create
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Prints the URL and opens the Atlassian Developer Console (the command fails if no browser
|
|
26
|
+
opens; finish from the printed URL). Create a new OAuth 2.0 (3LO) app with the scopes it
|
|
27
|
+
prints, which are exactly the ones `jira auth login` requests:
|
|
26
28
|
|
|
27
29
|
**Permissions:**
|
|
28
30
|
|
|
@@ -252,6 +254,7 @@ set, including surplus copies of a URL that _is_ desired. Other categories are n
|
|
|
252
254
|
|
|
253
255
|
```bash
|
|
254
256
|
jira auth create # Open Atlassian console to create OAuth app
|
|
257
|
+
jira auth manage # Open the console to edit the app's scopes
|
|
255
258
|
jira auth configure # Set client ID and secret
|
|
256
259
|
jira auth login # Authenticate via OAuth
|
|
257
260
|
jira auth profiles # List stored accounts/sites
|
package/dist/JiraAuth.d.ts
CHANGED
|
@@ -16,65 +16,18 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
|
-
import type
|
|
20
|
-
import type { AuthProfile, FileSystemError, HomeDirectoryError, OAuthConfig, OAuthUser } from "@knpkv/atlassian-common/config";
|
|
19
|
+
import { type AtlassianCliAuth, type AtlassianCliDescriptor } from "@knpkv/atlassian-common/cli-auth";
|
|
21
20
|
import * as Context from "effect/Context";
|
|
22
|
-
import type * as Effect from "effect/Effect";
|
|
23
21
|
import * as Layer from "effect/Layer";
|
|
24
|
-
import type * as PlatformError from "effect/PlatformError";
|
|
25
|
-
import type * as Redacted from "effect/Redacted";
|
|
26
22
|
import type { AuthMissingError } from "./JiraCliError.js";
|
|
23
|
+
export type { AccessibleSite, LoginOptions } from "@knpkv/atlassian-common/cli-auth";
|
|
27
24
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @category Types
|
|
31
|
-
*/
|
|
32
|
-
export interface LoginOptions {
|
|
33
|
-
/** Site URL to select (for accounts with multiple sites) */
|
|
34
|
-
readonly siteUrl?: string;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Information about an accessible Jira site.
|
|
38
|
-
*
|
|
39
|
-
* @category Types
|
|
40
|
-
*/
|
|
41
|
-
export interface AccessibleSite {
|
|
42
|
-
readonly id: string;
|
|
43
|
-
readonly name: string;
|
|
44
|
-
readonly url: string;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* JiraAuth service interface.
|
|
25
|
+
* JiraAuth service interface: the shared Atlassian CLI auth, failing with Jira's own
|
|
26
|
+
* {@link AuthMissingError} when nobody is logged in.
|
|
48
27
|
*
|
|
49
28
|
* @category Services
|
|
50
29
|
*/
|
|
51
|
-
export interface JiraAuthService {
|
|
52
|
-
/** Configure OAuth client credentials */
|
|
53
|
-
readonly configure: (config: OAuthConfig) => Effect.Effect<void, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
54
|
-
/** Check if OAuth is configured */
|
|
55
|
-
readonly isConfigured: () => Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
56
|
-
/** Start OAuth login flow. Returns list of sites if multiple are available. */
|
|
57
|
-
readonly login: (options?: LoginOptions) => Effect.Effect<ReadonlyArray<AccessibleSite> | void, OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
58
|
-
/** Remove stored authentication */
|
|
59
|
-
readonly logout: () => Effect.Effect<void, OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
60
|
-
/** Get access token, refreshing if needed */
|
|
61
|
-
readonly getAccessToken: () => Effect.Effect<Redacted.Redacted<string>, AuthMissingError | OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
62
|
-
/** Get cloud ID from stored token */
|
|
63
|
-
readonly getCloudId: () => Effect.Effect<string, AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
64
|
-
/** Get site URL from stored token */
|
|
65
|
-
readonly getSiteUrl: () => Effect.Effect<string, AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
66
|
-
/** Get current user info from stored token */
|
|
67
|
-
readonly getCurrentUser: () => Effect.Effect<OAuthUser | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
68
|
-
/** Get active auth profile */
|
|
69
|
-
readonly getActiveProfile: () => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
70
|
-
/** List stored auth profiles */
|
|
71
|
-
readonly listProfiles: () => Effect.Effect<ReadonlyArray<AuthProfile>, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
72
|
-
/** Switch active profile by ID, name, site URL, cloud ID, or account ID */
|
|
73
|
-
readonly switchProfile: (selector: string) => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
74
|
-
/** Remove stored profile by ID, name, site URL, cloud ID, or account ID */
|
|
75
|
-
readonly removeProfile: (selector: string) => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
76
|
-
/** Check if user is logged in */
|
|
77
|
-
readonly isLoggedIn: () => Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>;
|
|
30
|
+
export interface JiraAuthService extends AtlassianCliAuth<AuthMissingError> {
|
|
78
31
|
}
|
|
79
32
|
declare const JiraAuth_base: Context.ServiceClass<JiraAuth, "@knpkv/jira-cli/JiraAuth", JiraAuthService>;
|
|
80
33
|
/**
|
|
@@ -98,11 +51,12 @@ declare const JiraAuth_base: Context.ServiceClass<JiraAuth, "@knpkv/jira-cli/Jir
|
|
|
98
51
|
*/
|
|
99
52
|
export declare class JiraAuth extends JiraAuth_base {
|
|
100
53
|
}
|
|
54
|
+
/** Who the Jira CLI is: shared by its auth and its `auth` commands, so both name the same scopes. */
|
|
55
|
+
export declare const jiraCliDescriptor: AtlassianCliDescriptor;
|
|
101
56
|
/**
|
|
102
57
|
* Layer for JiraAuth service.
|
|
103
58
|
*
|
|
104
59
|
* @category Layers
|
|
105
60
|
*/
|
|
106
61
|
export declare const layer: Layer.Layer<JiraAuth, never, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/Crypto").Crypto | import("effect/http/HttpClient").HttpClient>;
|
|
107
|
-
export {};
|
|
108
62
|
//# sourceMappingURL=JiraAuth.d.ts.map
|
package/dist/JiraAuth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JiraAuth.d.ts","sourceRoot":"","sources":["../src/JiraAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"JiraAuth.d.ts","sourceRoot":"","sources":["../src/JiraAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAG5B,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAqBzD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEpF;;;;;GAKG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB,CAAC,gBAAgB,CAAC;CAAG;;AAE9E;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,QAAS,SAAQ,aAGC;CAAG;AAElC,qGAAqG;AACrG,eAAO,MAAM,iBAAiB,EAAE,sBAI/B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,KAAK,+KAOoB,CAAA"}
|
package/dist/JiraAuth.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2 authentication service for Jira CLI with token refresh and concurrency locking.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **Service pattern**: {@link JiraAuth} is a `Context.Tag` whose layer requires `HttpClient`,
|
|
7
|
+
* `ChildProcessSpawner` and `Crypto`. The flow — refresh lock, rotating-token persistence,
|
|
8
|
+
* browser login, profiles — is `@knpkv/atlassian-common/cli-auth`'s, bound to the
|
|
9
|
+
* `"jira-cli"` storage namespace, Jira's scopes and Jira's names.
|
|
10
|
+
*
|
|
11
|
+
* **Common tasks**
|
|
12
|
+
*
|
|
13
|
+
* - Get a valid access token: `auth.getAccessToken()` (auto-refreshes if expired)
|
|
14
|
+
* - Full login flow: `auth.login()`
|
|
15
|
+
* - Check auth state: `auth.isLoggedIn()`
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
1
19
|
import { makeAtlassianCliAuth, NodeCliAuthLive } from "@knpkv/atlassian-common/cli-auth";
|
|
2
20
|
import * as Context from "effect/Context";
|
|
3
21
|
import * as Layer from "effect/Layer";
|
|
@@ -40,16 +58,20 @@ const JIRA_CLI_SCOPES = [
|
|
|
40
58
|
*/
|
|
41
59
|
export class JiraAuth extends Context.Service()("@knpkv/jira-cli/JiraAuth") {
|
|
42
60
|
}
|
|
61
|
+
/** Who the Jira CLI is: shared by its auth and its `auth` commands, so both name the same scopes. */
|
|
62
|
+
export const jiraCliDescriptor = {
|
|
63
|
+
commandName: "jira",
|
|
64
|
+
productName: "Jira",
|
|
65
|
+
scopes: JIRA_CLI_SCOPES
|
|
66
|
+
};
|
|
43
67
|
/**
|
|
44
68
|
* Layer for JiraAuth service.
|
|
45
69
|
*
|
|
46
70
|
* @category Layers
|
|
47
71
|
*/
|
|
48
72
|
export const layer = Layer.effect(JiraAuth, makeAtlassianCliAuth({
|
|
73
|
+
...jiraCliDescriptor,
|
|
49
74
|
toolName: "jira-cli",
|
|
50
|
-
commandName: "jira",
|
|
51
|
-
productName: "Jira",
|
|
52
|
-
scopes: JIRA_CLI_SCOPES,
|
|
53
75
|
authMissing
|
|
54
76
|
})).pipe(Layer.provide(NodeCliAuthLive));
|
|
55
77
|
//# sourceMappingURL=JiraAuth.js.map
|
package/dist/JiraAuth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JiraAuth.js","sourceRoot":"","sources":["../src/JiraAuth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"JiraAuth.js","sourceRoot":"","sources":["../src/JiraAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAGL,oBAAoB,EACpB,eAAe,EAChB,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,+GAA+G;AAC/G,MAAM,eAAe,GAAG;IACtB,0CAA0C;IAC1C,gBAAgB;IAChB,kCAAkC;IAClC,iBAAiB;IACjB,oEAAoE;IACpE,sEAAsE;IACtE,iEAAiE;IACjE,qBAAqB;IACrB,0EAA0E;IAC1E,gBAAgB;IAChB,oEAAoE;IACpE,SAAS;IACT,gEAAgE;IAChE,gBAAgB;CACjB,CAAA;AAYD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,QAAS,SAAQ,OAAO,CAAC,OAAO,EAG1C,CAAC,0BAA0B,CAAC;CAAG;AAElC,qGAAqG;AACrG,MAAM,CAAC,MAAM,iBAAiB,GAA2B;IACvD,WAAW,EAAE,MAAM;IACnB,WAAW,EAAE,MAAM;IACnB,MAAM,EAAE,eAAe;CACxB,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAC/B,QAAQ,EACR,oBAAoB,CAAC;IACnB,GAAG,iBAAiB;IACpB,QAAQ,EAAE,UAAU;IACpB,WAAW;CACZ,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAA"}
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Command, Prompt } from "effect/cli";
|
|
2
1
|
import { JiraAuth } from "../JiraAuth.js";
|
|
3
|
-
export declare const authCommand: Command.Command<"auth", {}, {}, import("@knpkv/atlassian-common/
|
|
2
|
+
export declare const authCommand: import("effect/cli/Command").Command<"auth", {}, {}, import("@knpkv/atlassian-common/cli-auth").BrowserOpenError | import("@knpkv/atlassian-common").FileSystemError | import("@knpkv/atlassian-common").HomeDirectoryError | import("@knpkv/atlassian-common").OAuthError | import("effect/PlatformError").PlatformError | import("@knpkv/atlassian-common/cli-auth").ProfileNotFoundError | import("effect/Terminal").QuitError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | JiraAuth | import("effect/cli/Prompt").Environment>;
|
|
4
3
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAqB,MAAM,gBAAgB,CAAA;AAE5D,eAAO,MAAM,WAAW,2hBAA2E,CAAA"}
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,133 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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.
|
|
2
|
+
* `jira auth`: the shared Atlassian auth commands over {@link JiraAuth}.
|
|
8
3
|
*
|
|
9
4
|
* @internal
|
|
10
5
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import * as Effect from "effect/Effect";
|
|
15
|
-
import * as Option from "effect/Option";
|
|
16
|
-
import { JiraAuth } from "../JiraAuth.js";
|
|
17
|
-
// === Auth create command ===
|
|
18
|
-
const createCommand = Command.make("create", {}, () => Effect.gen(function* () {
|
|
19
|
-
yield* Console.log(`
|
|
20
|
-
Creating OAuth app in Atlassian Developer Console...
|
|
21
|
-
|
|
22
|
-
1. Browser will open to create a new OAuth 2.0 (3LO) app
|
|
23
|
-
2. Enter app name (e.g., "Jira CLI")
|
|
24
|
-
3. After creation, go to "Permissions" and add:
|
|
25
|
-
|
|
26
|
-
Jira API:
|
|
27
|
-
- read:jira-work Search and read issues, comments, attachments
|
|
28
|
-
- read:jira-user Read user info for assignee/reporter fields
|
|
29
|
-
|
|
30
|
-
User Identity API:
|
|
31
|
-
- read:me Get your account ID and email for auth
|
|
32
|
-
|
|
33
|
-
4. Go to "Authorization" and set callback URL:
|
|
34
|
-
http://localhost:8585/callback
|
|
35
|
-
5. Go to "Settings" and copy Client ID and Secret
|
|
36
|
-
6. Run: jira auth configure --client-id <ID> --client-secret <SECRET>
|
|
37
|
-
`);
|
|
38
|
-
const url = "https://developer.atlassian.com/console/myapps/create-3lo-app/";
|
|
39
|
-
yield* openBrowser(url).pipe(Effect.catch(() => Effect.void));
|
|
40
|
-
})).pipe(Command.withDescription("Create OAuth app in Atlassian Developer Console"));
|
|
41
|
-
// === Auth configure command ===
|
|
42
|
-
const clientIdOption = Options.String("client-id").pipe(Options.withDescription("OAuth client ID from Atlassian Developer Console"), Options.optional);
|
|
43
|
-
const clientSecretOption = Options.String("client-secret").pipe(Options.withDescription("OAuth client secret"), Options.optional);
|
|
44
|
-
const configureCommand = Command.make("configure", { clientId: clientIdOption, clientSecret: clientSecretOption }, ({ clientId, clientSecret }) => Effect.gen(function* () {
|
|
45
|
-
const auth = yield* JiraAuth;
|
|
46
|
-
const rawClientId = Option.isSome(clientId)
|
|
47
|
-
? clientId.value
|
|
48
|
-
: yield* Prompt.String({ message: "Enter OAuth client ID:" });
|
|
49
|
-
const rawClientSecret = Option.isSome(clientSecret)
|
|
50
|
-
? clientSecret.value
|
|
51
|
-
: yield* Prompt.String({ message: "Enter OAuth client secret:" });
|
|
52
|
-
yield* auth.configure({ clientId: rawClientId, clientSecret: rawClientSecret });
|
|
53
|
-
yield* Console.log("OAuth configured. Run 'jira auth login' to authenticate.");
|
|
54
|
-
})).pipe(Command.withDescription("Configure OAuth client credentials"));
|
|
55
|
-
// === Auth login command ===
|
|
56
|
-
const siteOption = Options.String("site").pipe(Options.withDescription("Jira site URL to use (for accounts with multiple sites)"), Options.optional);
|
|
57
|
-
const loginCommand = Command.make("login", { site: siteOption }, ({ site }) => Effect.gen(function* () {
|
|
58
|
-
const auth = yield* JiraAuth;
|
|
59
|
-
const result = yield* auth.login(Option.isSome(site) ? { siteUrl: site.value } : undefined);
|
|
60
|
-
if (Array.isArray(result) && result.length > 0) {
|
|
61
|
-
yield* Console.log("\nRe-run with --site to select a specific site.");
|
|
62
|
-
}
|
|
63
|
-
})).pipe(Command.withDescription("Authenticate with Atlassian via OAuth"));
|
|
64
|
-
// === Auth logout command ===
|
|
65
|
-
const logoutCommand = Command.make("logout", {}, () => Effect.gen(function* () {
|
|
66
|
-
const auth = yield* JiraAuth;
|
|
67
|
-
yield* auth.logout();
|
|
68
|
-
yield* Console.log("Logged out");
|
|
69
|
-
})).pipe(Command.withDescription("Remove stored authentication"));
|
|
70
|
-
// === Auth status command ===
|
|
71
|
-
const statusCommand = Command.make("status", {}, () => Effect.gen(function* () {
|
|
72
|
-
const auth = yield* JiraAuth;
|
|
73
|
-
const profile = yield* auth.getActiveProfile();
|
|
74
|
-
if (profile) {
|
|
75
|
-
const user = profile.token.user;
|
|
76
|
-
const account = user ? `${user.name} (${user.email})` : "unknown user";
|
|
77
|
-
yield* Console.log(`Active profile: ${profile.name}`);
|
|
78
|
-
yield* Console.log(`Account: ${account}`);
|
|
79
|
-
yield* Console.log(`Site: ${profile.token.site_url}`);
|
|
80
|
-
yield* Console.log(`Profile ID: ${profile.id}`);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
yield* Console.log("Not logged in. Use 'jira auth login' to authenticate.");
|
|
84
|
-
}
|
|
85
|
-
})).pipe(Command.withDescription("Show authentication status"));
|
|
86
|
-
// === Auth profiles command ===
|
|
87
|
-
const profilesCommand = Command.make("profiles", {}, () => Effect.gen(function* () {
|
|
88
|
-
const auth = yield* JiraAuth;
|
|
89
|
-
const [profiles, active] = yield* Effect.all([auth.listProfiles(), auth.getActiveProfile()]);
|
|
90
|
-
if (profiles.length === 0) {
|
|
91
|
-
yield* Console.log("No auth profiles. Use 'jira auth login' to authenticate.");
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
for (const profile of profiles) {
|
|
95
|
-
const marker = active?.id === profile.id ? "*" : " ";
|
|
96
|
-
yield* Console.log(`${marker} ${profile.id}`);
|
|
97
|
-
yield* Console.log(` ${profile.name}`);
|
|
98
|
-
yield* Console.log(` ${profile.token.site_url}`);
|
|
99
|
-
}
|
|
100
|
-
})).pipe(Command.withDescription("List stored auth profiles"));
|
|
101
|
-
const profileArg = Args.String("profile").pipe(Args.withDescription("Profile ID, name, site URL, cloud ID, or account ID"));
|
|
102
|
-
// === Auth use command ===
|
|
103
|
-
const useCommand = Command.make("use", { profile: profileArg }, ({ profile }) => Effect.gen(function* () {
|
|
104
|
-
const auth = yield* JiraAuth;
|
|
105
|
-
const selected = yield* auth.switchProfile(profile);
|
|
106
|
-
if (!selected) {
|
|
107
|
-
yield* Console.log(`Profile not found: ${profile}`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
yield* Console.log(`Active profile: ${selected.name}`);
|
|
111
|
-
})).pipe(Command.withDescription("Switch active auth profile"));
|
|
112
|
-
// === Auth remove command ===
|
|
113
|
-
const removeCommand = Command.make("remove", { profile: profileArg }, ({ profile }) => Effect.gen(function* () {
|
|
114
|
-
const auth = yield* JiraAuth;
|
|
115
|
-
const removed = yield* auth.removeProfile(profile);
|
|
116
|
-
if (!removed) {
|
|
117
|
-
yield* Console.log(`Profile not found: ${profile}`);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
yield* Console.log(`Removed profile: ${removed.name}`);
|
|
121
|
-
})).pipe(Command.withDescription("Remove stored auth profile"));
|
|
122
|
-
// === Auth command group ===
|
|
123
|
-
export const authCommand = Command.make("auth").pipe(Command.withDescription("Manage OAuth authentication"), Command.withSubcommands([
|
|
124
|
-
createCommand,
|
|
125
|
-
configureCommand,
|
|
126
|
-
loginCommand,
|
|
127
|
-
logoutCommand,
|
|
128
|
-
statusCommand,
|
|
129
|
-
profilesCommand,
|
|
130
|
-
useCommand,
|
|
131
|
-
removeCommand
|
|
132
|
-
]));
|
|
6
|
+
import { makeAuthCommand } from "@knpkv/atlassian-common/cli-auth";
|
|
7
|
+
import { JiraAuth, jiraCliDescriptor } from "../JiraAuth.js";
|
|
8
|
+
export const authCommand = makeAuthCommand(JiraAuth, jiraCliDescriptor, { apiSection: "Jira API" });
|
|
133
9
|
//# sourceMappingURL=auth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAE5D,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,CAAA"}
|
|
@@ -11,7 +11,7 @@ import { VersionService } from "../VersionService.js";
|
|
|
11
11
|
*
|
|
12
12
|
* @category Layers
|
|
13
13
|
*/
|
|
14
|
-
export declare const AppLayer: Layer.Layer<AttachmentService | import("effect/http/HttpClient").HttpClient | IssueService | JiraApiClient | JiraAuth | MarkdownWriter | SiteUrl | VersionService | NodeServices.NodeServices, import("../JiraCliError.js").AuthMissingError | import("@knpkv/atlassian-common
|
|
14
|
+
export declare const AppLayer: Layer.Layer<AttachmentService | import("effect/http/HttpClient").HttpClient | IssueService | JiraApiClient | JiraAuth | MarkdownWriter | SiteUrl | VersionService | NodeServices.NodeServices, import("../JiraCliError.js").AuthMissingError | import("@knpkv/atlassian-common").OAuthError | (import("@knpkv/atlassian-common").FileSystemError | import("@knpkv/atlassian-common").HomeDirectoryError | import("effect/PlatformError").PlatformError), never>;
|
|
15
15
|
/**
|
|
16
16
|
* Auth-only layer for auth commands.
|
|
17
17
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/commands/layers.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAClE,OAAO,EAAE,aAAa,EAAiB,MAAM,wBAAwB,CAAA;AAErE,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,iBAAiB,EAAmC,MAAM,yBAAyB,CAAA;AAC5F,OAAO,EAAE,YAAY,EAA8B,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACtF,OAAO,EAAE,QAAQ,EAA0B,MAAM,gBAAgB,CAAA;AACjE,OAAO,EAAgC,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACnF,OAAO,EAAgC,cAAc,EAAE,MAAM,sBAAsB,CAAA;AA+FnF;;;;GAIG;AACH,eAAO,MAAM,QAAQ,
|
|
1
|
+
{"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/commands/layers.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,YAAY,MAAM,oCAAoC,CAAA;AAClE,OAAO,EAAE,aAAa,EAAiB,MAAM,wBAAwB,CAAA;AAErE,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,iBAAiB,EAAmC,MAAM,yBAAyB,CAAA;AAC5F,OAAO,EAAE,YAAY,EAA8B,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACtF,OAAO,EAAE,QAAQ,EAA0B,MAAM,gBAAgB,CAAA;AACjE,OAAO,EAAgC,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACnF,OAAO,EAAgC,cAAc,EAAE,MAAM,sBAAsB,CAAA;AA+FnF;;;;GAIG;AACH,eAAO,MAAM,QAAQ,icASpB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,oLAOzB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,sIAMxB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,SAAU,aAAa,CAAC,MAAM,CAAC,KAAG,MAAM,GAAG,MAAM,GAAG,SAkB5E,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/jira-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "CLI tool to fetch Jira tickets and export to markdown",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@effect/platform-node": "4.0.0",
|
|
61
61
|
"gray-matter": "^4.0.3",
|
|
62
62
|
"js-yaml": "5.4.2",
|
|
63
|
-
"@knpkv/agent-skills": "^0.3.
|
|
64
|
-
"@knpkv/atlassian-common": "1.
|
|
63
|
+
"@knpkv/agent-skills": "^0.3.3",
|
|
64
|
+
"@knpkv/atlassian-common": "1.8.0",
|
|
65
65
|
"@knpkv/jira-api-client": "2.0.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
package/skills/jira/SKILL.md
CHANGED
|
@@ -21,6 +21,7 @@ Use the `jira` binary for Jira Cloud issue export and release-version workflows.
|
|
|
21
21
|
```bash
|
|
22
22
|
jira auth status
|
|
23
23
|
jira auth create
|
|
24
|
+
jira auth manage
|
|
24
25
|
jira auth configure --client-id <id> --client-secret <secret>
|
|
25
26
|
jira auth login
|
|
26
27
|
jira auth login --site https://example.atlassian.net
|
package/src/JiraAuth.ts
CHANGED
|
@@ -16,20 +16,14 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
OAuthConfig,
|
|
26
|
-
OAuthUser
|
|
27
|
-
} from "@knpkv/atlassian-common/config"
|
|
19
|
+
import {
|
|
20
|
+
type AtlassianCliAuth,
|
|
21
|
+
type AtlassianCliDescriptor,
|
|
22
|
+
makeAtlassianCliAuth,
|
|
23
|
+
NodeCliAuthLive
|
|
24
|
+
} from "@knpkv/atlassian-common/cli-auth"
|
|
28
25
|
import * as Context from "effect/Context"
|
|
29
|
-
import type * as Effect from "effect/Effect"
|
|
30
26
|
import * as Layer from "effect/Layer"
|
|
31
|
-
import type * as PlatformError from "effect/PlatformError"
|
|
32
|
-
import type * as Redacted from "effect/Redacted"
|
|
33
27
|
import type { AuthMissingError } from "./JiraCliError.js"
|
|
34
28
|
import { authMissing } from "./JiraCliError.js"
|
|
35
29
|
|
|
@@ -51,95 +45,15 @@ const JIRA_CLI_SCOPES = [
|
|
|
51
45
|
"offline_access"
|
|
52
46
|
]
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
* Options for the login method.
|
|
56
|
-
*
|
|
57
|
-
* @category Types
|
|
58
|
-
*/
|
|
59
|
-
export interface LoginOptions {
|
|
60
|
-
/** Site URL to select (for accounts with multiple sites) */
|
|
61
|
-
readonly siteUrl?: string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Information about an accessible Jira site.
|
|
66
|
-
*
|
|
67
|
-
* @category Types
|
|
68
|
-
*/
|
|
69
|
-
export interface AccessibleSite {
|
|
70
|
-
readonly id: string
|
|
71
|
-
readonly name: string
|
|
72
|
-
readonly url: string
|
|
73
|
-
}
|
|
48
|
+
export type { AccessibleSite, LoginOptions } from "@knpkv/atlassian-common/cli-auth"
|
|
74
49
|
|
|
75
50
|
/**
|
|
76
|
-
* JiraAuth service interface
|
|
51
|
+
* JiraAuth service interface: the shared Atlassian CLI auth, failing with Jira's own
|
|
52
|
+
* {@link AuthMissingError} when nobody is logged in.
|
|
77
53
|
*
|
|
78
54
|
* @category Services
|
|
79
55
|
*/
|
|
80
|
-
export interface JiraAuthService {
|
|
81
|
-
/** Configure OAuth client credentials */
|
|
82
|
-
readonly configure: (
|
|
83
|
-
config: OAuthConfig
|
|
84
|
-
) => Effect.Effect<void, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
85
|
-
/** Check if OAuth is configured */
|
|
86
|
-
readonly isConfigured: () => Effect.Effect<
|
|
87
|
-
boolean,
|
|
88
|
-
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
89
|
-
>
|
|
90
|
-
/** Start OAuth login flow. Returns list of sites if multiple are available. */
|
|
91
|
-
readonly login: (
|
|
92
|
-
options?: LoginOptions
|
|
93
|
-
) => Effect.Effect<
|
|
94
|
-
ReadonlyArray<AccessibleSite> | void,
|
|
95
|
-
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
96
|
-
>
|
|
97
|
-
/** Remove stored authentication */
|
|
98
|
-
readonly logout: () => Effect.Effect<
|
|
99
|
-
void,
|
|
100
|
-
OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
101
|
-
>
|
|
102
|
-
/** Get access token, refreshing if needed */
|
|
103
|
-
readonly getAccessToken: () => Effect.Effect<
|
|
104
|
-
Redacted.Redacted<string>,
|
|
105
|
-
AuthMissingError | OAuthError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
106
|
-
>
|
|
107
|
-
/** Get cloud ID from stored token */
|
|
108
|
-
readonly getCloudId: () => Effect.Effect<
|
|
109
|
-
string,
|
|
110
|
-
AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
111
|
-
>
|
|
112
|
-
/** Get site URL from stored token */
|
|
113
|
-
readonly getSiteUrl: () => Effect.Effect<
|
|
114
|
-
string,
|
|
115
|
-
AuthMissingError | FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
116
|
-
>
|
|
117
|
-
/** Get current user info from stored token */
|
|
118
|
-
readonly getCurrentUser: () => Effect.Effect<
|
|
119
|
-
OAuthUser | null,
|
|
120
|
-
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
121
|
-
>
|
|
122
|
-
/** Get active auth profile */
|
|
123
|
-
readonly getActiveProfile: () => Effect.Effect<
|
|
124
|
-
AuthProfile | null,
|
|
125
|
-
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
126
|
-
>
|
|
127
|
-
/** List stored auth profiles */
|
|
128
|
-
readonly listProfiles: () => Effect.Effect<
|
|
129
|
-
ReadonlyArray<AuthProfile>,
|
|
130
|
-
FileSystemError | HomeDirectoryError | PlatformError.PlatformError
|
|
131
|
-
>
|
|
132
|
-
/** Switch active profile by ID, name, site URL, cloud ID, or account ID */
|
|
133
|
-
readonly switchProfile: (
|
|
134
|
-
selector: string
|
|
135
|
-
) => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
136
|
-
/** Remove stored profile by ID, name, site URL, cloud ID, or account ID */
|
|
137
|
-
readonly removeProfile: (
|
|
138
|
-
selector: string
|
|
139
|
-
) => Effect.Effect<AuthProfile | null, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
140
|
-
/** Check if user is logged in */
|
|
141
|
-
readonly isLoggedIn: () => Effect.Effect<boolean, FileSystemError | HomeDirectoryError | PlatformError.PlatformError>
|
|
142
|
-
}
|
|
56
|
+
export interface JiraAuthService extends AtlassianCliAuth<AuthMissingError> {}
|
|
143
57
|
|
|
144
58
|
/**
|
|
145
59
|
* JiraAuth service tag.
|
|
@@ -165,6 +79,13 @@ export class JiraAuth extends Context.Service<
|
|
|
165
79
|
JiraAuthService
|
|
166
80
|
>()("@knpkv/jira-cli/JiraAuth") {}
|
|
167
81
|
|
|
82
|
+
/** Who the Jira CLI is: shared by its auth and its `auth` commands, so both name the same scopes. */
|
|
83
|
+
export const jiraCliDescriptor: AtlassianCliDescriptor = {
|
|
84
|
+
commandName: "jira",
|
|
85
|
+
productName: "Jira",
|
|
86
|
+
scopes: JIRA_CLI_SCOPES
|
|
87
|
+
}
|
|
88
|
+
|
|
168
89
|
/**
|
|
169
90
|
* Layer for JiraAuth service.
|
|
170
91
|
*
|
|
@@ -173,10 +94,8 @@ export class JiraAuth extends Context.Service<
|
|
|
173
94
|
export const layer = Layer.effect(
|
|
174
95
|
JiraAuth,
|
|
175
96
|
makeAtlassianCliAuth({
|
|
97
|
+
...jiraCliDescriptor,
|
|
176
98
|
toolName: "jira-cli",
|
|
177
|
-
commandName: "jira",
|
|
178
|
-
productName: "Jira",
|
|
179
|
-
scopes: JIRA_CLI_SCOPES,
|
|
180
99
|
authMissing
|
|
181
100
|
})
|
|
182
101
|
).pipe(Layer.provide(NodeCliAuthLive))
|
package/src/commands/auth.ts
CHANGED
|
@@ -1,172 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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.
|
|
2
|
+
* `jira auth`: the shared Atlassian auth commands over {@link JiraAuth}.
|
|
8
3
|
*
|
|
9
4
|
* @internal
|
|
10
5
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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* openBrowser(url).pipe(Effect.catch(() => Effect.void))
|
|
42
|
-
})).pipe(Command.withDescription("Create OAuth app in Atlassian Developer Console"))
|
|
43
|
-
|
|
44
|
-
// === Auth configure command ===
|
|
45
|
-
const clientIdOption = Options.String("client-id").pipe(
|
|
46
|
-
Options.withDescription("OAuth client ID from Atlassian Developer Console"),
|
|
47
|
-
Options.optional
|
|
48
|
-
)
|
|
49
|
-
const clientSecretOption = Options.String("client-secret").pipe(
|
|
50
|
-
Options.withDescription("OAuth client secret"),
|
|
51
|
-
Options.optional
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
const configureCommand = Command.make(
|
|
55
|
-
"configure",
|
|
56
|
-
{ clientId: clientIdOption, clientSecret: clientSecretOption },
|
|
57
|
-
({ clientId, clientSecret }) =>
|
|
58
|
-
Effect.gen(function*() {
|
|
59
|
-
const auth = yield* JiraAuth
|
|
60
|
-
|
|
61
|
-
const rawClientId = Option.isSome(clientId)
|
|
62
|
-
? clientId.value
|
|
63
|
-
: yield* Prompt.String({ message: "Enter OAuth client ID:" })
|
|
64
|
-
const rawClientSecret = Option.isSome(clientSecret)
|
|
65
|
-
? clientSecret.value
|
|
66
|
-
: yield* Prompt.String({ message: "Enter OAuth client secret:" })
|
|
67
|
-
|
|
68
|
-
yield* auth.configure({ clientId: rawClientId, clientSecret: rawClientSecret })
|
|
69
|
-
yield* Console.log("OAuth configured. Run 'jira auth login' to authenticate.")
|
|
70
|
-
})
|
|
71
|
-
).pipe(Command.withDescription("Configure OAuth client credentials"))
|
|
72
|
-
|
|
73
|
-
// === Auth login command ===
|
|
74
|
-
const siteOption = Options.String("site").pipe(
|
|
75
|
-
Options.withDescription("Jira site URL to use (for accounts with multiple sites)"),
|
|
76
|
-
Options.optional
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
const loginCommand = Command.make("login", { site: siteOption }, ({ site }) =>
|
|
80
|
-
Effect.gen(function*() {
|
|
81
|
-
const auth = yield* JiraAuth
|
|
82
|
-
const result = yield* auth.login(Option.isSome(site) ? { siteUrl: site.value } : undefined)
|
|
83
|
-
if (Array.isArray(result) && result.length > 0) {
|
|
84
|
-
yield* Console.log("\nRe-run with --site to select a specific site.")
|
|
85
|
-
}
|
|
86
|
-
})).pipe(Command.withDescription("Authenticate with Atlassian via OAuth"))
|
|
87
|
-
|
|
88
|
-
// === Auth logout command ===
|
|
89
|
-
const logoutCommand = Command.make("logout", {}, () =>
|
|
90
|
-
Effect.gen(function*() {
|
|
91
|
-
const auth = yield* JiraAuth
|
|
92
|
-
yield* auth.logout()
|
|
93
|
-
yield* Console.log("Logged out")
|
|
94
|
-
})).pipe(Command.withDescription("Remove stored authentication"))
|
|
95
|
-
|
|
96
|
-
// === Auth status command ===
|
|
97
|
-
const statusCommand = Command.make("status", {}, () =>
|
|
98
|
-
Effect.gen(function*() {
|
|
99
|
-
const auth = yield* JiraAuth
|
|
100
|
-
const profile = yield* auth.getActiveProfile()
|
|
101
|
-
if (profile) {
|
|
102
|
-
const user = profile.token.user
|
|
103
|
-
const account = user ? `${user.name} (${user.email})` : "unknown user"
|
|
104
|
-
yield* Console.log(`Active profile: ${profile.name}`)
|
|
105
|
-
yield* Console.log(`Account: ${account}`)
|
|
106
|
-
yield* Console.log(`Site: ${profile.token.site_url}`)
|
|
107
|
-
yield* Console.log(`Profile ID: ${profile.id}`)
|
|
108
|
-
} else {
|
|
109
|
-
yield* Console.log("Not logged in. Use 'jira auth login' to authenticate.")
|
|
110
|
-
}
|
|
111
|
-
})).pipe(Command.withDescription("Show authentication status"))
|
|
112
|
-
|
|
113
|
-
// === Auth profiles command ===
|
|
114
|
-
const profilesCommand = Command.make("profiles", {}, () =>
|
|
115
|
-
Effect.gen(function*() {
|
|
116
|
-
const auth = yield* JiraAuth
|
|
117
|
-
const [profiles, active] = yield* Effect.all([auth.listProfiles(), auth.getActiveProfile()])
|
|
118
|
-
if (profiles.length === 0) {
|
|
119
|
-
yield* Console.log("No auth profiles. Use 'jira auth login' to authenticate.")
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const profile of profiles) {
|
|
124
|
-
const marker = active?.id === profile.id ? "*" : " "
|
|
125
|
-
yield* Console.log(`${marker} ${profile.id}`)
|
|
126
|
-
yield* Console.log(` ${profile.name}`)
|
|
127
|
-
yield* Console.log(` ${profile.token.site_url}`)
|
|
128
|
-
}
|
|
129
|
-
})).pipe(Command.withDescription("List stored auth profiles"))
|
|
130
|
-
|
|
131
|
-
const profileArg = Args.String("profile").pipe(
|
|
132
|
-
Args.withDescription("Profile ID, name, site URL, cloud ID, or account ID")
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
// === Auth use command ===
|
|
136
|
-
const useCommand = Command.make("use", { profile: profileArg }, ({ profile }) =>
|
|
137
|
-
Effect.gen(function*() {
|
|
138
|
-
const auth = yield* JiraAuth
|
|
139
|
-
const selected = yield* auth.switchProfile(profile)
|
|
140
|
-
if (!selected) {
|
|
141
|
-
yield* Console.log(`Profile not found: ${profile}`)
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
yield* Console.log(`Active profile: ${selected.name}`)
|
|
145
|
-
})).pipe(Command.withDescription("Switch active auth profile"))
|
|
146
|
-
|
|
147
|
-
// === Auth remove command ===
|
|
148
|
-
const removeCommand = Command.make("remove", { profile: profileArg }, ({ profile }) =>
|
|
149
|
-
Effect.gen(function*() {
|
|
150
|
-
const auth = yield* JiraAuth
|
|
151
|
-
const removed = yield* auth.removeProfile(profile)
|
|
152
|
-
if (!removed) {
|
|
153
|
-
yield* Console.log(`Profile not found: ${profile}`)
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
yield* Console.log(`Removed profile: ${removed.name}`)
|
|
157
|
-
})).pipe(Command.withDescription("Remove stored auth profile"))
|
|
6
|
+
import { makeAuthCommand } from "@knpkv/atlassian-common/cli-auth"
|
|
7
|
+
import { JiraAuth, jiraCliDescriptor } from "../JiraAuth.js"
|
|
158
8
|
|
|
159
|
-
|
|
160
|
-
export const authCommand = Command.make("auth").pipe(
|
|
161
|
-
Command.withDescription("Manage OAuth authentication"),
|
|
162
|
-
Command.withSubcommands([
|
|
163
|
-
createCommand,
|
|
164
|
-
configureCommand,
|
|
165
|
-
loginCommand,
|
|
166
|
-
logoutCommand,
|
|
167
|
-
statusCommand,
|
|
168
|
-
profilesCommand,
|
|
169
|
-
useCommand,
|
|
170
|
-
removeCommand
|
|
171
|
-
])
|
|
172
|
-
)
|
|
9
|
+
export const authCommand = makeAuthCommand(JiraAuth, jiraCliDescriptor, { apiSection: "Jira API" })
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** @effect-diagnostics strictEffectProvide:skip-file */
|
|
2
|
+
/**
|
|
3
|
+
* `jira auth create` must list every scope `jira auth login` requests: Atlassian rejects an
|
|
4
|
+
* authorization request naming a scope the app lacks, so a shorter list is a setup that cannot log in.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeServices } from "@effect/platform-node"
|
|
7
|
+
import { expect, it } from "@effect/vitest"
|
|
8
|
+
import { Command } from "effect/cli"
|
|
9
|
+
import * as Console from "effect/Console"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import { FetchHttpClient } from "effect/http"
|
|
12
|
+
import * as Layer from "effect/Layer"
|
|
13
|
+
import { ChildProcessSpawner } from "effect/process"
|
|
14
|
+
import * as Sink from "effect/Sink"
|
|
15
|
+
import * as Stream from "effect/Stream"
|
|
16
|
+
import { authCommand } from "../src/commands/auth.js"
|
|
17
|
+
import { jiraCliDescriptor, layer } from "../src/JiraAuth.js"
|
|
18
|
+
|
|
19
|
+
const browserOpens = ChildProcessSpawner.make(() =>
|
|
20
|
+
Effect.succeed(ChildProcessSpawner.makeHandle({
|
|
21
|
+
all: Stream.empty,
|
|
22
|
+
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
|
|
23
|
+
getInputFd: () => Sink.drain,
|
|
24
|
+
getOutputFd: () => Stream.empty,
|
|
25
|
+
isRunning: Effect.succeed(false),
|
|
26
|
+
kill: () => Effect.void,
|
|
27
|
+
pid: ChildProcessSpawner.ProcessId(1),
|
|
28
|
+
reref: Effect.void,
|
|
29
|
+
stderr: Stream.empty,
|
|
30
|
+
stdin: Sink.drain,
|
|
31
|
+
stdout: Stream.empty,
|
|
32
|
+
unref: Effect.succeed(Effect.void)
|
|
33
|
+
}))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
it.effect("auth create lists every scope JiraAuth logs in with", () =>
|
|
37
|
+
Effect.gen(function*() {
|
|
38
|
+
const lines: Array<string> = []
|
|
39
|
+
const capture: Console.Console = Object.assign(Object.create(console), {
|
|
40
|
+
log: (...parts: ReadonlyArray<unknown>) => lines.push(parts.join(" "))
|
|
41
|
+
})
|
|
42
|
+
const cli = Command.runWith(Command.make("jira").pipe(Command.withSubcommands([authCommand])), { version: "0" })
|
|
43
|
+
const exit = yield* cli(["auth", "create"]).pipe(Effect.provideService(Console.Console, capture), Effect.exit)
|
|
44
|
+
expect(exit._tag).toBe("Success")
|
|
45
|
+
const output = lines.join("\n")
|
|
46
|
+
const permissions = jiraCliDescriptor.scopes.filter((scope) => scope !== "offline_access")
|
|
47
|
+
expect(permissions.length).toBeGreaterThan(2)
|
|
48
|
+
for (const scope of permissions) expect(output).toContain(scope)
|
|
49
|
+
expect(output).toContain("jira auth configure")
|
|
50
|
+
}).pipe(
|
|
51
|
+
// The command's browser launch sees the fake; the auth layer is built over real platform services.
|
|
52
|
+
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, browserOpens),
|
|
53
|
+
Effect.provide(layer.pipe(Layer.provideMerge(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer))))
|
|
54
|
+
))
|