@knpkv/jira-clockify 1.5.0 → 1.6.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/README.md +36 -5
- package/dist/package.json +15 -5
- package/dist/src/bin.js +16 -5
- package/dist/src/bin.js.map +1 -1
- package/dist/src/cli/CommandFailed.d.ts +27 -0
- package/dist/src/cli/CommandFailed.d.ts.map +1 -0
- package/dist/src/cli/CommandFailed.js +25 -0
- package/dist/src/cli/CommandFailed.js.map +1 -0
- package/dist/src/cli/auth.d.ts +23 -3
- package/dist/src/cli/auth.d.ts.map +1 -1
- package/dist/src/cli/auth.js +123 -102
- package/dist/src/cli/auth.js.map +1 -1
- package/dist/src/cli/config.d.ts +2 -1
- package/dist/src/cli/config.d.ts.map +1 -1
- package/dist/src/cli/config.js +43 -29
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/fetchTicket.d.ts +3 -3
- package/dist/src/cli/fetchTicket.d.ts.map +1 -1
- package/dist/src/cli/fetchTicket.js +7 -7
- package/dist/src/cli/fetchTicket.js.map +1 -1
- package/dist/src/cli/layers.d.ts +6 -5
- package/dist/src/cli/layers.d.ts.map +1 -1
- package/dist/src/cli/layers.js +16 -15
- package/dist/src/cli/layers.js.map +1 -1
- package/dist/src/cli/list.d.ts +3 -2
- package/dist/src/cli/list.d.ts.map +1 -1
- package/dist/src/cli/list.js +11 -5
- package/dist/src/cli/list.js.map +1 -1
- package/dist/src/cli/reconcile.d.ts +12 -3
- package/dist/src/cli/reconcile.d.ts.map +1 -1
- package/dist/src/cli/reconcile.js +71 -23
- package/dist/src/cli/reconcile.js.map +1 -1
- package/dist/src/cli/root.d.ts.map +1 -1
- package/dist/src/cli/root.js +3 -3
- package/dist/src/cli/root.js.map +1 -1
- package/dist/src/cli/setup.d.ts +14 -4
- package/dist/src/cli/setup.d.ts.map +1 -1
- package/dist/src/cli/setup.js +45 -94
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/timer/discard.d.ts +1 -1
- package/dist/src/cli/timer/discard.d.ts.map +1 -1
- package/dist/src/cli/timer/discard.js +2 -1
- package/dist/src/cli/timer/discard.js.map +1 -1
- package/dist/src/cli/timer/log.d.ts +1 -1
- package/dist/src/cli/timer/log.d.ts.map +1 -1
- package/dist/src/cli/timer/log.js +2 -1
- package/dist/src/cli/timer/log.js.map +1 -1
- package/dist/src/cli/timer/start.d.ts +2 -1
- package/dist/src/cli/timer/start.d.ts.map +1 -1
- package/dist/src/cli/timer/start.js +6 -8
- package/dist/src/cli/timer/start.js.map +1 -1
- package/dist/src/cli/timer/stop.d.ts +1 -1
- package/dist/src/cli/timer/stop.d.ts.map +1 -1
- package/dist/src/cli/timer.d.ts.map +1 -1
- package/dist/src/cli/timer.js +8 -1
- package/dist/src/cli/timer.js.map +1 -1
- package/dist/src/cli/unknownCommand.d.ts +24 -0
- package/dist/src/cli/unknownCommand.d.ts.map +1 -0
- package/dist/src/cli/unknownCommand.js +84 -0
- package/dist/src/cli/unknownCommand.js.map +1 -0
- package/dist/src/cli/watch.d.ts +2 -2
- package/dist/src/cli/watch.d.ts.map +1 -1
- package/dist/src/services/ConfigService.d.ts +2 -0
- package/dist/src/services/ConfigService.d.ts.map +1 -1
- package/dist/src/services/ConfigService.js +2 -1
- package/dist/src/services/ConfigService.js.map +1 -1
- package/dist/src/services/IssueFacts.d.ts +2 -2
- package/dist/src/services/IssueFacts.d.ts.map +1 -1
- package/dist/src/services/IssueFacts.js +5 -5
- package/dist/src/services/IssueFacts.js.map +1 -1
- package/dist/src/services/JiraAccess.d.ts +140 -0
- package/dist/src/services/JiraAccess.d.ts.map +1 -0
- package/dist/src/services/JiraAccess.js +204 -0
- package/dist/src/services/JiraAccess.js.map +1 -0
- package/dist/src/services/ProviderSnapshots.d.ts +2 -2
- package/dist/src/services/ProviderSnapshots.d.ts.map +1 -1
- package/dist/src/services/ProviderSnapshots.js +14 -24
- package/dist/src/services/ProviderSnapshots.js.map +1 -1
- package/dist/src/services/ReconcileService.d.ts +2 -2
- package/dist/src/services/ReconcileService.d.ts.map +1 -1
- package/dist/src/services/ReconcileService.js +11 -4
- package/dist/src/services/ReconcileService.js.map +1 -1
- package/dist/src/services/SavedEntries.d.ts +1 -1
- package/dist/src/services/SavedEntries.d.ts.map +1 -1
- package/dist/src/services/TimerService.d.ts +3 -3
- package/dist/src/services/TimerService.d.ts.map +1 -1
- package/dist/src/services/TimerService.js +6 -17
- package/dist/src/services/TimerService.js.map +1 -1
- package/dist/src/testing/fakeHeadless.d.ts +1 -1
- package/dist/src/testing/fakeHeadless.d.ts.map +1 -1
- package/dist/src/testing/fakeHeadless.js +17 -7
- package/dist/src/testing/fakeHeadless.js.map +1 -1
- package/dist/src/tui/App.js +1 -1
- package/dist/src/tui/atoms/runtime.d.ts +1 -1
- package/dist/src/tui/atoms/runtime.d.ts.map +1 -1
- package/dist/src/utils/hints.d.ts +4 -2
- package/dist/src/utils/hints.d.ts.map +1 -1
- package/dist/src/utils/hints.js +4 -2
- package/dist/src/utils/hints.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/skills/jcf/SKILL.md +9 -6
package/README.md
CHANGED
|
@@ -10,35 +10,59 @@ Built with Effect-TS and [@opentui/react](https://github.com/anomalyco/opentui).
|
|
|
10
10
|
pnpm add @knpkv/jira-clockify
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Or link globally
|
|
13
|
+
Or link globally from this repository. The linked `jcf` runs the build output, so build it and the
|
|
14
|
+
workspace packages it imports first:
|
|
14
15
|
|
|
15
16
|
```bash
|
|
17
|
+
pnpm --filter "@knpkv/jira-clockify..." build
|
|
16
18
|
cd packages/jira-clockify && pnpm link --global
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
## Setup
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Run `jcf` with nothing set up and it asks about each system in turn; either can be skipped and
|
|
24
|
+
added later. `jcf auth status` shows what is connected and what to run next. Every step below can
|
|
25
|
+
also be run on its own.
|
|
26
|
+
|
|
27
|
+
### 1. Jira
|
|
22
28
|
|
|
23
29
|
```bash
|
|
24
|
-
jcf auth jira
|
|
30
|
+
jcf auth jira token # Site, email and an API token from https://id.atlassian.com/manage-profile/security/api-tokens
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The token is checked against Jira before it is saved to `~/.jcf/jira.json` (owner-only, `0600`) and is
|
|
34
|
+
never printed. Only Jira Cloud addresses (`*.atlassian.net`) are accepted, so a typo or look-alike
|
|
35
|
+
domain never receives the token. Classic and scoped tokens both work: a scoped token, which the
|
|
36
|
+
site itself refuses, is used through Atlassian's gateway for that site (`api.atlassian.com/ex/jira/<cloud id>`). A failed check says whether the site, the token or the network was
|
|
37
|
+
the problem.
|
|
38
|
+
|
|
39
|
+
Advanced: connect through your own Atlassian OAuth app instead.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
jcf auth jira create # Opens Atlassian console — create OAuth 2.0 app
|
|
25
43
|
jcf auth jira configure # Set client ID and secret
|
|
26
44
|
jcf auth jira login # Authenticate via browser
|
|
27
45
|
```
|
|
28
46
|
|
|
47
|
+
When both exist, the API token is used. `jcf auth jira logout` removes both.
|
|
48
|
+
|
|
29
49
|
### 2. Clockify API Key
|
|
30
50
|
|
|
31
51
|
```bash
|
|
32
52
|
jcf auth clockify setup # Enter API key from https://app.clockify.me/manage-api-keys
|
|
53
|
+
jcf auth clockify setup --api-key <key> --workspace <name> # For scripts; the key is visible to other processes
|
|
33
54
|
```
|
|
34
55
|
|
|
56
|
+
Commands that need a system that is not connected fail with one line naming the command that
|
|
57
|
+
connects it, and exit non-zero.
|
|
58
|
+
|
|
35
59
|
### 3. Configure Defaults
|
|
36
60
|
|
|
37
61
|
```bash
|
|
38
62
|
jcf config set project # Select default Clockify project
|
|
39
63
|
jcf config set billable # Set default billable flag
|
|
40
64
|
jcf config set jql <jql> # Set default JQL filter
|
|
41
|
-
jcf config show # Show current config
|
|
65
|
+
jcf config show # Show current config; marks values that are jcf's defaults
|
|
42
66
|
jcf config reset # Reset to defaults
|
|
43
67
|
```
|
|
44
68
|
|
|
@@ -96,8 +120,13 @@ jcf sync reconcile jira-to-clockify # Fill Clockify from Jira
|
|
|
96
120
|
jcf sync reconcile --agent claude # Propose worklogs from local Claude Code sessions
|
|
97
121
|
jcf sync reconcile --agent claude --calendar # ...with an hour-by-hour grid of when it happened
|
|
98
122
|
jcf sync reconcile --agent claude --json # Reporting only: one JSON value, nothing logged
|
|
123
|
+
jcf sync reconcile --agent claude --only clockify # Read and write Clockify alone (or --only jira)
|
|
99
124
|
```
|
|
100
125
|
|
|
126
|
+
An agent run reads both systems before it proposes anything, because a side it cannot read looks the
|
|
127
|
+
same as a side with nothing recorded. If one is not connected the run stops before planning and says
|
|
128
|
+
how to connect it, or to pass `--only` for the other one.
|
|
129
|
+
|
|
101
130
|
`--agent claude` is for time _neither_ side recorded: it reads Claude Code and Codex transcripts,
|
|
102
131
|
keeps work inside your session roots, and works out which issue each
|
|
103
132
|
session belongs to and how long it accounts for, subtracts
|
|
@@ -319,12 +348,14 @@ Stored in `~/.jcf/`:
|
|
|
319
348
|
~/.jcf/
|
|
320
349
|
├── config.json # JQL, project, billable defaults, session roots
|
|
321
350
|
├── clockify.json # Clockify API key, workspace, user
|
|
351
|
+
├── jira.json # Jira site, cloud id, email, API token, account (0600)
|
|
322
352
|
├── poll.lock # Kernel lock held by the active managed poll
|
|
323
353
|
├── poll.stamp # Last managed poll attempt, including failures
|
|
324
354
|
└── state.json # Current timer state and polling authority
|
|
325
355
|
```
|
|
326
356
|
|
|
327
|
-
Jira OAuth credentials stored via `@knpkv/atlassian-common` in
|
|
357
|
+
Jira OAuth credentials (the advanced path) are stored via `@knpkv/atlassian-common` in
|
|
358
|
+
`~/.config/atlassian/`. That store holds OAuth logins only, so the API token lives in `~/.jcf/jira.json`.
|
|
328
359
|
|
|
329
360
|
## License
|
|
330
361
|
|
package/dist/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/jira-clockify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "TUI for Jira-Clockify time tracking, attachable to neovim",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"main": "src/index.
|
|
8
|
+
"main": "dist/src/index.js",
|
|
9
|
+
"types": "src/index.ts",
|
|
9
10
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"default": "./dist/src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./testing.js": {
|
|
16
|
+
"types": "./src/testing/fakeHeadless.ts",
|
|
17
|
+
"default": "./dist/src/testing/fakeHeadless.js"
|
|
18
|
+
},
|
|
19
|
+
"./*.js": {
|
|
20
|
+
"types": "./src/*.ts",
|
|
21
|
+
"default": "./dist/src/*.js"
|
|
22
|
+
}
|
|
13
23
|
},
|
|
14
24
|
"bin": {
|
|
15
25
|
"jcf": "dist/src/bin.js"
|
package/dist/src/bin.js
CHANGED
|
@@ -5,22 +5,33 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { NodeRuntime } from "@effect/platform-node";
|
|
8
|
-
import { Effect } from "effect";
|
|
8
|
+
import { Console, Data, Effect } from "effect";
|
|
9
9
|
import { Command } from "effect/cli";
|
|
10
10
|
import * as Runtime from "effect/Runtime";
|
|
11
11
|
import * as Stdio from "effect/Stdio";
|
|
12
|
+
import pkg from "../package.json" with { type: "json" };
|
|
12
13
|
import { HeadlessLayer } from "./cli/layers.js";
|
|
13
14
|
import { root } from "./cli/root.js";
|
|
14
15
|
import { reportUnhandled } from "./cli/runtimeFailure.js";
|
|
16
|
+
import { unknownCommandLine } from "./cli/unknownCommand.js";
|
|
15
17
|
const processArgv = Effect.gen(function* () {
|
|
16
18
|
const stdio = yield* Stdio.Stdio;
|
|
17
19
|
const args = yield* stdio.args;
|
|
18
20
|
return args;
|
|
19
21
|
});
|
|
20
|
-
const cli = Command.runWith(root, {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const cli = Command.runWith(root, { version: pkg.version });
|
|
23
|
+
/** An unknown command fails with one line, before effect/cli prints the whole help above it. */
|
|
24
|
+
class UnknownCommand extends Data.TaggedError("UnknownCommand") {
|
|
25
|
+
[Runtime.errorReported] = false;
|
|
26
|
+
}
|
|
27
|
+
const program = reportUnhandled(processArgv.pipe(Effect.flatMap((argv) => Effect.gen(function* () {
|
|
28
|
+
const unknown = unknownCommandLine(argv, root);
|
|
29
|
+
if (unknown !== undefined) {
|
|
30
|
+
yield* Console.error(unknown);
|
|
31
|
+
return yield* new UnknownCommand();
|
|
32
|
+
}
|
|
33
|
+
return yield* cli(argv);
|
|
34
|
+
})))).pipe(
|
|
24
35
|
// This *is* the entry point: the one place the whole layer graph is composed and provided.
|
|
25
36
|
// @effect-diagnostics-next-line strictEffectProvide:off
|
|
26
37
|
Effect.provide(HeadlessLayer));
|
package/dist/src/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/bin.ts"],"names":[],"mappings":";AACA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/bin.ts"],"names":[],"mappings":";AACA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAA;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAA;IAC9B,OAAO,IAAI,CAAA;AACb,CAAC,CAAC,CAAA;AAEF,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;AAE3D,gGAAgG;AAChG,MAAM,cAAe,SAAQ,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAK;IAC/C,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,KAAK,CAAA;CAClD;AAED,MAAM,OAAO,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,CAC9C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACtB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC9C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC7B,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,EAAE,CAAA;IACpC,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC,CAAC,CACH,CACF,CAAC,CAAC,IAAI;AACL,2FAA2F;AAC3F,wDAAwD;AACxD,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAC9B,CAAA;AAED,gFAAgF;AAChF,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAC9E,8EAA8E;AAC9E,sEAAsE;AACtE,MAAM,iBAAiB,GAAqB,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;AAEjH,mGAAmG;AACnG,oEAAoE;AACpE,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command that could not do its job, with the one line that says why and what to run.
|
|
3
|
+
*
|
|
4
|
+
* Fail with it instead of printing and returning: the command boundary (`reportUnhandled`) prints
|
|
5
|
+
* the message once on stderr, and the process exits non-zero so a script can tell.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { Effect, Stdio } from "effect";
|
|
10
|
+
import { JiraAccess } from "../services/JiraAccess.js";
|
|
11
|
+
declare const CommandFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
12
|
+
readonly _tag: "CommandFailed";
|
|
13
|
+
} & Readonly<A>;
|
|
14
|
+
export declare class CommandFailed extends CommandFailed_base<{
|
|
15
|
+
readonly message: string;
|
|
16
|
+
}> {
|
|
17
|
+
}
|
|
18
|
+
/** Fails unless Jira is connected, naming the command that connects it. */
|
|
19
|
+
export declare const requireJira: Effect.Effect<import("../services/JiraAccess.js").JiraConnection, CommandFailed, JiraAccess>;
|
|
20
|
+
/** Fails before a prompt when nobody can answer it, saying what to run instead. */
|
|
21
|
+
export declare const requireTerminal: (instead: string) => Effect.Effect<void, CommandFailed, Stdio.Stdio>;
|
|
22
|
+
/** Any failure with a message, as the one line the command prints. */
|
|
23
|
+
export declare const toCommandFailed: (error: {
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}) => CommandFailed;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=CommandFailed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandFailed.d.ts","sourceRoot":"","sources":["../../../src/cli/CommandFailed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAQ,MAAM,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;;;;AAGtD,qBAAa,aAAc,SAAQ,mBAAkC;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;CAAG;AAErG,2EAA2E;AAC3E,eAAO,MAAM,WAAW,8FAMvB,CAAA;AAED,mFAAmF;AACnF,eAAO,MAAM,eAAe,YAAa,MAAM,oDAO5C,CAAA;AAEH,sEAAsE;AACtE,eAAO,MAAM,eAAe,UAAW;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAG,aACvB,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command that could not do its job, with the one line that says why and what to run.
|
|
3
|
+
*
|
|
4
|
+
* Fail with it instead of printing and returning: the command boundary (`reportUnhandled`) prints
|
|
5
|
+
* the message once on stderr, and the process exits non-zero so a script can tell.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { Data, Effect, Option, Stdio } from "effect";
|
|
10
|
+
import { JiraAccess } from "../services/JiraAccess.js";
|
|
11
|
+
import { NOT_LOGGED_IN_HINT } from "../utils/hints.js";
|
|
12
|
+
export class CommandFailed extends Data.TaggedError("CommandFailed") {
|
|
13
|
+
}
|
|
14
|
+
/** Fails unless Jira is connected, naming the command that connects it. */
|
|
15
|
+
export const requireJira = JiraAccess.use((access) => access.connection).pipe(Effect.mapError((error) => new CommandFailed({ message: error.message })), Effect.flatMap(Option.match({
|
|
16
|
+
onNone: () => Effect.fail(new CommandFailed({ message: NOT_LOGGED_IN_HINT })),
|
|
17
|
+
onSome: Effect.succeed
|
|
18
|
+
})));
|
|
19
|
+
/** Fails before a prompt when nobody can answer it, saying what to run instead. */
|
|
20
|
+
export const requireTerminal = (instead) => Stdio.Stdio.use((stdio) => stdio.stdinIsTerminal).pipe(Effect.flatMap((terminal) => terminal
|
|
21
|
+
? Effect.void
|
|
22
|
+
: Effect.fail(new CommandFailed({ message: `This step needs an interactive terminal. ${instead}` }))));
|
|
23
|
+
/** Any failure with a message, as the one line the command prints. */
|
|
24
|
+
export const toCommandFailed = (error) => new CommandFailed({ message: error.message });
|
|
25
|
+
//# sourceMappingURL=CommandFailed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandFailed.js","sourceRoot":"","sources":["../../../src/cli/CommandFailed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD,MAAM,OAAO,aAAc,SAAQ,IAAI,CAAC,WAAW,CAAC,eAAe,CAA+B;CAAG;AAErG,2EAA2E;AAC3E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAC3E,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,EACzE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;IAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC7E,MAAM,EAAE,MAAM,CAAC,OAAO;CACvB,CAAC,CAAC,CACJ,CAAA;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAE,EAAE,CACjD,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,IAAI,CACpD,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAC1B,QAAQ;IACN,CAAC,CAAC,MAAM,CAAC,IAAI;IACb,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,4CAA4C,OAAO,EAAE,EAAE,CAAC,CAAC,CACvG,CACF,CAAA;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAmC,EAAiB,EAAE,CACpF,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA"}
|
package/dist/src/cli/auth.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { JiraAuth } from "@knpkv/jira-cli/JiraAuth";
|
|
2
|
-
import { Effect } from "effect";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
3
|
import { Command, Prompt } from "effect/cli";
|
|
4
4
|
import * as HttpClient from "effect/http/HttpClient";
|
|
5
5
|
import { ClockifyAuth } from "../services/ClockifyAuth.js";
|
|
6
|
+
import { JiraAccess } from "../services/JiraAccess.js";
|
|
7
|
+
import { CommandFailed } from "./CommandFailed.js";
|
|
6
8
|
declare const InvalidClockifyApiKeyError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
7
9
|
readonly _tag: "InvalidClockifyApiKeyError";
|
|
8
10
|
} & Readonly<A>;
|
|
@@ -38,8 +40,26 @@ export declare const loadClockifyAccount: (client: import("@knpkv/clockify-api-c
|
|
|
38
40
|
readonly name: string;
|
|
39
41
|
}[];
|
|
40
42
|
}, ClockifyRequestError | InvalidClockifyApiKeyError, never>;
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Asks for a site, email and API token, checks them against Jira and saves them. The token is read
|
|
45
|
+
* without echo and never printed; a failed check names its cause and saves nothing.
|
|
46
|
+
*/
|
|
47
|
+
export declare const connectJiraWithToken: (given: {
|
|
48
|
+
readonly site: Option.Option<string>;
|
|
49
|
+
readonly email: Option.Option<string>;
|
|
50
|
+
}) => Effect.Effect<import("../services/JiraAccess.js").StoredToken, CommandFailed, JiraAccess | import("effect/Stdio").Stdio | Prompt.Environment>;
|
|
51
|
+
/**
|
|
52
|
+
* Asks for a Clockify API key (unless given), checks it, picks a workspace and saves it. A key that
|
|
53
|
+
* Clockify refuses, a failed request and an account with no workspace each fail with their own line.
|
|
54
|
+
*/
|
|
55
|
+
export declare const connectClockify: (given: Option.Option<string>, workspaceName?: Option.Option<string>) => Effect.Effect<undefined, CommandFailed, ClockifyAuth | HttpClient.HttpClient | import("effect/Stdio").Stdio | Prompt.Environment>;
|
|
56
|
+
export declare const clockifySetup: Command.Command<"setup", {
|
|
57
|
+
readonly apiKey: Option.Option<string>;
|
|
58
|
+
readonly workspace: Option.Option<string>;
|
|
59
|
+
}, {}, CommandFailed, ClockifyAuth | HttpClient.HttpClient | import("effect/Stdio").Stdio | Prompt.Environment>;
|
|
60
|
+
/** Both systems' status lines, Jira first. */
|
|
61
|
+
export declare const printAuthStatus: Effect.Effect<void, never, ClockifyAuth | HttpClient.HttpClient | JiraAccess>;
|
|
42
62
|
/** Top-level `auth` command with jira/clockify/status subcommands. */
|
|
43
|
-
export declare const auth: Command.Command<"auth", {} | {}, {},
|
|
63
|
+
export declare const auth: Command.Command<"auth", {} | {}, {}, CommandFailed, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | ClockifyAuth | HttpClient.HttpClient | JiraAccess | JiraAuth | import("effect/Stdio").Stdio | Prompt.Environment>;
|
|
44
64
|
export {};
|
|
45
65
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/cli/auth.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAiB,MAAM,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/cli/auth.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAiB,MAAM,EAAE,MAAM,EAA+B,MAAM,QAAQ,CAAA;AACnF,OAAO,EAAE,OAAO,EAAmB,MAAM,EAAE,MAAM,YAAY,CAAA;AAC7D,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAA;AAGpD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAEtD,OAAO,EAAE,aAAa,EAAoC,MAAM,oBAAoB,CAAA;;;;AAEpF,qBAAa,0BAA2B,SAAQ,gCAA+C,EAAE,CAAC;IAChG,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,0BAAyC;IACjF,QAAQ,CAAC,SAAS,EAAE,eAAe,GAAG,qBAAqB,CAAA;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAG7B;CACF;AAsBD;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;4DAoB/B,CAAA;AAQD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,UAAW;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACtC,kJAoBE,CAAA;AA2IH;;;GAGG;AACH,eAAO,MAAM,eAAe,UACnB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,kBACb,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,sIAmDnC,CAAA;AAEH,eAAO,MAAM,aAAa;;;+GAayC,CAAA;AAiCnE,8CAA8C;AAC9C,eAAO,MAAM,eAAe,+EAG1B,CAAA;AAMF,sEAAsE;AACtE,eAAO,MAAM,IAAI,0OAGhB,CAAA"}
|
package/dist/src/cli/auth.js
CHANGED
|
@@ -10,6 +10,9 @@ import { Command, Flag as Options, Prompt } from "effect/cli";
|
|
|
10
10
|
import * as HttpClient from "effect/http/HttpClient";
|
|
11
11
|
import * as ChildProcess from "effect/process/ChildProcess";
|
|
12
12
|
import { ClockifyAuth } from "../services/ClockifyAuth.js";
|
|
13
|
+
import { JiraAccess } from "../services/JiraAccess.js";
|
|
14
|
+
import { CONNECT_JIRA_COMMAND } from "../utils/hints.js";
|
|
15
|
+
import { CommandFailed, requireTerminal, toCommandFailed } from "./CommandFailed.js";
|
|
13
16
|
export class InvalidClockifyApiKeyError extends Data.TaggedError("InvalidClockifyApiKeyError") {
|
|
14
17
|
get message() {
|
|
15
18
|
return "Invalid API key — check the value and try again";
|
|
@@ -53,11 +56,38 @@ export const loadClockifyAccount = Effect.fn("ClockifyAuth.loadClockifyAccount")
|
|
|
53
56
|
return { user, workspaces };
|
|
54
57
|
});
|
|
55
58
|
// ---------------------------------------------------------------------------
|
|
56
|
-
// Jira
|
|
59
|
+
// Jira
|
|
57
60
|
// ---------------------------------------------------------------------------
|
|
61
|
+
const apiTokenPage = "https://id.atlassian.com/manage-profile/security/api-tokens";
|
|
62
|
+
/**
|
|
63
|
+
* Asks for a site, email and API token, checks them against Jira and saves them. The token is read
|
|
64
|
+
* without echo and never printed; a failed check names its cause and saves nothing.
|
|
65
|
+
*/
|
|
66
|
+
export const connectJiraWithToken = (given) => Effect.gen(function* () {
|
|
67
|
+
const access = yield* JiraAccess;
|
|
68
|
+
yield* requireTerminal(`Run ${CONNECT_JIRA_COMMAND} in a terminal to enter the API token.`);
|
|
69
|
+
yield* Console.log(`Create an API token at ${apiTokenPage}, then paste it here.`);
|
|
70
|
+
const site = Option.isSome(given.site)
|
|
71
|
+
? given.site.value
|
|
72
|
+
: yield* Prompt.String({ message: "Jira address (your-team.atlassian.net):" });
|
|
73
|
+
const email = Option.isSome(given.email)
|
|
74
|
+
? given.email.value
|
|
75
|
+
: yield* Prompt.String({ message: "Email you sign in to Atlassian with:" });
|
|
76
|
+
const apiToken = yield* Prompt.Password({ message: "API token:" });
|
|
77
|
+
yield* Console.log("Checking with Jira...");
|
|
78
|
+
const verified = yield* access.verifyToken({ site, email, apiToken });
|
|
79
|
+
yield* access.saveToken(verified);
|
|
80
|
+
yield* Console.log(`Jira connected: ${verified.displayName} on ${verified.siteUrl}.`);
|
|
81
|
+
return verified;
|
|
82
|
+
}).pipe(Effect.catchTag("QuitError", () => Effect.fail(new CommandFailed({ message: "Jira was not connected." }))), Effect.mapError(toCommandFailed));
|
|
83
|
+
const jiraToken = Command.make("token", {
|
|
84
|
+
site: Options.String("site").pipe(Options.withDescription("Your Jira address, such as your-team.atlassian.net"), Options.optional),
|
|
85
|
+
email: Options.String("email").pipe(Options.withDescription("The email you sign in to Atlassian with"), Options.optional)
|
|
86
|
+
}, (given) => Effect.asVoid(connectJiraWithToken(given))).pipe(Command.withDescription("Connect Jira with an API token (recommended)"));
|
|
58
87
|
const jiraCreate = Command.make("create", {}, () => Effect.gen(function* () {
|
|
59
88
|
yield* Console.log(`
|
|
60
|
-
|
|
89
|
+
Advanced: connect Jira through your own Atlassian OAuth app.
|
|
90
|
+
For most people ${CONNECT_JIRA_COMMAND} is simpler.
|
|
61
91
|
|
|
62
92
|
1. Browser will open to create a new OAuth 2.0 (3LO) app
|
|
63
93
|
2. Enter app name (e.g., "jcf")
|
|
@@ -74,12 +104,15 @@ Create OAuth app in Atlassian Developer Console:
|
|
|
74
104
|
const url = "https://developer.atlassian.com/console/myapps/create-3lo-app/";
|
|
75
105
|
const exitCode = (command) => Effect.scoped(command.pipe(Effect.flatMap((handle) => handle.exitCode)));
|
|
76
106
|
yield* exitCode(ChildProcess.make("open", [url])).pipe(Effect.catch(() => exitCode(ChildProcess.make("xdg-open", [url]))), Effect.catch(() => exitCode(ChildProcess.make("rundll32.exe", ["url.dll,FileProtocolHandler", url]))), Effect.asVoid, Effect.catch(() => Effect.void));
|
|
77
|
-
}));
|
|
107
|
+
})).pipe(Command.withDescription("Advanced: open the Atlassian console to create your own OAuth app"));
|
|
78
108
|
const jiraConfigure = Command.make("configure", {
|
|
79
109
|
clientId: Options.String("client-id").pipe(Options.withDescription("OAuth client ID"), Options.optional),
|
|
80
110
|
clientSecret: Options.String("client-secret").pipe(Options.withDescription("OAuth client secret"), Options.optional)
|
|
81
111
|
}, ({ clientId, clientSecret }) => Effect.gen(function* () {
|
|
82
112
|
const auth = yield* JiraAuth;
|
|
113
|
+
if (Option.isNone(clientId) || Option.isNone(clientSecret)) {
|
|
114
|
+
yield* requireTerminal("Pass --client-id and --client-secret.");
|
|
115
|
+
}
|
|
83
116
|
const id = Option.isSome(clientId)
|
|
84
117
|
? clientId.value
|
|
85
118
|
: yield* Prompt.String({ message: "Enter OAuth client ID:" });
|
|
@@ -88,126 +121,114 @@ const jiraConfigure = Command.make("configure", {
|
|
|
88
121
|
: yield* Prompt.String({ message: "Enter OAuth client secret:" });
|
|
89
122
|
yield* auth.configure({ clientId: id, clientSecret: secret });
|
|
90
123
|
yield* Console.log("OAuth configured. Run: jcf auth jira login");
|
|
91
|
-
}));
|
|
124
|
+
}).pipe(Effect.mapError(toCommandFailed))).pipe(Command.withDescription("Advanced: store your OAuth app's client ID and secret"));
|
|
92
125
|
const jiraLogin = Command.make("login", { site: Options.String("site").pipe(Options.withDescription("Jira site URL"), Options.optional) }, ({ site }) => Effect.gen(function* () {
|
|
93
126
|
const auth = yield* JiraAuth;
|
|
127
|
+
if (!(yield* auth.isConfigured())) {
|
|
128
|
+
return yield* new CommandFailed({
|
|
129
|
+
message: `No OAuth app is configured. Run ${CONNECT_JIRA_COMMAND} to use an API token instead, ` +
|
|
130
|
+
"or jcf auth jira configure to set up your own OAuth app."
|
|
131
|
+
});
|
|
132
|
+
}
|
|
94
133
|
const result = yield* auth.login(Option.isSome(site) ? { siteUrl: site.value } : undefined);
|
|
95
134
|
if (Array.isArray(result) && result.length > 0) {
|
|
96
135
|
yield* Console.log("\nRe-run with --site <url> to select a specific site.");
|
|
97
136
|
}
|
|
98
|
-
}).pipe(Effect.
|
|
137
|
+
}).pipe(Effect.mapError(toCommandFailed))).pipe(Command.withDescription("Advanced: sign in through your OAuth app in the browser"));
|
|
99
138
|
const jiraLogout = Command.make("logout", {}, () => Effect.gen(function* () {
|
|
139
|
+
const access = yield* JiraAccess;
|
|
100
140
|
const auth = yield* JiraAuth;
|
|
101
|
-
yield*
|
|
102
|
-
yield*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const authJira = Command.make("jira", {}, () => Console.log("Jira auth: create, configure, login, logout, status"))
|
|
116
|
-
.pipe(Command.withSubcommands([jiraCreate, jiraConfigure, jiraLogin, jiraLogout, jiraStatus]));
|
|
141
|
+
const removedToken = yield* access.removeToken;
|
|
142
|
+
const oauth = yield* auth.isLoggedIn();
|
|
143
|
+
if (oauth)
|
|
144
|
+
yield* auth.logout();
|
|
145
|
+
yield* Console.log(removedToken || oauth ? "Jira disconnected." : "Jira was not connected.");
|
|
146
|
+
}).pipe(Effect.mapError(toCommandFailed))).pipe(Command.withDescription("Disconnect Jira: remove the API token and sign out of OAuth"));
|
|
147
|
+
/** One line saying how Jira is connected, or what to run when it is not. */
|
|
148
|
+
const jiraStatusLine = JiraAccess.use((access) => access.connection).pipe(Effect.map(Option.match({
|
|
149
|
+
onNone: () => `Jira: not connected. Run ${CONNECT_JIRA_COMMAND}`,
|
|
150
|
+
onSome: (connection) => `Jira: connected as ${connection.displayName === "" ? "unknown user" : connection.displayName} on ` +
|
|
151
|
+
`${connection.siteUrl} (${connection.method === "api-token" ? "API token" : "OAuth app"})`
|
|
152
|
+
})), Effect.catch((error) => Effect.succeed(`Jira: ${error.message}`)));
|
|
153
|
+
const jiraStatus = Command.make("status", {}, () => Effect.flatMap(jiraStatusLine, Console.log)).pipe(Command.withDescription("Show whether Jira is connected, and how"));
|
|
154
|
+
const authJira = Command.make("jira", {}, () => Console.log(`Connect Jira: ${CONNECT_JIRA_COMMAND}. Run jcf auth jira --help for every option.`)).pipe(Command.withDescription("Connect Jira with an API token, or with your own OAuth app (advanced)"), Command.withSubcommands([jiraToken, jiraStatus, jiraLogout, jiraCreate, jiraConfigure, jiraLogin]));
|
|
117
155
|
// ---------------------------------------------------------------------------
|
|
118
156
|
// Clockify API key
|
|
119
157
|
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
158
|
+
const clockifyBaseUrl = "https://api.clockify.me/api";
|
|
159
|
+
/**
|
|
160
|
+
* Asks for a Clockify API key (unless given), checks it, picks a workspace and saves it. A key that
|
|
161
|
+
* Clockify refuses, a failed request and an account with no workspace each fail with their own line.
|
|
162
|
+
*/
|
|
163
|
+
export const connectClockify = (given, workspaceName = Option.none()) => Effect.gen(function* () {
|
|
121
164
|
const auth = yield* ClockifyAuth;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const apiKey = yield* Prompt.String({ message: "Enter API key:" });
|
|
126
|
-
if (!apiKey) {
|
|
127
|
-
yield* Console.log("No API key provided, aborting.");
|
|
128
|
-
return;
|
|
165
|
+
if (Option.isNone(given)) {
|
|
166
|
+
yield* requireTerminal("Pass the key with --api-key, or run jcf auth clockify setup in a terminal.");
|
|
167
|
+
yield* Console.log("Get your API key from https://app.clockify.me/manage-api-keys");
|
|
129
168
|
}
|
|
130
|
-
|
|
169
|
+
const apiKey = Option.isSome(given)
|
|
170
|
+
? given.value
|
|
171
|
+
: Redacted.value(yield* Prompt.Password({ message: "Clockify API key:" }));
|
|
172
|
+
if (apiKey.trim() === "") {
|
|
173
|
+
return yield* new CommandFailed({ message: "No API key given; Clockify was not connected." });
|
|
174
|
+
}
|
|
175
|
+
yield* Console.log("Checking with Clockify...");
|
|
131
176
|
const httpClient = yield* HttpClient.HttpClient;
|
|
132
|
-
const client = makeClockifyApi(httpClient, {
|
|
133
|
-
apiKey: Redacted.make(apiKey),
|
|
134
|
-
baseUrl: "https://api.clockify.me/api"
|
|
135
|
-
});
|
|
177
|
+
const client = makeClockifyApi(httpClient, { apiKey: Redacted.make(apiKey), baseUrl: clockifyBaseUrl });
|
|
136
178
|
const { user, workspaces } = yield* loadClockifyAccount(client);
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
yield*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
yield* Console.log("");
|
|
143
|
-
yield* Console.log("Workspaces:");
|
|
144
|
-
for (let i = 0; i < workspaces.length; i++) {
|
|
145
|
-
const workspace = workspaces[i];
|
|
146
|
-
if (workspace !== undefined) {
|
|
147
|
-
yield* Console.log(` ${i + 1}. ${workspace.name}`);
|
|
148
|
-
}
|
|
179
|
+
const [first] = workspaces;
|
|
180
|
+
if (first === undefined) {
|
|
181
|
+
return yield* new CommandFailed({
|
|
182
|
+
message: "This Clockify account has no workspace. Create one at https://app.clockify.me, then run this again."
|
|
183
|
+
});
|
|
149
184
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
185
|
+
const names = workspaces.map((candidate) => candidate.name).join(", ");
|
|
186
|
+
const workspace = Option.isSome(workspaceName)
|
|
187
|
+
? workspaces.find((candidate) => candidate.name === workspaceName.value || candidate.id === workspaceName.value)
|
|
188
|
+
: workspaces.length === 1
|
|
189
|
+
? first
|
|
190
|
+
: yield* requireTerminal(`This key can use ${workspaces.length} workspaces; pass --workspace with one of: ${names}.`)
|
|
191
|
+
.pipe(Effect.andThen(Prompt.Select({
|
|
192
|
+
message: "Workspace:",
|
|
193
|
+
choices: workspaces.map((candidate) => ({ title: candidate.name, value: candidate }))
|
|
194
|
+
})));
|
|
195
|
+
if (workspace === undefined) {
|
|
196
|
+
return yield* new CommandFailed({
|
|
197
|
+
message: `No workspace named "${Option.getOrElse(workspaceName, () => "")}" for this key. Its workspaces: ${names}.`
|
|
198
|
+
});
|
|
154
199
|
}
|
|
155
|
-
|
|
156
|
-
yield* Console.log(`
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
yield* Console.log("Clockify configured! Saved to ~/.jcf/clockify.json");
|
|
165
|
-
}));
|
|
166
|
-
const clockifyStatus = Command.make("status", {}, () => Effect.gen(function* () {
|
|
200
|
+
yield* auth.save({ apiKey, workspaceId: workspace.id, userId: user.id, baseUrl: clockifyBaseUrl });
|
|
201
|
+
yield* Console.log(`Clockify connected: ${user.name ?? user.email ?? user.id}, workspace ${workspace.name}.`);
|
|
202
|
+
}).pipe(Effect.catchTag("QuitError", () => Effect.fail(new CommandFailed({ message: "Clockify was not connected." }))), Effect.mapError(toCommandFailed));
|
|
203
|
+
export const clockifySetup = Command.make("setup", {
|
|
204
|
+
apiKey: Options.String("api-key").pipe(Options.withDescription("Clockify API key, for scripts; without it the key is asked for and not echoed"), Options.optional),
|
|
205
|
+
workspace: Options.String("workspace").pipe(Options.withDescription("Workspace name or id, when the key can use more than one"), Options.optional)
|
|
206
|
+
}, ({ apiKey, workspace }) => connectClockify(apiKey, workspace)).pipe(Command.withDescription("Connect Clockify with an API key"));
|
|
207
|
+
/** One line saying which Clockify workspace is connected, or what to run when none is. */
|
|
208
|
+
const clockifyStatusLine = Effect.gen(function* () {
|
|
167
209
|
const auth = yield* ClockifyAuth;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
210
|
+
if (!(yield* auth.isConfigured))
|
|
211
|
+
return "Clockify: not connected. Run jcf auth clockify setup";
|
|
212
|
+
const config = yield* auth.getConfig;
|
|
213
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
214
|
+
const account = yield* Effect.result(loadClockifyAccount(makeClockifyApi(httpClient, config)));
|
|
215
|
+
if (account._tag === "Failure") {
|
|
216
|
+
return `Clockify: connected, workspace ${config.workspaceId} (could not read its name: ${account.failure.message})`;
|
|
173
217
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}).pipe(
|
|
179
|
-
const authClockify = Command.make("clockify", {}, () => Console.log("Clockify auth: setup, status")).pipe(Command.withSubcommands([clockifySetup, clockifyStatus]));
|
|
218
|
+
const name = account.success.workspaces.find((workspace) => workspace.id === config.workspaceId)?.name;
|
|
219
|
+
return `Clockify: connected, workspace ${name ?? `${config.workspaceId}, which this key can no longer see`}`;
|
|
220
|
+
}).pipe(Effect.catch((error) => Effect.succeed(`Clockify: ${error.message}. Run jcf auth clockify setup`)));
|
|
221
|
+
const clockifyStatus = Command.make("status", {}, () => Effect.flatMap(clockifyStatusLine, Console.log)).pipe(Command.withDescription("Show whether Clockify is connected, and to which workspace"));
|
|
222
|
+
const authClockify = Command.make("clockify", {}, () => Console.log("Connect Clockify: jcf auth clockify setup. Run jcf auth clockify --help for every option.")).pipe(Command.withDescription("Connect Clockify with an API key"), Command.withSubcommands([clockifySetup, clockifyStatus]));
|
|
180
223
|
// ---------------------------------------------------------------------------
|
|
181
224
|
// Combined status
|
|
182
225
|
// ---------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
yield* Console.log(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (jiraUser) {
|
|
190
|
-
yield* Console.log(`Jira: ✓ ${jiraUser.name} (${jiraUser.email})`);
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
yield* Console.log("Jira: ✗ not logged in");
|
|
194
|
-
}
|
|
195
|
-
// Clockify
|
|
196
|
-
const clockifyAuth = yield* ClockifyAuth;
|
|
197
|
-
const clockifyOk = yield* clockifyAuth.isConfigured;
|
|
198
|
-
if (clockifyOk) {
|
|
199
|
-
const cfg = yield* clockifyAuth.getConfig.pipe(Effect.catch(() => Effect.succeed(null)));
|
|
200
|
-
if (cfg) {
|
|
201
|
-
yield* Console.log(`Clockify: ✓ workspace ${cfg.workspaceId}`);
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
yield* Console.log("Clockify: ✗ config corrupt");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
yield* Console.log("Clockify: ✗ not configured");
|
|
209
|
-
}
|
|
210
|
-
}));
|
|
226
|
+
/** Both systems' status lines, Jira first. */
|
|
227
|
+
export const printAuthStatus = Effect.gen(function* () {
|
|
228
|
+
yield* Console.log(yield* jiraStatusLine);
|
|
229
|
+
yield* Console.log(yield* clockifyStatusLine);
|
|
230
|
+
});
|
|
231
|
+
const authStatus = Command.make("status", {}, () => printAuthStatus).pipe(Command.withDescription("Show whether Jira and Clockify are connected"));
|
|
211
232
|
/** Top-level `auth` command with jira/clockify/status subcommands. */
|
|
212
|
-
export const auth = Command.make("auth", {}, () =>
|
|
233
|
+
export const auth = Command.make("auth", {}, () => printAuthStatus).pipe(Command.withDescription("Connect Jira and Clockify, and see which are connected"), Command.withSubcommands([authJira, authClockify, authStatus]));
|
|
213
234
|
//# sourceMappingURL=auth.js.map
|