git-juggler 0.2.1__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.
- {git_juggler-0.2.1 → git_juggler-0.2.3}/.gitignore +2 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/PKG-INFO +2 -2
- {git_juggler-0.2.1 → git_juggler-0.2.3}/README.md +1 -1
- git_juggler-0.2.3/src/backend/git_juggler/agents/__init__.py +1 -0
- git_juggler-0.2.1/src/backend/git_juggler/agent_hook_events.py → git_juggler-0.2.3/src/backend/git_juggler/agents/hook_events.py +12 -7
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/activity_models.py +2 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/claude_sessions.py +3 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/__init__.py +1 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/agents.py +97 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/ci.py +97 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/config.py +114 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/git.py +77 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/repos.py +26 -0
- git_juggler-0.2.3/src/backend/git_juggler/api/terminal.py +31 -0
- git_juggler-0.2.3/src/backend/git_juggler/app.py +51 -0
- git_juggler-0.2.1/src/backend/git_juggler/ci.py → git_juggler-0.2.3/src/backend/git_juggler/ci/__init__.py +13 -2
- {git_juggler-0.2.1/src/backend/git_juggler → git_juggler-0.2.3/src/backend/git_juggler/ci}/github_actions.py +15 -12
- git_juggler-0.2.3/src/backend/git_juggler/ci/http.py +28 -0
- {git_juggler-0.2.1/src/backend/git_juggler → git_juggler-0.2.3/src/backend/git_juggler/ci}/jenkins.py +29 -15
- git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/assets/index-BGVpgOst.css +1 -0
- git_juggler-0.2.1/src/backend/git_juggler/frontend_dist/assets/index-CnmsXu_6.js → git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/assets/index-C1_44PRZ.js +15 -14
- git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/index.html +2 -2
- git_juggler-0.2.3/src/backend/git_juggler/frontend_dist/logo.png +0 -0
- git_juggler-0.2.3/src/backend/git_juggler/git/__init__.py +1 -0
- git_juggler-0.2.3/src/backend/git_juggler/git/command.py +50 -0
- git_juggler-0.2.1/src/backend/git_juggler/git_data.py → git_juggler-0.2.3/src/backend/git_juggler/git/data.py +49 -199
- git_juggler-0.2.1/src/backend/git_juggler/commit_detail.py → git_juggler-0.2.3/src/backend/git_juggler/git/detail.py +22 -2
- git_juggler-0.2.1/src/backend/git_juggler/file_diff.py → git_juggler-0.2.3/src/backend/git_juggler/git/diff.py +1 -1
- git_juggler-0.2.3/src/backend/git_juggler/git/refs.py +66 -0
- git_juggler-0.2.3/src/backend/git_juggler/git/status.py +146 -0
- git_juggler-0.2.1/src/backend/git_juggler/repos.py → git_juggler-0.2.3/src/backend/git_juggler/repo_discovery.py +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/schemas.py +9 -0
- git_juggler-0.2.3/src/backend/git_juggler/ui/__init__.py +1 -0
- git_juggler-0.2.1/src/backend/git_juggler/pick_folder.py → git_juggler-0.2.3/src/backend/git_juggler/ui/folder_picker.py +26 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_agent_hooks.py +38 -12
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_agent_tracking.py +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_ci.py +14 -2
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_ci_poll.py +2 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_ci_poll_route.py +8 -3
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_claude_sessions_data.py +5 -5
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_file_diff.py +15 -3
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_git_data.py +67 -5
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_github_actions.py +2 -2
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_jenkins.py +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_opencode_sessions.py +9 -9
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_pick_folder.py +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_repos.py +1 -1
- git_juggler-0.2.3/src/frontend/public/ci-logos/jenkins.webp +0 -0
- git_juggler-0.2.3/src/frontend/public/logo.png +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/App.css +373 -334
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/App.tsx +13 -8
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/api/client.ts +22 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/api/types.ts +10 -0
- git_juggler-0.2.3/src/frontend/src/components/Agents/AgentHoverCard.tsx +67 -0
- git_juggler-0.2.3/src/frontend/src/components/Agents/AgentStatusPopover.tsx +208 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Badges/BranchBadge.tsx +1 -1
- git_juggler-0.2.3/src/frontend/src/components/Badges/CiRunBadge.tsx +161 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Badges/StashBadge.tsx +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Badges/TagBadge.tsx +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Badges/ciStatus.tsx +4 -14
- git_juggler-0.2.3/src/frontend/src/components/CIRunPopover.tsx +344 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Commits/CommitDetail.tsx +2 -0
- git_juggler-0.2.3/src/frontend/src/components/Commits/LineCounts.tsx +16 -0
- git_juggler-0.2.3/src/frontend/src/components/Commits/UncommittedRow.tsx +241 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +2 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +14 -0
- git_juggler-0.2.3/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +77 -0
- git_juggler-0.2.3/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +101 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +2 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/AgentsSettings.tsx +1 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/GitHubSettings.tsx +45 -8
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/JenkinsSettings.tsx +47 -10
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Search/MultiSelect.tsx +2 -3
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Search/SearchBox.tsx +3 -7
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +3 -1
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Sidebar/RepoList.tsx +82 -110
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Tabs/TabsBar.tsx +7 -3
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Terminal/TerminalPanel.tsx +1 -1
- git_juggler-0.2.3/src/frontend/src/components/icons.tsx +71 -0
- git_juggler-0.2.3/src/frontend/src/lib/agentStatusPopoverData.ts +91 -0
- git_juggler-0.2.3/src/frontend/src/lib/ciRunPopoverData.ts +94 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/state/store.ts +60 -4
- git_juggler-0.2.3/src/frontend/tests/agentStatusPopoverData.test.ts +93 -0
- git_juggler-0.2.3/src/frontend/tests/ciRunPopoverData.test.ts +85 -0
- git_juggler-0.2.1/plans/github-actions-settings-and-build-status.md +0 -449
- git_juggler-0.2.1/plans/jenkins-settings-and-build-status.md +0 -351
- git_juggler-0.2.1/src/backend/git_juggler/app.py +0 -388
- git_juggler-0.2.1/src/backend/git_juggler/browse.py +0 -27
- git_juggler-0.2.1/src/backend/git_juggler/frontend_dist/assets/index-C-FENuHS.css +0 -1
- git_juggler-0.2.1/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -99
- git_juggler-0.2.1/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -92
- git_juggler-0.2.1/src/frontend/src/components/Badges/icons.tsx +0 -32
- git_juggler-0.2.1/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -128
- git_juggler-0.2.1/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -29
- git_juggler-0.2.1/src/frontend/src/components/Pipelines/StageGraph.tsx +0 -159
- git_juggler-0.2.1/src/frontend/src/lib/stageGraphLayout.ts +0 -117
- git_juggler-0.2.1/src/frontend/tests/stageGraphLayout.test.ts +0 -95
- {git_juggler-0.2.1 → git_juggler-0.2.3}/.github/workflows/build-package.yml +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/CLAUDE.md +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/LICENSE +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/notes.txt +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/pyproject.toml +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/scripts/build-package.sh +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/scripts/dev.sh +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/__init__.py +0 -0
- /git_juggler-0.2.1/src/backend/git_juggler/agent_hooks.py → /git_juggler-0.2.3/src/backend/git_juggler/agents/hooks.py +0 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/__init__.py +0 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/claude_transcripts.py +0 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/git_resolver.py +0 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/opencode_sessions.py +0 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/paths.py +0 -0
- {git_juggler-0.2.1/src/backend/git_juggler/agent_tracking → git_juggler-0.2.3/src/backend/git_juggler/agents/tracking}/text.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/config.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/dev_app.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/favicon.svg +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/frontend_dist/icons.svg +0 -0
- /git_juggler-0.2.1/src/backend/git_juggler/git_utils.py → /git_juggler-0.2.3/src/backend/git_juggler/git/utils.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/main.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/terminal.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/git_juggler/themes.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_preferences.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/backend/tests/test_themes.py +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/.gitignore +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/README.md +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/index.html +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/package-lock.json +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/package.json +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/public/agent-logos/claude.png +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/public/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/public/favicon.svg +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/public/git-juggler.png +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/public/icons.svg +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Agents/agentFormat.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Graph/GraphPanel.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Graph/branchColor.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Pipelines/StageStrip.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Sidebar/Sidebar.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/inputs/TextField.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/components/inputs/styles.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/index.css +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/index.tsx +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/appTheme.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/branchVisibility.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/bundledThemes.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/ciPoll.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/color.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/flip.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/formatDate.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/preferenceSender.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/scrollToCommit.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/sideBySide.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/stageWindow.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/stripAnsi.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/swallowNextClick.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/lib/vscodeTheme.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/state/overlayStack.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/state/preferenceSync.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/state/preferences.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/src/state/themes.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/agentFormat.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/ciPoll.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/computeColumns.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/preferenceSender.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/sideBySide.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/stageWindow.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tests/vscodeTheme.test.ts +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tsconfig.app.json +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tsconfig.json +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/tsconfig.node.json +0 -0
- {git_juggler-0.2.1 → git_juggler-0.2.3}/src/frontend/vite.config.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-juggler
|
|
3
|
-
Version: 0.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
|
-
|
|
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
|
|
|
@@ -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 .
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
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
|
|
@@ -151,12 +151,16 @@ class AgentHookEventReader:
|
|
|
151
151
|
card: ClaudeSession | None = None
|
|
152
152
|
opencode_session: OpenCodeSession | None = None
|
|
153
153
|
busy: bool | None = None # what the agent itself reports, when it does
|
|
154
|
+
waiting_for: str | None = None
|
|
154
155
|
if provider == "claude" and registry is not None and session_id:
|
|
155
156
|
card = registry.get(session_id)
|
|
156
157
|
if card is None or not _pid_alive(card.pid):
|
|
157
158
|
continue
|
|
158
159
|
process_pid: int | None = card.pid
|
|
159
|
-
|
|
160
|
+
# "waiting" (on a permission prompt or a question) is still mid-turn.
|
|
161
|
+
busy = card.status in ("busy", "waiting") if card.status in ("busy", "idle", "waiting") else None
|
|
162
|
+
if card.status == "waiting":
|
|
163
|
+
waiting_for = card.waiting_for or "permission prompt"
|
|
160
164
|
else:
|
|
161
165
|
process_pid = next((event.agent_pid for event in reversed(session_events) if event.agent_pid is not None), None)
|
|
162
166
|
if process_pid is not None and not _pid_alive(process_pid):
|
|
@@ -235,6 +239,7 @@ class AgentHookEventReader:
|
|
|
235
239
|
process_pid=process_pid,
|
|
236
240
|
name=card.name if card is not None else (opencode_session.title if opencode_session is not None else None),
|
|
237
241
|
details=details,
|
|
242
|
+
waiting_for=waiting_for,
|
|
238
243
|
)
|
|
239
244
|
)
|
|
240
245
|
return scans, next_change
|
|
@@ -21,6 +21,8 @@ class ClaudeSession:
|
|
|
21
21
|
status_updated_at: int | None = None
|
|
22
22
|
version: str | None = None
|
|
23
23
|
entrypoint: str | None = None
|
|
24
|
+
# Set with status "waiting": "permission prompt" or "input needed".
|
|
25
|
+
waiting_for: str | None = None
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def registry_signature(directory: Path | None) -> tuple[tuple[str, int], ...] | None:
|
|
@@ -69,5 +71,6 @@ def read_registry(directory: Path | None) -> dict[str, ClaudeSession] | None:
|
|
|
69
71
|
status_updated_at=data.get("statusUpdatedAt") if isinstance(data.get("statusUpdatedAt"), int) else None,
|
|
70
72
|
version=data.get("version") if isinstance(data.get("version"), str) else None,
|
|
71
73
|
entrypoint=data.get("entrypoint") if isinstance(data.get("entrypoint"), str) else None,
|
|
74
|
+
waiting_for=data.get("waitingFor") if isinstance(data.get("waitingFor"), str) else None,
|
|
72
75
|
)
|
|
73
76
|
return sessions
|
|
@@ -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
|
|
9
|
+
from ..schemas import ActivePipeline, CiRunInfo, CiStage, RepoSummary
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class CiProvider(Protocol):
|
|
@@ -120,6 +120,13 @@ def _active_runs_for_repo(repo: RepoSummary, github_config: dict | None, jenkins
|
|
|
120
120
|
return [ActivePipeline(repo_id=repo.id, repo_name=repo.name, run=run) for run in runs]
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def _pipeline_key(pipeline: ActivePipeline) -> tuple[str, str]:
|
|
124
|
+
run = pipeline.run
|
|
125
|
+
if run.run_id:
|
|
126
|
+
return (run.provider, run.run_id)
|
|
127
|
+
return (run.provider, run.url)
|
|
128
|
+
|
|
129
|
+
|
|
123
130
|
def get_active_pipelines(
|
|
124
131
|
repos: list[RepoSummary],
|
|
125
132
|
github_config: dict | None,
|
|
@@ -132,4 +139,8 @@ def get_active_pipelines(
|
|
|
132
139
|
with ThreadPoolExecutor(max_workers=min(8, len(repos))) as pool:
|
|
133
140
|
per_repo = pool.map(lambda repo: _active_runs_for_repo(repo, github_config, jenkins_config), repos)
|
|
134
141
|
pipelines = [item for group in per_repo for item in group]
|
|
135
|
-
|
|
142
|
+
deduped_by_key: dict[tuple[str, str], ActivePipeline] = {}
|
|
143
|
+
for pipeline in pipelines:
|
|
144
|
+
deduped_by_key.setdefault(_pipeline_key(pipeline), pipeline)
|
|
145
|
+
deduped = list(deduped_by_key.values())
|
|
146
|
+
return sorted(deduped, key=lambda item: item.run.created_at or "", reverse=True)
|