git-juggler 0.2.2__tar.gz → 0.2.3__tar.gz

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 (190) hide show
  1. {git_juggler-0.2.2 → git_juggler-0.2.3}/.gitignore +2 -0
  2. {git_juggler-0.2.2 → git_juggler-0.2.3}/PKG-INFO +2 -2
  3. {git_juggler-0.2.2 → git_juggler-0.2.3}/README.md +1 -1
  4. git_juggler-0.2.3/src/backend/git_juggler/agents/__init__.py +1 -0
  5. git_juggler-0.2.2/src/backend/git_juggler/agent_hook_events.py → git_juggler-0.2.3/src/backend/git_juggler/agents/hook_events.py +6 -6
  6. git_juggler-0.2.3/src/backend/git_juggler/api/__init__.py +1 -0
  7. git_juggler-0.2.3/src/backend/git_juggler/api/agents.py +97 -0
  8. git_juggler-0.2.3/src/backend/git_juggler/api/ci.py +97 -0
  9. git_juggler-0.2.3/src/backend/git_juggler/api/config.py +114 -0
  10. git_juggler-0.2.3/src/backend/git_juggler/api/git.py +77 -0
  11. git_juggler-0.2.3/src/backend/git_juggler/api/repos.py +26 -0
  12. git_juggler-0.2.3/src/backend/git_juggler/api/terminal.py +31 -0
  13. git_juggler-0.2.3/src/backend/git_juggler/app.py +51 -0
  14. git_juggler-0.2.2/src/backend/git_juggler/ci.py → git_juggler-0.2.3/src/backend/git_juggler/ci/__init__.py +1 -1
  15. {git_juggler-0.2.2/src/backend/git_juggler → git_juggler-0.2.3/src/backend/git_juggler/ci}/github_actions.py +3 -12
  16. git_juggler-0.2.3/src/backend/git_juggler/ci/http.py +28 -0
  17. {git_juggler-0.2.2/src/backend/git_juggler → git_juggler-0.2.3/src/backend/git_juggler/ci}/jenkins.py +4 -15
  18. git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/assets/index-BGVpgOst.css +1 -0
  19. git_juggler-0.2.2/src/backend/git_juggler/frontend_dist/assets/index-XfKcnwHm.js → git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/assets/index-C1_44PRZ.js +15 -15
  20. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/index.html +2 -2
  21. git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/logo.png +0 -0
  22. git_juggler-0.2.3/src/backend/git_juggler/git/__init__.py +1 -0
  23. git_juggler-0.2.3/src/backend/git_juggler/git/command.py +50 -0
  24. git_juggler-0.2.2/src/backend/git_juggler/git_data.py → git_juggler-0.2.3/src/backend/git_juggler/git/data.py +19 -254
  25. git_juggler-0.2.2/src/backend/git_juggler/commit_detail.py → git_juggler-0.2.3/src/backend/git_juggler/git/detail.py +6 -5
  26. git_juggler-0.2.2/src/backend/git_juggler/file_diff.py → git_juggler-0.2.3/src/backend/git_juggler/git/diff.py +1 -1
  27. git_juggler-0.2.3/src/backend/git_juggler/git/refs.py +66 -0
  28. git_juggler-0.2.3/src/backend/git_juggler/git/status.py +146 -0
  29. git_juggler-0.2.2/src/backend/git_juggler/repos.py → git_juggler-0.2.3/src/backend/git_juggler/repo_discovery.py +1 -1
  30. git_juggler-0.2.3/src/backend/git_juggler/ui/__init__.py +1 -0
  31. git_juggler-0.2.2/src/backend/git_juggler/pick_folder.py → git_juggler-0.2.3/src/backend/git_juggler/ui/folder_picker.py +26 -1
  32. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_agent_hooks.py +11 -11
  33. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_agent_tracking.py +1 -1
  34. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_ci.py +2 -1
  35. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_ci_poll.py +2 -1
  36. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_ci_poll_route.py +8 -3
  37. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_claude_sessions_data.py +5 -5
  38. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_file_diff.py +3 -3
  39. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_git_data.py +6 -6
  40. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_github_actions.py +2 -2
  41. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_jenkins.py +1 -1
  42. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_opencode_sessions.py +9 -9
  43. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_pick_folder.py +1 -1
  44. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_repos.py +1 -1
  45. git_juggler-0.2.3/src/frontend/public/logo.png +0 -0
  46. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/App.css +11 -11
  47. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Badges/BranchBadge.tsx +1 -1
  48. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Badges/StashBadge.tsx +1 -1
  49. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Badges/TagBadge.tsx +1 -1
  50. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Badges/ciStatus.tsx +1 -20
  51. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Search/MultiSelect.tsx +2 -3
  52. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Search/SearchBox.tsx +3 -9
  53. git_juggler-0.2.3/src/frontend/src/components/icons.tsx +71 -0
  54. git_juggler-0.2.2/plans/github-actions-settings-and-build-status.md +0 -449
  55. git_juggler-0.2.2/plans/jenkins-settings-and-build-status.md +0 -351
  56. git_juggler-0.2.2/src/backend/git_juggler/app.py +0 -402
  57. git_juggler-0.2.2/src/backend/git_juggler/browse.py +0 -27
  58. git_juggler-0.2.2/src/backend/git_juggler/frontend_dist/assets/index--rgc3DQg.css +0 -1
  59. git_juggler-0.2.2/src/frontend/src/components/Badges/icons.tsx +0 -32
  60. {git_juggler-0.2.2 → git_juggler-0.2.3}/.github/workflows/build-package.yml +0 -0
  61. {git_juggler-0.2.2 → git_juggler-0.2.3}/CLAUDE.md +0 -0
  62. {git_juggler-0.2.2 → git_juggler-0.2.3}/LICENSE +0 -0
  63. {git_juggler-0.2.2 → git_juggler-0.2.3}/notes.txt +0 -0
  64. {git_juggler-0.2.2 → git_juggler-0.2.3}/pyproject.toml +0 -0
  65. {git_juggler-0.2.2 → git_juggler-0.2.3}/scripts/build-package.sh +0 -0
  66. {git_juggler-0.2.2 → git_juggler-0.2.3}/scripts/dev.sh +0 -0
  67. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/__init__.py +0 -0
  68. /git_juggler-0.2.2/src/backend/git_juggler/agent_hooks.py → /git_juggler-0.2.3/src/backend/git_juggler/agents/hooks.py +0 -0
  69. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/__init__.py +0 -0
  70. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/activity_models.py +0 -0
  71. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/claude_sessions.py +0 -0
  72. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/claude_transcripts.py +0 -0
  73. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/git_resolver.py +0 -0
  74. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/opencode_sessions.py +0 -0
  75. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/paths.py +0 -0
  76. {git_juggler-0.2.2/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/text.py +0 -0
  77. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/config.py +0 -0
  78. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/dev_app.py +0 -0
  79. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
  80. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
  81. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
  82. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/favicon.svg +0 -0
  83. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
  84. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/icons.svg +0 -0
  85. /git_juggler-0.2.2/src/backend/git_juggler/git_utils.py → /git_juggler-0.2.3/src/backend/git_juggler/git/utils.py +0 -0
  86. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/main.py +0 -0
  87. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/schemas.py +0 -0
  88. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/terminal.py +0 -0
  89. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/git_juggler/themes.py +0 -0
  90. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_preferences.py +0 -0
  91. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/backend/tests/test_themes.py +0 -0
  92. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/.gitignore +0 -0
  93. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/README.md +0 -0
  94. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/index.html +0 -0
  95. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/package-lock.json +0 -0
  96. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/package.json +0 -0
  97. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/public/agent-logos/claude.png +0 -0
  98. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/public/agent-logos/opencode.webp +0 -0
  99. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/public/ci-logos/jenkins.webp +0 -0
  100. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/public/favicon.svg +0 -0
  101. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/public/git-juggler.png +0 -0
  102. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/public/icons.svg +0 -0
  103. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/App.tsx +0 -0
  104. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/api/client.ts +0 -0
  105. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/api/types.ts +0 -0
  106. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -0
  107. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
  108. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +0 -0
  109. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Agents/agentFormat.ts +0 -0
  110. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
  111. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
  112. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/CIRunPopover.tsx +0 -0
  113. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
  114. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
  115. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
  116. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
  117. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
  118. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
  119. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
  120. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
  121. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
  122. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
  123. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
  124. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
  125. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
  126. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Graph/GraphPanel.tsx +0 -0
  127. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Graph/branchColor.ts +0 -0
  128. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
  129. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
  130. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
  131. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
  132. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
  133. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
  134. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
  135. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -0
  136. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
  137. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
  138. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
  139. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
  140. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -0
  141. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Pipelines/StageStrip.tsx +0 -0
  142. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
  143. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
  144. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Sidebar/RepoList.tsx +0 -0
  145. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Sidebar/Sidebar.tsx +0 -0
  146. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
  147. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
  148. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
  149. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
  150. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
  151. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/inputs/TextField.tsx +0 -0
  152. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
  153. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/components/inputs/styles.ts +0 -0
  154. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/index.css +0 -0
  155. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/index.tsx +0 -0
  156. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/agentStatusPopoverData.ts +0 -0
  157. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/appTheme.ts +0 -0
  158. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/branchVisibility.ts +0 -0
  159. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/bundledThemes.ts +0 -0
  160. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/ciPoll.ts +0 -0
  161. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/ciRunPopoverData.ts +0 -0
  162. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/color.ts +0 -0
  163. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
  164. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/flip.ts +0 -0
  165. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/formatDate.ts +0 -0
  166. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/preferenceSender.ts +0 -0
  167. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/scrollToCommit.ts +0 -0
  168. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/sideBySide.ts +0 -0
  169. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/stageWindow.ts +0 -0
  170. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/stripAnsi.ts +0 -0
  171. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/swallowNextClick.ts +0 -0
  172. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/lib/vscodeTheme.ts +0 -0
  173. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/state/overlayStack.ts +0 -0
  174. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/state/preferenceSync.ts +0 -0
  175. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/state/preferences.ts +0 -0
  176. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/state/store.ts +0 -0
  177. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/src/state/themes.ts +0 -0
  178. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/agentFormat.test.ts +0 -0
  179. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/agentStatusPopoverData.test.ts +0 -0
  180. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/ciPoll.test.ts +0 -0
  181. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
  182. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/computeColumns.test.ts +0 -0
  183. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/preferenceSender.test.ts +0 -0
  184. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/sideBySide.test.ts +0 -0
  185. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/stageWindow.test.ts +0 -0
  186. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tests/vscodeTheme.test.ts +0 -0
  187. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tsconfig.app.json +0 -0
  188. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tsconfig.json +0 -0
  189. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/tsconfig.node.json +0 -0
  190. {git_juggler-0.2.2 → git_juggler-0.2.3}/src/frontend/vite.config.ts +0 -0
@@ -27,3 +27,5 @@ secrets.env
27
27
 
28
28
  # Personal task notes (see CLAUDE.md)
29
29
  todo.txt
30
+
31
+ plans/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-juggler
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A local Git dashboard for people juggling agents and too many repos.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -11,7 +11,7 @@ Requires-Dist: pywinpty>=2.0; sys_platform == 'win32'
11
11
  Requires-Dist: uvicorn[standard]>=0.29
12
12
  Description-Content-Type: text/markdown
13
13
 
14
- # git-juggler
14
+ <img src="https://github.com/franz38/git-juggler/blob/main/src/frontend/public/logo.png?raw=true" alt="git-juggler" width="400">
15
15
 
16
16
  A local Git dashboard for people juggling agents and too many repos.
17
17
 
@@ -1,4 +1,4 @@
1
- # git-juggler
1
+ <img src="https://github.com/franz38/git-juggler/blob/main/src/frontend/public/logo.png?raw=true" alt="git-juggler" width="400">
2
2
 
3
3
  A local Git dashboard for people juggling agents and too many repos.
4
4
 
@@ -0,0 +1 @@
1
+ """Agent hook and activity tracking helpers."""
@@ -12,12 +12,12 @@ from dataclasses import dataclass, replace
12
12
  from pathlib import Path
13
13
  from typing import Any
14
14
 
15
- from .agent_hooks import EVENT_PATH
16
- from .agent_tracking.activity_models import ActivityEvidence, AgentRepositoryScan, AgentWorktreeActivity, SessionDetails
17
- from .agent_tracking.claude_sessions import CLAUDE_SESSIONS_DIR, ClaudeSession, read_registry, registry_signature
18
- from .agent_tracking.claude_transcripts import CLAUDE_PROJECTS_DIR, TranscriptInfo, find_transcript, read_transcript_info, transcript_signature
19
- from .agent_tracking.git_resolver import GitResolver, GitWorktreeInfo
20
- from .agent_tracking.opencode_sessions import OPENCODE_DB_PATH, OpenCodeSession, db_signature, read_session
15
+ from .hooks import EVENT_PATH
16
+ from .tracking.activity_models import ActivityEvidence, AgentRepositoryScan, AgentWorktreeActivity, SessionDetails
17
+ from .tracking.claude_sessions import CLAUDE_SESSIONS_DIR, ClaudeSession, read_registry, registry_signature
18
+ from .tracking.claude_transcripts import CLAUDE_PROJECTS_DIR, TranscriptInfo, find_transcript, read_transcript_info, transcript_signature
19
+ from .tracking.git_resolver import GitResolver, GitWorktreeInfo
20
+ from .tracking.opencode_sessions import OPENCODE_DB_PATH, OpenCodeSession, db_signature, read_session
21
21
 
22
22
 
23
23
  HOOK_ACTIVITY_TTL_MS = 120_000
@@ -0,0 +1 @@
1
+ """FastAPI route modules."""
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from ..agents.hook_events import AgentHookEventReader
6
+ from ..agents.hooks import hooks_status, install_claude_hooks, install_opencode_hooks
7
+ from ..agents.tracking.activity_models import AgentRepositoryScan
8
+ from ..schemas import (
9
+ AgentActivityResponse,
10
+ AgentHookProviderStatusResponse,
11
+ AgentHooksResponse,
12
+ AgentRepositoryScanResponse,
13
+ )
14
+
15
+
16
+ router = APIRouter()
17
+ hook_event_reader = AgentHookEventReader()
18
+
19
+
20
+ def _agent_scan_response(scan: AgentRepositoryScan) -> AgentRepositoryScanResponse:
21
+ return AgentRepositoryScanResponse(
22
+ agent_pid=scan.agent_pid,
23
+ session_directory=scan.session_directory,
24
+ worktrees=[
25
+ {
26
+ "repository_id": activity.repository_id,
27
+ "worktree_path": activity.worktree_path,
28
+ "branch": activity.branch,
29
+ "commit": activity.commit,
30
+ "process_ids": activity.process_ids,
31
+ "first_seen": activity.first_seen,
32
+ "last_seen": activity.last_seen,
33
+ "last_activity": activity.last_activity,
34
+ "evidence": [evidence.__dict__ for evidence in activity.evidence],
35
+ "activity_score": activity.activity_score,
36
+ "state": activity.state,
37
+ "is_home": activity.is_home,
38
+ }
39
+ for activity in scan.worktrees
40
+ ],
41
+ scanned_at=scan.scanned_at,
42
+ state=scan.state,
43
+ provider=scan.provider,
44
+ session_id=scan.session_id,
45
+ process_pid=scan.process_pid,
46
+ name=scan.name,
47
+ details=scan.details.__dict__ if scan.details is not None else None,
48
+ waiting_for=scan.waiting_for,
49
+ )
50
+
51
+
52
+ def _hook_status_response(status) -> AgentHookProviderStatusResponse:
53
+ return AgentHookProviderStatusResponse(**status.__dict__)
54
+
55
+
56
+ @router.get("/api/agents/activity", response_model=AgentActivityResponse)
57
+ def api_agent_activity() -> AgentActivityResponse:
58
+ hook_scans = hook_event_reader.recent_scans()
59
+ scans = [_agent_scan_response(scan) for scan in hook_scans]
60
+ scanned_at = max((scan.scanned_at for scan in scans), default=0)
61
+ return AgentActivityResponse(
62
+ agents=[
63
+ {
64
+ "pid": scan.agent_pid,
65
+ "command_line": "hook activity",
66
+ "matched_pattern": "hook",
67
+ }
68
+ for scan in hook_scans
69
+ ],
70
+ scans=scans,
71
+ scanned_at=scanned_at,
72
+ )
73
+
74
+
75
+ @router.get("/api/agents/hooks", response_model=AgentHooksResponse)
76
+ def api_agent_hooks() -> AgentHooksResponse:
77
+ statuses = hooks_status()
78
+ return AgentHooksResponse(
79
+ claude=_hook_status_response(statuses["claude"]),
80
+ opencode=_hook_status_response(statuses["opencode"]),
81
+ )
82
+
83
+
84
+ @router.post(
85
+ "/api/agents/hooks/claude/install",
86
+ response_model=AgentHookProviderStatusResponse,
87
+ )
88
+ def api_install_claude_hooks() -> AgentHookProviderStatusResponse:
89
+ return _hook_status_response(install_claude_hooks())
90
+
91
+
92
+ @router.post(
93
+ "/api/agents/hooks/opencode/install",
94
+ response_model=AgentHookProviderStatusResponse,
95
+ )
96
+ def api_install_opencode_hooks() -> AgentHookProviderStatusResponse:
97
+ return _hook_status_response(install_opencode_hooks())
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, HTTPException, Query
7
+
8
+ from .. import config
9
+ from ..ci import github_actions, jenkins
10
+ from ..ci import get_active_pipelines, get_ci_run_stages, get_ci_runs, poll_ci_runs
11
+ from ..git.data import get_commit_hashes
12
+ from ..repo_discovery import list_repos, resolve_repo_path
13
+ from ..schemas import (
14
+ ActivePipeline,
15
+ CiConnectionTestResponse,
16
+ CiRunInfo,
17
+ CiStage,
18
+ GitHubConfig,
19
+ JenkinsConfig,
20
+ )
21
+
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ def _resolve_repo_path(repo_id: str) -> Path:
27
+ path = resolve_repo_path(config.load_repo_paths(), repo_id)
28
+ if path is None:
29
+ raise HTTPException(status_code=404, detail="repo not found")
30
+ return path
31
+
32
+
33
+ @router.get("/api/repos/{repo_id}/ci/runs", response_model=dict[str, list[CiRunInfo]])
34
+ def api_ci_runs(repo_id: str) -> dict[str, list[CiRunInfo]]:
35
+ path = _resolve_repo_path(repo_id)
36
+ return get_ci_runs(
37
+ path,
38
+ get_commit_hashes(path),
39
+ config.load_github_config(),
40
+ config.load_jenkins_config(),
41
+ )
42
+
43
+
44
+ # Live poll of a repo's pipelines. Without `run`, every active run of the
45
+ # repo (`head_sha` narrows it where the provider can filter): used right
46
+ # after a commit action, when the run may not be known yet. With one or more
47
+ # `run=<provider>:<run_id>` (CiRunInfo.provider / run_id), only those runs,
48
+ # including their final status once finished.
49
+ @router.get("/api/repos/{repo_id}/ci/poll", response_model=list[CiRunInfo])
50
+ def api_ci_poll(
51
+ repo_id: str,
52
+ run: Annotated[list[str] | None, Query()] = None,
53
+ head_sha: str | None = None,
54
+ ) -> list[CiRunInfo]:
55
+ path = _resolve_repo_path(repo_id)
56
+ return poll_ci_runs(
57
+ path, run, head_sha, config.load_github_config(), config.load_jenkins_config()
58
+ )
59
+
60
+
61
+ # Stages (GitHub jobs / Jenkins pipeline stages) of one run. `run_id` is the
62
+ # value CiRunInfo.run_id carried; 404 when the provider has no stage data.
63
+ @router.get("/api/repos/{repo_id}/ci/stages", response_model=list[CiStage])
64
+ def api_ci_stages(repo_id: str, provider: str, run_id: str) -> list[CiStage]:
65
+ path = _resolve_repo_path(repo_id)
66
+ stages = get_ci_run_stages(
67
+ path,
68
+ provider,
69
+ run_id,
70
+ config.load_github_config(),
71
+ config.load_jenkins_config(),
72
+ )
73
+ if stages is None:
74
+ raise HTTPException(status_code=404, detail="stages not available")
75
+ return stages
76
+
77
+
78
+ # Queued/running pipelines across all repos, for the Pipelines tab.
79
+ @router.get("/api/ci/active", response_model=list[ActivePipeline])
80
+ def api_ci_active() -> list[ActivePipeline]:
81
+ return get_active_pipelines(
82
+ list_repos(config.load_repo_paths()),
83
+ config.load_github_config(),
84
+ config.load_jenkins_config(),
85
+ )
86
+
87
+
88
+ @router.post("/api/ci/github/test", response_model=CiConnectionTestResponse)
89
+ def api_test_github_connection(body: GitHubConfig) -> CiConnectionTestResponse:
90
+ ok, message = github_actions.test_connection(body.model_dump())
91
+ return CiConnectionTestResponse(ok=ok, message=message)
92
+
93
+
94
+ @router.post("/api/ci/jenkins/test", response_model=CiConnectionTestResponse)
95
+ def api_test_jenkins_connection(body: JenkinsConfig) -> CiConnectionTestResponse:
96
+ ok, message = jenkins.test_connection(body.model_dump())
97
+ return CiConnectionTestResponse(ok=ok, message=message)
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+
8
+ from .. import config
9
+ from ..schemas import (
10
+ ConfigResponse,
11
+ ConfigUpdateRequest,
12
+ PickFolderResponse,
13
+ Preferences,
14
+ ThemesResponse,
15
+ VscodeTheme,
16
+ )
17
+ from ..themes import discover_themes
18
+ from ..ui.folder_picker import NativePickerUnavailable, pick_folder
19
+
20
+
21
+ def create_config_router(root_path: Path) -> APIRouter:
22
+ router = APIRouter()
23
+ terminal_shell = "cmd" if sys.platform == "win32" else "posix"
24
+
25
+ def _current_config() -> ConfigResponse:
26
+ return ConfigResponse(
27
+ repo_paths=[str(p) for p in config.load_repo_paths()],
28
+ pinned_repo_paths=config.load_pinned_repo_paths(),
29
+ repo_groups=config.load_repo_groups(),
30
+ excluded_paths=config.load_excluded_paths(),
31
+ github=config.load_github_config(),
32
+ jenkins=config.load_jenkins_config(),
33
+ terminal_shell=terminal_shell,
34
+ )
35
+
36
+ @router.get("/api/config", response_model=ConfigResponse)
37
+ def api_get_config() -> ConfigResponse:
38
+ return _current_config()
39
+
40
+ @router.put("/api/config", response_model=ConfigResponse)
41
+ def api_update_config(body: ConfigUpdateRequest) -> ConfigResponse:
42
+ if body.repo_paths is not None:
43
+ resolved: list[Path] = []
44
+ seen: set[str] = set()
45
+ for raw in body.repo_paths:
46
+ path = Path(raw).expanduser().resolve()
47
+ if not path.is_dir():
48
+ raise HTTPException(status_code=400, detail=f"not a directory: {raw}")
49
+ key = str(path)
50
+ if key in seen:
51
+ continue
52
+ seen.add(key)
53
+ resolved.append(path)
54
+ config.save_repo_paths(resolved)
55
+
56
+ if body.pinned_repo_paths is not None:
57
+ deduped = list(dict.fromkeys(body.pinned_repo_paths))
58
+ config.save_pinned_repo_paths(deduped)
59
+
60
+ if body.repo_groups is not None:
61
+ config.save_repo_groups([group.model_dump() for group in body.repo_groups])
62
+
63
+ if body.excluded_paths is not None:
64
+ config.save_excluded_paths(body.excluded_paths)
65
+
66
+ if body.github is not None:
67
+ config.save_github_config(body.github.model_dump())
68
+
69
+ if body.jenkins is not None:
70
+ config.save_jenkins_config(body.jenkins.model_dump())
71
+
72
+ return _current_config()
73
+
74
+ # Wipes every stored setting back to a first-run state. App settings only,
75
+ # nothing here touches a repo.
76
+ @router.post("/api/config/reset", response_model=ConfigResponse)
77
+ def api_reset_config() -> ConfigResponse:
78
+ config.reset_to_factory(root_path)
79
+ return _current_config()
80
+
81
+ @router.post("/api/pick-folder", response_model=PickFolderResponse)
82
+ def api_pick_folder() -> PickFolderResponse:
83
+ try:
84
+ return PickFolderResponse(path=pick_folder())
85
+ except NativePickerUnavailable as e:
86
+ raise HTTPException(status_code=501, detail=str(e))
87
+
88
+ def _current_themes() -> ThemesResponse:
89
+ imported = [VscodeTheme(**theme) for theme in config.load_imported_themes()]
90
+ return ThemesResponse(installed=discover_themes(), imported=imported)
91
+
92
+ @router.get("/api/themes", response_model=ThemesResponse)
93
+ def api_get_themes() -> ThemesResponse:
94
+ return _current_themes()
95
+
96
+ # Stores theme *files* the user imported in the UI (app settings, not repo
97
+ # state, so this doesn't fall under the git-mutations-via-terminal rule).
98
+ @router.put("/api/themes/imported", response_model=ThemesResponse)
99
+ def api_put_imported_themes(body: list[VscodeTheme]) -> ThemesResponse:
100
+ config.save_imported_themes([theme.model_dump() for theme in body])
101
+ return _current_themes()
102
+
103
+ # UI preferences (theme, pinned themes, key bindings, agent settings, ...)
104
+ # kept server-side so every browser shows the same setup. App settings only,
105
+ # nothing here touches a repo.
106
+ @router.get("/api/preferences", response_model=Preferences)
107
+ def api_get_preferences() -> Preferences:
108
+ return config.load_preferences()
109
+
110
+ @router.put("/api/preferences", response_model=Preferences)
111
+ def api_put_preferences(body: Preferences) -> Preferences:
112
+ return config.update_preferences(body.model_dump(mode="json", exclude_unset=True))
113
+
114
+ return router
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from fastapi import APIRouter, HTTPException, Query
6
+
7
+ from .. import config
8
+ from ..git.data import GRAPH_PAGE_SIZE, HistoryChangedError, get_graph, get_repo_status
9
+ from ..git.detail import get_commit_detail
10
+ from ..git.diff import get_commit_file_diff, get_working_file_diff
11
+ from ..repo_discovery import resolve_repo_path
12
+ from ..schemas import CommitDetail, FileDiff, GraphResponse, RepoStatusResponse
13
+
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ def _resolve_repo_path(repo_id: str) -> Path:
19
+ path = resolve_repo_path(config.load_repo_paths(), repo_id)
20
+ if path is None:
21
+ raise HTTPException(status_code=404, detail="repo not found")
22
+ return path
23
+
24
+
25
+ @router.get("/api/repos/{repo_id}/graph", response_model=GraphResponse)
26
+ def api_graph(
27
+ repo_id: str,
28
+ before: str | None = None,
29
+ limit: int = Query(GRAPH_PAGE_SIZE, ge=1, le=10 * GRAPH_PAGE_SIZE),
30
+ ) -> GraphResponse:
31
+ path = _resolve_repo_path(repo_id)
32
+ try:
33
+ return GraphResponse(**get_graph(path, limit=limit, before=before)._asdict())
34
+ except HistoryChangedError as exc:
35
+ # The cursor commit is gone (history rewritten): the client should reload from the first page.
36
+ raise HTTPException(status_code=409, detail="graph history changed") from exc
37
+
38
+
39
+ @router.get("/api/repos/{repo_id}/status", response_model=RepoStatusResponse)
40
+ def api_repo_status(repo_id: str) -> RepoStatusResponse:
41
+ path = _resolve_repo_path(repo_id)
42
+ return get_repo_status(path)
43
+
44
+
45
+ @router.get("/api/repos/{repo_id}/commits/{sha}", response_model=CommitDetail)
46
+ def api_commit_detail(repo_id: str, sha: str) -> CommitDetail:
47
+ path = _resolve_repo_path(repo_id)
48
+ try:
49
+ return get_commit_detail(path, sha)
50
+ except Exception as exc: # noqa: BLE001 - surfaced as a 404 either way
51
+ raise HTTPException(status_code=404, detail="commit not found") from exc
52
+
53
+
54
+ @router.get("/api/repos/{repo_id}/commits/{sha}/diff", response_model=FileDiff)
55
+ def api_commit_file_diff(
56
+ repo_id: str,
57
+ sha: str,
58
+ path: str,
59
+ old_path: str | None = None,
60
+ full: bool = False,
61
+ ) -> FileDiff:
62
+ repo_path = _resolve_repo_path(repo_id)
63
+ try:
64
+ return get_commit_file_diff(repo_path, sha, path, old_path, full)
65
+ except Exception as exc: # noqa: BLE001 - surfaced as a 404 either way
66
+ raise HTTPException(status_code=404, detail="diff not found") from exc
67
+
68
+
69
+ @router.get("/api/repos/{repo_id}/diff", response_model=FileDiff)
70
+ def api_working_file_diff(
71
+ repo_id: str, path: str, old_path: str | None = None, full: bool = False
72
+ ) -> FileDiff:
73
+ repo_path = _resolve_repo_path(repo_id)
74
+ try:
75
+ return get_working_file_diff(repo_path, path, old_path, full)
76
+ except Exception as exc: # noqa: BLE001 - surfaced as a 404 either way
77
+ raise HTTPException(status_code=404, detail="diff not found") from exc
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from .. import config
6
+ from ..repo_discovery import get_scan_progress, list_repos
7
+ from ..schemas import BrowseDirectoryResponse, RepoScanProgress, RepoSummary
8
+ from ..ui.folder_picker import browse_directory
9
+
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.get("/api/repos", response_model=list[RepoSummary])
15
+ def api_list_repos() -> list[RepoSummary]:
16
+ return list_repos(config.load_repo_paths())
17
+
18
+
19
+ @router.get("/api/repos/scan-progress", response_model=RepoScanProgress)
20
+ def api_repos_scan_progress() -> RepoScanProgress:
21
+ return RepoScanProgress(**get_scan_progress())
22
+
23
+
24
+ @router.get("/api/browse", response_model=BrowseDirectoryResponse)
25
+ def api_browse(path: str | None = None) -> BrowseDirectoryResponse:
26
+ return browse_directory(path)
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
6
+
7
+ from .. import config
8
+ from ..repo_discovery import resolve_repo_path
9
+ from ..terminal import run_terminal_session
10
+
11
+
12
+ def create_terminal_router(root_path: Path) -> APIRouter:
13
+ router = APIRouter()
14
+
15
+ @router.websocket("/ws/terminal")
16
+ async def ws_terminal(websocket: WebSocket) -> None:
17
+ repo_id = websocket.query_params.get("repo")
18
+ cwd = root_path
19
+ if repo_id:
20
+ resolved = resolve_repo_path(config.load_repo_paths(), repo_id)
21
+ if resolved is None:
22
+ await websocket.close(code=1008)
23
+ return
24
+ cwd = resolved
25
+ await websocket.accept()
26
+ try:
27
+ await run_terminal_session(websocket, cwd=cwd)
28
+ except WebSocketDisconnect:
29
+ pass
30
+
31
+ return router
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.responses import FileResponse
8
+ from fastapi.staticfiles import StaticFiles
9
+
10
+ from . import config
11
+ from .api.agents import router as agents_router
12
+ from .api.ci import router as ci_router
13
+ from .api.config import create_config_router
14
+ from .api.git import router as git_router
15
+ from .api.repos import router as repos_router
16
+ from .api.terminal import create_terminal_router
17
+
18
+
19
+ def create_app(root_path: Path, frontend_dist: Path | None = None) -> FastAPI:
20
+ app = FastAPI(title="git-juggler")
21
+ app.state.root_path = root_path
22
+ config.ensure_seeded(root_path)
23
+
24
+ # Only needed for local dev, when the Vite dev server (a different origin)
25
+ # talks to this API directly instead of through its proxy.
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+ app.include_router(repos_router)
33
+ app.include_router(git_router)
34
+ app.include_router(ci_router)
35
+ app.include_router(agents_router)
36
+ app.include_router(create_config_router(root_path))
37
+ app.include_router(create_terminal_router(root_path))
38
+
39
+ if frontend_dist and frontend_dist.exists():
40
+ assets_dir = frontend_dist / "assets"
41
+ if assets_dir.exists():
42
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
43
+
44
+ @app.get("/{full_path:path}")
45
+ def spa(full_path: str) -> FileResponse:
46
+ candidate = frontend_dist / full_path
47
+ if full_path and candidate.is_file():
48
+ return FileResponse(candidate)
49
+ return FileResponse(frontend_dist / "index.html")
50
+
51
+ return app
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from typing import Protocol
7
7
 
8
8
  from . import github_actions, jenkins
9
- from .schemas import ActivePipeline, CiRunInfo, CiStage, RepoSummary
9
+ from ..schemas import ActivePipeline, CiRunInfo, CiStage, RepoSummary
10
10
 
11
11
 
12
12
  class CiProvider(Protocol):
@@ -10,7 +10,8 @@ from urllib.request import Request, urlopen
10
10
 
11
11
  from git import Repo
12
12
 
13
- from .schemas import CiRunInfo, CiStage
13
+ from .http import fetch_json
14
+ from ..schemas import CiRunInfo, CiStage
14
15
 
15
16
 
16
17
  GITHUB_RUNS_PER_PAGE = 100
@@ -130,17 +131,7 @@ def _repo_api_url(github_config: dict, repo_config: dict) -> str | None:
130
131
 
131
132
 
132
133
  def _get_json(url: str, headers: dict[str, str]) -> dict | None:
133
- request = Request(url, headers=headers)
134
- try:
135
- with urlopen(request, timeout=10) as response: # noqa: S310 - configured user URL, read-only local app integration
136
- body = response.read().decode("utf-8")
137
- except (HTTPError, URLError, TimeoutError, OSError):
138
- return None
139
- try:
140
- data = json.loads(body)
141
- except json.JSONDecodeError:
142
- return None
143
- return data if isinstance(data, dict) else None
134
+ return fetch_json(url, headers, opener=urlopen)
144
135
 
145
136
 
146
137
  # Conditional-request cache for the polling calls: GitHub answers a request
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Callable, ContextManager, Protocol
5
+ from urllib.error import HTTPError, URLError
6
+ from urllib.request import Request, urlopen
7
+
8
+
9
+ class _Response(Protocol):
10
+ def read(self) -> bytes: ...
11
+
12
+
13
+ def fetch_json(
14
+ url: str,
15
+ headers: dict[str, str],
16
+ opener: Callable[..., ContextManager[_Response]] = urlopen,
17
+ ) -> dict | None:
18
+ request = Request(url, headers=headers)
19
+ try:
20
+ with opener(request, timeout=10) as response: # noqa: S310 - configured user URL, read-only local app integration
21
+ body = response.read().decode("utf-8")
22
+ except (HTTPError, URLError, TimeoutError, OSError):
23
+ return None
24
+ try:
25
+ data = json.loads(body)
26
+ except json.JSONDecodeError:
27
+ return None
28
+ return data if isinstance(data, dict) else None
@@ -1,15 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
- import json
5
4
  import os
6
5
  from datetime import datetime, timezone
7
6
  from pathlib import Path
8
- from urllib.error import HTTPError, URLError
9
7
  from urllib.parse import unquote, urlencode, urlparse
10
- from urllib.request import Request, urlopen
8
+ from urllib.request import urlopen
11
9
 
12
- from .schemas import CiRunInfo, CiStage
10
+ from .http import fetch_json
11
+ from ..schemas import CiRunInfo, CiStage
13
12
 
14
13
 
15
14
  def _matching_job_configs(jenkins_config: dict, repo_path: Path) -> list[dict]:
@@ -42,17 +41,7 @@ def _headers(jenkins_config: dict) -> dict[str, str]:
42
41
 
43
42
 
44
43
  def _fetch_json(url: str, headers: dict[str, str]) -> dict | None:
45
- request = Request(url, headers=headers)
46
- try:
47
- with urlopen(request, timeout=10) as response: # noqa: S310 - configured user URL, read-only local app integration
48
- body = response.read().decode("utf-8")
49
- except (HTTPError, URLError, TimeoutError, OSError):
50
- return None
51
- try:
52
- data = json.loads(body)
53
- except json.JSONDecodeError:
54
- return None
55
- return data if isinstance(data, dict) else None
44
+ return fetch_json(url, headers, opener=urlopen)
56
45
 
57
46
 
58
47
  def _normalize_status(result: object, building: object) -> str: