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.
Files changed (146) hide show
  1. titan_cli/__init__.py +3 -0
  2. titan_cli/__main__.py +4 -0
  3. titan_cli/ai/__init__.py +0 -0
  4. titan_cli/ai/agents/__init__.py +15 -0
  5. titan_cli/ai/agents/base.py +152 -0
  6. titan_cli/ai/client.py +170 -0
  7. titan_cli/ai/constants.py +56 -0
  8. titan_cli/ai/exceptions.py +48 -0
  9. titan_cli/ai/models.py +34 -0
  10. titan_cli/ai/oauth_helper.py +120 -0
  11. titan_cli/ai/providers/__init__.py +9 -0
  12. titan_cli/ai/providers/anthropic.py +117 -0
  13. titan_cli/ai/providers/base.py +75 -0
  14. titan_cli/ai/providers/gemini.py +278 -0
  15. titan_cli/cli.py +59 -0
  16. titan_cli/clients/__init__.py +1 -0
  17. titan_cli/clients/gcloud_client.py +52 -0
  18. titan_cli/core/__init__.py +3 -0
  19. titan_cli/core/config.py +274 -0
  20. titan_cli/core/discovery.py +51 -0
  21. titan_cli/core/errors.py +81 -0
  22. titan_cli/core/models.py +52 -0
  23. titan_cli/core/plugins/available.py +36 -0
  24. titan_cli/core/plugins/models.py +67 -0
  25. titan_cli/core/plugins/plugin_base.py +108 -0
  26. titan_cli/core/plugins/plugin_registry.py +163 -0
  27. titan_cli/core/secrets.py +141 -0
  28. titan_cli/core/workflows/__init__.py +22 -0
  29. titan_cli/core/workflows/models.py +88 -0
  30. titan_cli/core/workflows/project_step_source.py +86 -0
  31. titan_cli/core/workflows/workflow_exceptions.py +17 -0
  32. titan_cli/core/workflows/workflow_filter_service.py +137 -0
  33. titan_cli/core/workflows/workflow_registry.py +419 -0
  34. titan_cli/core/workflows/workflow_sources.py +307 -0
  35. titan_cli/engine/__init__.py +39 -0
  36. titan_cli/engine/builder.py +159 -0
  37. titan_cli/engine/context.py +82 -0
  38. titan_cli/engine/mock_context.py +176 -0
  39. titan_cli/engine/results.py +91 -0
  40. titan_cli/engine/steps/ai_assistant_step.py +185 -0
  41. titan_cli/engine/steps/command_step.py +93 -0
  42. titan_cli/engine/utils/__init__.py +3 -0
  43. titan_cli/engine/utils/venv.py +31 -0
  44. titan_cli/engine/workflow_executor.py +187 -0
  45. titan_cli/external_cli/__init__.py +0 -0
  46. titan_cli/external_cli/configs.py +17 -0
  47. titan_cli/external_cli/launcher.py +65 -0
  48. titan_cli/messages.py +121 -0
  49. titan_cli/ui/tui/__init__.py +205 -0
  50. titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
  51. titan_cli/ui/tui/app.py +113 -0
  52. titan_cli/ui/tui/icons.py +70 -0
  53. titan_cli/ui/tui/screens/__init__.py +24 -0
  54. titan_cli/ui/tui/screens/ai_config.py +498 -0
  55. titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
  56. titan_cli/ui/tui/screens/base.py +110 -0
  57. titan_cli/ui/tui/screens/cli_launcher.py +151 -0
  58. titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
  59. titan_cli/ui/tui/screens/main_menu.py +162 -0
  60. titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
  61. titan_cli/ui/tui/screens/plugin_management.py +377 -0
  62. titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
  63. titan_cli/ui/tui/screens/workflow_execution.py +592 -0
  64. titan_cli/ui/tui/screens/workflows.py +249 -0
  65. titan_cli/ui/tui/textual_components.py +537 -0
  66. titan_cli/ui/tui/textual_workflow_executor.py +405 -0
  67. titan_cli/ui/tui/theme.py +102 -0
  68. titan_cli/ui/tui/widgets/__init__.py +40 -0
  69. titan_cli/ui/tui/widgets/button.py +108 -0
  70. titan_cli/ui/tui/widgets/header.py +116 -0
  71. titan_cli/ui/tui/widgets/panel.py +81 -0
  72. titan_cli/ui/tui/widgets/status_bar.py +115 -0
  73. titan_cli/ui/tui/widgets/table.py +77 -0
  74. titan_cli/ui/tui/widgets/text.py +177 -0
  75. titan_cli/utils/__init__.py +0 -0
  76. titan_cli/utils/autoupdate.py +155 -0
  77. titan_cli-0.1.0.dist-info/METADATA +149 -0
  78. titan_cli-0.1.0.dist-info/RECORD +146 -0
  79. titan_cli-0.1.0.dist-info/WHEEL +4 -0
  80. titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
  81. titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  82. titan_plugin_git/__init__.py +1 -0
  83. titan_plugin_git/clients/__init__.py +8 -0
  84. titan_plugin_git/clients/git_client.py +772 -0
  85. titan_plugin_git/exceptions.py +40 -0
  86. titan_plugin_git/messages.py +112 -0
  87. titan_plugin_git/models.py +39 -0
  88. titan_plugin_git/plugin.py +118 -0
  89. titan_plugin_git/steps/__init__.py +1 -0
  90. titan_plugin_git/steps/ai_commit_message_step.py +171 -0
  91. titan_plugin_git/steps/branch_steps.py +104 -0
  92. titan_plugin_git/steps/commit_step.py +80 -0
  93. titan_plugin_git/steps/push_step.py +63 -0
  94. titan_plugin_git/steps/status_step.py +59 -0
  95. titan_plugin_git/workflows/__previews__/__init__.py +1 -0
  96. titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
  97. titan_plugin_git/workflows/commit-ai.yaml +28 -0
  98. titan_plugin_github/__init__.py +11 -0
  99. titan_plugin_github/agents/__init__.py +6 -0
  100. titan_plugin_github/agents/config_loader.py +130 -0
  101. titan_plugin_github/agents/issue_generator.py +353 -0
  102. titan_plugin_github/agents/pr_agent.py +528 -0
  103. titan_plugin_github/clients/__init__.py +8 -0
  104. titan_plugin_github/clients/github_client.py +1105 -0
  105. titan_plugin_github/config/__init__.py +0 -0
  106. titan_plugin_github/config/pr_agent.toml +85 -0
  107. titan_plugin_github/exceptions.py +28 -0
  108. titan_plugin_github/messages.py +88 -0
  109. titan_plugin_github/models.py +330 -0
  110. titan_plugin_github/plugin.py +131 -0
  111. titan_plugin_github/steps/__init__.py +12 -0
  112. titan_plugin_github/steps/ai_pr_step.py +172 -0
  113. titan_plugin_github/steps/create_pr_step.py +86 -0
  114. titan_plugin_github/steps/github_prompt_steps.py +171 -0
  115. titan_plugin_github/steps/issue_steps.py +143 -0
  116. titan_plugin_github/steps/preview_step.py +40 -0
  117. titan_plugin_github/utils.py +82 -0
  118. titan_plugin_github/workflows/__previews__/__init__.py +1 -0
  119. titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
  120. titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
  121. titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
  122. titan_plugin_jira/__init__.py +8 -0
  123. titan_plugin_jira/agents/__init__.py +6 -0
  124. titan_plugin_jira/agents/config_loader.py +154 -0
  125. titan_plugin_jira/agents/jira_agent.py +553 -0
  126. titan_plugin_jira/agents/prompts.py +364 -0
  127. titan_plugin_jira/agents/response_parser.py +435 -0
  128. titan_plugin_jira/agents/token_tracker.py +223 -0
  129. titan_plugin_jira/agents/validators.py +246 -0
  130. titan_plugin_jira/clients/jira_client.py +745 -0
  131. titan_plugin_jira/config/jira_agent.toml +92 -0
  132. titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
  133. titan_plugin_jira/exceptions.py +37 -0
  134. titan_plugin_jira/formatters/__init__.py +6 -0
  135. titan_plugin_jira/formatters/markdown_formatter.py +245 -0
  136. titan_plugin_jira/messages.py +115 -0
  137. titan_plugin_jira/models.py +89 -0
  138. titan_plugin_jira/plugin.py +264 -0
  139. titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
  140. titan_plugin_jira/steps/get_issue_step.py +82 -0
  141. titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
  142. titan_plugin_jira/steps/search_saved_query_step.py +238 -0
  143. titan_plugin_jira/utils/__init__.py +13 -0
  144. titan_plugin_jira/utils/issue_sorter.py +140 -0
  145. titan_plugin_jira/utils/saved_queries.py +150 -0
  146. 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))