@knpkv/jira-clockify 0.2.0
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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/package.json +47 -0
- package/dist/src/bin.js +28 -0
- package/dist/src/bin.js.map +1 -0
- package/dist/src/cli/auth.js +169 -0
- package/dist/src/cli/auth.js.map +1 -0
- package/dist/src/cli/config.js +107 -0
- package/dist/src/cli/config.js.map +1 -0
- package/dist/src/cli/fuzzySelect.js +149 -0
- package/dist/src/cli/fuzzySelect.js.map +1 -0
- package/dist/src/cli/layers.js +59 -0
- package/dist/src/cli/layers.js.map +1 -0
- package/dist/src/cli/list.js +27 -0
- package/dist/src/cli/list.js.map +1 -0
- package/dist/src/cli/setup.js +134 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/timer/discard.js +33 -0
- package/dist/src/cli/timer/discard.js.map +1 -0
- package/dist/src/cli/timer/edit.js +139 -0
- package/dist/src/cli/timer/edit.js.map +1 -0
- package/dist/src/cli/timer/index.js +12 -0
- package/dist/src/cli/timer/index.js.map +1 -0
- package/dist/src/cli/timer/log.js +96 -0
- package/dist/src/cli/timer/log.js.map +1 -0
- package/dist/src/cli/timer/start.js +156 -0
- package/dist/src/cli/timer/start.js.map +1 -0
- package/dist/src/cli/timer/status.js +80 -0
- package/dist/src/cli/timer/status.js.map +1 -0
- package/dist/src/cli/timer/stop.js +96 -0
- package/dist/src/cli/timer/stop.js.map +1 -0
- package/dist/src/cli/timer.js +7 -0
- package/dist/src/cli/timer.js.map +1 -0
- package/dist/src/main.js +24 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/services/ClockifyAuth.js +77 -0
- package/dist/src/services/ClockifyAuth.js.map +1 -0
- package/dist/src/services/ConfigService.js +66 -0
- package/dist/src/services/ConfigService.js.map +1 -0
- package/dist/src/services/StateWriter.js +69 -0
- package/dist/src/services/StateWriter.js.map +1 -0
- package/dist/src/services/TicketService.js +106 -0
- package/dist/src/services/TicketService.js.map +1 -0
- package/dist/src/services/TimerService.js +271 -0
- package/dist/src/services/TimerService.js.map +1 -0
- package/dist/src/tui/App.js +204 -0
- package/dist/src/tui/App.js.map +1 -0
- package/dist/src/tui/atoms/runtime.js +9 -0
- package/dist/src/tui/atoms/runtime.js.map +1 -0
- package/dist/src/tui/atoms/tickets.js +17 -0
- package/dist/src/tui/atoms/tickets.js.map +1 -0
- package/dist/src/tui/atoms/timer.js +31 -0
- package/dist/src/tui/atoms/timer.js.map +1 -0
- package/dist/src/tui/atoms/ui.js +10 -0
- package/dist/src/tui/atoms/ui.js.map +1 -0
- package/dist/src/tui/components/BigTimer.js +60 -0
- package/dist/src/tui/components/BigTimer.js.map +1 -0
- package/dist/src/tui/components/Footer.js +16 -0
- package/dist/src/tui/components/Footer.js.map +1 -0
- package/dist/src/tui/components/Header.js +16 -0
- package/dist/src/tui/components/Header.js.map +1 -0
- package/dist/src/tui/components/PopupInput.js +30 -0
- package/dist/src/tui/components/PopupInput.js.map +1 -0
- package/dist/src/tui/components/PopupMessage.js +47 -0
- package/dist/src/tui/components/PopupMessage.js.map +1 -0
- package/dist/src/tui/components/TicketList.js +40 -0
- package/dist/src/tui/components/TicketList.js.map +1 -0
- package/dist/src/tui/components/TicketRow.js +48 -0
- package/dist/src/tui/components/TicketRow.js.map +1 -0
- package/dist/src/tui/components/TimerDisplay.js +30 -0
- package/dist/src/tui/components/TimerDisplay.js.map +1 -0
- package/dist/src/tui/components/index.js +12 -0
- package/dist/src/tui/components/index.js.map +1 -0
- package/dist/src/tui/context/theme.js +15 -0
- package/dist/src/tui/context/theme.js.map +1 -0
- package/dist/src/tui/hooks/useElapsedTimer.js +35 -0
- package/dist/src/tui/hooks/useElapsedTimer.js.map +1 -0
- package/dist/src/tui/hooks/useTerminalSize.js +32 -0
- package/dist/src/tui/hooks/useTerminalSize.js.map +1 -0
- package/dist/src/utils/time.js +23 -0
- package/dist/src/utils/time.js.map +1 -0
- package/dist/test/TimerService.test.js +355 -0
- package/dist/test/TimerService.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/nvim/lua/jcf/branch.lua +26 -0
- package/nvim/lua/jcf/float.lua +87 -0
- package/nvim/lua/jcf/init.lua +70 -0
- package/nvim/lua/jcf/state.lua +56 -0
- package/nvim/lua/jcf/statusline.lua +31 -0
- package/nvim/lua/jcf/telescope.lua +50 -0
- package/nvim/plugin/jcf.vim +2 -0
- package/package.json +47 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User configuration persistence for jcf defaults (JQL, project, billable).
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **File-backed with defaults**: Reads `~/.jcf/config.json`, merging stored values over
|
|
7
|
+
* {@link defaultConfig}. Missing or corrupt files silently fall back to defaults.
|
|
8
|
+
* - **Partial updates**: {@link ConfigServiceShape.set} merges a patch over the current config.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
13
|
+
import * as Path from "@effect/platform/Path";
|
|
14
|
+
import * as Context from "effect/Context";
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Layer from "effect/Layer";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
const defaultConfig = {
|
|
19
|
+
defaultJql: "assignee = currentUser() AND status != Done ORDER BY updated DESC",
|
|
20
|
+
refreshInterval: 30,
|
|
21
|
+
projectMap: {},
|
|
22
|
+
workspaceId: null,
|
|
23
|
+
defaultProjectId: null,
|
|
24
|
+
defaultProjectName: null,
|
|
25
|
+
defaultBillable: true
|
|
26
|
+
};
|
|
27
|
+
export class ConfigService extends Context.Tag("jcf/ConfigService")() {
|
|
28
|
+
}
|
|
29
|
+
const CONFIG_DIR = ".jcf";
|
|
30
|
+
const CONFIG_FILE = "config.json";
|
|
31
|
+
export const layer = Layer.effect(ConfigService, Effect.gen(function* () {
|
|
32
|
+
const fs = yield* FileSystem.FileSystem;
|
|
33
|
+
const path = yield* Path.Path;
|
|
34
|
+
const home = yield* Effect.sync(() => homedir());
|
|
35
|
+
const dir = path.join(home, CONFIG_DIR);
|
|
36
|
+
const filePath = path.join(dir, CONFIG_FILE);
|
|
37
|
+
const ensureDir = Effect.gen(function* () {
|
|
38
|
+
const exists = yield* fs.exists(dir);
|
|
39
|
+
if (!exists)
|
|
40
|
+
yield* fs.makeDirectory(dir, { recursive: true });
|
|
41
|
+
});
|
|
42
|
+
const read = Effect.gen(function* () {
|
|
43
|
+
const exists = yield* fs.exists(filePath);
|
|
44
|
+
if (!exists)
|
|
45
|
+
return defaultConfig;
|
|
46
|
+
const content = yield* fs.readFileString(filePath);
|
|
47
|
+
const parsed = yield* Effect.try({
|
|
48
|
+
try: () => JSON.parse(content),
|
|
49
|
+
catch: () => ({})
|
|
50
|
+
});
|
|
51
|
+
return { ...defaultConfig, ...parsed };
|
|
52
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(defaultConfig)));
|
|
53
|
+
const write = (config) => Effect.gen(function* () {
|
|
54
|
+
yield* ensureDir;
|
|
55
|
+
yield* fs.writeFileString(filePath, JSON.stringify(config, null, 2));
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
get: read,
|
|
59
|
+
set: (patch) => Effect.gen(function* () {
|
|
60
|
+
const current = yield* read;
|
|
61
|
+
yield* write({ ...current, ...patch });
|
|
62
|
+
}).pipe(Effect.catchAll(() => Effect.void)),
|
|
63
|
+
configDir: Effect.succeed(dir)
|
|
64
|
+
};
|
|
65
|
+
}));
|
|
66
|
+
//# sourceMappingURL=ConfigService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfigService.js","sourceRoot":"","sources":["../../../src/services/ConfigService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic timer state persistence to `~/.jcf/state.json`.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **Atomic writes**: Write goes to a `.tmp` file then `rename` — prevents corruption if
|
|
7
|
+
* the process crashes mid-write.
|
|
8
|
+
* - **External consumption**: The state file is read by the Neovim plugin and statusline
|
|
9
|
+
* integrations to show timer state outside the TUI.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
14
|
+
import * as Path from "@effect/platform/Path";
|
|
15
|
+
import * as Context from "effect/Context";
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import * as Layer from "effect/Layer";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
const emptyState = {
|
|
20
|
+
active: false,
|
|
21
|
+
ticketKey: null,
|
|
22
|
+
summary: null,
|
|
23
|
+
project: null,
|
|
24
|
+
startedAt: null,
|
|
25
|
+
startedAt_unix: null,
|
|
26
|
+
elapsed: 0,
|
|
27
|
+
clockifyEntryId: null
|
|
28
|
+
};
|
|
29
|
+
export class StateWriter extends Context.Tag("jcf/StateWriter")() {
|
|
30
|
+
}
|
|
31
|
+
const STATE_DIR = ".jcf";
|
|
32
|
+
const STATE_FILE = "state.json";
|
|
33
|
+
export const layer = Layer.effect(StateWriter, Effect.gen(function* () {
|
|
34
|
+
const fs = yield* FileSystem.FileSystem;
|
|
35
|
+
const path = yield* Path.Path;
|
|
36
|
+
const home = yield* Effect.sync(() => homedir());
|
|
37
|
+
const dir = path.join(home, STATE_DIR);
|
|
38
|
+
const filePath = path.join(dir, STATE_FILE);
|
|
39
|
+
const ensureDir = Effect.gen(function* () {
|
|
40
|
+
const exists = yield* fs.exists(dir);
|
|
41
|
+
if (!exists)
|
|
42
|
+
yield* fs.makeDirectory(dir, { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
write: (state) => Effect.gen(function* () {
|
|
46
|
+
yield* ensureDir;
|
|
47
|
+
const tmpPath = `${filePath}.tmp`;
|
|
48
|
+
yield* fs.writeFileString(tmpPath, JSON.stringify(state, null, 2));
|
|
49
|
+
yield* fs.rename(tmpPath, filePath);
|
|
50
|
+
}).pipe(Effect.catchAll(() => Effect.void)),
|
|
51
|
+
read: Effect.gen(function* () {
|
|
52
|
+
const exists = yield* fs.exists(filePath);
|
|
53
|
+
if (!exists)
|
|
54
|
+
return emptyState;
|
|
55
|
+
const content = yield* fs.readFileString(filePath);
|
|
56
|
+
return yield* Effect.try({
|
|
57
|
+
try: () => JSON.parse(content),
|
|
58
|
+
catch: () => emptyState
|
|
59
|
+
});
|
|
60
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(emptyState))),
|
|
61
|
+
clear: Effect.gen(function* () {
|
|
62
|
+
yield* ensureDir;
|
|
63
|
+
const tmpPath = `${filePath}.tmp`;
|
|
64
|
+
yield* fs.writeFileString(tmpPath, JSON.stringify(emptyState, null, 2));
|
|
65
|
+
yield* fs.rename(tmpPath, filePath);
|
|
66
|
+
}).pipe(Effect.catchAll(() => Effect.void))
|
|
67
|
+
};
|
|
68
|
+
}));
|
|
69
|
+
//# sourceMappingURL=StateWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StateWriter.js","sourceRoot":"","sources":["../../../src/services/StateWriter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira ticket fetching with reactive state via SubscriptionRef.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **SubscriptionRef-backed state**: {@link TicketServiceShape.state} is a `SubscriptionRef<TicketState>`
|
|
7
|
+
* that the TUI subscribes to for live updates. {@link TicketServiceShape.refresh} fetches
|
|
8
|
+
* tickets from Jira and updates the ref.
|
|
9
|
+
* - **Field extraction helpers**: `extractNested` and `extractString` safely navigate the
|
|
10
|
+
* loosely-typed Jira API response without runtime crashes.
|
|
11
|
+
* - **In-memory search**: {@link TicketServiceShape.search} filters the cached ticket list
|
|
12
|
+
* by key or summary substring.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { JiraApiClient, toEffect } from "@knpkv/jira-api-client";
|
|
17
|
+
import * as Context from "effect/Context";
|
|
18
|
+
import * as Data from "effect/Data";
|
|
19
|
+
import * as Effect from "effect/Effect";
|
|
20
|
+
import * as Layer from "effect/Layer";
|
|
21
|
+
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
22
|
+
import { ConfigService } from "./ConfigService.js";
|
|
23
|
+
export class TicketError extends Data.TaggedError("TicketError") {
|
|
24
|
+
}
|
|
25
|
+
const emptyState = {
|
|
26
|
+
tickets: [],
|
|
27
|
+
loading: false,
|
|
28
|
+
error: null,
|
|
29
|
+
lastRefreshed: null
|
|
30
|
+
};
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const extractField = (fields, key) => fields?.[key] ?? null;
|
|
35
|
+
const extractString = (fields, key) => {
|
|
36
|
+
const val = extractField(fields, key);
|
|
37
|
+
return typeof val === "string" ? val : null;
|
|
38
|
+
};
|
|
39
|
+
const extractNested = (fields, key, nested) => {
|
|
40
|
+
const val = extractField(fields, key);
|
|
41
|
+
if (val && typeof val === "object" && nested in val) {
|
|
42
|
+
const v = val[nested];
|
|
43
|
+
return typeof v === "string" ? v : null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
export class TicketService extends Context.Tag("jcf/TicketService")() {
|
|
48
|
+
}
|
|
49
|
+
export const layer = Layer.effect(TicketService, Effect.gen(function* () {
|
|
50
|
+
const config = yield* ConfigService;
|
|
51
|
+
const jira = yield* JiraApiClient;
|
|
52
|
+
const ref = yield* SubscriptionRef.make(emptyState);
|
|
53
|
+
const refresh = Effect.gen(function* () {
|
|
54
|
+
yield* SubscriptionRef.set(ref, { ...emptyState, loading: true });
|
|
55
|
+
const cfg = yield* config.get;
|
|
56
|
+
const jql = cfg.defaultJql;
|
|
57
|
+
const result = yield* toEffect(jira.v3.client.GET("/rest/api/3/search/jql", {
|
|
58
|
+
params: {
|
|
59
|
+
query: {
|
|
60
|
+
jql,
|
|
61
|
+
maxResults: 50,
|
|
62
|
+
fields: ["summary", "status", "priority", "assignee", "issuetype", "labels", "updated"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})).pipe(Effect.mapError((e) => new TicketError({ message: `Jira search failed: ${String(e)}`, cause: e })));
|
|
66
|
+
const tickets = (result.issues ?? []).map((issue) => {
|
|
67
|
+
const fields = issue.fields;
|
|
68
|
+
return {
|
|
69
|
+
key: String(issue.key ?? "?"),
|
|
70
|
+
summary: extractString(fields, "summary") ?? "(no summary)",
|
|
71
|
+
status: extractNested(fields, "status", "name") ?? "Unknown",
|
|
72
|
+
priority: extractNested(fields, "priority", "name"),
|
|
73
|
+
assignee: extractNested(fields, "assignee", "displayName"),
|
|
74
|
+
type: extractNested(fields, "issuetype", "name") ?? "Task",
|
|
75
|
+
labels: Array.isArray(fields?.["labels"]) ? fields["labels"] : [],
|
|
76
|
+
updated: extractString(fields, "updated") ?? new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
yield* SubscriptionRef.set(ref, {
|
|
80
|
+
tickets,
|
|
81
|
+
loading: false,
|
|
82
|
+
error: null,
|
|
83
|
+
lastRefreshed: new Date()
|
|
84
|
+
});
|
|
85
|
+
}).pipe(Effect.catchAll((e) => {
|
|
86
|
+
const msg = e._tag === "TicketError"
|
|
87
|
+
? e.message
|
|
88
|
+
: `Failed to fetch tickets: ${String(e)}`;
|
|
89
|
+
return Effect.logDebug(`TicketService refresh error: ${msg}`).pipe(Effect.flatMap(() => SubscriptionRef.set(ref, {
|
|
90
|
+
tickets: [],
|
|
91
|
+
loading: false,
|
|
92
|
+
error: msg,
|
|
93
|
+
lastRefreshed: null
|
|
94
|
+
})));
|
|
95
|
+
}));
|
|
96
|
+
const search = (text) => Effect.gen(function* () {
|
|
97
|
+
const state = yield* SubscriptionRef.get(ref);
|
|
98
|
+
if (!text.trim())
|
|
99
|
+
return state.tickets;
|
|
100
|
+
const lower = text.toLowerCase();
|
|
101
|
+
return state.tickets.filter((t) => t.key.toLowerCase().includes(lower) ||
|
|
102
|
+
t.summary.toLowerCase().includes(lower));
|
|
103
|
+
});
|
|
104
|
+
return { state: ref, refresh, search };
|
|
105
|
+
}));
|
|
106
|
+
//# sourceMappingURL=TicketService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TicketService.js","sourceRoot":"","sources":["../../../src/services/TicketService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core timer lifecycle — start, stop, detect running — bridging Clockify and Jira worklog.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **Dual write**: Starting a timer creates a Clockify time entry AND updates the local
|
|
7
|
+
* state file (for Neovim/statusline). Stopping updates the Clockify entry AND posts
|
|
8
|
+
* a Jira worklog via raw HTTP (generated client swallows 4xx as void).
|
|
9
|
+
* - **Auto-resolution**: Project ID, billable flag, and tags are resolved from config defaults,
|
|
10
|
+
* Clockify project name matching, and Jira issue type/labels.
|
|
11
|
+
* - **External detection**: {@link TimerServiceShape.detectRunning} polls Clockify for a
|
|
12
|
+
* running timer not started by jcf and syncs local state.
|
|
13
|
+
*
|
|
14
|
+
* **Gotchas**
|
|
15
|
+
*
|
|
16
|
+
* - Jira worklog uses raw HTTP because the generated client returns void for 4xx — check
|
|
17
|
+
* `response.status` manually.
|
|
18
|
+
* - `timeSpentSeconds` is floored to 60s minimum (Jira rejects <60s worklogs).
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import * as HttpClient from "@effect/platform/HttpClient";
|
|
23
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
|
|
24
|
+
import { ClockifyApiClient } from "@knpkv/clockify-api-client";
|
|
25
|
+
import { JiraApiClient } from "@knpkv/jira-api-client";
|
|
26
|
+
import { JiraAuth } from "@knpkv/jira-cli/JiraAuth";
|
|
27
|
+
import * as Context from "effect/Context";
|
|
28
|
+
import * as Data from "effect/Data";
|
|
29
|
+
import * as Duration from "effect/Duration";
|
|
30
|
+
import * as Effect from "effect/Effect";
|
|
31
|
+
import * as Layer from "effect/Layer";
|
|
32
|
+
import * as Redacted from "effect/Redacted";
|
|
33
|
+
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
34
|
+
import { ClockifyAuth } from "./ClockifyAuth.js";
|
|
35
|
+
import { ConfigService } from "./ConfigService.js";
|
|
36
|
+
import { StateWriter } from "./StateWriter.js";
|
|
37
|
+
export class TimerError extends Data.TaggedError("TimerError") {
|
|
38
|
+
}
|
|
39
|
+
const emptyState = {
|
|
40
|
+
active: false,
|
|
41
|
+
ticketKey: null,
|
|
42
|
+
summary: null,
|
|
43
|
+
project: null,
|
|
44
|
+
startedAt: null,
|
|
45
|
+
clockifyEntryId: null,
|
|
46
|
+
projectId: null,
|
|
47
|
+
projectName: null,
|
|
48
|
+
billable: null,
|
|
49
|
+
startedViaJcf: false
|
|
50
|
+
};
|
|
51
|
+
export class TimerService extends Context.Tag("jcf/TimerService")() {
|
|
52
|
+
}
|
|
53
|
+
export const layer = Layer.effect(TimerService, Effect.gen(function* () {
|
|
54
|
+
const clockify = yield* ClockifyApiClient;
|
|
55
|
+
yield* JiraApiClient; // ensure dep is in layer
|
|
56
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
57
|
+
const jiraAuth = yield* JiraAuth;
|
|
58
|
+
const clockifyAuth = yield* ClockifyAuth;
|
|
59
|
+
const config = yield* ConfigService;
|
|
60
|
+
const stateWriter = yield* StateWriter;
|
|
61
|
+
const ref = yield* SubscriptionRef.make(emptyState);
|
|
62
|
+
const tagCache = new Map();
|
|
63
|
+
const writeStateFile = (state) => {
|
|
64
|
+
const file = {
|
|
65
|
+
active: state.active,
|
|
66
|
+
ticketKey: state.ticketKey,
|
|
67
|
+
summary: state.summary,
|
|
68
|
+
project: state.project,
|
|
69
|
+
startedAt: state.startedAt?.toISOString() ?? null,
|
|
70
|
+
startedAt_unix: state.startedAt ? Math.floor(state.startedAt.getTime() / 1000) : null,
|
|
71
|
+
elapsed: state.startedAt ? Math.floor((Date.now() - state.startedAt.getTime()) / 1000) : 0,
|
|
72
|
+
clockifyEntryId: state.clockifyEntryId
|
|
73
|
+
};
|
|
74
|
+
return stateWriter.write(file);
|
|
75
|
+
};
|
|
76
|
+
const getAuth = clockifyAuth.getConfig.pipe(Effect.mapError((e) => new TimerError({ message: e.message })));
|
|
77
|
+
const start = (ticket, options) => Effect.gen(function* () {
|
|
78
|
+
const auth = yield* getAuth;
|
|
79
|
+
const cfg = yield* config.get;
|
|
80
|
+
// Auto-stop existing timer
|
|
81
|
+
const current = yield* SubscriptionRef.get(ref);
|
|
82
|
+
if (current.active) {
|
|
83
|
+
yield* internalStop().pipe(Effect.catchAll((e) => Effect.logWarning(`Auto-stop failed, Clockify entry may be orphaned: ${e.message}`)));
|
|
84
|
+
}
|
|
85
|
+
// Resolve projectId: explicit > config default > auto-match by name > null
|
|
86
|
+
let projectId = options?.projectId ?? cfg.defaultProjectId ?? null;
|
|
87
|
+
if (!projectId) {
|
|
88
|
+
const jiraProject = ticket.key.split("-")[0] ?? "";
|
|
89
|
+
const clockifyProjectName = cfg.projectMap[jiraProject] ?? jiraProject;
|
|
90
|
+
const project = yield* clockify.getProjectByName(auth.workspaceId, clockifyProjectName).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
91
|
+
if (project)
|
|
92
|
+
projectId = project.id;
|
|
93
|
+
}
|
|
94
|
+
// Resolve project name
|
|
95
|
+
let projectName = cfg.defaultProjectName ?? null;
|
|
96
|
+
if (projectId && !projectName) {
|
|
97
|
+
const projects = yield* clockify.getProjects(auth.workspaceId).pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
98
|
+
projectName = projects.find((p) => p.id === projectId)?.name ?? null;
|
|
99
|
+
}
|
|
100
|
+
// Resolve billable: explicit > config default > null (will prompt on stop)
|
|
101
|
+
const billable = options?.billable ?? cfg.defaultBillable ?? null;
|
|
102
|
+
// Resolve tags: ticket type + Jira labels → Clockify tags
|
|
103
|
+
const tagNames = [ticket.type, ...ticket.labels].filter(Boolean);
|
|
104
|
+
const tagIds = [];
|
|
105
|
+
for (const tagName of tagNames) {
|
|
106
|
+
const cached = tagCache.get(tagName);
|
|
107
|
+
if (cached) {
|
|
108
|
+
tagIds.push(cached);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const tag = yield* clockify.findOrCreateTag(auth.workspaceId, tagName).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
112
|
+
if (tag) {
|
|
113
|
+
tagIds.push(tag.id);
|
|
114
|
+
tagCache.set(tagName, tag.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
yield* Effect.logDebug(`Clockify tags: ${tagNames.join(", ")} → ${tagIds.length} tag IDs`);
|
|
118
|
+
// Start timer
|
|
119
|
+
const now = new Date();
|
|
120
|
+
const entry = yield* clockify.createTimeEntry(auth.workspaceId, {
|
|
121
|
+
description: `[${ticket.key}] ${ticket.summary}`,
|
|
122
|
+
start: now.toISOString(),
|
|
123
|
+
...(projectId ? { projectId } : {}),
|
|
124
|
+
...(billable !== null ? { billable } : {}),
|
|
125
|
+
...(tagIds.length > 0 ? { tagIds } : {})
|
|
126
|
+
}).pipe(Effect.mapError((e) => new TimerError({ message: `Failed to start timer: ${e.message}`, cause: e })));
|
|
127
|
+
const newState = {
|
|
128
|
+
active: true,
|
|
129
|
+
ticketKey: ticket.key,
|
|
130
|
+
summary: ticket.summary,
|
|
131
|
+
project: ticket.key.split("-")[0] ?? null,
|
|
132
|
+
startedAt: now,
|
|
133
|
+
clockifyEntryId: entry.id,
|
|
134
|
+
projectId,
|
|
135
|
+
projectName,
|
|
136
|
+
billable,
|
|
137
|
+
startedViaJcf: true
|
|
138
|
+
};
|
|
139
|
+
yield* writeStateFile(newState);
|
|
140
|
+
yield* SubscriptionRef.set(ref, newState);
|
|
141
|
+
});
|
|
142
|
+
const internalStop = (options) => Effect.gen(function* () {
|
|
143
|
+
const auth = yield* getAuth;
|
|
144
|
+
const current = yield* SubscriptionRef.get(ref);
|
|
145
|
+
if (!current.active || !current.startedAt) {
|
|
146
|
+
return yield* Effect.fail(new TimerError({ message: "No active timer" }));
|
|
147
|
+
}
|
|
148
|
+
const now = new Date();
|
|
149
|
+
const durationMs = now.getTime() - current.startedAt.getTime();
|
|
150
|
+
const duration = Duration.millis(durationMs);
|
|
151
|
+
// Merge: stop options override current state
|
|
152
|
+
const projectId = options?.projectId ?? current.projectId ?? null;
|
|
153
|
+
const billable = options?.billable ?? current.billable ?? null;
|
|
154
|
+
const comment = options?.comment;
|
|
155
|
+
// Stop via PUT — preserve existing tagIds from the entry
|
|
156
|
+
if (current.clockifyEntryId) {
|
|
157
|
+
const existing = yield* clockify.getTimeEntry(auth.workspaceId, current.clockifyEntryId).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
158
|
+
const tagIds = existing?.tagIds ?? [];
|
|
159
|
+
// Append comment to Clockify description if provided
|
|
160
|
+
const description = comment
|
|
161
|
+
? `${existing?.description ?? ""} | ${comment}`
|
|
162
|
+
: undefined;
|
|
163
|
+
yield* clockify.updateTimeEntry(auth.workspaceId, current.clockifyEntryId, {
|
|
164
|
+
start: current.startedAt.toISOString(),
|
|
165
|
+
end: now.toISOString(),
|
|
166
|
+
...(description !== undefined ? { description } : {}),
|
|
167
|
+
...(projectId ? { projectId } : {}),
|
|
168
|
+
...(billable !== null ? { billable } : {}),
|
|
169
|
+
...(tagIds.length > 0 ? { tagIds: [...tagIds] } : {})
|
|
170
|
+
}).pipe(Effect.mapError((e) => new TimerError({ message: `Failed to stop timer: ${e.message}`, cause: e })));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
yield* clockify.stopTimer(auth.workspaceId, auth.userId, {
|
|
174
|
+
end: now.toISOString()
|
|
175
|
+
}).pipe(Effect.mapError((e) => new TimerError({ message: `Failed to stop timer: ${e.message}`, cause: e })));
|
|
176
|
+
}
|
|
177
|
+
// Log Jira worklog (raw HTTP — generated client swallows 4xx as void)
|
|
178
|
+
let jiraLogged = false;
|
|
179
|
+
if (current.ticketKey) {
|
|
180
|
+
// Jira rejects worklogs <60s — floor to 60s. Clockify keeps actual elapsed.
|
|
181
|
+
const timeSpent = Math.max(60, Math.floor(durationMs / 1000));
|
|
182
|
+
const started = current.startedAt.toISOString().replace("Z", "+0000");
|
|
183
|
+
yield* Effect.logDebug(`Jira worklog: ${current.ticketKey} ${timeSpent}s`);
|
|
184
|
+
const accessToken = yield* jiraAuth.getAccessToken().pipe(Effect.tapError((e) => Effect.logDebug(`Jira getAccessToken failed: ${String(e)}`)), Effect.catchAll(() => Effect.succeed(Redacted.make(""))));
|
|
185
|
+
const cloudId = yield* jiraAuth.getCloudId().pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
186
|
+
const response = yield* httpClient.execute(HttpClientRequest.post(`https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${current.ticketKey}/worklog`).pipe(HttpClientRequest.setHeader("Authorization", `Bearer ${Redacted.value(accessToken)}`), HttpClientRequest.setHeader("Content-Type", "application/json"), HttpClientRequest.bodyUnsafeJson({
|
|
187
|
+
started,
|
|
188
|
+
timeSpentSeconds: timeSpent,
|
|
189
|
+
...(comment ?
|
|
190
|
+
{
|
|
191
|
+
comment: {
|
|
192
|
+
type: "doc",
|
|
193
|
+
version: 1,
|
|
194
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: comment }] }]
|
|
195
|
+
}
|
|
196
|
+
} :
|
|
197
|
+
{})
|
|
198
|
+
}))).pipe(Effect.catchAll((e) => Effect.logDebug(`Jira worklog failed: ${String(e)}`).pipe(Effect.map(() => null))));
|
|
199
|
+
if (response && response.status >= 200 && response.status < 300) {
|
|
200
|
+
jiraLogged = true;
|
|
201
|
+
yield* Effect.logDebug(`Jira worklog created (${response.status})`);
|
|
202
|
+
}
|
|
203
|
+
else if (response) {
|
|
204
|
+
const body = yield* response.text.pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
205
|
+
yield* Effect.logDebug(`Jira worklog failed (${response.status}): ${body.slice(0, 300)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
yield* SubscriptionRef.set(ref, emptyState);
|
|
209
|
+
yield* stateWriter.clear;
|
|
210
|
+
return {
|
|
211
|
+
duration,
|
|
212
|
+
clockifyLogged: true,
|
|
213
|
+
jiraWorklogLogged: jiraLogged,
|
|
214
|
+
needsProjectId: !projectId,
|
|
215
|
+
needsBillable: billable === null
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
const detectRunning = Effect.gen(function* () {
|
|
219
|
+
const auth = yield* getAuth;
|
|
220
|
+
const running = yield* clockify.getRunningTimer(auth.workspaceId, auth.userId).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
221
|
+
if (running && running.timeInterval.start) {
|
|
222
|
+
const startedAt = new Date(running.timeInterval.start);
|
|
223
|
+
// Parse "[KEY] summary" or "KEY: summary" format
|
|
224
|
+
const desc = running.description ?? "";
|
|
225
|
+
const bracketMatch = desc.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
226
|
+
const colonMatch = desc.match(/^([^:]+):\s*(.*)$/);
|
|
227
|
+
const ticketKey = bracketMatch?.[1]?.trim() ?? colonMatch?.[1]?.trim() ?? null;
|
|
228
|
+
const summary = bracketMatch?.[2]?.trim() ?? colonMatch?.[2]?.trim() ?? null;
|
|
229
|
+
if (!ticketKey) {
|
|
230
|
+
yield* Effect.logWarning(`Running Clockify timer has unparseable description: "${desc}"`);
|
|
231
|
+
}
|
|
232
|
+
// Resolve project name
|
|
233
|
+
let resolvedProjectName = null;
|
|
234
|
+
if (running.projectId) {
|
|
235
|
+
const projects = yield* clockify.getProjects(auth.workspaceId).pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
236
|
+
resolvedProjectName = projects.find((p) => p.id === running.projectId)?.name ?? null;
|
|
237
|
+
}
|
|
238
|
+
const newState = {
|
|
239
|
+
active: true,
|
|
240
|
+
ticketKey,
|
|
241
|
+
summary,
|
|
242
|
+
project: ticketKey?.split("-")[0] ?? null,
|
|
243
|
+
startedAt,
|
|
244
|
+
clockifyEntryId: running.id,
|
|
245
|
+
projectId: running.projectId ?? null,
|
|
246
|
+
projectName: resolvedProjectName,
|
|
247
|
+
billable: running.billable ?? null,
|
|
248
|
+
startedViaJcf: false
|
|
249
|
+
};
|
|
250
|
+
yield* writeStateFile(newState);
|
|
251
|
+
yield* SubscriptionRef.set(ref, newState);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// Discard: delete the Clockify entry, clear state, no Jira worklog
|
|
255
|
+
const discard = Effect.gen(function* () {
|
|
256
|
+
const current = yield* SubscriptionRef.get(ref);
|
|
257
|
+
if (!current.active) {
|
|
258
|
+
return yield* Effect.fail(new TimerError({ message: "No active timer to discard" }));
|
|
259
|
+
}
|
|
260
|
+
const auth = yield* getAuth;
|
|
261
|
+
if (current.clockifyEntryId) {
|
|
262
|
+
yield* clockify
|
|
263
|
+
.deleteTimeEntry(auth.workspaceId, current.clockifyEntryId)
|
|
264
|
+
.pipe(Effect.mapError(() => new TimerError({ message: "delete failed" })), Effect.catchTag("TimerError", () => Effect.void));
|
|
265
|
+
}
|
|
266
|
+
yield* SubscriptionRef.set(ref, emptyState);
|
|
267
|
+
yield* stateWriter.clear;
|
|
268
|
+
});
|
|
269
|
+
return { state: ref, start, stop: internalStop, discard, detectRunning };
|
|
270
|
+
}));
|
|
271
|
+
//# sourceMappingURL=TimerService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimerService.js","sourceRoot":"","sources":["../../../src/services/TimerService.ts"],"names":[],"mappings":"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"}
|