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,307 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import List, Optional, Set, Protocol
6
+ import yaml
7
+ from dataclasses import dataclass, field
8
+
9
+
10
+ class PluginRegistryProtocol(Protocol):
11
+ """Protocol defining the interface that PluginRegistry must implement for workflow sources."""
12
+
13
+ def list_installed(self) -> List[str]:
14
+ """List successfully loaded plugins."""
15
+ ...
16
+
17
+ def get_plugin(self, name: str):
18
+ """Get plugin instance by name."""
19
+ ...
20
+
21
+ @dataclass
22
+ class WorkflowInfo:
23
+ """Metadata about a discovered, but not yet parsed, workflow."""
24
+ name: str
25
+ description: str
26
+ source: str # "project", "user", "system", "plugin:github"
27
+ path: Path
28
+ category: Optional[str] = None
29
+ required_plugins: Set[str] = field(default_factory=set)
30
+
31
+ def _parse_workflow_info(file: Path, source_name: str, plugin_registry: PluginRegistryProtocol) -> WorkflowInfo:
32
+ """
33
+ Helper to extract metadata and plugin dependencies from a workflow file.
34
+ Does not resolve 'extends' or nested 'workflow' calls to keep discovery fast.
35
+ """
36
+ try:
37
+ with open(file, 'r', encoding='utf-8') as f:
38
+ config = yaml.safe_load(f) or {}
39
+ except Exception:
40
+ config = {}
41
+
42
+ required_plugins: Set[str] = set()
43
+
44
+ # Check direct plugin dependencies in steps
45
+ steps = config.get("steps", [])
46
+ if isinstance(steps, list):
47
+ for step in steps:
48
+ if isinstance(step, dict) and "plugin" in step and step["plugin"] not in ["core", "project"]:
49
+ required_plugins.add(step["plugin"])
50
+
51
+ # Check 'extends' field for plugin dependencies
52
+ extends_ref = config.get("extends")
53
+ if extends_ref and isinstance(extends_ref, str):
54
+ if extends_ref.startswith("plugin:"):
55
+ # Extract plugin name from "plugin:git/commit-ai" -> "git"
56
+ plugin_part = extends_ref.split(':', 1)[1]
57
+ plugin_name = plugin_part.split('/', 1)[0]
58
+ required_plugins.add(plugin_name)
59
+
60
+ return WorkflowInfo(
61
+ name=file.stem,
62
+ description=config.get("description", "No description available."),
63
+ source=source_name,
64
+ path=file,
65
+ category=config.get("category"),
66
+ required_plugins=required_plugins
67
+ )
68
+
69
+
70
+ class WorkflowSource(ABC):
71
+ """
72
+ Abstract base class for a source of workflows.
73
+ This pattern allows discovering workflows from the project, user's home,
74
+ system-wide, or from plugins, in a uniform way.
75
+ """
76
+ def __init__(self, plugin_registry: PluginRegistryProtocol):
77
+ self._plugin_registry = plugin_registry
78
+
79
+ @property
80
+ @abstractmethod
81
+ def name(self) -> str:
82
+ """The unique name of the source (e.g., 'project', 'user', 'plugin:github')."""
83
+ pass
84
+
85
+ @abstractmethod
86
+ def discover(self) -> List[WorkflowInfo]:
87
+ """Discover all available workflows from this source."""
88
+ pass
89
+
90
+ @abstractmethod
91
+ def find(self, name: str) -> Optional[Path]:
92
+ """Find a specific workflow file by its name within this source."""
93
+ pass
94
+
95
+ @abstractmethod
96
+ def contains(self, path: Path) -> bool:
97
+ """Check if a given file path belongs to this source."""
98
+ pass
99
+
100
+ class ProjectWorkflowSource(WorkflowSource):
101
+ """
102
+ Represents workflows defined within the current project
103
+ at the conventional '.titan/workflows/' directory.
104
+ """
105
+
106
+ def __init__(self, path: Path, plugin_registry: PluginRegistryProtocol):
107
+ super().__init__(plugin_registry)
108
+ self._path = path.resolve()
109
+
110
+ @property
111
+ def name(self) -> str:
112
+ return "project"
113
+
114
+ def discover(self) -> List[WorkflowInfo]:
115
+ """Discovers all .yaml or .yml files in the project's workflow directory."""
116
+ if not self._path.is_dir():
117
+ return []
118
+
119
+ workflows = []
120
+ for file in self._path.glob("*.yaml"):
121
+ workflows.append(self._to_workflow_info(file))
122
+ for file in self._path.glob("*.yml"):
123
+ if file.stem not in [w.name for w in workflows]: # Avoid duplicates if both .yaml and .yml exist
124
+ workflows.append(self._to_workflow_info(file))
125
+ return workflows
126
+
127
+ def find(self, name: str) -> Optional[Path]:
128
+ """Finds a workflow by name in the project directory."""
129
+ yaml_file = self._path / f"{name}.yaml"
130
+ if yaml_file.is_file():
131
+ return yaml_file
132
+
133
+ yml_file = self._path / f"{name}.yml"
134
+ if yml_file.is_file():
135
+ return yml_file
136
+
137
+ return None
138
+
139
+ def contains(self, path: Path) -> bool:
140
+ """Checks if the given path is within this project's workflow directory."""
141
+ try:
142
+ path.resolve().relative_to(self._path)
143
+ return True
144
+ except ValueError:
145
+ return False
146
+
147
+ def _to_workflow_info(self, file: Path) -> WorkflowInfo:
148
+ """Helper to extract metadata from a workflow file."""
149
+ return _parse_workflow_info(file, self.name, self._plugin_registry)
150
+
151
+ class UserWorkflowSource(WorkflowSource):
152
+ """
153
+ Represents workflows defined by the user in their home directory
154
+ at '~/.titan/workflows/'.
155
+ """
156
+
157
+ def __init__(self, path: Path, plugin_registry: PluginRegistryProtocol):
158
+ super().__init__(plugin_registry)
159
+ self._path = path.expanduser().resolve()
160
+
161
+ @property
162
+ def name(self) -> str:
163
+ return "user"
164
+
165
+ def discover(self) -> List[WorkflowInfo]:
166
+ if not self._path.is_dir():
167
+ return []
168
+
169
+ workflows = []
170
+ for file in self._path.glob("*.yaml"):
171
+ workflows.append(self._to_workflow_info(file))
172
+ for file in self._path.glob("*.yml"):
173
+ if file.stem not in [w.name for w in workflows]:
174
+ workflows.append(self._to_workflow_info(file))
175
+ return workflows
176
+
177
+ def find(self, name: str) -> Optional[Path]:
178
+ yaml_file = self._path / f"{name}.yaml"
179
+ if yaml_file.is_file():
180
+ return yaml_file
181
+ yml_file = self._path / f"{name}.yml"
182
+ if yml_file.is_file():
183
+ return yml_file
184
+ return None
185
+
186
+ def contains(self, path: Path) -> bool:
187
+ try:
188
+ path.resolve().relative_to(self._path)
189
+ return True
190
+ except ValueError:
191
+ return False
192
+
193
+ def _to_workflow_info(self, file: Path) -> WorkflowInfo:
194
+ return _parse_workflow_info(file, self.name, self._plugin_registry)
195
+
196
+ class SystemWorkflowSource(WorkflowSource):
197
+ """
198
+ Represents workflows bundled with the Titan CLI itself,
199
+ typically found in a 'workflows' directory within the installed package.
200
+ """
201
+
202
+ def __init__(self, path: Path, plugin_registry: PluginRegistryProtocol):
203
+ super().__init__(plugin_registry)
204
+ self._path = path.resolve()
205
+
206
+ @property
207
+ def name(self) -> str:
208
+ return "system"
209
+
210
+ def discover(self) -> List[WorkflowInfo]:
211
+ if not self._path.is_dir():
212
+ return []
213
+
214
+ workflows = []
215
+ for file in self._path.glob("*.yaml"):
216
+ workflows.append(self._to_workflow_info(file))
217
+ for file in self._path.glob("*.yml"):
218
+ if file.stem not in [w.name for w in workflows]:
219
+ workflows.append(self._to_workflow_info(file))
220
+ return workflows
221
+
222
+ def find(self, name: str) -> Optional[Path]:
223
+ yaml_file = self._path / f"{name}.yaml"
224
+ if yaml_file.is_file():
225
+ return yaml_file
226
+ yml_file = self._path / f"{name}.yml"
227
+ if yml_file.is_file():
228
+ return yml_file
229
+ return None
230
+
231
+ def contains(self, path: Path) -> bool:
232
+ try:
233
+ path.resolve().relative_to(self._path)
234
+ return True
235
+ except ValueError:
236
+ return False
237
+
238
+ def _to_workflow_info(self, file: Path) -> WorkflowInfo:
239
+ return _parse_workflow_info(file, self.name, self._plugin_registry)
240
+
241
+ class PluginWorkflowSource(WorkflowSource):
242
+ """
243
+ Represents workflows provided by installed Titan plugins.
244
+ Discovers workflows via the `workflows_path` property of `TitanPlugin` instances.
245
+ """
246
+
247
+ def __init__(self, plugin_registry: PluginRegistryProtocol): # Remove registry here
248
+ super().__init__(plugin_registry)
249
+ self._plugin_registry = plugin_registry
250
+
251
+ @property
252
+ def name(self) -> str:
253
+ return "plugin"
254
+
255
+ def discover(self) -> List[WorkflowInfo]:
256
+ workflows = []
257
+ for plugin_name in self._plugin_registry.list_installed():
258
+ plugin_instance = self._plugin_registry.get_plugin(plugin_name)
259
+ if plugin_instance and plugin_instance.workflows_path:
260
+ plugin_workflows_dir = plugin_instance.workflows_path
261
+ if plugin_workflows_dir.is_dir():
262
+ for file in plugin_workflows_dir.glob("*.yaml"):
263
+ workflows.append(self._to_workflow_info(file, plugin_name))
264
+ for file in plugin_workflows_dir.glob("*.yml"):
265
+ if file.stem not in [w.name for w in workflows]:
266
+ workflows.append(self._to_workflow_info(file, plugin_name))
267
+ return workflows
268
+
269
+ def find(self, name: str) -> Optional[Path]:
270
+ # Handle qualified names like "github/create-pr"
271
+ if "/" in name:
272
+ plugin_name_ref, workflow_name = name.split('/', 1)
273
+ plugin_instance = self._plugin_registry.get_plugin(plugin_name_ref)
274
+ if plugin_instance and plugin_instance.workflows_path:
275
+ plugin_workflows_dir = plugin_instance.workflows_path
276
+ yaml_file = plugin_workflows_dir / f"{workflow_name}.yaml"
277
+ if yaml_file.is_file():
278
+ return yaml_file
279
+ yml_file = plugin_workflows_dir / f"{workflow_name}.yml"
280
+ if yml_file.is_file():
281
+ return yml_file
282
+ return None # If qualified name is used, only search that plugin
283
+
284
+ # Fallback to original behavior for unqualified names
285
+ for plugin_name in self._plugin_registry.list_installed():
286
+ plugin_instance = self._plugin_registry.get_plugin(plugin_name)
287
+ if plugin_instance and plugin_instance.workflows_path:
288
+ plugin_workflows_dir = plugin_instance.workflows_path
289
+ yaml_file = plugin_workflows_dir / f"{name}.yaml"
290
+ if yaml_file.is_file():
291
+ return yaml_file
292
+ yml_file = plugin_workflows_dir / f"{name}.yml"
293
+ if yml_file.is_file():
294
+ return yml_file
295
+ return None
296
+
297
+ def contains(self, path: Path) -> bool:
298
+ # This is complex to determine definitively without iterating all plugins.
299
+ # For simplicity, we can assume if a path contains 'plugins' in its parts, it might be a plugin workflow.
300
+ # A more robust check would involve checking against all known plugin_instance.workflows_path.
301
+ return "plugins" in path.parts # Heuristic, might need refinement
302
+
303
+ def _to_workflow_info(self, file: Path, plugin_name: str) -> WorkflowInfo:
304
+ info = _parse_workflow_info(file, f"plugin:{plugin_name}", self._plugin_registry) # Pass plugin_registry
305
+ # For plugin workflows, the name is qualified, e.g., "github/create-pr"
306
+ # but the file.stem is just "create-pr". We'll handle this in the registry.
307
+ return info
@@ -0,0 +1,39 @@
1
+ """
2
+ Titan CLI Workflow Engine
3
+
4
+ This module provides the execution engine for composing and running workflows
5
+ using the Atomic Steps Pattern.
6
+
7
+ Core components:
8
+ - WorkflowResult types (Success, Error, Skip)
9
+ - WorkflowContext for dependency injection
10
+ - WorkflowContextBuilder for fluent API
11
+ - WorkflowExecutor for executing YAML workflows (see workflow_executor.py)
12
+ """
13
+
14
+ from .results import (
15
+ WorkflowResult,
16
+ Success,
17
+ Error,
18
+ Skip,
19
+ is_success,
20
+ is_error,
21
+ is_skip,
22
+ )
23
+ from .context import WorkflowContext
24
+ from .builder import WorkflowContextBuilder
25
+
26
+ __all__ = [
27
+ # Result types
28
+ "WorkflowResult",
29
+ "Success",
30
+ "Error",
31
+ "Skip",
32
+ # Helper functions
33
+ "is_success",
34
+ "is_error",
35
+ "is_skip",
36
+ # Context & builder
37
+ "WorkflowContext",
38
+ "WorkflowContextBuilder",
39
+ ]
@@ -0,0 +1,159 @@
1
+ """
2
+ WorkflowContextBuilder - Fluent API for building WorkflowContext.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from typing import Optional, Any
7
+
8
+ from titan_cli.core.plugins.plugin_registry import PluginRegistry
9
+ from titan_cli.core.models import AIConfig
10
+ from titan_cli.core.secrets import SecretManager
11
+ from .context import WorkflowContext
12
+ from titan_cli.ai.client import AIClient
13
+ from titan_cli.ai.exceptions import AIConfigurationError
14
+
15
+
16
+ class WorkflowContextBuilder:
17
+ """
18
+ Fluent builder for WorkflowContext.
19
+
20
+ Example:
21
+ plugin_registry = PluginRegistry()
22
+ secrets = SecretManager()
23
+ ai_config = AIConfig(provider="anthropic", model="claude-3-haiku-20240307")
24
+ ctx = WorkflowContextBuilder(plugin_registry, secrets, ai_config) \\
25
+ .with_ai() \\
26
+ .build()
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ plugin_registry: PluginRegistry,
32
+ secrets: SecretManager,
33
+ ai_config: Optional[AIConfig] = None
34
+ ):
35
+ """
36
+ Initialize builder.
37
+
38
+ Args:
39
+ plugin_registry: The PluginRegistry instance.
40
+ secrets: The SecretManager instance.
41
+ ai_config: Optional AI configuration.
42
+ """
43
+ self._plugin_registry = plugin_registry
44
+ self._secrets = secrets
45
+ self._ai_config = ai_config
46
+
47
+ # Service clients
48
+ self._ai = None
49
+ self._git = None
50
+ self._github = None
51
+ self._jira = None
52
+
53
+ def with_ai(self, ai_client: Optional[Any] = None) -> WorkflowContextBuilder:
54
+ """
55
+ Add AI client.
56
+
57
+ Args:
58
+ ai_client: Optional AIClient instance (auto-created if None)
59
+ """
60
+ if ai_client:
61
+ # DI pure
62
+ self._ai = ai_client
63
+ else:
64
+ # Convenience - auto-create from ai_config
65
+ if self._ai_config:
66
+ try:
67
+ self._ai = AIClient(self._ai_config, self._secrets)
68
+ except AIConfigurationError:
69
+ self._ai = None
70
+ else:
71
+ self._ai = None
72
+ return self
73
+
74
+ def with_git(self, git_client: Optional[Any] = None) -> "WorkflowContextBuilder":
75
+ """
76
+ Add Git client.
77
+
78
+ Args:
79
+ git_client: Optional GitClient instance (auto-created if None)
80
+ """
81
+ if git_client:
82
+ self._git = git_client
83
+ else:
84
+ # Auto-create from plugin registry
85
+ git_plugin = self._plugin_registry.get_plugin("git")
86
+ if git_plugin and git_plugin.is_available():
87
+ try:
88
+ self._git = git_plugin.get_client()
89
+ except Exception: # Catch any exception during client retrieval
90
+ self._git = None # Fail silently
91
+ else:
92
+ self._git = None
93
+ return self
94
+
95
+ def with_github(self, github_client: Optional[Any] = None) -> "WorkflowContextBuilder":
96
+ """
97
+ Add GitHub client.
98
+
99
+ Args:
100
+ github_client: Optional GitHubClient instance (auto-loaded if None)
101
+ """
102
+ if github_client:
103
+ self._github = github_client
104
+ else:
105
+ # Auto-create from plugin registry
106
+ github_plugin = self._plugin_registry.get_plugin("github")
107
+ if github_plugin and github_plugin.is_available():
108
+ try:
109
+ self._github = github_plugin.get_client()
110
+ except Exception: # Catch any exception during client retrieval
111
+ self._github = None # Fail silently
112
+ else:
113
+ self._github = None
114
+ return self
115
+
116
+ def with_jira(self, jira_client: Optional[Any] = None) -> "WorkflowContextBuilder":
117
+ """
118
+ Add JIRA client to workflow context.
119
+
120
+ The JIRA client is optional and only used by JIRA plugin steps.
121
+ Other plugin steps will have ctx.jira = None and should ignore it.
122
+
123
+ Args:
124
+ jira_client: Optional JiraClient instance (auto-loaded if None).
125
+ If None, attempts to load from JIRA plugin registry.
126
+ If plugin is not available or fails to load, sets ctx.jira = None.
127
+
128
+ Returns:
129
+ Self for method chaining
130
+
131
+ Note:
132
+ Steps from other plugins do not need to handle ctx.jira.
133
+ Only JIRA plugin steps should check for and use ctx.jira.
134
+ """
135
+ if jira_client:
136
+ self._jira = jira_client
137
+ else:
138
+ # Auto-create from plugin registry
139
+ jira_plugin = self._plugin_registry.get_plugin("jira")
140
+ if jira_plugin and jira_plugin.is_available():
141
+ try:
142
+ self._jira = jira_plugin.get_client()
143
+ except Exception: # Catch any exception during client retrieval
144
+ self._jira = None # Fail silently
145
+ else:
146
+ self._jira = None
147
+ return self
148
+
149
+
150
+ def build(self) -> WorkflowContext:
151
+ """Build the WorkflowContext."""
152
+ return WorkflowContext(
153
+ secrets=self._secrets,
154
+ plugin_manager=self._plugin_registry,
155
+ ai=self._ai,
156
+ git=self._git,
157
+ github=self._github,
158
+ jira=self._jira,
159
+ )
@@ -0,0 +1,82 @@
1
+ """
2
+ WorkflowContext - Dependency injection container for workflows.
3
+ """
4
+
5
+ from typing import Optional, Dict, Any, List
6
+ from dataclasses import dataclass, field
7
+
8
+ from titan_cli.core.secrets import SecretManager
9
+
10
+
11
+ @dataclass
12
+ class WorkflowContext:
13
+ """
14
+ Context container for workflow execution.
15
+
16
+ Provides:
17
+ - Dependency injection (clients, services)
18
+ - Shared data storage between steps
19
+ - Textual TUI components
20
+ - Access to secrets
21
+
22
+ UI Architecture:
23
+ ctx.textual.text # Textual TUI components
24
+ ctx.textual.panel
25
+ ctx.textual.spacer
26
+ ctx.textual.prompts
27
+ """
28
+
29
+ # Core dependencies
30
+ secrets: SecretManager
31
+
32
+ # Textual TUI components (for TUI mode)
33
+ textual: Optional[Any] = None
34
+
35
+ # Plugin registry
36
+ plugin_manager: Optional[Any] = None
37
+
38
+ # Service clients (populated by builder)
39
+ ai: Optional[Any] = None
40
+ git: Optional[Any] = None
41
+ github: Optional[Any] = None
42
+ jira: Optional[Any] = None
43
+
44
+ # Workflow metadata (set by executor)
45
+ workflow_name: Optional[str] = None
46
+ current_step: Optional[int] = None
47
+ total_steps: Optional[int] = None
48
+
49
+ # Shared data storage between steps
50
+ data: Dict[str, Any] = field(default_factory=dict)
51
+
52
+ # Internal state for workflow execution
53
+ _workflow_stack: List[str] = field(default_factory=list)
54
+
55
+ def set(self, key: str, value: Any) -> None:
56
+ """Set shared data."""
57
+ self.data[key] = value
58
+
59
+ def get(self, key: str, default: Any = None) -> Any:
60
+ """Get shared data."""
61
+ return self.data.get(key, default)
62
+
63
+ def has(self, key: str) -> bool:
64
+ """Check if key exists in shared data."""
65
+ return key in self.data
66
+
67
+ def enter_workflow(self, workflow_name: str):
68
+ """
69
+ Track entering a workflow execution to prevent circular dependencies.
70
+ """
71
+ if workflow_name in self._workflow_stack:
72
+ raise ValueError(
73
+ f"Circular workflow dependency detected: {' -> '.join(self._workflow_stack)} -> {workflow_name}"
74
+ )
75
+ self._workflow_stack.append(workflow_name)
76
+
77
+ def exit_workflow(self, workflow_name: str):
78
+ """
79
+ Track exiting a workflow execution.
80
+ """
81
+ if self._workflow_stack and self._workflow_stack[-1] == workflow_name:
82
+ self._workflow_stack.pop()