@knpkv/jira-clockify 0.2.0 → 0.4.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 +18 -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 +52 -79
- package/dist/src/cli/timer/log.js.map +1 -1
- package/dist/src/cli/timer/start.js +52 -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 +164 -58
- 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 +116 -58
- package/dist/src/services/TimerService.js.map +1 -1
- package/dist/src/tui/App.js +6 -5
- 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 +2 -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/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 +163 -31
- package/dist/test/TimerService.test.js.map +1 -1
- package/dist/test/fetchTicket.test.js +62 -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tickets.js","sourceRoot":"","sources":["../../../../src/tui/atoms/tickets.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"tickets.js","sourceRoot":"","sources":["../../../../src/tui/atoms/tickets.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,eAAe,CACpD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,aAAa,CAAA;IACpC,OAAO,OAAO,CAAC,KAAK,CAAA;AACtB,CAAC,CAAC,CACH,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,EAAE,CACvC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;IACzB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,aAAa,CAAA;IACpC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAA;AACxB,CAAC,CAAC,CACH,CAAA"}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
import { Atom } from "@effect-atom/atom-react";
|
|
7
6
|
import { Effect } from "effect";
|
|
7
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
8
8
|
import { TimerService } from "../../services/TimerService.js";
|
|
9
9
|
import { runtimeAtom } from "./runtime.js";
|
|
10
|
-
export const timerStateAtom = runtimeAtom.
|
|
10
|
+
export const timerStateAtom = runtimeAtom.subscriptionRef(Effect.gen(function* () {
|
|
11
11
|
const service = yield* TimerService;
|
|
12
12
|
return service.state;
|
|
13
13
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timer.js","sourceRoot":"","sources":["../../../../src/tui/atoms/timer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"timer.js","sourceRoot":"","sources":["../../../../src/tui/atoms/timer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAA;AAEvD,OAAO,EAAoB,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAC,eAAe,CACvD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,YAAY,CAAA;IACnC,OAAO,OAAO,CAAC,KAAK,CAAA;AACtB,CAAC,CAAC,CACH,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAE5D,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAC,EAAE,CAC1C,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAC,MAAkB;IAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,YAAY,CAAA;IACnC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;AAC9B,CAAC,CAAC,CACH,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAC,EAAE,CACzC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAC,OAAqB;IAC/C,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,YAAY,CAAA;IACnC,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AACrC,CAAC,CAAC,CACH,CAAA;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAC,EAAE,CAC5C,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;IACzB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,YAAY,CAAA;IACnC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAA;AACxB,CAAC,CAAC,CACH,CAAA;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAC,EAAE,CAC7C,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;IACzB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,YAAY,CAAA;IACnC,KAAK,CAAC,CAAC,OAAO,CAAC,aAAa,CAAA;AAC9B,CAAC,CAAC,CACH,CAAA"}
|
package/dist/src/tui/atoms/ui.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
7
7
|
export const selectedIndexAtom = Atom.make(0).pipe(Atom.keepAlive);
|
|
8
8
|
export const filterTextAtom = Atom.make("").pipe(Atom.keepAlive);
|
|
9
9
|
export const isFilteringAtom = Atom.make(false).pipe(Atom.keepAlive);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../../../src/tui/atoms/ui.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../../../src/tui/atoms/ui.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAA;AAIvD,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAClE,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAChE,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA"}
|
|
@@ -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"}
|
|
@@ -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,14 @@ 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
|
+
})))));
|
|
152
156
|
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));
|
|
157
|
+
// Build a TimerService layer with overridden Clockify / HttpClient mocks.
|
|
158
|
+
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
159
|
// ---------------------------------------------------------------------------
|
|
154
160
|
// Tests
|
|
155
161
|
// ---------------------------------------------------------------------------
|
|
@@ -264,10 +270,7 @@ describe("TimerService", () => {
|
|
|
264
270
|
});
|
|
265
271
|
describe("detectRunning", () => {
|
|
266
272
|
// 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;
|
|
273
|
+
it.effect("parses bracket format [KEY] summary", () => {
|
|
271
274
|
const runningEntry = makeTimeEntry("ext-1", "[PROJ-42] Implement feature", new Date("2025-01-01T10:00:00Z"), "proj-id");
|
|
272
275
|
const clockifyWithRunning = {
|
|
273
276
|
...mockClockify,
|
|
@@ -283,36 +286,41 @@ describe("TimerService", () => {
|
|
|
283
286
|
note: ""
|
|
284
287
|
}])
|
|
285
288
|
};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
return Effect.gen(function* () {
|
|
290
|
+
resetCaptures();
|
|
291
|
+
writtenStates = [];
|
|
292
|
+
cleared = false;
|
|
293
|
+
const svc = yield* TimerService;
|
|
294
|
+
yield* svc.detectRunning;
|
|
295
|
+
// State was updated via the layer's SubscriptionRef, so read from svc
|
|
296
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
297
|
+
expect(state.active).toBe(true);
|
|
298
|
+
expect(state.ticketKey).toBe("PROJ-42");
|
|
299
|
+
expect(state.summary).toBe("Implement feature");
|
|
300
|
+
expect(state.projectId).toBe("proj-id");
|
|
301
|
+
expect(state.projectName).toBe("MyProject");
|
|
302
|
+
expect(state.startedViaJcf).toBe(false);
|
|
303
|
+
}).pipe(Effect.provide(makeTestLayer(clockifyWithRunning)));
|
|
304
|
+
});
|
|
298
305
|
// 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 = [];
|
|
306
|
+
it.effect("parses colon format KEY: summary", () => {
|
|
302
307
|
const runningEntry = makeTimeEntry("ext-2", "PROJ-99: Review PR", new Date("2025-01-01T10:00:00Z"));
|
|
303
308
|
const clockifyWithRunning = {
|
|
304
309
|
...mockClockify,
|
|
305
310
|
getRunningTimer: () => Effect.succeed(runningEntry)
|
|
306
311
|
};
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
return Effect.gen(function* () {
|
|
313
|
+
resetCaptures();
|
|
314
|
+
writtenStates = [];
|
|
315
|
+
const svc = yield* TimerService;
|
|
316
|
+
yield* svc.detectRunning;
|
|
317
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
318
|
+
expect(state.active).toBe(true);
|
|
319
|
+
expect(state.ticketKey).toBe("PROJ-99");
|
|
320
|
+
expect(state.summary).toBe("Review PR");
|
|
321
|
+
expect(state.startedViaJcf).toBe(false);
|
|
322
|
+
}).pipe(Effect.provide(makeTestLayer(clockifyWithRunning)));
|
|
323
|
+
});
|
|
316
324
|
// No running timer in Clockify must leave local state unchanged — polling should be safe no-op
|
|
317
325
|
it.effect("no running timer is a no-op", () => Effect.gen(function* () {
|
|
318
326
|
resetCaptures();
|
|
@@ -351,5 +359,129 @@ describe("TimerService", () => {
|
|
|
351
359
|
expect(result.needsProjectId).toBe(true);
|
|
352
360
|
}).pipe(Effect.provide(TestLayer)));
|
|
353
361
|
});
|
|
362
|
+
describe("logManual (correction interval)", () => {
|
|
363
|
+
// Logging a forgotten interval writes a closed Clockify entry (start + end) and a Jira worklog
|
|
364
|
+
it.effect("creates a closed Clockify entry and posts a Jira worklog", () => Effect.gen(function* () {
|
|
365
|
+
resetCaptures();
|
|
366
|
+
writtenStates = [];
|
|
367
|
+
cleared = false;
|
|
368
|
+
const svc = yield* TimerService;
|
|
369
|
+
const start = new Date("2025-01-01T09:00:00.000Z");
|
|
370
|
+
const result = yield* svc.logManual(makeTicket(), { start, durationSeconds: 1800 });
|
|
371
|
+
expect(result.clockifyLogged).toBe(true);
|
|
372
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
373
|
+
expect(createdEntries).toHaveLength(1);
|
|
374
|
+
const params = createdEntries[0].params;
|
|
375
|
+
expect(params.start).toBe(start.toISOString());
|
|
376
|
+
expect(params.end).toBe(new Date(start.getTime() + 1800 * 1000).toISOString());
|
|
377
|
+
expect(params.description).toBe("[PROJ-123] Fix the widget");
|
|
378
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
379
|
+
// A correction must never disturb the running-timer state (there was no running timer)
|
|
380
|
+
it.effect("leaves timer state inactive", () => Effect.gen(function* () {
|
|
381
|
+
resetCaptures();
|
|
382
|
+
writtenStates = [];
|
|
383
|
+
cleared = false;
|
|
384
|
+
const svc = yield* TimerService;
|
|
385
|
+
yield* svc.logManual(makeTicket(), { start: new Date("2025-01-01T09:00:00.000Z"), durationSeconds: 600 });
|
|
386
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
387
|
+
expect(state.active).toBe(false);
|
|
388
|
+
expect(state.ticketKey).toBeNull();
|
|
389
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
390
|
+
// Explicit projectId/billable options flow through to the Clockify entry
|
|
391
|
+
it.effect("honours explicit projectId and billable options", () => Effect.gen(function* () {
|
|
392
|
+
resetCaptures();
|
|
393
|
+
writtenStates = [];
|
|
394
|
+
cleared = false;
|
|
395
|
+
const svc = yield* TimerService;
|
|
396
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
397
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
398
|
+
durationSeconds: 600,
|
|
399
|
+
projectId: "proj-x",
|
|
400
|
+
billable: false
|
|
401
|
+
});
|
|
402
|
+
expect(result.projectId).toBe("proj-x");
|
|
403
|
+
expect(result.billable).toBe(false);
|
|
404
|
+
const params = createdEntries[0].params;
|
|
405
|
+
expect(params.projectId).toBe("proj-x");
|
|
406
|
+
expect(params.billable).toBe(false);
|
|
407
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
408
|
+
// A failing Clockify createTimeEntry must flip clockifyLogged to false (not crash)
|
|
409
|
+
it.effect("clockifyLogged is false when the Clockify entry fails", () => Effect.gen(function* () {
|
|
410
|
+
resetCaptures();
|
|
411
|
+
const svc = yield* TimerService;
|
|
412
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
413
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
414
|
+
durationSeconds: 600
|
|
415
|
+
});
|
|
416
|
+
// Jira worklog still succeeds via the 201 mock; only Clockify failed.
|
|
417
|
+
expect(result.clockifyLogged).toBe(false);
|
|
418
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
419
|
+
}).pipe(Effect.provide(makeTestLayer({
|
|
420
|
+
...mockClockify,
|
|
421
|
+
createTimeEntry: () => Effect.fail(new FetchClientError({ error: "boom", status: 500, message: "boom" }))
|
|
422
|
+
}, MockHttpClientLayer))));
|
|
423
|
+
// A failing Jira worklog POST must flip jiraWorklogLogged to false (not crash)
|
|
424
|
+
it.effect("jiraWorklogLogged is false when the Jira worklog POST fails", () => Effect.gen(function* () {
|
|
425
|
+
resetCaptures();
|
|
426
|
+
const svc = yield* TimerService;
|
|
427
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
428
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
429
|
+
durationSeconds: 600
|
|
430
|
+
});
|
|
431
|
+
expect(result.clockifyLogged).toBe(true);
|
|
432
|
+
expect(result.jiraWorklogLogged).toBe(false);
|
|
433
|
+
}).pipe(Effect.provide(makeTestLayer(mockClockify, MockHttpClientFailLayer))));
|
|
434
|
+
// The 60s Jira floor applies to backdated/manual logs too: a <60s duration
|
|
435
|
+
// must still post a worklog (floored), so jiraWorklogLogged stays true.
|
|
436
|
+
it.effect("applies the 60s worklog floor on a sub-minute manual log", () => Effect.gen(function* () {
|
|
437
|
+
resetCaptures();
|
|
438
|
+
const svc = yield* TimerService;
|
|
439
|
+
const result = yield* svc.logManual(makeTicket(), {
|
|
440
|
+
start: new Date("2025-01-01T09:00:00.000Z"),
|
|
441
|
+
durationSeconds: 30
|
|
442
|
+
});
|
|
443
|
+
expect(result.jiraWorklogLogged).toBe(true);
|
|
444
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
445
|
+
// Future start times are rejected by the shared guard in logManual
|
|
446
|
+
it.effect("fails when the start time is in the future", () => Effect.gen(function* () {
|
|
447
|
+
resetCaptures();
|
|
448
|
+
const svc = yield* TimerService;
|
|
449
|
+
const future = new Date(Date.now() + 60 * 60 * 1000);
|
|
450
|
+
const error = yield* svc.logManual(makeTicket(), { start: future, durationSeconds: 600 }).pipe(Effect.flip);
|
|
451
|
+
expect(error).toBeInstanceOf(TimerError);
|
|
452
|
+
expect(error.message).toMatch(/future/i);
|
|
453
|
+
// No Clockify entry should have been created.
|
|
454
|
+
expect(createdEntries).toHaveLength(0);
|
|
455
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
456
|
+
// The whole manual interval must be in the past; otherwise Clockify/Jira
|
|
457
|
+
// would receive a worklog whose end time has not happened yet.
|
|
458
|
+
it.effect("fails when the manual interval would end in the future", () => Effect.gen(function* () {
|
|
459
|
+
resetCaptures();
|
|
460
|
+
const svc = yield* TimerService;
|
|
461
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
462
|
+
const error = yield* svc.logManual(makeTicket(), {
|
|
463
|
+
start: fiveMinutesAgo,
|
|
464
|
+
durationSeconds: 10 * 60
|
|
465
|
+
}).pipe(Effect.flip);
|
|
466
|
+
expect(error).toBeInstanceOf(TimerError);
|
|
467
|
+
expect(error.message).toMatch(/end time is in the future/i);
|
|
468
|
+
expect(createdEntries).toHaveLength(0);
|
|
469
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
470
|
+
});
|
|
471
|
+
describe("start backdating", () => {
|
|
472
|
+
// A backdated start records the corrected start time on the Clockify entry and state
|
|
473
|
+
it.effect("uses the provided startedAt instead of now", () => Effect.gen(function* () {
|
|
474
|
+
resetCaptures();
|
|
475
|
+
writtenStates = [];
|
|
476
|
+
cleared = false;
|
|
477
|
+
const svc = yield* TimerService;
|
|
478
|
+
const startedAt = new Date("2025-01-01T08:30:00.000Z");
|
|
479
|
+
yield* svc.start(makeTicket(), { startedAt });
|
|
480
|
+
const params = createdEntries[0].params;
|
|
481
|
+
expect(params.start).toBe(startedAt.toISOString());
|
|
482
|
+
const state = yield* SubscriptionRef.get(svc.state);
|
|
483
|
+
expect(state.startedAt?.toISOString()).toBe(startedAt.toISOString());
|
|
484
|
+
}).pipe(Effect.provide(TestLayer)));
|
|
485
|
+
});
|
|
354
486
|
});
|
|
355
487
|
//# sourceMappingURL=TimerService.test.js.map
|