@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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/package.json +47 -0
  4. package/dist/src/bin.js +28 -0
  5. package/dist/src/bin.js.map +1 -0
  6. package/dist/src/cli/auth.js +169 -0
  7. package/dist/src/cli/auth.js.map +1 -0
  8. package/dist/src/cli/config.js +107 -0
  9. package/dist/src/cli/config.js.map +1 -0
  10. package/dist/src/cli/fuzzySelect.js +149 -0
  11. package/dist/src/cli/fuzzySelect.js.map +1 -0
  12. package/dist/src/cli/layers.js +59 -0
  13. package/dist/src/cli/layers.js.map +1 -0
  14. package/dist/src/cli/list.js +27 -0
  15. package/dist/src/cli/list.js.map +1 -0
  16. package/dist/src/cli/setup.js +134 -0
  17. package/dist/src/cli/setup.js.map +1 -0
  18. package/dist/src/cli/timer/discard.js +33 -0
  19. package/dist/src/cli/timer/discard.js.map +1 -0
  20. package/dist/src/cli/timer/edit.js +139 -0
  21. package/dist/src/cli/timer/edit.js.map +1 -0
  22. package/dist/src/cli/timer/index.js +12 -0
  23. package/dist/src/cli/timer/index.js.map +1 -0
  24. package/dist/src/cli/timer/log.js +96 -0
  25. package/dist/src/cli/timer/log.js.map +1 -0
  26. package/dist/src/cli/timer/start.js +156 -0
  27. package/dist/src/cli/timer/start.js.map +1 -0
  28. package/dist/src/cli/timer/status.js +80 -0
  29. package/dist/src/cli/timer/status.js.map +1 -0
  30. package/dist/src/cli/timer/stop.js +96 -0
  31. package/dist/src/cli/timer/stop.js.map +1 -0
  32. package/dist/src/cli/timer.js +7 -0
  33. package/dist/src/cli/timer.js.map +1 -0
  34. package/dist/src/main.js +24 -0
  35. package/dist/src/main.js.map +1 -0
  36. package/dist/src/services/ClockifyAuth.js +77 -0
  37. package/dist/src/services/ClockifyAuth.js.map +1 -0
  38. package/dist/src/services/ConfigService.js +66 -0
  39. package/dist/src/services/ConfigService.js.map +1 -0
  40. package/dist/src/services/StateWriter.js +69 -0
  41. package/dist/src/services/StateWriter.js.map +1 -0
  42. package/dist/src/services/TicketService.js +106 -0
  43. package/dist/src/services/TicketService.js.map +1 -0
  44. package/dist/src/services/TimerService.js +271 -0
  45. package/dist/src/services/TimerService.js.map +1 -0
  46. package/dist/src/tui/App.js +204 -0
  47. package/dist/src/tui/App.js.map +1 -0
  48. package/dist/src/tui/atoms/runtime.js +9 -0
  49. package/dist/src/tui/atoms/runtime.js.map +1 -0
  50. package/dist/src/tui/atoms/tickets.js +17 -0
  51. package/dist/src/tui/atoms/tickets.js.map +1 -0
  52. package/dist/src/tui/atoms/timer.js +31 -0
  53. package/dist/src/tui/atoms/timer.js.map +1 -0
  54. package/dist/src/tui/atoms/ui.js +10 -0
  55. package/dist/src/tui/atoms/ui.js.map +1 -0
  56. package/dist/src/tui/components/BigTimer.js +60 -0
  57. package/dist/src/tui/components/BigTimer.js.map +1 -0
  58. package/dist/src/tui/components/Footer.js +16 -0
  59. package/dist/src/tui/components/Footer.js.map +1 -0
  60. package/dist/src/tui/components/Header.js +16 -0
  61. package/dist/src/tui/components/Header.js.map +1 -0
  62. package/dist/src/tui/components/PopupInput.js +30 -0
  63. package/dist/src/tui/components/PopupInput.js.map +1 -0
  64. package/dist/src/tui/components/PopupMessage.js +47 -0
  65. package/dist/src/tui/components/PopupMessage.js.map +1 -0
  66. package/dist/src/tui/components/TicketList.js +40 -0
  67. package/dist/src/tui/components/TicketList.js.map +1 -0
  68. package/dist/src/tui/components/TicketRow.js +48 -0
  69. package/dist/src/tui/components/TicketRow.js.map +1 -0
  70. package/dist/src/tui/components/TimerDisplay.js +30 -0
  71. package/dist/src/tui/components/TimerDisplay.js.map +1 -0
  72. package/dist/src/tui/components/index.js +12 -0
  73. package/dist/src/tui/components/index.js.map +1 -0
  74. package/dist/src/tui/context/theme.js +15 -0
  75. package/dist/src/tui/context/theme.js.map +1 -0
  76. package/dist/src/tui/hooks/useElapsedTimer.js +35 -0
  77. package/dist/src/tui/hooks/useElapsedTimer.js.map +1 -0
  78. package/dist/src/tui/hooks/useTerminalSize.js +32 -0
  79. package/dist/src/tui/hooks/useTerminalSize.js.map +1 -0
  80. package/dist/src/utils/time.js +23 -0
  81. package/dist/src/utils/time.js.map +1 -0
  82. package/dist/test/TimerService.test.js +355 -0
  83. package/dist/test/TimerService.test.js.map +1 -0
  84. package/dist/tsconfig.tsbuildinfo +1 -0
  85. package/nvim/lua/jcf/branch.lua +26 -0
  86. package/nvim/lua/jcf/float.lua +87 -0
  87. package/nvim/lua/jcf/init.lua +70 -0
  88. package/nvim/lua/jcf/state.lua +56 -0
  89. package/nvim/lua/jcf/statusline.lua +31 -0
  90. package/nvim/lua/jcf/telescope.lua +50 -0
  91. package/nvim/plugin/jcf.vim +2 -0
  92. package/package.json +47 -0
@@ -0,0 +1,26 @@
1
+ local M = {}
2
+
3
+ local patterns = {
4
+ "^feature/(%u+%-%d+)",
5
+ "^bugfix/(%u+%-%d+)",
6
+ "^hotfix/(%u+%-%d+)",
7
+ "^(%u+%-%d+)/",
8
+ "^(%u+%-%d+)%-",
9
+ }
10
+
11
+ function M.detect()
12
+ local branch = vim.fn.system("git branch --show-current 2>/dev/null"):gsub("%s+", "")
13
+ if branch == "" then
14
+ return nil
15
+ end
16
+ branch = branch:upper()
17
+ for _, pat in ipairs(patterns) do
18
+ local key = branch:match(pat)
19
+ if key then
20
+ return key
21
+ end
22
+ end
23
+ return nil
24
+ end
25
+
26
+ return M
@@ -0,0 +1,87 @@
1
+ local M = {}
2
+ local win_id = nil
3
+ local buf_id = nil
4
+
5
+ function M.toggle(config)
6
+ if win_id and vim.api.nvim_win_is_valid(win_id) then
7
+ vim.api.nvim_win_close(win_id, true)
8
+ win_id = nil
9
+ buf_id = nil
10
+ return
11
+ end
12
+
13
+ local width = math.floor(vim.o.columns * (config.float.width or 0.8))
14
+ local height = math.floor(vim.o.lines * (config.float.height or 0.8))
15
+ local col = math.floor((vim.o.columns - width) / 2)
16
+ local row = math.floor((vim.o.lines - height) / 2)
17
+
18
+ buf_id = vim.api.nvim_create_buf(false, true)
19
+ win_id = vim.api.nvim_open_win(buf_id, true, {
20
+ relative = "editor",
21
+ width = width,
22
+ height = height,
23
+ col = col,
24
+ row = row,
25
+ style = "minimal",
26
+ border = "rounded",
27
+ title = " jcf ",
28
+ title_pos = "center",
29
+ })
30
+
31
+ vim.fn.termopen(config.binary or "jcf")
32
+ vim.cmd("startinsert")
33
+
34
+ -- Close on window leave
35
+ vim.api.nvim_create_autocmd("WinLeave", {
36
+ buffer = buf_id,
37
+ once = true,
38
+ callback = function()
39
+ if win_id and vim.api.nvim_win_is_valid(win_id) then
40
+ vim.api.nvim_win_close(win_id, true)
41
+ win_id = nil
42
+ buf_id = nil
43
+ end
44
+ end,
45
+ })
46
+ end
47
+
48
+ function M.run_command(config, ...)
49
+ local args = { ... }
50
+ local binary = config.binary or "jcf"
51
+
52
+ local width = math.floor(vim.o.columns * (config.float.width or 0.8))
53
+ local height = math.floor(vim.o.lines * (config.float.height or 0.8))
54
+ local col = math.floor((vim.o.columns - width) / 2)
55
+ local row = math.floor((vim.o.lines - height) / 2)
56
+
57
+ local b = vim.api.nvim_create_buf(false, true)
58
+ local w = vim.api.nvim_open_win(b, true, {
59
+ relative = "editor",
60
+ width = width,
61
+ height = height,
62
+ col = col,
63
+ row = row,
64
+ style = "minimal",
65
+ border = "rounded",
66
+ title = " jcf " .. table.concat(args, " ") .. " ",
67
+ title_pos = "center",
68
+ })
69
+
70
+ local cmd_table = { binary, unpack(args) }
71
+ vim.fn.termopen(cmd_table, {
72
+ on_exit = function()
73
+ vim.defer_fn(function()
74
+ if w and vim.api.nvim_win_is_valid(w) then
75
+ vim.api.nvim_win_close(w, true)
76
+ end
77
+ end, 500)
78
+ end,
79
+ })
80
+ vim.cmd("startinsert")
81
+ end
82
+
83
+ function M.is_open()
84
+ return win_id ~= nil and vim.api.nvim_win_is_valid(win_id)
85
+ end
86
+
87
+ return M
@@ -0,0 +1,70 @@
1
+ local M = {}
2
+
3
+ local defaults = {
4
+ binary = "jcf",
5
+ state_path = vim.fn.expand("~/.jcf/state.json"),
6
+ float = { width = 0.8, height = 0.8 },
7
+ auto_detect_branch = true,
8
+ poll_interval = 30000, -- ms, check Clockify for external changes
9
+ }
10
+
11
+ M.config = defaults
12
+
13
+ function M.setup(opts)
14
+ M.config = vim.tbl_deep_extend("force", defaults, opts or {})
15
+
16
+ vim.api.nvim_create_user_command("JcfToggle", function()
17
+ require("jcf.float").toggle(M.config)
18
+ end, { desc = "Toggle jcf floating window" })
19
+
20
+ vim.api.nvim_create_user_command("JcfStart", function(args)
21
+ local key = args.args ~= "" and args.args or nil
22
+ if not key and M.config.auto_detect_branch then
23
+ key = require("jcf.branch").detect()
24
+ end
25
+ if key then
26
+ vim.fn.jobstart({ M.config.binary, "start", key }, { detach = true })
27
+ vim.notify("jcf: starting timer on " .. key, vim.log.levels.INFO)
28
+ else
29
+ -- No key and no branch match → open jcf start in float (has built-in fuzzy selector)
30
+ require("jcf.float").run_command(M.config, "start")
31
+ end
32
+ end, { nargs = "?", desc = "Start jcf timer" })
33
+
34
+ vim.api.nvim_create_user_command("JcfStop", function()
35
+ -- Open in float so the comment prompt is interactive
36
+ require("jcf.float").run_command(M.config, "stop")
37
+ end, { desc = "Stop jcf timer" })
38
+
39
+ vim.api.nvim_create_user_command("JcfDiscard", function()
40
+ require("jcf.float").run_command(M.config, "discard")
41
+ end, { desc = "Discard jcf timer (delete Clockify entry)" })
42
+
43
+ vim.api.nvim_create_user_command("JcfLog", function(args)
44
+ local cmd_args = { "log" }
45
+ if args.args ~= "" then
46
+ for word in args.args:gmatch("%S+") do
47
+ table.insert(cmd_args, word)
48
+ end
49
+ end
50
+ require("jcf.float").run_command(M.config, unpack(cmd_args))
51
+ end, { nargs = "*", desc = "Log past work manually" })
52
+
53
+ vim.api.nvim_create_user_command("JcfEdit", function()
54
+ require("jcf.float").run_command(M.config, "edit")
55
+ end, { desc = "Edit running timer" })
56
+
57
+ vim.api.nvim_create_user_command("JcfStatus", function()
58
+ require("jcf.float").run_command(M.config, "status")
59
+ end, { desc = "Show timer status" })
60
+
61
+ -- Start polling for external timer changes
62
+ require("jcf.state").start_poll(M.config, M.config.poll_interval)
63
+
64
+ -- Clean up poll timer on exit
65
+ vim.api.nvim_create_autocmd("VimLeave", {
66
+ callback = function() require("jcf.state").stop_poll() end
67
+ })
68
+ end
69
+
70
+ return M
@@ -0,0 +1,56 @@
1
+ local M = {}
2
+ local uv = vim.loop or vim.uv
3
+ local cached = { active = false }
4
+ local last_mtime = 0
5
+ local poll_timer = nil
6
+
7
+ function M.read(state_path)
8
+ local path = state_path or vim.fn.expand("~/.jcf/state.json")
9
+ local stat = uv.fs_stat(path)
10
+ if not stat then
11
+ return cached
12
+ end
13
+ if stat.mtime.sec == last_mtime then
14
+ return cached
15
+ end
16
+ last_mtime = stat.mtime.sec
17
+ local f = io.open(path, "r")
18
+ if f then
19
+ local content = f:read("*a")
20
+ f:close()
21
+ local ok, data = pcall(vim.json.decode, content)
22
+ if ok and data then
23
+ cached = data
24
+ end
25
+ end
26
+ return cached
27
+ end
28
+
29
+ -- Periodically run `jcf status` to sync state file with Clockify
30
+ -- This detects externally stopped timers
31
+ function M.start_poll(config, interval_ms)
32
+ if poll_timer then
33
+ return
34
+ end
35
+ interval_ms = interval_ms or 30000 -- 30s default
36
+
37
+ poll_timer = uv.new_timer()
38
+ poll_timer:start(interval_ms, interval_ms, vim.schedule_wrap(function()
39
+ -- jcf status updates ~/.jcf/state.json and detects external stops
40
+ vim.fn.jobstart({ config.binary or "jcf", "status" }, {
41
+ on_stdout = function() end,
42
+ on_stderr = function() end,
43
+ detach = true,
44
+ })
45
+ end))
46
+ end
47
+
48
+ function M.stop_poll()
49
+ if poll_timer then
50
+ poll_timer:stop()
51
+ poll_timer:close()
52
+ poll_timer = nil
53
+ end
54
+ end
55
+
56
+ return M
@@ -0,0 +1,31 @@
1
+ local state = require("jcf.state")
2
+
3
+ local M = {}
4
+
5
+ function M.status()
6
+ local config = require("jcf").config
7
+ local s = state.read(config.state_path)
8
+ if not s.active then
9
+ return ""
10
+ end
11
+ local started = s.startedAt_unix or 0
12
+ if started == 0 then
13
+ return s.ticketKey or "??"
14
+ end
15
+ local elapsed = s.elapsed or (os.time() - started)
16
+ local h = math.floor(elapsed / 3600)
17
+ local m = math.floor((elapsed % 3600) / 60)
18
+ local sec = elapsed % 60
19
+ local summary = s.summary or ""
20
+ if #summary > 30 then
21
+ summary = summary:sub(1, 30) .. "…"
22
+ end
23
+ return string.format("● %s %s %02d:%02d:%02d", s.ticketKey or "??", summary, h, m, sec)
24
+ end
25
+
26
+ function M.is_active()
27
+ local config = require("jcf").config
28
+ return state.read(config.state_path).active == true
29
+ end
30
+
31
+ return M
@@ -0,0 +1,50 @@
1
+ local M = {}
2
+
3
+ function M.pick(config)
4
+ local ok, pickers = pcall(require, "telescope.pickers")
5
+ if not ok then
6
+ vim.notify("jcf: telescope.nvim not installed", vim.log.levels.WARN)
7
+ return
8
+ end
9
+
10
+ local finders = require("telescope.finders")
11
+ local conf = require("telescope.config").values
12
+ local actions = require("telescope.actions")
13
+ local action_state = require("telescope.actions.state")
14
+
15
+ -- Get tickets via jcf list --json
16
+ local result = vim.fn.system({ config.binary, "list", "--json" })
17
+ local tickets_ok, tickets = pcall(vim.json.decode, result)
18
+ if not tickets_ok or not tickets then
19
+ vim.notify("jcf: failed to get tickets", vim.log.levels.ERROR)
20
+ return
21
+ end
22
+
23
+ pickers.new({}, {
24
+ prompt_title = "jcf - Select Ticket",
25
+ finder = finders.new_table({
26
+ results = tickets,
27
+ entry_maker = function(entry)
28
+ return {
29
+ value = entry,
30
+ display = string.format("%s %s [%s]", entry.key or "?", entry.summary or "", entry.status or ""),
31
+ ordinal = (entry.key or "") .. " " .. (entry.summary or ""),
32
+ }
33
+ end,
34
+ }),
35
+ sorter = conf.generic_sorter({}),
36
+ attach_mappings = function(prompt_bufnr)
37
+ actions.select_default:replace(function()
38
+ actions.close(prompt_bufnr)
39
+ local selection = action_state.get_selected_entry()
40
+ if selection and selection.value and selection.value.key then
41
+ vim.fn.jobstart({ config.binary, "start", selection.value.key }, { detach = true })
42
+ vim.notify("jcf: starting timer on " .. selection.value.key, vim.log.levels.INFO)
43
+ end
44
+ end)
45
+ return true
46
+ end,
47
+ }):find()
48
+ end
49
+
50
+ return M
@@ -0,0 +1,2 @@
1
+ " jcf.nvim - Jira Clockify Timer for Neovim
2
+ " Auto-loads if user has called require("jcf").setup()
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@knpkv/jira-clockify",
3
+ "version": "0.2.0",
4
+ "description": "TUI for Jira-Clockify time tracking, attachable to neovim",
5
+ "license": "MIT",
6
+ "author": "knpkv",
7
+ "type": "module",
8
+ "bin": {
9
+ "jcf": "dist/src/bin.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "nvim"
17
+ ],
18
+ "dependencies": {
19
+ "@effect-atom/atom": "latest",
20
+ "@effect-atom/atom-react": "latest",
21
+ "@effect/cli": "latest",
22
+ "@effect/platform": "latest",
23
+ "@effect/platform-node": "latest",
24
+ "@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@367a94087821b3b5feedd35bbb57df43b10a286e",
25
+ "@opentui/react": "https://pkg.pr.new/anomalyco/opentui/@opentui/react@367a94087821b3b5feedd35bbb57df43b10a286e",
26
+ "effect": "latest",
27
+ "react": "^19.1.0",
28
+ "@knpkv/atlassian-common": "^0.2.0",
29
+ "@knpkv/clockify-api-client": "^0.2.0",
30
+ "@knpkv/jira-api-client": "^0.2.0",
31
+ "@knpkv/jira-cli": "^0.1.1"
32
+ },
33
+ "devDependencies": {
34
+ "@effect/vitest": "latest",
35
+ "@types/node": "latest",
36
+ "@types/react": "^19.1.8",
37
+ "tsx": "^4.19.4",
38
+ "vitest": "latest"
39
+ },
40
+ "scripts": {
41
+ "start": "tsx src/bin.ts",
42
+ "build": "tsc -b && chmod +x dist/src/bin.js",
43
+ "check": "tsc -b tsconfig.json",
44
+ "test": "vitest",
45
+ "lint": "eslint \"{src,test}/**/*.{ts,tsx}\""
46
+ }
47
+ }