@knpkv/jira-clockify 0.2.0 → 0.5.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/README.md +4 -1
- package/dist/package.json +12 -13
- package/dist/src/bin.js +30 -13
- package/dist/src/bin.js.map +1 -1
- package/dist/src/cli/auth.js +23 -19
- package/dist/src/cli/auth.js.map +1 -1
- package/dist/src/cli/config.js +4 -4
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/fetchTicket.js +30 -0
- package/dist/src/cli/fetchTicket.js.map +1 -0
- package/dist/src/cli/fuzzySelect.js +6 -5
- package/dist/src/cli/fuzzySelect.js.map +1 -1
- package/dist/src/cli/layers.js +10 -8
- package/dist/src/cli/layers.js.map +1 -1
- package/dist/src/cli/list.js +1 -1
- package/dist/src/cli/list.js.map +1 -1
- package/dist/src/cli/setup.js +19 -19
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/timer/discard.js +2 -2
- package/dist/src/cli/timer/discard.js.map +1 -1
- package/dist/src/cli/timer/edit.js +11 -11
- package/dist/src/cli/timer/edit.js.map +1 -1
- package/dist/src/cli/timer/log.js +56 -79
- package/dist/src/cli/timer/log.js.map +1 -1
- package/dist/src/cli/timer/start.js +56 -40
- package/dist/src/cli/timer/start.js.map +1 -1
- package/dist/src/cli/timer/status.js +7 -7
- package/dist/src/cli/timer/status.js.map +1 -1
- package/dist/src/cli/timer/stop.js +183 -59
- package/dist/src/cli/timer/stop.js.map +1 -1
- package/dist/src/main.js +15 -3
- package/dist/src/main.js.map +1 -1
- package/dist/src/services/ClockifyAuth.js +12 -23
- package/dist/src/services/ClockifyAuth.js.map +1 -1
- package/dist/src/services/ConfigService.js +7 -7
- package/dist/src/services/ConfigService.js.map +1 -1
- package/dist/src/services/HomeDirectory.js +14 -0
- package/dist/src/services/HomeDirectory.js.map +1 -0
- package/dist/src/services/StateWriter.js +8 -8
- package/dist/src/services/StateWriter.js.map +1 -1
- package/dist/src/services/TicketService.js +22 -15
- package/dist/src/services/TicketService.js.map +1 -1
- package/dist/src/services/TimerService.js +126 -60
- package/dist/src/services/TimerService.js.map +1 -1
- package/dist/src/tui/App.js +72 -8
- package/dist/src/tui/App.js.map +1 -1
- package/dist/src/tui/atoms/runtime.js +1 -1
- package/dist/src/tui/atoms/runtime.js.map +1 -1
- package/dist/src/tui/atoms/tickets.js +1 -1
- package/dist/src/tui/atoms/tickets.js.map +1 -1
- package/dist/src/tui/atoms/timer.js +7 -2
- package/dist/src/tui/atoms/timer.js.map +1 -1
- package/dist/src/tui/atoms/ui.js +1 -1
- package/dist/src/tui/atoms/ui.js.map +1 -1
- package/dist/src/tui/components/BigTimer.js +2 -2
- package/dist/src/tui/components/BigTimer.js.map +1 -1
- package/dist/src/tui/components/Footer.js +1 -1
- package/dist/src/tui/components/Footer.js.map +1 -1
- package/dist/src/tui/components/Header.js +3 -2
- package/dist/src/tui/components/Header.js.map +1 -1
- package/dist/src/tui/components/PopupMessage.js +7 -2
- package/dist/src/tui/components/PopupMessage.js.map +1 -1
- package/dist/src/tui/components/TicketList.js +3 -2
- package/dist/src/tui/components/TicketList.js.map +1 -1
- package/dist/src/tui/context/theme.js.map +1 -1
- package/dist/src/tui/hooks/useElapsedTimer.js +3 -2
- package/dist/src/tui/hooks/useElapsedTimer.js.map +1 -1
- package/dist/src/tui/hooks/useTerminalSize.js +1 -21
- package/dist/src/tui/hooks/useTerminalSize.js.map +1 -1
- package/dist/src/utils/time.js +55 -5
- package/dist/src/utils/time.js.map +1 -1
- package/dist/test/TimerService.test.js +224 -31
- package/dist/test/TimerService.test.js.map +1 -1
- package/dist/test/fetchTicket.test.js +89 -0
- package/dist/test/fetchTicket.test.js.map +1 -0
- package/dist/test/stopPrompts.test.js +93 -0
- package/dist/test/stopPrompts.test.js.map +1 -0
- package/dist/test/time.test.js +84 -0
- package/dist/test/time.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -17
- package/skills/jcf/SKILL.md +89 -0
- package/skills/jcf/agents/openai.yaml +4 -0
|
@@ -4,8 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
-
import nodeProcess from "node:process";
|
|
8
7
|
import { useElapsedTimer } from "../hooks/useElapsedTimer.js";
|
|
8
|
+
import { useTerminalSize } from "../hooks/useTerminalSize.js";
|
|
9
9
|
// 5-line tall digit font (each digit is 5 rows × 5 cols)
|
|
10
10
|
const DIGITS = {
|
|
11
11
|
"0": ["╔═══╗", "║ ║", "║ ║", "║ ║", "╚═══╝"],
|
|
@@ -43,7 +43,7 @@ export function BigTimer() {
|
|
|
43
43
|
const m = Math.floor((elapsed % 3600) / 60);
|
|
44
44
|
const s = elapsed % 60;
|
|
45
45
|
const rows = renderBigTime(h, m, s);
|
|
46
|
-
const cols =
|
|
46
|
+
const cols = useTerminalSize();
|
|
47
47
|
// Progress bar
|
|
48
48
|
const barWidth = 40;
|
|
49
49
|
const filled = Math.floor((s / 60) * barWidth);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BigTimer.js","sourceRoot":"","sources":["../../../../src/tui/components/BigTimer.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"BigTimer.js","sourceRoot":"","sources":["../../../../src/tui/components/BigTimer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
-
import { useAtomValue } from "@effect
|
|
7
|
+
import { useAtomValue } from "@effect/atom-react";
|
|
8
8
|
import { isFilteringAtom } from "../atoms/ui.js";
|
|
9
9
|
export function Footer() {
|
|
10
10
|
const isFiltering = useAtomValue(isFilteringAtom);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Footer.js","sourceRoot":"","sources":["../../../../src/tui/components/Footer.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"Footer.js","sourceRoot":"","sources":["../../../../src/tui/components/Footer.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,MAAM,UAAU,MAAM;IACpB,MAAM,WAAW,GAAG,YAAY,CAAC,eAAe,CAAC,CAAA;IAEjD,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,CACL,eACE,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAS,aAE5G,cAAK,KAAK,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAS,YAChF,eAAM,EAAE,EAAC,SAAS,kBAAS,GACvB,EACN,eAAM,EAAE,EAAC,SAAS,sCAAwB,EAC1C,eAAM,EAAE,EAAC,SAAS,6CAA+B,IAC7C,CACP,CAAA;IACH,CAAC;IAED,OAAO,CACL,cAAK,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAS,YACzF,eAAM,EAAE,EAAC,SAAS,YAAE,6DAA6D,GAAQ,GACrF,CACP,CAAA;AACH,CAAC"}
|
|
@@ -4,11 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { useAtomValue } from "@effect/atom-react";
|
|
8
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
8
9
|
import { timerStateAtom } from "../atoms/timer.js";
|
|
9
10
|
export function Header() {
|
|
10
11
|
const timerResult = useAtomValue(timerStateAtom);
|
|
11
|
-
const timerState =
|
|
12
|
+
const timerState = AsyncResult.isSuccess(timerResult) ? timerResult.value : null;
|
|
12
13
|
const statusIcon = timerState?.active ? "●" : "○";
|
|
13
14
|
const statusColor = timerState?.active ? "#00CC66" : "#888888";
|
|
14
15
|
return (_jsxs("box", { style: { height: 1, width: "100%", backgroundColor: "#1a1a2e", flexDirection: "row" }, children: [_jsx("text", { fg: statusColor, style: { fontWeight: "bold" }, children: ` ${statusIcon} ` }), timerState?.active && timerState.ticketKey ? (_jsx("text", { fg: "#00CCFF", style: { fontWeight: "bold" }, children: timerState.ticketKey })) : (_jsx("text", { fg: "#888888", children: "jcf" })), timerState?.active && timerState.summary ? (_jsx("text", { fg: "#888888", children: ` ${timerState.summary.slice(0, 50)}` })) : null, timerState?.active && timerState.projectName ? (_jsx("text", { fg: "#555555", children: ` [${timerState.projectName}]` })) : null] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.js","sourceRoot":"","sources":["../../../../src/tui/components/Header.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"Header.js","sourceRoot":"","sources":["../../../../src/tui/components/Header.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,WAAW,MAAM,wCAAwC,CAAA;AAErE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAElD,MAAM,UAAU,MAAM;IACpB,MAAM,WAAW,GAAG,YAAY,CAAC,cAAc,CAAC,CAAA;IAChD,MAAM,UAAU,GAAsB,WAAW,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAEnG,MAAM,UAAU,GAAG,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IACjD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;IAE9D,OAAO,CACL,eAAK,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,EAAS,aAC/F,eAAM,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAS,YACxD,KAAK,UAAU,GAAG,GACd,EACN,UAAU,EAAE,MAAM,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAC5C,eAAM,EAAE,EAAC,SAAS,EAAC,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAS,YACpD,UAAU,CAAC,SAAS,GAChB,CACR,CAAC,CAAC,CAAC,CACF,eAAM,EAAE,EAAC,SAAS,oBAAW,CAC9B,EACA,UAAU,EAAE,MAAM,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAC1C,eAAM,EAAE,EAAC,SAAS,YAAE,KAAK,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,GAAQ,CACnE,CAAC,CAAC,CAAC,IAAI,EACP,UAAU,EAAE,MAAM,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAC9C,eAAM,EAAE,EAAC,SAAS,YAAE,MAAM,UAAU,CAAC,WAAW,GAAG,GAAQ,CAC5D,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC"}
|
|
@@ -15,15 +15,20 @@ const typeIcons = {
|
|
|
15
15
|
error: "✗",
|
|
16
16
|
info: "●"
|
|
17
17
|
};
|
|
18
|
-
export function PopupMessage({ lines, onDismiss, title, type = "info" }) {
|
|
18
|
+
export function PopupMessage({ lines, onDismiss, onRetry, retrying = false, title, type = "info" }) {
|
|
19
19
|
const color = typeColors[type];
|
|
20
20
|
const icon = typeIcons[type];
|
|
21
21
|
useKeyboard((key) => {
|
|
22
|
+
if (onRetry && !retrying && (key.name === "r" || key.char === "r")) {
|
|
23
|
+
onRetry();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
if (key.name === "return" || key.name === "escape" || key.name === "q") {
|
|
23
27
|
onDismiss();
|
|
24
28
|
}
|
|
25
29
|
});
|
|
26
30
|
const height = 4 + lines.length + 2; // border + title + gap + lines + gap + button
|
|
31
|
+
const retryBg = retrying ? "#555555" : "#FFAA00";
|
|
27
32
|
return (_jsx("box", { style: {
|
|
28
33
|
position: "absolute",
|
|
29
34
|
top: 0,
|
|
@@ -42,6 +47,6 @@ export function PopupMessage({ lines, onDismiss, title, type = "info" }) {
|
|
|
42
47
|
paddingLeft: 2,
|
|
43
48
|
paddingRight: 2,
|
|
44
49
|
paddingTop: 1
|
|
45
|
-
}, children: [_jsx("box", { style: { height: 1 }, children: _jsx("text", { fg: color, style: { fontWeight: "bold" }, children: `${icon} ${title}` }) }), _jsx("box", { style: { height: 1 } }), lines.map((line, i) => (_jsx("box", { style: { height: 1 }, children: _jsx("text", { fg: line.color ?? "#CCCCCC", children: ` ${line.text}` }) }, i))), _jsx("box", { style: { height: 1 } }),
|
|
50
|
+
}, children: [_jsx("box", { style: { height: 1 }, children: _jsx("text", { fg: color, style: { fontWeight: "bold" }, children: `${icon} ${title}` }) }), _jsx("box", { style: { height: 1 } }), lines.map((line, i) => (_jsx("box", { style: { height: 1 }, children: _jsx("text", { fg: line.color ?? "#CCCCCC", children: ` ${line.text}` }) }, i))), _jsx("box", { style: { height: 1 } }), _jsxs("box", { style: { height: 1, justifyContent: "center", gap: 2 }, children: [onRetry ? (_jsx("box", { style: { backgroundColor: retryBg, paddingLeft: 2, paddingRight: 2 }, children: _jsx("text", { fg: "#000000", style: { fontWeight: "bold" }, children: retrying ? "Retrying…" : "Retry (r)" }) })) : null, _jsx("box", { style: { backgroundColor: color, paddingLeft: 2, paddingRight: 2 }, children: _jsx("text", { fg: "#000000", style: { fontWeight: "bold" }, children: "OK (enter)" }) })] })] }) }));
|
|
46
51
|
}
|
|
47
52
|
//# sourceMappingURL=PopupMessage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PopupMessage.js","sourceRoot":"","sources":["../../../../src/tui/components/PopupMessage.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"PopupMessage.js","sourceRoot":"","sources":["../../../../src/tui/components/PopupMessage.tsx"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { useAtomValue } from "@effect/atom-react";
|
|
8
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
8
9
|
import { ticketsAtom } from "../atoms/tickets.js";
|
|
9
10
|
import { filterTextAtom, isFilteringAtom, selectedIndexAtom } from "../atoms/ui.js";
|
|
10
11
|
import { TicketRow } from "./TicketRow.js";
|
|
@@ -13,7 +14,7 @@ export function TicketList() {
|
|
|
13
14
|
const selectedIndex = useAtomValue(selectedIndexAtom);
|
|
14
15
|
const filterText = useAtomValue(filterTextAtom);
|
|
15
16
|
const isFiltering = useAtomValue(isFilteringAtom);
|
|
16
|
-
const ticketState =
|
|
17
|
+
const ticketState = AsyncResult.isSuccess(ticketResult) ? ticketResult.value : null;
|
|
17
18
|
if (!ticketState || ticketState.loading) {
|
|
18
19
|
return (_jsx("box", { style: { flexGrow: 1, paddingLeft: 2 }, children: _jsx("text", { fg: "#FFCC00", children: "Loading tickets..." }) }));
|
|
19
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TicketList.js","sourceRoot":"","sources":["../../../../src/tui/components/TicketList.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"TicketList.js","sourceRoot":"","sources":["../../../../src/tui/components/TicketList.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../../../src/tui/context/theme.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../../../src/tui/context/theme.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAA;AAMjE,MAAM,YAAY,GAAG,aAAa,CAAoB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;AAExE,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAAoC;IAC1E,OAAO,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAG,QAAQ,GAAyB,CAAA;AAC5F,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,OAAO,UAAU,CAAC,YAAY,CAAC,CAAA;AACjC,CAAC"}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
7
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
7
8
|
import { useEffect, useRef } from "react";
|
|
8
9
|
import { elapsedAtom, timerStateAtom } from "../atoms/timer.js";
|
|
9
10
|
export function useElapsedTimer() {
|
|
@@ -11,7 +12,7 @@ export function useElapsedTimer() {
|
|
|
11
12
|
const elapsed = useAtomValue(elapsedAtom);
|
|
12
13
|
const setElapsed = useAtomSet(elapsedAtom);
|
|
13
14
|
const intervalRef = useRef(null);
|
|
14
|
-
const timerState =
|
|
15
|
+
const timerState = AsyncResult.isSuccess(timerResult) ? timerResult.value : null;
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
if (timerState?.active && timerState.startedAt) {
|
|
17
18
|
const startTime = timerState.startedAt.getTime();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useElapsedTimer.js","sourceRoot":"","sources":["../../../../src/tui/hooks/useElapsedTimer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"useElapsedTimer.js","sourceRoot":"","sources":["../../../../src/tui/hooks/useElapsedTimer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,KAAK,WAAW,MAAM,wCAAwC,CAAA;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAE/D,MAAM,UAAU,eAAe;IAC7B,MAAM,WAAW,GAAG,YAAY,CAAC,cAAc,CAAC,CAAA;IAChD,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,CAAC,CAAA;IACzC,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAA;IAC1C,MAAM,WAAW,GAAG,MAAM,CAAwC,IAAI,CAAC,CAAA;IAEvE,MAAM,UAAU,GAAsB,WAAW,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAEnG,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,MAAM,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;YAC/C,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,OAAO,EAAE,CAAA;YAChD,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;YAC1E,IAAI,EAAE,CAAA;YACN,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;YAC7C,OAAO,GAAG,EAAE;gBACV,IAAI,WAAW,CAAC,OAAO;oBAAE,aAAa,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;YAC7D,CAAC,CAAA;QACH,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,CAAC,CAAC,CAAA;YACb,IAAI,WAAW,CAAC,OAAO;gBAAE,aAAa,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAC7D,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAA;IAE3D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,CAAA;AAChC,CAAC"}
|
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook that tracks terminal column width via Node stdout.
|
|
3
|
-
*
|
|
4
|
-
* Uses `node:process` import instead of the global — keeps the dependency explicit
|
|
5
|
-
* and avoids bare `process` globals. This is a TUI boundary: React hooks require
|
|
6
|
-
* synchronous access to terminal dimensions, which Effect's Terminal service can't
|
|
7
|
-
* provide without breaking the React render contract.
|
|
8
|
-
*
|
|
9
|
-
* @internal
|
|
10
|
-
*/
|
|
11
|
-
import nodeProcess from "node:process";
|
|
12
|
-
import { useEffect, useState } from "react";
|
|
13
1
|
export function useTerminalSize() {
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
const handler = () => setCols(nodeProcess.stdout.columns ?? 80);
|
|
17
|
-
nodeProcess.stdout.on("resize", handler);
|
|
18
|
-
return () => {
|
|
19
|
-
nodeProcess.stdout.off("resize", handler);
|
|
20
|
-
};
|
|
21
|
-
}, []);
|
|
22
|
-
return cols;
|
|
2
|
+
return 80;
|
|
23
3
|
}
|
|
24
4
|
export function useDisplayMode() {
|
|
25
5
|
const cols = useTerminalSize();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTerminalSize.js","sourceRoot":"","sources":["../../../../src/tui/hooks/useTerminalSize.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useTerminalSize.js","sourceRoot":"","sources":["../../../../src/tui/hooks/useTerminalSize.tsx"],"names":[],"mappings":"AAEA,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,CAAA;AACX,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,IAAI,GAAG,eAAe,EAAE,CAAA;IAC9B,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,SAAS,CAAA;IAC/B,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,SAAS,CAAA;IAC/B,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/dist/src/utils/time.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared time formatting utilities.
|
|
2
|
+
* Shared time formatting and parsing utilities.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
/** Format seconds as `HH:MM:SS`. */
|
|
6
|
+
/** Format seconds as `HH:MM:SS`. Negative input is clamped to 0. */
|
|
7
7
|
export function formatElapsed(seconds) {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
8
|
+
const clamped = Math.max(0, seconds);
|
|
9
|
+
const h = Math.floor(clamped / 3600);
|
|
10
|
+
const m = Math.floor((clamped % 3600) / 60);
|
|
11
|
+
const s = clamped % 60;
|
|
11
12
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
12
13
|
}
|
|
13
14
|
/** Format seconds as human-readable duration (`1h 23m` or `45s`). */
|
|
@@ -20,4 +21,53 @@ export function formatDuration(seconds) {
|
|
|
20
21
|
const m = Math.floor((seconds % 3600) / 60);
|
|
21
22
|
return `${h}h ${m}m`;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a duration string like `1h30m`, `2h`, `45m`, or `90m` into seconds.
|
|
26
|
+
* Returns `null` when the input is empty or malformed (unlike the loose regex
|
|
27
|
+
* that previously lived in the `log` command, which silently matched garbage).
|
|
28
|
+
*/
|
|
29
|
+
export function parseDuration(input) {
|
|
30
|
+
const match = input.trim().match(/^(?:(\d+)h)?(?:(\d+)m)?$/);
|
|
31
|
+
if (!match || (!match[1] && !match[2]))
|
|
32
|
+
return null;
|
|
33
|
+
const hours = parseInt(match[1] ?? "0", 10);
|
|
34
|
+
const minutes = parseInt(match[2] ?? "0", 10);
|
|
35
|
+
return hours * 3600 + minutes * 60;
|
|
36
|
+
}
|
|
37
|
+
/** Full ISO-8601 timestamp: `YYYY-MM-DDTHH:MM[:SS[.sss]][Z|±HH:MM]`. */
|
|
38
|
+
const ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/;
|
|
39
|
+
/** True when `input` is a full ISO-8601 timestamp (carries its own date). */
|
|
40
|
+
export function isFullIsoTimestamp(input) {
|
|
41
|
+
return ISO_8601.test(input.trim());
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse a past start time. Accepts `HH:MM` (interpreted as that time today, in
|
|
45
|
+
* local time, relative to `now`) or a full ISO-8601 timestamp. Returns `null`
|
|
46
|
+
* when unparseable.
|
|
47
|
+
*
|
|
48
|
+
* Note: the `HH:MM` branch uses `setHours` on the local day, so across a DST
|
|
49
|
+
* transition (a clock-time that is skipped or repeated locally) the resulting
|
|
50
|
+
* instant can shift by an hour. Pass a full ISO timestamp to avoid the ambiguity.
|
|
51
|
+
*
|
|
52
|
+
* The ISO fallback deliberately requires a *full* timestamp; bare years (`"2024"`)
|
|
53
|
+
* or locale strings (`"Jan 5"`) — which `new Date()` would otherwise accept — are
|
|
54
|
+
* rejected as `null` so malformed input never becomes a surprising instant.
|
|
55
|
+
*/
|
|
56
|
+
export function parseStartTime(input, now = new Date()) {
|
|
57
|
+
const trimmed = input.trim();
|
|
58
|
+
const hm = trimmed.match(/^(\d{1,2}):(\d{2})$/);
|
|
59
|
+
if (hm) {
|
|
60
|
+
const hours = parseInt(hm[1], 10);
|
|
61
|
+
const minutes = parseInt(hm[2], 10);
|
|
62
|
+
if (hours > 23 || minutes > 59)
|
|
63
|
+
return null;
|
|
64
|
+
const d = new Date(now);
|
|
65
|
+
d.setHours(hours, minutes, 0, 0);
|
|
66
|
+
return d;
|
|
67
|
+
}
|
|
68
|
+
if (!ISO_8601.test(trimmed))
|
|
69
|
+
return null;
|
|
70
|
+
const parsed = new Date(trimmed);
|
|
71
|
+
return isNaN(parsed.getTime()) ? null : parsed;
|
|
72
|
+
}
|
|
23
73
|
//# sourceMappingURL=time.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time.js","sourceRoot":"","sources":["../../../src/utils/time.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,
|
|
1
|
+
{"version":3,"file":"time.js","sourceRoot":"","sources":["../../../src/utils/time.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import * as HttpClient from "@effect/platform/HttpClient";
|
|
2
|
-
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
|
|
3
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
4
2
|
import { ClockifyApiClient } from "@knpkv/clockify-api-client";
|
|
5
|
-
import { JiraApiClient } from "@knpkv/jira-api-client";
|
|
3
|
+
import { FetchClientError, JiraApiClient } from "@knpkv/jira-api-client";
|
|
6
4
|
import { JiraAuth } from "@knpkv/jira-cli/JiraAuth";
|
|
7
5
|
import * as Effect from "effect/Effect";
|
|
8
6
|
import * as Layer from "effect/Layer";
|
|
9
7
|
import * as Redacted from "effect/Redacted";
|
|
10
8
|
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
9
|
+
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
|
|
11
10
|
import { ClockifyAuth } from "../src/services/ClockifyAuth.js";
|
|
12
11
|
import { ConfigService } from "../src/services/ConfigService.js";
|
|
13
12
|
import { StateWriter } from "../src/services/StateWriter.js";
|
|
@@ -149,7 +148,28 @@ const MockHttpClientLayer = Layer.succeed(HttpClient.HttpClient, HttpClient.make
|
|
|
149
148
|
status: 201,
|
|
150
149
|
headers: { "content-type": "application/json" }
|
|
151
150
|
})))));
|
|
151
|
+
// HttpClient that returns a 400 for any Jira worklog POST (simulates Jira failure)
|
|
152
|
+
const MockHttpClientFailLayer = Layer.succeed(HttpClient.HttpClient, HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, new Response(JSON.stringify({ errorMessages: ["nope"] }), {
|
|
153
|
+
status: 400,
|
|
154
|
+
headers: { "content-type": "application/json" }
|
|
155
|
+
})))));
|
|
156
|
+
// HttpClient that fails the Jira worklog POST `failures` times, then succeeds —
|
|
157
|
+
// models a transient Jira outage so a retry can recover.
|
|
158
|
+
const makeFlakyHttpClientLayer = (failures) => {
|
|
159
|
+
let calls = 0;
|
|
160
|
+
return Layer.succeed(HttpClient.HttpClient, HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, calls++ < failures
|
|
161
|
+
? new Response(JSON.stringify({ errorMessages: ["nope"] }), {
|
|
162
|
+
status: 400,
|
|
163
|
+
headers: { "content-type": "application/json" }
|
|
164
|
+
})
|
|
165
|
+
: new Response(JSON.stringify({ id: "wl-1" }), {
|
|
166
|
+
status: 201,
|
|
167
|
+
headers: { "content-type": "application/json" }
|
|
168
|
+
})))));
|
|
169
|
+
};
|
|
152
170
|
const TestLayer = timerLayer.pipe(Layer.provide(MockClockifyLayer), Layer.provide(MockJiraApiClientLayer), Layer.provide(MockClockifyAuthLayer), Layer.provide(MockConfigLayer), Layer.provide(MockStateWriterLayer), Layer.provide(MockJiraAuthLayer), Layer.provide(MockHttpClientLayer));
|
|
171
|
+
// Build a TimerService layer with overridden Clockify / HttpClient mocks.
|
|
172
|
+
const makeTestLayer = (clockify = mockClockify, httpLayer = MockHttpClientLayer) => timerLayer.pipe(Layer.provide(Layer.succeed(ClockifyApiClient, clockify)), Layer.provide(MockJiraApiClientLayer), Layer.provide(MockClockifyAuthLayer), Layer.provide(MockConfigLayer), Layer.provide(MockStateWriterLayer), Layer.provide(MockJiraAuthLayer), Layer.provide(httpLayer));
|
|
153
173
|
// ---------------------------------------------------------------------------
|
|
154
174
|
// Tests
|
|
155
175
|
// ---------------------------------------------------------------------------
|
|
@@ -264,10 +284,7 @@ describe("TimerService", () => {
|
|
|
264
284
|
});
|
|
265
285
|
describe("detectRunning", () => {
|
|
266
286
|
// Detects externally-started Clockify timers with "[KEY] summary" format (jcf native format)
|
|
267
|
-
it.effect("parses bracket format [KEY] summary", () =>
|
|
268
|
-
resetCaptures();
|
|
269
|
-
writtenStates = [];
|
|
270
|
-
cleared = false;
|
|
287
|
+
it.effect("parses bracket format [KEY] summary", () => {
|
|
271
288
|
const runningEntry = makeTimeEntry("ext-1", "[PROJ-42] Implement feature", new Date("2025-01-01T10:00:00Z"), "proj-id");
|
|
272
289
|
const clockifyWithRunning = {
|
|
273
290
|
...mockClockify,
|
|
@@ -283,36 +300,41 @@ describe("TimerService", () => {
|
|
|
283
300
|
note: ""
|
|
284
301
|
}])
|
|
285
302
|
};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
return Effect.gen(function* () {
|
|
304
|
+
resetCaptures();
|
|
305
|
+
writtenStates = [];
|
|
306
|
+
cleared = false;
|
|
307
|
+
const svc = yield* TimerService;
|
|
308
|
+
yield* svc.detectRunning;
|
|
309
|
+
// State was updated via the layer's SubscriptionRef, so read from svc
|
|
310
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
311
|
+
expect(state.active).toBe(true);
|
|
312
|
+
expect(state.ticketKey).toBe("PROJ-42");
|
|
313
|
+
expect(state.summary).toBe("Implement feature");
|
|
314
|
+
expect(state.projectId).toBe("proj-id");
|
|
315
|
+
expect(state.projectName).toBe("MyProject");
|
|
316
|
+
expect(state.startedViaJcf).toBe(false);
|
|
317
|
+
}).pipe(Effect.provide(makeTestLayer(clockifyWithRunning)));
|
|
318
|
+
});
|
|
298
319
|
// Also detects "KEY: summary" format — common when timers are started manually in Clockify
|
|
299
|
-
it.effect("parses colon format KEY: summary", () =>
|
|
300
|
-
resetCaptures();
|
|
301
|
-
writtenStates = [];
|
|
320
|
+
it.effect("parses colon format KEY: summary", () => {
|
|
302
321
|
const runningEntry = makeTimeEntry("ext-2", "PROJ-99: Review PR", new Date("2025-01-01T10:00:00Z"));
|
|
303
322
|
const clockifyWithRunning = {
|
|
304
323
|
...mockClockify,
|
|
305
324
|
getRunningTimer: () => Effect.succeed(runningEntry)
|
|
306
325
|
};
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
326
|
+
return Effect.gen(function* () {
|
|
327
|
+
resetCaptures();
|
|
328
|
+
writtenStates = [];
|
|
329
|
+
const svc = yield* TimerService;
|
|
330
|
+
yield* svc.detectRunning;
|
|
331
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
332
|
+
expect(state.active).toBe(true);
|
|
333
|
+
expect(state.ticketKey).toBe("PROJ-99");
|
|
334
|
+
expect(state.summary).toBe("Review PR");
|
|
335
|
+
expect(state.startedViaJcf).toBe(false);
|
|
336
|
+
}).pipe(Effect.provide(makeTestLayer(clockifyWithRunning)));
|
|
337
|
+
});
|
|
316
338
|
// No running timer in Clockify must leave local state unchanged — polling should be safe no-op
|
|
317
339
|
it.effect("no running timer is a no-op", () => Effect.gen(function* () {
|
|
318
340
|
resetCaptures();
|
|
@@ -351,5 +373,176 @@ describe("TimerService", () => {
|
|
|
351
373
|
expect(result.needsProjectId).toBe(true);
|
|
352
374
|
}).pipe(Effect.provide(TestLayer)));
|
|
353
375
|
});
|
|
376
|
+
describe("logManual (correction interval)", () => {
|
|
377
|
+
// Logging a forgotten interval writes a closed Clockify entry (start + end) and a Jira worklog
|
|
378
|
+
it.effect("creates a closed Clockify entry and posts a Jira worklog", () => Effect.gen(function* () {
|
|
379
|
+
resetCaptures();
|
|
380
|
+
writtenStates = [];
|
|
381
|
+
cleared = false;
|
|
382
|
+
const svc = yield* TimerService;
|
|
383
|
+
const start = new Date("2025-01-01T09:00:00.000Z");
|
|
384
|
+
const result = yield* svc.logManual(makeTicket(), { start, durationSeconds: 1800 });
|
|
385
|
+
expect(result.clockifyLogged).toBe(true);
|
|
386
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
387
|
+
expect(createdEntries).toHaveLength(1);
|
|
388
|
+
const params = createdEntries[0].params;
|
|
389
|
+
expect(params.start).toBe(start.toISOString());
|
|
390
|
+
expect(params.end).toBe(new Date(start.getTime() + 1800 * 1000).toISOString());
|
|
391
|
+
expect(params.description).toBe("[PROJ-123] Fix the widget");
|
|
392
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
393
|
+
// A correction must never disturb the running-timer state (there was no running timer)
|
|
394
|
+
it.effect("leaves timer state inactive", () => Effect.gen(function* () {
|
|
395
|
+
resetCaptures();
|
|
396
|
+
writtenStates = [];
|
|
397
|
+
cleared = false;
|
|
398
|
+
const svc = yield* TimerService;
|
|
399
|
+
yield* svc.logManual(makeTicket(), { start: new Date("2025-01-01T09:00:00.000Z"), durationSeconds: 600 });
|
|
400
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
401
|
+
expect(state.active).toBe(false);
|
|
402
|
+
expect(state.ticketKey).toBeNull();
|
|
403
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
404
|
+
// Explicit projectId/billable options flow through to the Clockify entry
|
|
405
|
+
it.effect("honours explicit projectId and billable options", () => Effect.gen(function* () {
|
|
406
|
+
resetCaptures();
|
|
407
|
+
writtenStates = [];
|
|
408
|
+
cleared = false;
|
|
409
|
+
const svc = yield* TimerService;
|
|
410
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
411
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
412
|
+
durationSeconds: 600,
|
|
413
|
+
projectId: "proj-x",
|
|
414
|
+
billable: false
|
|
415
|
+
});
|
|
416
|
+
expect(result.projectId).toBe("proj-x");
|
|
417
|
+
expect(result.billable).toBe(false);
|
|
418
|
+
const params = createdEntries[0].params;
|
|
419
|
+
expect(params.projectId).toBe("proj-x");
|
|
420
|
+
expect(params.billable).toBe(false);
|
|
421
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
422
|
+
// A failing Clockify createTimeEntry must flip clockifyLogged to false (not crash)
|
|
423
|
+
it.effect("clockifyLogged is false when the Clockify entry fails", () => Effect.gen(function* () {
|
|
424
|
+
resetCaptures();
|
|
425
|
+
const svc = yield* TimerService;
|
|
426
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
427
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
428
|
+
durationSeconds: 600
|
|
429
|
+
});
|
|
430
|
+
// Jira worklog still succeeds via the 201 mock; only Clockify failed.
|
|
431
|
+
expect(result.clockifyLogged).toBe(false);
|
|
432
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
433
|
+
}).pipe(Effect.provide(makeTestLayer({
|
|
434
|
+
...mockClockify,
|
|
435
|
+
createTimeEntry: () => Effect.fail(new FetchClientError({ error: "boom", status: 500, message: "boom" }))
|
|
436
|
+
}, MockHttpClientLayer))));
|
|
437
|
+
// A failing Jira worklog POST must flip jiraWorklogLogged to false (not crash)
|
|
438
|
+
it.effect("jiraWorklogLogged is false when the Jira worklog POST fails", () => Effect.gen(function* () {
|
|
439
|
+
resetCaptures();
|
|
440
|
+
const svc = yield* TimerService;
|
|
441
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
442
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
443
|
+
durationSeconds: 600
|
|
444
|
+
});
|
|
445
|
+
expect(result.clockifyLogged).toBe(true);
|
|
446
|
+
expect(result.jiraWorklogLogged).toBe(false);
|
|
447
|
+
}).pipe(Effect.provide(makeTestLayer(mockClockify, MockHttpClientFailLayer))));
|
|
448
|
+
// The 60s Jira floor applies to backdated/manual logs too: a <60s duration
|
|
449
|
+
// must still post a worklog (floored), so jiraWorklogLogged stays true.
|
|
450
|
+
it.effect("applies the 60s worklog floor on a sub-minute manual log", () => Effect.gen(function* () {
|
|
451
|
+
resetCaptures();
|
|
452
|
+
const svc = yield* TimerService;
|
|
453
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
454
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
455
|
+
durationSeconds: 30
|
|
456
|
+
});
|
|
457
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
458
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
459
|
+
// Future start times are rejected by the shared guard in logManual
|
|
460
|
+
it.effect("fails when the start time is in the future", () => Effect.gen(function* () {
|
|
461
|
+
resetCaptures();
|
|
462
|
+
const svc = yield* TimerService;
|
|
463
|
+
const future = new Date(Date.now() + 60 * 60 * 1000);
|
|
464
|
+
const error = yield* svc.logManual(makeTicket(), { start: future, durationSeconds: 600 }).pipe(Effect.flip);
|
|
465
|
+
expect(error).toBeInstanceOf(TimerError);
|
|
466
|
+
expect(error.message).toMatch(/future/i);
|
|
467
|
+
// No Clockify entry should have been created.
|
|
468
|
+
expect(createdEntries).toHaveLength(0);
|
|
469
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
470
|
+
// The whole manual interval must be in the past; otherwise Clockify/Jira
|
|
471
|
+
// would receive a worklog whose end time has not happened yet.
|
|
472
|
+
it.effect("fails when the manual interval would end in the future", () => Effect.gen(function* () {
|
|
473
|
+
resetCaptures();
|
|
474
|
+
const svc = yield* TimerService;
|
|
475
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
476
|
+
const error = yield* svc.logManual(makeTicket(), {
|
|
477
|
+
start: fiveMinutesAgo,
|
|
478
|
+
durationSeconds: 10 * 60
|
|
479
|
+
}).pipe(Effect.flip);
|
|
480
|
+
expect(error).toBeInstanceOf(TimerError);
|
|
481
|
+
expect(error.message).toMatch(/end time is in the future/i);
|
|
482
|
+
expect(createdEntries).toHaveLength(0);
|
|
483
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
484
|
+
});
|
|
485
|
+
describe("worklog retry", () => {
|
|
486
|
+
// A successful stop leaves nothing to retry — worklog must be null.
|
|
487
|
+
it.effect("stop exposes no worklog params when Jira succeeds", () => Effect.gen(function* () {
|
|
488
|
+
resetCaptures();
|
|
489
|
+
const svc = yield* TimerService;
|
|
490
|
+
yield* svc.start(makeTicket());
|
|
491
|
+
const result = yield* svc.stop({ comment: "done" });
|
|
492
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
493
|
+
expect(result.worklog).toBeNull();
|
|
494
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
495
|
+
// A partial stop (Clockify saved, Jira failed) must surface the params needed to retry.
|
|
496
|
+
it.effect("stop exposes worklog params when Jira fails", () => Effect.gen(function* () {
|
|
497
|
+
resetCaptures();
|
|
498
|
+
const svc = yield* TimerService;
|
|
499
|
+
yield* svc.start(makeTicket());
|
|
500
|
+
const result = yield* svc.stop({ comment: "done" });
|
|
501
|
+
expect(result.clockifyLogged).toBe(true);
|
|
502
|
+
expect(result.jiraWorklogLogged).toBe(false);
|
|
503
|
+
expect(result.worklog).not.toBeNull();
|
|
504
|
+
expect(result.worklog?.ticketKey).toBe("PROJ-123");
|
|
505
|
+
expect(result.worklog?.comment).toBe("done");
|
|
506
|
+
}).pipe(Effect.provide(makeTestLayer(mockClockify, MockHttpClientFailLayer))));
|
|
507
|
+
// logWorklog reposts in isolation and succeeds once Jira recovers — the retry round-trip.
|
|
508
|
+
it.effect("logWorklog recovers after a transient Jira failure", () => Effect.gen(function* () {
|
|
509
|
+
resetCaptures();
|
|
510
|
+
const svc = yield* TimerService;
|
|
511
|
+
yield* svc.start(makeTicket());
|
|
512
|
+
const result = yield* svc.stop({ comment: "done" });
|
|
513
|
+
expect(result.jiraWorklogLogged).toBe(false);
|
|
514
|
+
expect(result.worklog).not.toBeNull();
|
|
515
|
+
// Jira was flaky on the first POST (during stop) but recovers on retry.
|
|
516
|
+
const retried = yield* svc.logWorklog(result.worklog);
|
|
517
|
+
expect(retried).toBe(true);
|
|
518
|
+
}).pipe(Effect.provide(makeTestLayer(mockClockify, makeFlakyHttpClientLayer(1)))));
|
|
519
|
+
// A retry against a still-down Jira reports failure rather than crashing.
|
|
520
|
+
it.effect("logWorklog returns false while Jira is still failing", () => Effect.gen(function* () {
|
|
521
|
+
resetCaptures();
|
|
522
|
+
const svc = yield* TimerService;
|
|
523
|
+
const retried = yield* svc.logWorklog({
|
|
524
|
+
ticketKey: "PROJ-123",
|
|
525
|
+
startedAt: new Date("2025-01-01T09:00:00.000Z"),
|
|
526
|
+
durationSeconds: 600,
|
|
527
|
+
comment: "done"
|
|
528
|
+
});
|
|
529
|
+
expect(retried).toBe(false);
|
|
530
|
+
}).pipe(Effect.provide(makeTestLayer(mockClockify, MockHttpClientFailLayer))));
|
|
531
|
+
});
|
|
532
|
+
describe("start backdating", () => {
|
|
533
|
+
// A backdated start records the corrected start time on the Clockify entry and state
|
|
534
|
+
it.effect("uses the provided startedAt instead of now", () => Effect.gen(function* () {
|
|
535
|
+
resetCaptures();
|
|
536
|
+
writtenStates = [];
|
|
537
|
+
cleared = false;
|
|
538
|
+
const svc = yield* TimerService;
|
|
539
|
+
const startedAt = new Date("2025-01-01T08:30:00.000Z");
|
|
540
|
+
yield* svc.start(makeTicket(), { startedAt });
|
|
541
|
+
const params = createdEntries[0].params;
|
|
542
|
+
expect(params.start).toBe(startedAt.toISOString());
|
|
543
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
544
|
+
expect(state.startedAt?.toISOString()).toBe(startedAt.toISOString());
|
|
545
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
546
|
+
});
|
|
354
547
|
});
|
|
355
548
|
//# sourceMappingURL=TimerService.test.js.map
|