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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Exceptions
|
|
3
|
+
|
|
4
|
+
Custom exceptions for Git operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GitError(Exception):
|
|
9
|
+
"""Base exception for Git errors"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GitCommandError(GitError):
|
|
14
|
+
"""Git command failed"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitClientError(GitError):
|
|
19
|
+
"""Git client initialization or configuration error"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GitBranchNotFoundError(GitError):
|
|
24
|
+
"""Branch not found"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GitDirtyWorkingTreeError(GitError):
|
|
29
|
+
"""Working tree has uncommitted changes"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GitNotRepositoryError(GitError):
|
|
34
|
+
"""Not a git repository"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GitMergeConflictError(GitError):
|
|
39
|
+
"""Merge conflict occurred"""
|
|
40
|
+
pass
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
class Messages:
|
|
3
|
+
class Prompts:
|
|
4
|
+
ENTER_COMMIT_MESSAGE: str = "Enter commit message:"
|
|
5
|
+
|
|
6
|
+
class Git:
|
|
7
|
+
"""Git operations messages"""
|
|
8
|
+
CLI_NOT_FOUND: str = "Git CLI not found. Please install Git."
|
|
9
|
+
NOT_A_REPOSITORY: str = "'{repo_path}' is not a git repository"
|
|
10
|
+
COMMAND_FAILED: str = "Git command failed: {error_msg}"
|
|
11
|
+
UNEXPECTED_ERROR: str = "An unexpected error occurred: {e}"
|
|
12
|
+
UNCOMMITTED_CHANGES_OVERWRITE_KEYWORD: str = "would be overwritten"
|
|
13
|
+
CANNOT_CHECKOUT_UNCOMMITTED_CHANGES: str = "Cannot checkout: uncommitted changes would be overwritten"
|
|
14
|
+
MERGE_CONFLICT_KEYWORD: str = "Merge conflict"
|
|
15
|
+
MERGE_CONFLICT_WHILE_UPDATING: str = "Merge conflict while updating branch '{branch}'"
|
|
16
|
+
AUTO_STASH_MESSAGE: str = "titan-cli-auto-stash at {timestamp}"
|
|
17
|
+
CANNOT_CHECKOUT_UNCOMMITTED_CHANGES_EXIST: str = "Cannot checkout {branch}: uncommitted changes exist"
|
|
18
|
+
STASH_FAILED_BEFORE_CHECKOUT: str = "Failed to stash changes before checkout"
|
|
19
|
+
SAFE_SWITCH_STASH_MESSAGE: str = "titan-cli-safe-switch: from {current} to {branch}"
|
|
20
|
+
|
|
21
|
+
# Commits
|
|
22
|
+
COMMITTING = "Committing changes..."
|
|
23
|
+
COMMIT_SUCCESS = "Committed: {sha}"
|
|
24
|
+
COMMIT_FAILED = "Commit failed: {error}"
|
|
25
|
+
NO_CHANGES = "No changes to commit"
|
|
26
|
+
|
|
27
|
+
# Branches
|
|
28
|
+
BRANCH_CREATING = "Creating branch: {name}"
|
|
29
|
+
BRANCH_CREATED = "Branch created: {name}"
|
|
30
|
+
BRANCH_SWITCHING = "Switching to branch: {name}"
|
|
31
|
+
BRANCH_SWITCHED = "Switched to branch: {name}"
|
|
32
|
+
BRANCH_DELETING = "Deleting branch: {name}"
|
|
33
|
+
BRANCH_DELETED = "Branch deleted: {name}"
|
|
34
|
+
BRANCH_EXISTS = "Branch already exists: {name}"
|
|
35
|
+
BRANCH_NOT_FOUND = "Branch not found: {name}"
|
|
36
|
+
BRANCH_INVALID_NAME = "Invalid branch name: {name}"
|
|
37
|
+
BRANCH_PROTECTED = "Cannot delete protected branch: {branch}"
|
|
38
|
+
|
|
39
|
+
# Push/Pull
|
|
40
|
+
PUSHING = "Pushing to remote..."
|
|
41
|
+
PUSH_SUCCESS = "Pushed to {remote}/{branch}"
|
|
42
|
+
PUSH_FAILED = "Push failed: {error}"
|
|
43
|
+
PULLING = "Pulling from remote..."
|
|
44
|
+
PULL_SUCCESS = "Pulled from {remote}/{branch}"
|
|
45
|
+
PULL_FAILED = "Pull failed: {error}"
|
|
46
|
+
|
|
47
|
+
# Status
|
|
48
|
+
STATUS_CLEAN = "Working directory clean"
|
|
49
|
+
STATUS_DIRTY = "Uncommitted changes detected"
|
|
50
|
+
|
|
51
|
+
# Repository
|
|
52
|
+
REPO_INIT = "Initializing git repository..."
|
|
53
|
+
REPO_INITIALIZED = "Git repository initialized"
|
|
54
|
+
|
|
55
|
+
class Steps:
|
|
56
|
+
class Status:
|
|
57
|
+
GIT_CLIENT_NOT_AVAILABLE: str = "Git client is not available in the workflow context."
|
|
58
|
+
STATUS_RETRIEVED_SUCCESS: str = "Git status retrieved successfully."
|
|
59
|
+
STATUS_RETRIEVED_WITH_UNCOMMITTED: str = "Git status retrieved. Working directory is not clean."
|
|
60
|
+
WORKING_DIRECTORY_NOT_CLEAN: str = " Working directory is not clean."
|
|
61
|
+
WORKING_DIRECTORY_IS_CLEAN: str = "Git status retrieved. Working directory is clean."
|
|
62
|
+
FAILED_TO_GET_STATUS: str = "Failed to get git status: {e}"
|
|
63
|
+
|
|
64
|
+
class Commit:
|
|
65
|
+
GIT_CLIENT_NOT_AVAILABLE: str = "Git client is not available in the workflow context."
|
|
66
|
+
COMMIT_MESSAGE_REQUIRED: str = "Commit message cannot be empty."
|
|
67
|
+
COMMIT_SUCCESS: str = "Commit created successfully: {commit_hash}"
|
|
68
|
+
CLIENT_ERROR_DURING_COMMIT: str = "Git client error during commit: {e}"
|
|
69
|
+
COMMAND_FAILED_DURING_COMMIT: str = "Git command failed during commit: {e}"
|
|
70
|
+
UNEXPECTED_ERROR_DURING_COMMIT: str = "An unexpected error occurred during commit: {e}"
|
|
71
|
+
WORKING_DIRECTORY_CLEAN: str = "Working directory is clean, skipping commit."
|
|
72
|
+
NO_COMMIT_MESSAGE: str = "No commit message provided, skipping commit."
|
|
73
|
+
|
|
74
|
+
class Push:
|
|
75
|
+
GIT_CLIENT_NOT_AVAILABLE: str = "Git client is not available in the workflow context."
|
|
76
|
+
PUSH_FAILED: str = "Git push failed: {e}"
|
|
77
|
+
|
|
78
|
+
class Branch:
|
|
79
|
+
GET_CURRENT_BRANCH_SUCCESS: str = "Current branch is '{branch}'"
|
|
80
|
+
GET_CURRENT_BRANCH_FAILED: str = "Failed to get current branch: {e}"
|
|
81
|
+
GET_BASE_BRANCH_SUCCESS: str = "Base branch is '{branch}'"
|
|
82
|
+
GET_BASE_BRANCH_FAILED: str = "Failed to get base branch: {e}"
|
|
83
|
+
|
|
84
|
+
class Prompt:
|
|
85
|
+
WORKING_DIRECTORY_CLEAN: str = "Working directory is clean, no need for a commit message."
|
|
86
|
+
COMMIT_MESSAGE_CAPTURED: str = "Commit message captured"
|
|
87
|
+
USER_CANCELLED: str = "User cancelled."
|
|
88
|
+
PROMPT_FAILED: str = "Failed to prompt for commit message: {e}"
|
|
89
|
+
|
|
90
|
+
class AICommitMessage:
|
|
91
|
+
AI_NOT_CONFIGURED: str = "AI not configured. Run 'titan ai configure' to enable AI features."
|
|
92
|
+
NO_CHANGES_TO_COMMIT: str = "No changes to commit"
|
|
93
|
+
ANALYZING_CHANGES: str = "Analyzing uncommitted changes..."
|
|
94
|
+
NO_UNCOMMITTED_CHANGES: str = "No uncommitted changes to analyze"
|
|
95
|
+
DIFF_TRUNCATED: str = "... (diff truncated for brevity)"
|
|
96
|
+
GENERATING_MESSAGE: str = "Generating commit message with AI..."
|
|
97
|
+
GENERATED_MESSAGE_TITLE: str = "AI Generated Commit Message:"
|
|
98
|
+
MESSAGE_LENGTH_WARNING: str = "Message is {length} chars (recommended: ≤72)"
|
|
99
|
+
CONFIRM_USE_MESSAGE: str = "Use this commit message?"
|
|
100
|
+
USER_DECLINED: str = "User declined AI-generated commit message"
|
|
101
|
+
SUCCESS_MESSAGE: str = "AI generated commit message"
|
|
102
|
+
GENERATION_FAILED: str = "AI generation failed: {e}"
|
|
103
|
+
FALLBACK_TO_MANUAL: str = "Falling back to manual commit message..."
|
|
104
|
+
GIT_CLIENT_NOT_AVAILABLE: str = "Git client is not available in the workflow context."
|
|
105
|
+
|
|
106
|
+
class Plugin:
|
|
107
|
+
GIT_CLIENT_INIT_WARNING: str = "Warning: GitPlugin could not initialize GitClient: {e}"
|
|
108
|
+
GIT_CLIENT_NOT_AVAILABLE: str = "GitPlugin not initialized or Git CLI not available."
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
msg = Messages()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Models
|
|
3
|
+
|
|
4
|
+
Data models for Git operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class GitBranch:
|
|
13
|
+
"""Git branch representation"""
|
|
14
|
+
name: str
|
|
15
|
+
is_current: bool = False
|
|
16
|
+
is_remote: bool = False
|
|
17
|
+
upstream: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class GitStatus:
|
|
22
|
+
"""Git repository status"""
|
|
23
|
+
branch: str
|
|
24
|
+
is_clean: bool
|
|
25
|
+
modified_files: List[str]
|
|
26
|
+
untracked_files: List[str]
|
|
27
|
+
staged_files: List[str]
|
|
28
|
+
ahead: int = 0
|
|
29
|
+
behind: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class GitCommit:
|
|
34
|
+
"""Git commit representation"""
|
|
35
|
+
hash: str
|
|
36
|
+
short_hash: str
|
|
37
|
+
message: str
|
|
38
|
+
author: str
|
|
39
|
+
date: str
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# plugins/titan-plugin-git/titan_plugin_git/plugin.py
|
|
2
|
+
import shutil
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from titan_cli.core.plugins.models import GitPluginConfig
|
|
6
|
+
from titan_cli.core.plugins.plugin_base import TitanPlugin
|
|
7
|
+
from titan_cli.core.config import TitanConfig # Needed for type hinting
|
|
8
|
+
from titan_cli.core.secrets import SecretManager # Needed for type hinting
|
|
9
|
+
from .clients.git_client import GitClient
|
|
10
|
+
from .exceptions import GitClientError
|
|
11
|
+
from .messages import msg
|
|
12
|
+
from .steps.status_step import get_git_status_step
|
|
13
|
+
from .steps.commit_step import create_git_commit_step
|
|
14
|
+
from .steps.push_step import create_git_push_step
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitPlugin(TitanPlugin):
|
|
18
|
+
"""
|
|
19
|
+
Titan CLI Plugin for Git operations.
|
|
20
|
+
Provides a GitClient for interacting with the Git CLI.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return "git"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def description(self) -> str:
|
|
29
|
+
return "Provides core Git CLI functionalities."
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def dependencies(self) -> list[str]:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
def initialize(self, config: TitanConfig, secrets: SecretManager) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Initialize with configuration.
|
|
38
|
+
|
|
39
|
+
Reads config from:
|
|
40
|
+
config.config.plugins["git"].config
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Get plugin-specific configuration data
|
|
44
|
+
plugin_config_data = self._get_plugin_config(config)
|
|
45
|
+
|
|
46
|
+
# Validate configuration using Pydantic model
|
|
47
|
+
validated_config = GitPluginConfig(**plugin_config_data)
|
|
48
|
+
|
|
49
|
+
# Initialize client with validated configuration
|
|
50
|
+
self._client = GitClient(
|
|
51
|
+
main_branch=validated_config.main_branch,
|
|
52
|
+
default_remote=validated_config.default_remote
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _get_plugin_config(self, config: TitanConfig) -> dict:
|
|
56
|
+
"""
|
|
57
|
+
Extract plugin-specific configuration.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: TitanConfig instance
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Plugin config dict (empty if not configured)
|
|
64
|
+
"""
|
|
65
|
+
if "git" not in config.config.plugins:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
plugin_entry = config.config.plugins["git"]
|
|
69
|
+
return plugin_entry.config if hasattr(plugin_entry, 'config') else {}
|
|
70
|
+
|
|
71
|
+
def get_config_schema(self) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Return JSON schema for plugin configuration.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
JSON schema dict
|
|
77
|
+
"""
|
|
78
|
+
from titan_cli.core.plugins.models import GitPluginConfig
|
|
79
|
+
return GitPluginConfig.model_json_schema()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_available(self) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Checks if the Git CLI is installed and available.
|
|
85
|
+
"""
|
|
86
|
+
# Leverage the GitClient's own check
|
|
87
|
+
return shutil.which("git") is not None and hasattr(self, '_client') and self._client is not None
|
|
88
|
+
|
|
89
|
+
def get_client(self) -> GitClient:
|
|
90
|
+
"""
|
|
91
|
+
Returns the initialized GitClient instance.
|
|
92
|
+
"""
|
|
93
|
+
if not hasattr(self, '_client') or self._client is None:
|
|
94
|
+
raise GitClientError(msg.Plugin.git_client_not_available)
|
|
95
|
+
return self._client
|
|
96
|
+
|
|
97
|
+
def get_steps(self) -> dict:
|
|
98
|
+
"""
|
|
99
|
+
Returns a dictionary of available workflow steps.
|
|
100
|
+
"""
|
|
101
|
+
from .steps.branch_steps import get_current_branch_step, get_base_branch_step
|
|
102
|
+
from .steps.ai_commit_message_step import ai_generate_commit_message
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"get_status": get_git_status_step,
|
|
106
|
+
"create_commit": create_git_commit_step,
|
|
107
|
+
"push": create_git_push_step,
|
|
108
|
+
"get_current_branch": get_current_branch_step,
|
|
109
|
+
"get_base_branch": get_base_branch_step,
|
|
110
|
+
"ai_generate_commit_message": ai_generate_commit_message,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def workflows_path(self) -> Optional[Path]:
|
|
115
|
+
"""
|
|
116
|
+
Returns the path to the workflows directory for this plugin.
|
|
117
|
+
"""
|
|
118
|
+
return Path(__file__).parent / "workflows"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file makes the directory a Python package.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# plugins/titan-plugin-git/titan_plugin_git/steps/ai_commit_message_step.py
|
|
2
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
|
|
3
|
+
from titan_plugin_git.messages import msg
|
|
4
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
|
|
8
|
+
"""
|
|
9
|
+
Generate a commit message using AI based on the current changes.
|
|
10
|
+
|
|
11
|
+
Uses AI to analyze the diff of uncommitted changes and generate a
|
|
12
|
+
conventional commit message (type(scope): description).
|
|
13
|
+
|
|
14
|
+
Requires:
|
|
15
|
+
ctx.git: An initialized GitClient.
|
|
16
|
+
ctx.ai: An initialized AIClient.
|
|
17
|
+
|
|
18
|
+
Inputs (from ctx.data):
|
|
19
|
+
git_status: Current git status with changes.
|
|
20
|
+
|
|
21
|
+
Outputs (saved to ctx.data):
|
|
22
|
+
commit_message (str): AI-generated commit message.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Success: If the commit message was generated successfully.
|
|
26
|
+
Error: If the operation fails.
|
|
27
|
+
Skip: If no changes, AI not configured, or user declined.
|
|
28
|
+
"""
|
|
29
|
+
if not ctx.textual:
|
|
30
|
+
return Error("Textual UI context is not available for this step.")
|
|
31
|
+
|
|
32
|
+
# Check if AI is configured
|
|
33
|
+
if not ctx.ai or not ctx.ai.is_available():
|
|
34
|
+
ctx.textual.mount(
|
|
35
|
+
Panel(
|
|
36
|
+
text=msg.Steps.AICommitMessage.AI_NOT_CONFIGURED,
|
|
37
|
+
panel_type="info"
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return Skip(msg.Steps.AICommitMessage.AI_NOT_CONFIGURED)
|
|
41
|
+
|
|
42
|
+
# Get git client
|
|
43
|
+
if not ctx.git:
|
|
44
|
+
return Error(msg.Steps.AICommitMessage.GIT_CLIENT_NOT_AVAILABLE)
|
|
45
|
+
|
|
46
|
+
# Get git status
|
|
47
|
+
git_status = ctx.get('git_status')
|
|
48
|
+
if not git_status or git_status.is_clean:
|
|
49
|
+
ctx.textual.mount(
|
|
50
|
+
Panel(
|
|
51
|
+
text=msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT,
|
|
52
|
+
panel_type="info"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return Skip(msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Get diff of uncommitted changes
|
|
59
|
+
ctx.textual.text(msg.Steps.AICommitMessage.ANALYZING_CHANGES, markup="dim")
|
|
60
|
+
|
|
61
|
+
# Get diff of all uncommitted changes
|
|
62
|
+
diff_text = ctx.git.get_uncommitted_diff()
|
|
63
|
+
|
|
64
|
+
if not diff_text or diff_text.strip() == "":
|
|
65
|
+
return Skip(msg.Steps.AICommitMessage.NO_UNCOMMITTED_CHANGES)
|
|
66
|
+
|
|
67
|
+
# Build AI prompt
|
|
68
|
+
# Get list of modified files for the summary
|
|
69
|
+
all_files = git_status.modified_files + git_status.untracked_files + git_status.staged_files
|
|
70
|
+
files_summary = "\n".join([f" - {f}" for f in all_files]) if all_files else "(checking diff)"
|
|
71
|
+
|
|
72
|
+
# Limit diff size to avoid token overflow (keep first 8000 chars)
|
|
73
|
+
diff_preview = diff_text[:8000] if len(diff_text) > 8000 else diff_text
|
|
74
|
+
if len(diff_text) > 8000:
|
|
75
|
+
diff_preview += f"\n\n{msg.Steps.AICommitMessage.DIFF_TRUNCATED}"
|
|
76
|
+
|
|
77
|
+
prompt = f"""Analyze these code changes and generate a conventional commit message.
|
|
78
|
+
|
|
79
|
+
## Changed Files ({len(all_files)} total)
|
|
80
|
+
{files_summary}
|
|
81
|
+
|
|
82
|
+
## Diff
|
|
83
|
+
```diff
|
|
84
|
+
{diff_preview}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## CRITICAL Instructions
|
|
88
|
+
Generate ONE single-line conventional commit message following this EXACT format:
|
|
89
|
+
- type(scope): description
|
|
90
|
+
- Types: feat, fix, refactor, docs, test, chore, style, perf
|
|
91
|
+
- Scope: area affected (e.g., auth, api, ui)
|
|
92
|
+
- Description: clear summary in imperative mood (be descriptive, concise, and at least 5 words long)
|
|
93
|
+
- NO line breaks, NO body, NO additional explanation
|
|
94
|
+
|
|
95
|
+
Examples (notice they are all one line):
|
|
96
|
+
- feat(auth): add OAuth2 integration with Google provider
|
|
97
|
+
- fix(api): resolve race condition in cache invalidation
|
|
98
|
+
- refactor(ui): simplify menu component and remove unused props
|
|
99
|
+
- refactor(workflows): add support for nested workflow execution
|
|
100
|
+
|
|
101
|
+
Return ONLY the single-line commit message, absolutely nothing else."""
|
|
102
|
+
|
|
103
|
+
# Call AI with loading indicator
|
|
104
|
+
from titan_cli.ai.models import AIMessage
|
|
105
|
+
|
|
106
|
+
messages = [AIMessage(role="user", content=prompt)]
|
|
107
|
+
|
|
108
|
+
with ctx.textual.loading(msg.Steps.AICommitMessage.GENERATING_MESSAGE):
|
|
109
|
+
response = ctx.ai.generate(messages, max_tokens=1024, temperature=0.7)
|
|
110
|
+
|
|
111
|
+
commit_message = response.content.strip()
|
|
112
|
+
|
|
113
|
+
# Clean up the message (remove quotes, newlines, extra whitespace)
|
|
114
|
+
commit_message = commit_message.strip('"').strip("'").strip()
|
|
115
|
+
# Take only the first line if AI returned multiple lines
|
|
116
|
+
commit_message = commit_message.split('\n')[0].strip()
|
|
117
|
+
|
|
118
|
+
# Show preview to user
|
|
119
|
+
ctx.textual.text("") # spacing
|
|
120
|
+
ctx.textual.text(msg.Steps.AICommitMessage.GENERATED_MESSAGE_TITLE, markup="bold")
|
|
121
|
+
ctx.textual.text(f" {commit_message}", markup="bold cyan")
|
|
122
|
+
|
|
123
|
+
# Warn if message is too long
|
|
124
|
+
if len(commit_message) > 72:
|
|
125
|
+
ctx.textual.text(msg.Steps.AICommitMessage.MESSAGE_LENGTH_WARNING.format(length=len(commit_message)), markup="yellow")
|
|
126
|
+
|
|
127
|
+
ctx.textual.text("") # spacing
|
|
128
|
+
|
|
129
|
+
# Ask user if they want to use it
|
|
130
|
+
use_ai = ctx.textual.ask_confirm(
|
|
131
|
+
msg.Steps.AICommitMessage.CONFIRM_USE_MESSAGE,
|
|
132
|
+
default=True
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if not use_ai:
|
|
136
|
+
try:
|
|
137
|
+
manual_message = ctx.textual.ask_text(msg.Prompts.ENTER_COMMIT_MESSAGE)
|
|
138
|
+
if not manual_message:
|
|
139
|
+
return Error(msg.Steps.Commit.COMMIT_MESSAGE_REQUIRED)
|
|
140
|
+
|
|
141
|
+
# Overwrite the metadata to ensure the manual message is used
|
|
142
|
+
return Success(
|
|
143
|
+
message=msg.Steps.Prompt.COMMIT_MESSAGE_CAPTURED,
|
|
144
|
+
metadata={"commit_message": manual_message}
|
|
145
|
+
)
|
|
146
|
+
except (KeyboardInterrupt, EOFError):
|
|
147
|
+
return Error(msg.Steps.Prompt.USER_CANCELLED)
|
|
148
|
+
|
|
149
|
+
# Show success panel
|
|
150
|
+
ctx.textual.mount(
|
|
151
|
+
Panel(
|
|
152
|
+
text="AI commit message generated successfully",
|
|
153
|
+
panel_type="success"
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Success - save to context
|
|
158
|
+
return Success(
|
|
159
|
+
msg.Steps.AICommitMessage.SUCCESS_MESSAGE,
|
|
160
|
+
metadata={"commit_message": commit_message}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
ctx.textual.text(msg.Steps.AICommitMessage.GENERATION_FAILED.format(e=e), markup="yellow")
|
|
165
|
+
ctx.textual.text(msg.Steps.AICommitMessage.FALLBACK_TO_MANUAL, markup="dim")
|
|
166
|
+
|
|
167
|
+
return Skip(msg.Steps.AICommitMessage.GENERATION_FAILED.format(e=e))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Export for plugin registration
|
|
171
|
+
__all__ = ["ai_generate_commit_message"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# plugins/titan-plugin-git/titan_plugin_git/steps/branch_steps.py
|
|
2
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
3
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
4
|
+
from titan_plugin_git.messages import msg
|
|
5
|
+
|
|
6
|
+
def get_current_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
7
|
+
"""
|
|
8
|
+
Retrieves the current git branch name and saves it to the context.
|
|
9
|
+
|
|
10
|
+
Requires:
|
|
11
|
+
ctx.git: An initialized GitClient.
|
|
12
|
+
|
|
13
|
+
Outputs (saved to ctx.data):
|
|
14
|
+
pr_head_branch (str): The name of the current branch, to be used as the head branch for a PR.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Success: If the current branch was retrieved successfully.
|
|
18
|
+
Error: If the GitClient is not available or the git command fails.
|
|
19
|
+
"""
|
|
20
|
+
if not ctx.textual:
|
|
21
|
+
return Error("Textual UI context is not available for this step.")
|
|
22
|
+
|
|
23
|
+
if not ctx.git:
|
|
24
|
+
error_msg = msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE
|
|
25
|
+
ctx.textual.mount(
|
|
26
|
+
Panel(
|
|
27
|
+
text=error_msg,
|
|
28
|
+
panel_type="error"
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
return Error(error_msg)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
current_branch = ctx.git.get_current_branch()
|
|
35
|
+
success_msg = msg.Steps.Branch.GET_CURRENT_BRANCH_SUCCESS.format(branch=current_branch)
|
|
36
|
+
ctx.textual.mount(
|
|
37
|
+
Panel(
|
|
38
|
+
text=success_msg,
|
|
39
|
+
panel_type="success"
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return Success(
|
|
43
|
+
success_msg,
|
|
44
|
+
metadata={"pr_head_branch": current_branch}
|
|
45
|
+
)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
error_msg = msg.Steps.Branch.GET_CURRENT_BRANCH_FAILED.format(e=e)
|
|
48
|
+
ctx.textual.mount(
|
|
49
|
+
Panel(
|
|
50
|
+
text=error_msg,
|
|
51
|
+
panel_type="error"
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return Error(error_msg, exception=e)
|
|
55
|
+
|
|
56
|
+
def get_base_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
57
|
+
"""
|
|
58
|
+
Retrieves the configured main/base branch name and saves it to the context.
|
|
59
|
+
|
|
60
|
+
Requires:
|
|
61
|
+
ctx.git: An initialized GitClient.
|
|
62
|
+
|
|
63
|
+
Outputs (saved to ctx.data):
|
|
64
|
+
pr_base_branch (str): The name of the base branch, to be used as the base branch for a PR.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Success: If the base branch was retrieved successfully.
|
|
68
|
+
Error: If the GitClient is not available or the git command fails.
|
|
69
|
+
"""
|
|
70
|
+
if not ctx.textual:
|
|
71
|
+
return Error("Textual UI context is not available for this step.")
|
|
72
|
+
|
|
73
|
+
if not ctx.git:
|
|
74
|
+
error_msg = msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE
|
|
75
|
+
ctx.textual.mount(
|
|
76
|
+
Panel(
|
|
77
|
+
text=error_msg,
|
|
78
|
+
panel_type="error"
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return Error(error_msg)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
base_branch = ctx.git.main_branch
|
|
85
|
+
success_msg = msg.Steps.Branch.GET_BASE_BRANCH_SUCCESS.format(branch=base_branch)
|
|
86
|
+
ctx.textual.mount(
|
|
87
|
+
Panel(
|
|
88
|
+
text=success_msg,
|
|
89
|
+
panel_type="success"
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return Success(
|
|
93
|
+
success_msg,
|
|
94
|
+
metadata={"pr_base_branch": base_branch}
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
error_msg = msg.Steps.Branch.GET_BASE_BRANCH_FAILED.format(e=e)
|
|
98
|
+
ctx.textual.mount(
|
|
99
|
+
Panel(
|
|
100
|
+
text=error_msg,
|
|
101
|
+
panel_type="error"
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return Error(error_msg, exception=e)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# plugins/titan-plugin-git/titan_plugin_git/steps/commit_step.py
|
|
2
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
3
|
+
from titan_cli.engine.results import Skip
|
|
4
|
+
from titan_plugin_git.exceptions import GitClientError, GitCommandError
|
|
5
|
+
from titan_plugin_git.messages import msg
|
|
6
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
10
|
+
"""
|
|
11
|
+
Creates a git commit.
|
|
12
|
+
Skips if the working directory is clean or if a commit was already created.
|
|
13
|
+
|
|
14
|
+
Requires:
|
|
15
|
+
ctx.git: An initialized GitClient.
|
|
16
|
+
|
|
17
|
+
Inputs (from ctx.data):
|
|
18
|
+
git_status (GitStatus): The git status object, used to check if the working directory is clean.
|
|
19
|
+
commit_message (str): The message for the commit.
|
|
20
|
+
all_files (bool, optional): Whether to commit all modified and new files. Defaults to True.
|
|
21
|
+
commit_hash (str, optional): If present, indicates a commit was already created.
|
|
22
|
+
|
|
23
|
+
Outputs (saved to ctx.data):
|
|
24
|
+
commit_hash (str): The hash of the created commit.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Success: If the commit was created successfully.
|
|
28
|
+
Error: If the GitClient is not available, or the commit operation fails.
|
|
29
|
+
Skip: If there are no changes to commit or a commit was already created.
|
|
30
|
+
"""
|
|
31
|
+
if not ctx.textual:
|
|
32
|
+
return Error("Textual UI context is not available for this step.")
|
|
33
|
+
|
|
34
|
+
# Skip if there's nothing to commit
|
|
35
|
+
git_status = ctx.data.get("git_status")
|
|
36
|
+
if git_status and git_status.is_clean:
|
|
37
|
+
ctx.textual.mount(
|
|
38
|
+
Panel(
|
|
39
|
+
text=msg.Steps.Commit.WORKING_DIRECTORY_CLEAN,
|
|
40
|
+
panel_type="info"
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return Skip(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN)
|
|
44
|
+
|
|
45
|
+
if not ctx.git:
|
|
46
|
+
return Error(msg.Steps.Commit.GIT_CLIENT_NOT_AVAILABLE)
|
|
47
|
+
|
|
48
|
+
commit_message = ctx.get('commit_message')
|
|
49
|
+
if not commit_message:
|
|
50
|
+
ctx.textual.mount(
|
|
51
|
+
Panel(
|
|
52
|
+
text=msg.Steps.Commit.NO_COMMIT_MESSAGE,
|
|
53
|
+
panel_type="info"
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
return Skip(msg.Steps.Commit.NO_COMMIT_MESSAGE)
|
|
57
|
+
|
|
58
|
+
all_files = ctx.get('all_files', True)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
commit_hash = ctx.git.commit(message=commit_message, all=all_files)
|
|
62
|
+
|
|
63
|
+
# Show success panel
|
|
64
|
+
ctx.textual.mount(
|
|
65
|
+
Panel(
|
|
66
|
+
text=f"Commit created: {commit_hash[:7]}",
|
|
67
|
+
panel_type="success"
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return Success(
|
|
72
|
+
message=msg.Steps.Commit.COMMIT_SUCCESS.format(commit_hash=commit_hash),
|
|
73
|
+
metadata={"commit_hash": commit_hash}
|
|
74
|
+
)
|
|
75
|
+
except GitClientError as e:
|
|
76
|
+
return Error(msg.Steps.Commit.CLIENT_ERROR_DURING_COMMIT.format(e=e))
|
|
77
|
+
except GitCommandError as e:
|
|
78
|
+
return Error(msg.Steps.Commit.COMMAND_FAILED_DURING_COMMIT.format(e=e))
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return Error(msg.Steps.Commit.UNEXPECTED_ERROR_DURING_COMMIT.format(e=e))
|