titan-cli 0.1.0__py3-none-any.whl
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.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# PR Agent Configuration
|
|
2
|
+
# AI agent for analyzing code changes and generating PR descriptions
|
|
3
|
+
|
|
4
|
+
[agent]
|
|
5
|
+
name = "PRAgent"
|
|
6
|
+
description = "AI agent specialized in PR analysis and generation"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
|
|
9
|
+
[agent.capabilities]
|
|
10
|
+
# What this agent can do
|
|
11
|
+
code_analysis = true
|
|
12
|
+
pr_generation = true
|
|
13
|
+
commit_analysis = true
|
|
14
|
+
architecture_review = true
|
|
15
|
+
template_following = true
|
|
16
|
+
|
|
17
|
+
[agent.prompts]
|
|
18
|
+
# System prompts for different tasks
|
|
19
|
+
|
|
20
|
+
[agent.prompts.pr_description]
|
|
21
|
+
system = """You are an expert software engineer specialized in code review and documentation.
|
|
22
|
+
Your task is to analyze git changes and generate professional pull request descriptions.
|
|
23
|
+
|
|
24
|
+
Guidelines:
|
|
25
|
+
1. Be concise but comprehensive
|
|
26
|
+
2. Follow conventional commit formats for titles
|
|
27
|
+
3. Use markdown formatting
|
|
28
|
+
4. Highlight breaking changes
|
|
29
|
+
5. Focus on WHY, not just WHAT
|
|
30
|
+
6. Follow PR templates strictly when provided
|
|
31
|
+
7. Adapt detail level based on PR size (small/medium/large/very large)
|
|
32
|
+
|
|
33
|
+
Response format:
|
|
34
|
+
TITLE: <conventional commit title max 72 chars>
|
|
35
|
+
|
|
36
|
+
DESCRIPTION:
|
|
37
|
+
<structured description following template or standard format>
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
[agent.prompts.commit_message]
|
|
41
|
+
system = """You are an expert software engineer specialized in writing clear commit messages.
|
|
42
|
+
Your task is to analyze code changes and generate atomic commit messages.
|
|
43
|
+
|
|
44
|
+
Guidelines:
|
|
45
|
+
1. Follow conventional commits format: type(scope): description
|
|
46
|
+
2. Be specific and descriptive
|
|
47
|
+
3. Focus on WHY the change was made
|
|
48
|
+
4. Keep title under 72 characters
|
|
49
|
+
5. Group related changes logically
|
|
50
|
+
|
|
51
|
+
Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
[agent.prompts.architecture_review]
|
|
55
|
+
system = """You are a senior software architect reviewing code changes.
|
|
56
|
+
Your task is to identify architectural patterns, potential issues, and improvements.
|
|
57
|
+
|
|
58
|
+
Guidelines:
|
|
59
|
+
1. Identify design patterns used
|
|
60
|
+
2. Highlight potential issues (coupling, complexity, etc.)
|
|
61
|
+
3. Suggest architectural improvements
|
|
62
|
+
4. Consider scalability and maintainability
|
|
63
|
+
5. Be constructive and actionable
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
[agent.limits]
|
|
67
|
+
# Diff analysis limits
|
|
68
|
+
max_diff_size = 8000 # Characters
|
|
69
|
+
max_files_in_diff = 50
|
|
70
|
+
max_commits_to_analyze = 15
|
|
71
|
+
|
|
72
|
+
[agent.features]
|
|
73
|
+
# Feature flags
|
|
74
|
+
enable_template_detection = true
|
|
75
|
+
enable_dynamic_sizing = true
|
|
76
|
+
enable_user_confirmation = true
|
|
77
|
+
enable_fallback_prompts = true
|
|
78
|
+
enable_debug_output = false # Set to true for debugging
|
|
79
|
+
|
|
80
|
+
[agent.metadata]
|
|
81
|
+
# Agent metadata
|
|
82
|
+
author = "Titan CLI Team"
|
|
83
|
+
created = "2024-12-11"
|
|
84
|
+
updated = "2024-12-11"
|
|
85
|
+
tags = ["pr-generation", "code-analysis", "git", "ai", "github"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/exceptions.py
|
|
2
|
+
class GitHubError(Exception):
|
|
3
|
+
"""Base exception for all GitHub related errors."""
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
class GitHubAuthenticationError(GitHubError):
|
|
7
|
+
"""Raised when GitHub authentication fails (e.g., gh CLI not authenticated)."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class GitHubAPIError(GitHubError):
|
|
11
|
+
"""Raised when a GitHub API call or gh CLI command fails."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class PRNotFoundError(GitHubAPIError):
|
|
15
|
+
"""Raised when a specified Pull Request is not found."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class ReviewNotFoundError(GitHubAPIError):
|
|
19
|
+
"""Raised when a specified Review is not found."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class GitHubPermissionError(GitHubAPIError):
|
|
23
|
+
"""Raised when the authenticated user does not have sufficient permissions."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
class GitHubConfigurationError(GitHubError):
|
|
27
|
+
"""Raised when the GitHub plugin is misconfigured."""
|
|
28
|
+
pass
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/messages.py
|
|
2
|
+
class Messages:
|
|
3
|
+
class Prompts:
|
|
4
|
+
"""Prompts specific to the GitHub plugin"""
|
|
5
|
+
ENTER_PR_TITLE: str = "Enter Pull Request title:"
|
|
6
|
+
ENTER_PR_BODY: str = "Enter PR body/description (press Meta+Enter or Esc then Enter to finish):"
|
|
7
|
+
ENTER_ISSUE_BODY: str = "Enter issue body/description (press Meta+Enter or Esc then Enter to finish):"
|
|
8
|
+
ASSIGN_TO_SELF: str = "Assign this issue to yourself?"
|
|
9
|
+
SELECT_LABELS: str = "Select labels for this issue:"
|
|
10
|
+
ENTER_PR_BODY_INFO: str = "Enter a description for your pull request. When you are finished, press Meta+Enter (or Esc followed by Enter)."
|
|
11
|
+
|
|
12
|
+
class GitHub:
|
|
13
|
+
"""GitHub operations messages"""
|
|
14
|
+
# Client errors
|
|
15
|
+
CLI_NOT_FOUND: str = "GitHub CLI ('gh') not found. Please install it and ensure it's in your PATH."
|
|
16
|
+
NOT_AUTHENTICATED: str = "GitHub CLI is not authenticated. Run: gh auth login"
|
|
17
|
+
CONFIG_REPO_MISSING: str = "GitHub repository owner and name must be configured in [plugins.github.config]."
|
|
18
|
+
API_ERROR: str = "GitHub API error: {error_msg}"
|
|
19
|
+
PERMISSION_ERROR: str = "Permission denied for GitHub operation: {error_msg}"
|
|
20
|
+
UNEXPECTED_ERROR: str = "An unexpected GitHub error occurred: {error}"
|
|
21
|
+
|
|
22
|
+
# Pull Requests
|
|
23
|
+
PR_NOT_FOUND: str = "Pull Request #{pr_number} not found."
|
|
24
|
+
PR_CREATING: str = "Creating pull request..."
|
|
25
|
+
PR_CREATED: str = "PR #{number} created: {url}"
|
|
26
|
+
PR_UPDATED: str = "PR #{number} updated"
|
|
27
|
+
PR_MERGED: str = "PR #{number} merged"
|
|
28
|
+
PR_CLOSED: str = "PR #{number} closed"
|
|
29
|
+
PR_FAILED: str = "Failed to create PR: {error}"
|
|
30
|
+
PR_CREATION_FAILED: str = "Failed to create pull request: {error}"
|
|
31
|
+
FAILED_TO_PARSE_PR_NUMBER: str = "Failed to parse PR number from URL: {url}"
|
|
32
|
+
|
|
33
|
+
# Merge
|
|
34
|
+
INVALID_MERGE_METHOD: str = "Invalid merge method: {method}. Must be one of: {valid_methods}"
|
|
35
|
+
|
|
36
|
+
# Reviews
|
|
37
|
+
REVIEW_NOT_FOUND: str = "Review ID #{review_id} for Pull Request #{pr_number} not found."
|
|
38
|
+
REVIEW_CREATING: str = "Creating review..."
|
|
39
|
+
REVIEW_CREATED: str = "Review submitted"
|
|
40
|
+
REVIEW_FAILED: str = "Failed to submit review: {error}"
|
|
41
|
+
|
|
42
|
+
# Comments
|
|
43
|
+
COMMENT_CREATING: str = "Adding comment..."
|
|
44
|
+
COMMENT_CREATED: str = "Comment added"
|
|
45
|
+
COMMENT_FAILED: str = "Failed to add comment: {error}"
|
|
46
|
+
|
|
47
|
+
# Repository
|
|
48
|
+
REPO_NOT_FOUND: str = "Repository not found"
|
|
49
|
+
REPO_ACCESS_DENIED: str = "Access denied to repository"
|
|
50
|
+
|
|
51
|
+
# Authentication
|
|
52
|
+
AUTH_MISSING: str = "GitHub token not found. Set GITHUB_TOKEN environment variable."
|
|
53
|
+
AUTH_INVALID: str = "Invalid GitHub token"
|
|
54
|
+
|
|
55
|
+
class AI:
|
|
56
|
+
AI_NOT_CONFIGURED: str = "AI not configured. Run 'titan ai configure' to enable AI features."
|
|
57
|
+
GITHUB_CLIENT_NOT_AVAILABLE: str = "GitHub client is not available in the workflow context."
|
|
58
|
+
GIT_CLIENT_NOT_AVAILABLE: str = "Git client is not available in the workflow context."
|
|
59
|
+
MISSING_PR_HEAD_BRANCH: str = "Missing pr_head_branch in context"
|
|
60
|
+
ANALYZING_BRANCH_DIFF: str = "Analyzing branch diff: {head_branch} vs {base_branch}..."
|
|
61
|
+
FAILED_TO_GET_BRANCH_DIFF: str = "Failed to get branch diff: {e}"
|
|
62
|
+
NO_CHANGES_FOUND: str = "No changes found between branches"
|
|
63
|
+
COMMITS_TRUNCATED: str = "\n ... and {count} more commits"
|
|
64
|
+
NO_DIFF_AVAILABLE: str = "No diff available"
|
|
65
|
+
DIFF_TRUNCATED: str = "\n\n... (diff truncated for brevity)"
|
|
66
|
+
PR_SIZE_INFO: str = "PR Size: {pr_size} ({files_changed} files, {diff_lines} lines) → Max description: {max_chars} chars"
|
|
67
|
+
FAILED_TO_READ_PR_TEMPLATE: str = "Failed to read PR template: {e}"
|
|
68
|
+
GENERATING_PR_DESCRIPTION: str = "Generating PR description with AI..."
|
|
69
|
+
AI_RESPONSE_FORMAT_INCORRECT: str = "AI response format incorrect. Expected 'TITLE:' and 'DESCRIPTION:' sections.\nGot: {response_preview}..."
|
|
70
|
+
AI_GENERATED_TRUNCATING: str = "AI generated {actual_len} chars, truncating to {max_chars}"
|
|
71
|
+
AI_GENERATED_EMPTY_SHORT: str = "AI generated an empty or very short description."
|
|
72
|
+
FULL_AI_RESPONSE: str = "Full AI response:"
|
|
73
|
+
AI_GENERATED_INCOMPLETE: str = "AI generated an empty or incomplete PR description"
|
|
74
|
+
AI_GENERATED_COMMIT_MESSAGE: str = "AI Generated Commit Message"
|
|
75
|
+
COMMIT_MESSAGE_LABEL: str = "Commit Message:"
|
|
76
|
+
CONFIRM_USE_AI_COMMIT: str = "Use this AI-generated commit message?"
|
|
77
|
+
AI_COMMIT_REJECTED: str = "AI-generated commit message rejected"
|
|
78
|
+
AI_GENERATED_PR_TITLE: str = "AI Generated PR:"
|
|
79
|
+
TITLE_LABEL: str = "Title:"
|
|
80
|
+
TITLE_TOO_LONG_WARNING: str = "Title is {length} chars (recommended: ≤72)"
|
|
81
|
+
DESCRIPTION_LABEL: str = "Description:"
|
|
82
|
+
CONFIRM_USE_AI_PR: str = "Use this AI-generated PR?"
|
|
83
|
+
AI_SUGGESTION_REJECTED: str = "AI suggestion rejected. Will prompt for manual input."
|
|
84
|
+
AI_GENERATED_PR_DESCRIPTION_SUCCESS: str = "AI generated PR description"
|
|
85
|
+
AI_GENERATION_FAILED: str = "AI generation failed: {e}"
|
|
86
|
+
FALLBACK_TO_MANUAL: str = "Falling back to manual PR creation..."
|
|
87
|
+
|
|
88
|
+
msg = Messages()
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/models.py
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
|
+
|
|
5
|
+
# Import PRSizeEstimation from utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class User:
|
|
10
|
+
"""GitHub user representation"""
|
|
11
|
+
login: str
|
|
12
|
+
name: Optional[str] = None
|
|
13
|
+
email: Optional[str] = None
|
|
14
|
+
avatar_url: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dict(cls, data: dict) -> 'User':
|
|
18
|
+
"""
|
|
19
|
+
Create User from API response
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: User data from GitHub API
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
User instance
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> data = {"login": "john", "name": "John Doe"}
|
|
29
|
+
>>> user = User.from_dict(data)
|
|
30
|
+
"""
|
|
31
|
+
if not data:
|
|
32
|
+
return cls(login="unknown")
|
|
33
|
+
|
|
34
|
+
return cls(
|
|
35
|
+
login=data.get("login", "unknown"),
|
|
36
|
+
name=data.get("name"),
|
|
37
|
+
email=data.get("email"),
|
|
38
|
+
avatar_url=data.get("avatar_url")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ReviewComment:
|
|
44
|
+
"""GitHub PR review comment"""
|
|
45
|
+
id: int
|
|
46
|
+
path: str
|
|
47
|
+
line: int
|
|
48
|
+
body: str
|
|
49
|
+
user: User
|
|
50
|
+
created_at: str
|
|
51
|
+
side: str = "RIGHT" # RIGHT or LEFT
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict) -> 'ReviewComment':
|
|
55
|
+
"""Create ReviewComment from API response"""
|
|
56
|
+
return cls(
|
|
57
|
+
id=data.get("id", 0),
|
|
58
|
+
path=data.get("path", ""),
|
|
59
|
+
line=data.get("line", 0),
|
|
60
|
+
body=data.get("body", ""),
|
|
61
|
+
user=User.from_dict(data.get("user", {})),
|
|
62
|
+
created_at=data.get("created_at", ""),
|
|
63
|
+
side=data.get("side", "RIGHT")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Review:
|
|
69
|
+
"""GitHub PR review"""
|
|
70
|
+
id: int
|
|
71
|
+
user: User
|
|
72
|
+
body: str
|
|
73
|
+
state: str # PENDING, APPROVED, CHANGES_REQUESTED, COMMENTED
|
|
74
|
+
submitted_at: Optional[str] = None
|
|
75
|
+
commit_id: Optional[str] = None
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_dict(cls, data: dict) -> 'Review':
|
|
79
|
+
"""Create Review from API response"""
|
|
80
|
+
return cls(
|
|
81
|
+
id=data.get("id", 0),
|
|
82
|
+
user=User.from_dict(data.get("user", {})),
|
|
83
|
+
body=data.get("body", ""),
|
|
84
|
+
state=data.get("state", "PENDING"),
|
|
85
|
+
submitted_at=data.get("submitted_at"),
|
|
86
|
+
commit_id=data.get("commit_id")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class PullRequest:
|
|
92
|
+
"""
|
|
93
|
+
GitHub Pull Request representation
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
number: PR number
|
|
97
|
+
title: PR title
|
|
98
|
+
body: PR description
|
|
99
|
+
state: open, closed, merged
|
|
100
|
+
author: PR author
|
|
101
|
+
base_ref: Base branch (e.g., develop)
|
|
102
|
+
head_ref: Head branch (e.g., feature/xyz)
|
|
103
|
+
additions: Lines added
|
|
104
|
+
deletions: Lines deleted
|
|
105
|
+
changed_files: Number of files changed
|
|
106
|
+
mergeable: Can be merged
|
|
107
|
+
draft: Is draft PR
|
|
108
|
+
created_at: ISO date string
|
|
109
|
+
updated_at: ISO date string
|
|
110
|
+
merged_at: ISO date string (if merged)
|
|
111
|
+
reviews: List of reviews
|
|
112
|
+
labels: List of label names
|
|
113
|
+
"""
|
|
114
|
+
number: int
|
|
115
|
+
title: str
|
|
116
|
+
body: str
|
|
117
|
+
state: str
|
|
118
|
+
author: User
|
|
119
|
+
base_ref: str
|
|
120
|
+
head_ref: str
|
|
121
|
+
additions: int = 0
|
|
122
|
+
deletions: int = 0
|
|
123
|
+
changed_files: int = 0
|
|
124
|
+
mergeable: bool = True
|
|
125
|
+
draft: bool = False
|
|
126
|
+
created_at: Optional[str] = None
|
|
127
|
+
updated_at: Optional[str] = None
|
|
128
|
+
merged_at: Optional[str] = None
|
|
129
|
+
reviews: List[Review] = field(default_factory=list)
|
|
130
|
+
labels: List[str] = field(default_factory=list)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, data: dict) -> 'PullRequest':
|
|
134
|
+
"""
|
|
135
|
+
Create PullRequest from GitHub API response
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
data: PR data from GitHub API (gh pr view --json format)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
PullRequest instance
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
>>> data = gh_api_response
|
|
145
|
+
>>> pr = PullRequest.from_dict(data)
|
|
146
|
+
"""
|
|
147
|
+
# Parse author
|
|
148
|
+
author_data = data.get("author", {})
|
|
149
|
+
author = User.from_dict(author_data)
|
|
150
|
+
|
|
151
|
+
# Parse reviews
|
|
152
|
+
reviews_data = data.get("reviews", [])
|
|
153
|
+
reviews = [Review.from_dict(r) for r in reviews_data]
|
|
154
|
+
|
|
155
|
+
# Parse labels
|
|
156
|
+
labels_data = data.get("labels", [])
|
|
157
|
+
labels = [label.get("name", "") for label in labels_data]
|
|
158
|
+
|
|
159
|
+
return cls(
|
|
160
|
+
number=data.get("number", 0),
|
|
161
|
+
title=data.get("title", ""),
|
|
162
|
+
body=data.get("body", ""),
|
|
163
|
+
state=data.get("state", "OPEN"),
|
|
164
|
+
author=author,
|
|
165
|
+
base_ref=data.get("baseRefName", ""),
|
|
166
|
+
head_ref=data.get("headRefName", ""),
|
|
167
|
+
additions=data.get("additions", 0),
|
|
168
|
+
deletions=data.get("deletions", 0),
|
|
169
|
+
changed_files=data.get("changedFiles", 0),
|
|
170
|
+
mergeable=data.get("mergeable", "MERGEABLE") == "MERGEABLE",
|
|
171
|
+
draft=data.get("isDraft", False),
|
|
172
|
+
created_at=data.get("createdAt"),
|
|
173
|
+
updated_at=data.get("updatedAt"),
|
|
174
|
+
merged_at=data.get("mergedAt"),
|
|
175
|
+
reviews=reviews,
|
|
176
|
+
labels=labels
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def get_status_emoji(self) -> str:
|
|
180
|
+
"""Get emoji for PR state"""
|
|
181
|
+
if self.state == "MERGED":
|
|
182
|
+
return "🟣"
|
|
183
|
+
elif self.state == "CLOSED":
|
|
184
|
+
return "🔴"
|
|
185
|
+
elif self.draft:
|
|
186
|
+
return "📝"
|
|
187
|
+
elif self.state == "OPEN":
|
|
188
|
+
return "🟢"
|
|
189
|
+
return "⚪"
|
|
190
|
+
|
|
191
|
+
def get_review_status(self) -> str:
|
|
192
|
+
"""Get review status summary"""
|
|
193
|
+
if not self.reviews:
|
|
194
|
+
return "No reviews"
|
|
195
|
+
|
|
196
|
+
approved = sum(1 for r in self.reviews if r.state == "APPROVED")
|
|
197
|
+
changes = sum(1 for r in self.reviews if r.state == "CHANGES_REQUESTED")
|
|
198
|
+
|
|
199
|
+
if approved > 0 and changes == 0:
|
|
200
|
+
return f"✅ {approved} approved"
|
|
201
|
+
elif changes > 0:
|
|
202
|
+
return f"❌ {changes} changes requested"
|
|
203
|
+
else:
|
|
204
|
+
return f"💬 {len(self.reviews)} comments"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class PRSearchResult:
|
|
209
|
+
"""Result of searching pull requests"""
|
|
210
|
+
prs: List[PullRequest]
|
|
211
|
+
total: int
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_list(cls, data: List[dict]) -> 'PRSearchResult':
|
|
215
|
+
"""
|
|
216
|
+
Create PRSearchResult from list of PR data
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
data: List of PR dictionaries from GitHub API
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
PRSearchResult instance
|
|
223
|
+
"""
|
|
224
|
+
prs = [PullRequest.from_dict(pr_data) for pr_data in data]
|
|
225
|
+
return cls(prs=prs, total=len(prs))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class PRComment:
|
|
230
|
+
"""
|
|
231
|
+
Pull request comment (review comment or issue comment)
|
|
232
|
+
|
|
233
|
+
Attributes:
|
|
234
|
+
id: Comment ID
|
|
235
|
+
body: Comment text
|
|
236
|
+
user: User who created the comment
|
|
237
|
+
created_at: Creation timestamp
|
|
238
|
+
path: File path (for review comments)
|
|
239
|
+
line: Line number (for review comments)
|
|
240
|
+
diff_hunk: Diff context (for review comments)
|
|
241
|
+
pull_request_review_id: Review ID (for review comments)
|
|
242
|
+
in_reply_to_id: ID of parent comment (if it's a reply)
|
|
243
|
+
is_review_comment: True if inline review comment, False if issue comment
|
|
244
|
+
"""
|
|
245
|
+
id: int
|
|
246
|
+
body: str
|
|
247
|
+
user: User
|
|
248
|
+
created_at: str
|
|
249
|
+
path: Optional[str] = None
|
|
250
|
+
line: Optional[int] = None
|
|
251
|
+
diff_hunk: Optional[str] = None
|
|
252
|
+
pull_request_review_id: Optional[int] = None
|
|
253
|
+
in_reply_to_id: Optional[int] = None
|
|
254
|
+
is_review_comment: bool = True
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def from_dict(cls, data: Dict[str, Any], is_review: bool = True) -> 'PRComment':
|
|
258
|
+
"""Create from API response"""
|
|
259
|
+
user_data = data.get("user", {})
|
|
260
|
+
user = User(
|
|
261
|
+
login=user_data.get("login", ""),
|
|
262
|
+
name=user_data.get("name"),
|
|
263
|
+
email=user_data.get("email"),
|
|
264
|
+
avatar_url=user_data.get("avatar_url")
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return cls(
|
|
268
|
+
id=data.get("id", 0),
|
|
269
|
+
body=data.get("body", ""),
|
|
270
|
+
user=user,
|
|
271
|
+
created_at=data.get("created_at", ""),
|
|
272
|
+
path=data.get("path"),
|
|
273
|
+
line=data.get("line"),
|
|
274
|
+
diff_hunk=data.get("diff_hunk"),
|
|
275
|
+
pull_request_review_id=data.get("pull_request_review_id"),
|
|
276
|
+
in_reply_to_id=data.get("in_reply_to_id"),
|
|
277
|
+
is_review_comment=is_review
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class PRMergeResult:
|
|
283
|
+
"""
|
|
284
|
+
Result of merging a pull request
|
|
285
|
+
|
|
286
|
+
Attributes:
|
|
287
|
+
merged: Whether the PR was successfully merged
|
|
288
|
+
sha: Commit SHA of the merge (if successful)
|
|
289
|
+
message: Success or error message
|
|
290
|
+
"""
|
|
291
|
+
merged: bool
|
|
292
|
+
sha: Optional[str] = None
|
|
293
|
+
message: str = ""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@dataclass
|
|
297
|
+
class Issue:
|
|
298
|
+
"""
|
|
299
|
+
GitHub Issue representation.
|
|
300
|
+
"""
|
|
301
|
+
number: int
|
|
302
|
+
title: str
|
|
303
|
+
body: str
|
|
304
|
+
state: str
|
|
305
|
+
author: User
|
|
306
|
+
labels: List[str] = field(default_factory=list)
|
|
307
|
+
created_at: Optional[str] = None
|
|
308
|
+
updated_at: Optional[str] = None
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def from_dict(cls, data: dict) -> 'Issue':
|
|
312
|
+
"""
|
|
313
|
+
Create Issue from GitHub API response.
|
|
314
|
+
"""
|
|
315
|
+
author_data = data.get("author", {})
|
|
316
|
+
author = User.from_dict(author_data)
|
|
317
|
+
|
|
318
|
+
labels_data = data.get("labels", [])
|
|
319
|
+
labels = [label.get("name", "") for label in labels_data]
|
|
320
|
+
|
|
321
|
+
return cls(
|
|
322
|
+
number=data.get("number", 0),
|
|
323
|
+
title=data.get("title", ""),
|
|
324
|
+
body=data.get("body", ""),
|
|
325
|
+
state=data.get("state", "OPEN"),
|
|
326
|
+
author=author,
|
|
327
|
+
labels=labels,
|
|
328
|
+
created_at=data.get("createdAt"),
|
|
329
|
+
updated_at=data.get("updatedAt"),
|
|
330
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/plugin.py
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from titan_cli.core.plugins.plugin_base import TitanPlugin
|
|
6
|
+
from titan_cli.core.config import TitanConfig
|
|
7
|
+
from titan_cli.core.secrets import SecretManager
|
|
8
|
+
from titan_cli.core.plugins.models import GitHubPluginConfig
|
|
9
|
+
from .clients.github_client import GitHubClient, GitHubError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitHubPlugin(TitanPlugin):
|
|
13
|
+
"""
|
|
14
|
+
Titan CLI Plugin for GitHub operations.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "github"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "Provides GitHub integration for PRs, issues, and more."
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def dependencies(self) -> list[str]:
|
|
27
|
+
return ["git"]
|
|
28
|
+
|
|
29
|
+
def initialize(self, config: TitanConfig, secrets: SecretManager) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initializes the GitHubClient.
|
|
32
|
+
"""
|
|
33
|
+
# Get plugin-specific configuration data
|
|
34
|
+
plugin_config_data = self._get_plugin_config(config)
|
|
35
|
+
|
|
36
|
+
# Validate configuration using Pydantic model
|
|
37
|
+
validated_config = GitHubPluginConfig(**plugin_config_data)
|
|
38
|
+
|
|
39
|
+
repo_owner = validated_config.repo_owner
|
|
40
|
+
repo_name = validated_config.repo_name
|
|
41
|
+
|
|
42
|
+
# Get the git client from the registry
|
|
43
|
+
git_plugin = config.registry.get_plugin("git")
|
|
44
|
+
if not git_plugin or not git_plugin.is_available():
|
|
45
|
+
raise GitHubError("The 'git' plugin is a required dependency and is not available.")
|
|
46
|
+
git_client = git_plugin.get_client()
|
|
47
|
+
|
|
48
|
+
# Attempt to auto-detect if not explicitly configured
|
|
49
|
+
if not repo_owner or not repo_name:
|
|
50
|
+
detected_owner, detected_name = git_client.get_github_repo_info()
|
|
51
|
+
if detected_owner and detected_name:
|
|
52
|
+
repo_owner = repo_owner or detected_owner
|
|
53
|
+
repo_name = repo_name or detected_name
|
|
54
|
+
|
|
55
|
+
# If still missing, raise an error
|
|
56
|
+
if not repo_owner or not repo_name:
|
|
57
|
+
raise GitHubError("GitHub repository owner and name must be configured or auto-detected from git remote.")
|
|
58
|
+
|
|
59
|
+
# Initialize client with validated configuration and git_client
|
|
60
|
+
self._client = GitHubClient(
|
|
61
|
+
config=validated_config,
|
|
62
|
+
secrets=secrets,
|
|
63
|
+
git_client=git_client,
|
|
64
|
+
repo_owner=repo_owner, # Pass detected/configured owner
|
|
65
|
+
repo_name=repo_name # Pass detected/configured name
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _get_plugin_config(self, config: TitanConfig) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
Extract plugin-specific configuration.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
config: TitanConfig instance
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Plugin config dict (empty if not configured)
|
|
77
|
+
"""
|
|
78
|
+
if "github" not in config.config.plugins:
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
plugin_entry = config.config.plugins["github"]
|
|
82
|
+
return plugin_entry.config if hasattr(plugin_entry, 'config') else {}
|
|
83
|
+
|
|
84
|
+
def get_config_schema(self) -> dict:
|
|
85
|
+
"""Returns the JSON schema for the plugin's configuration."""
|
|
86
|
+
return GitHubPluginConfig.model_json_schema()
|
|
87
|
+
|
|
88
|
+
def is_available(self) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Checks if the GitHub CLI is installed and available.
|
|
91
|
+
"""
|
|
92
|
+
import shutil
|
|
93
|
+
return shutil.which("gh") is not None and hasattr(self, '_client') and self._client is not None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def workflows_path(self) -> Optional[Path]:
|
|
97
|
+
"""
|
|
98
|
+
Returns the path to the workflows directory for this plugin.
|
|
99
|
+
"""
|
|
100
|
+
return Path(__file__).parent / "workflows"
|
|
101
|
+
|
|
102
|
+
def get_client(self) -> GitHubClient:
|
|
103
|
+
"""
|
|
104
|
+
Returns the initialized GitHubClient instance.
|
|
105
|
+
"""
|
|
106
|
+
# Ensure the client is initialized, potentially adding a check here
|
|
107
|
+
if not hasattr(self, '_client') or self._client is None:
|
|
108
|
+
raise GitHubError("GitHubPlugin not initialized. GitHub client may not be available.")
|
|
109
|
+
return self._client
|
|
110
|
+
|
|
111
|
+
def get_steps(self) -> dict:
|
|
112
|
+
"""
|
|
113
|
+
Returns a dictionary of available workflow steps.
|
|
114
|
+
"""
|
|
115
|
+
from .steps.create_pr_step import create_pr_step
|
|
116
|
+
from .steps.github_prompt_steps import prompt_for_pr_title_step, prompt_for_pr_body_step, prompt_for_issue_body_step, prompt_for_self_assign_step, prompt_for_labels_step
|
|
117
|
+
from .steps.ai_pr_step import ai_suggest_pr_description_step
|
|
118
|
+
from .steps.issue_steps import ai_suggest_issue_title_and_body_step, create_issue_steps
|
|
119
|
+
from .steps.preview_step import preview_and_confirm_issue_step
|
|
120
|
+
return {
|
|
121
|
+
"create_pr": create_pr_step,
|
|
122
|
+
"prompt_for_pr_title": prompt_for_pr_title_step,
|
|
123
|
+
"prompt_for_pr_body": prompt_for_pr_body_step,
|
|
124
|
+
"prompt_for_issue_body_step": prompt_for_issue_body_step,
|
|
125
|
+
"prompt_for_self_assign": prompt_for_self_assign_step,
|
|
126
|
+
"prompt_for_labels": prompt_for_labels_step,
|
|
127
|
+
"ai_suggest_pr_description": ai_suggest_pr_description_step,
|
|
128
|
+
"ai_suggest_issue_title_and_body": ai_suggest_issue_title_and_body_step,
|
|
129
|
+
"create_issue": create_issue_steps,
|
|
130
|
+
"preview_and_confirm_issue": preview_and_confirm_issue_step,
|
|
131
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/steps/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Workflow steps for GitHub operations
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .create_pr_step import create_pr_step
|
|
7
|
+
from .ai_pr_step import ai_suggest_pr_description_step
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"create_pr_step",
|
|
11
|
+
"ai_suggest_pr_description_step",
|
|
12
|
+
]
|