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,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Textual Workflow Executor
|
|
3
|
+
|
|
4
|
+
Workflow executor specifically designed for Textual TUI.
|
|
5
|
+
Emits Textual messages instead of using Rich UI components.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
|
|
11
|
+
from titan_cli.core.workflows import ParsedWorkflow
|
|
12
|
+
from titan_cli.core.workflows.workflow_exceptions import WorkflowExecutionError
|
|
13
|
+
from titan_cli.core.workflows.workflow_registry import WorkflowRegistry
|
|
14
|
+
from titan_cli.core.plugins.plugin_registry import PluginRegistry
|
|
15
|
+
from titan_cli.core.workflows.models import WorkflowStepModel
|
|
16
|
+
from titan_cli.engine.context import WorkflowContext
|
|
17
|
+
from titan_cli.engine.results import WorkflowResult, Success, Error, is_error, is_skip
|
|
18
|
+
from titan_cli.engine.steps.command_step import execute_command_step as execute_external_command_step
|
|
19
|
+
from titan_cli.engine.steps.ai_assistant_step import execute_ai_assistant_step
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TextualWorkflowExecutor:
|
|
23
|
+
"""
|
|
24
|
+
Workflow executor for Textual TUI.
|
|
25
|
+
|
|
26
|
+
Instead of using ctx.ui (Rich components), this executor emits
|
|
27
|
+
Textual messages that the screen can listen to and display.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Core steps available to all workflows
|
|
31
|
+
CORE_STEPS = {
|
|
32
|
+
"ai_code_assistant": execute_ai_assistant_step,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Message classes for communication with the screen
|
|
36
|
+
class WorkflowStarted(Message):
|
|
37
|
+
"""Emitted when workflow execution starts."""
|
|
38
|
+
def __init__(self, workflow_name: str, description: Optional[str], source: Optional[str], total_steps: int, steps: list = None, is_nested: bool = False) -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.workflow_name = workflow_name
|
|
41
|
+
self.description = description
|
|
42
|
+
self.source = source
|
|
43
|
+
self.total_steps = total_steps
|
|
44
|
+
self.steps = steps or []
|
|
45
|
+
self.is_nested = is_nested
|
|
46
|
+
|
|
47
|
+
class WorkflowCompleted(Message):
|
|
48
|
+
"""Emitted when workflow completes successfully."""
|
|
49
|
+
def __init__(self, workflow_name: str, message: str, is_nested: bool = False) -> None:
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.workflow_name = workflow_name
|
|
52
|
+
self.message = message
|
|
53
|
+
self.is_nested = is_nested
|
|
54
|
+
|
|
55
|
+
class WorkflowFailed(Message):
|
|
56
|
+
"""Emitted when workflow fails."""
|
|
57
|
+
def __init__(self, workflow_name: str, step_name: str, error_message: str) -> None:
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.workflow_name = workflow_name
|
|
60
|
+
self.step_name = step_name
|
|
61
|
+
self.error_message = error_message
|
|
62
|
+
|
|
63
|
+
class StepStarted(Message):
|
|
64
|
+
"""Emitted when a step starts executing."""
|
|
65
|
+
def __init__(self, step_index: int, step_id: str, step_name: str) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.step_index = step_index
|
|
68
|
+
self.step_id = step_id
|
|
69
|
+
self.step_name = step_name
|
|
70
|
+
|
|
71
|
+
class StepCompleted(Message):
|
|
72
|
+
"""Emitted when a step completes successfully."""
|
|
73
|
+
def __init__(self, step_index: int, step_id: str, step_name: str) -> None:
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.step_index = step_index
|
|
76
|
+
self.step_id = step_id
|
|
77
|
+
self.step_name = step_name
|
|
78
|
+
|
|
79
|
+
class StepFailed(Message):
|
|
80
|
+
"""Emitted when a step fails."""
|
|
81
|
+
def __init__(self, step_index: int, step_id: str, step_name: str, error_message: str, on_error: str) -> None:
|
|
82
|
+
super().__init__()
|
|
83
|
+
self.step_index = step_index
|
|
84
|
+
self.step_id = step_id
|
|
85
|
+
self.step_name = step_name
|
|
86
|
+
self.error_message = error_message
|
|
87
|
+
self.on_error = on_error
|
|
88
|
+
|
|
89
|
+
class StepSkipped(Message):
|
|
90
|
+
"""Emitted when a step is skipped."""
|
|
91
|
+
def __init__(self, step_index: int, step_id: str, step_name: str) -> None:
|
|
92
|
+
super().__init__()
|
|
93
|
+
self.step_index = step_index
|
|
94
|
+
self.step_id = step_id
|
|
95
|
+
self.step_name = step_name
|
|
96
|
+
|
|
97
|
+
class StepOutput(Message):
|
|
98
|
+
"""Emitted when a step produces output."""
|
|
99
|
+
def __init__(self, step_index: int, step_id: str, output: str) -> None:
|
|
100
|
+
super().__init__()
|
|
101
|
+
self.step_index = step_index
|
|
102
|
+
self.step_id = step_id
|
|
103
|
+
self.output = output
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
plugin_registry: PluginRegistry,
|
|
108
|
+
workflow_registry: WorkflowRegistry,
|
|
109
|
+
message_target: Any = None
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Initialize the Textual workflow executor.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
plugin_registry: Plugin registry for resolving plugins
|
|
116
|
+
workflow_registry: Workflow registry for resolving workflows
|
|
117
|
+
message_target: Target to post messages to (typically a Textual Widget/Screen)
|
|
118
|
+
"""
|
|
119
|
+
self._plugin_registry = plugin_registry
|
|
120
|
+
self._workflow_registry = workflow_registry
|
|
121
|
+
self._message_target = message_target
|
|
122
|
+
|
|
123
|
+
def _post_message(self, message: Message) -> None:
|
|
124
|
+
"""Post a message to the target if available."""
|
|
125
|
+
if self._message_target and hasattr(self._message_target, 'post_message'):
|
|
126
|
+
self._message_target.post_message(message)
|
|
127
|
+
|
|
128
|
+
def _post_message_sync(self, message: Message) -> None:
|
|
129
|
+
"""Post a message synchronously (blocks until processed)."""
|
|
130
|
+
if self._message_target and hasattr(self._message_target, 'post_message'):
|
|
131
|
+
def _post():
|
|
132
|
+
self._message_target.post_message(message)
|
|
133
|
+
|
|
134
|
+
# Use call_from_thread to block until message is posted
|
|
135
|
+
if hasattr(self._message_target, 'app'):
|
|
136
|
+
try:
|
|
137
|
+
self._message_target.app.call_from_thread(_post)
|
|
138
|
+
except Exception:
|
|
139
|
+
# App is closing or worker was cancelled, fail silently
|
|
140
|
+
pass
|
|
141
|
+
else:
|
|
142
|
+
# Fallback to async post if no app available
|
|
143
|
+
self._message_target.post_message(message)
|
|
144
|
+
|
|
145
|
+
def execute(
|
|
146
|
+
self,
|
|
147
|
+
workflow: ParsedWorkflow,
|
|
148
|
+
ctx: WorkflowContext,
|
|
149
|
+
params_override: Optional[Dict[str, Any]] = None
|
|
150
|
+
) -> WorkflowResult:
|
|
151
|
+
"""
|
|
152
|
+
Execute the given ParsedWorkflow.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
workflow: The workflow to execute
|
|
156
|
+
ctx: Workflow context
|
|
157
|
+
params_override: Optional parameter overrides
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
WorkflowResult indicating success or failure
|
|
161
|
+
"""
|
|
162
|
+
# Inject Textual components into context if message_target is available
|
|
163
|
+
if self._message_target and hasattr(self._message_target, 'app'):
|
|
164
|
+
try:
|
|
165
|
+
from titan_cli.ui.tui.textual_components import TextualComponents
|
|
166
|
+
from titan_cli.ui.tui.screens.workflow_execution import WorkflowExecutionContent
|
|
167
|
+
|
|
168
|
+
app = self._message_target.app
|
|
169
|
+
output_widget = self._message_target.query_one("#execution-content", WorkflowExecutionContent)
|
|
170
|
+
|
|
171
|
+
ctx.textual = TextualComponents(app, output_widget)
|
|
172
|
+
except Exception:
|
|
173
|
+
# If we can't get the components, steps will fall back to ctx.ui
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Merge workflow params into ctx.data with optional overrides
|
|
177
|
+
effective_params = {**workflow.params}
|
|
178
|
+
if params_override:
|
|
179
|
+
effective_params.update(params_override)
|
|
180
|
+
|
|
181
|
+
# Load params into ctx.data so steps can access them
|
|
182
|
+
ctx.data.update(effective_params)
|
|
183
|
+
|
|
184
|
+
# Inject workflow metadata into context
|
|
185
|
+
ctx.workflow_name = workflow.name
|
|
186
|
+
ctx.total_steps = len([s for s in workflow.steps if not s.get("hook")])
|
|
187
|
+
|
|
188
|
+
# Check if this is a nested workflow (called from another workflow)
|
|
189
|
+
is_nested = len(ctx._workflow_stack) > 0
|
|
190
|
+
|
|
191
|
+
# Emit workflow started event
|
|
192
|
+
self._post_message(
|
|
193
|
+
self.WorkflowStarted(
|
|
194
|
+
workflow_name=workflow.name,
|
|
195
|
+
description=workflow.description,
|
|
196
|
+
source=workflow.source,
|
|
197
|
+
total_steps=ctx.total_steps,
|
|
198
|
+
steps=workflow.steps,
|
|
199
|
+
is_nested=is_nested
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
ctx.enter_workflow(workflow.name)
|
|
204
|
+
try:
|
|
205
|
+
step_index = 0
|
|
206
|
+
for step_data in workflow.steps:
|
|
207
|
+
step_config = WorkflowStepModel(**step_data)
|
|
208
|
+
|
|
209
|
+
# Hooks are resolved by the registry, so we just skip the placeholder
|
|
210
|
+
if step_config.hook:
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
step_index += 1
|
|
214
|
+
ctx.current_step = step_index
|
|
215
|
+
|
|
216
|
+
step_id = step_config.id
|
|
217
|
+
step_name = step_config.name or step_id
|
|
218
|
+
|
|
219
|
+
# Emit step started event
|
|
220
|
+
self._post_message_sync(
|
|
221
|
+
self.StepStarted(
|
|
222
|
+
step_index=step_index,
|
|
223
|
+
step_id=step_id,
|
|
224
|
+
step_name=step_name
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
if step_config.workflow:
|
|
230
|
+
step_result = self._execute_workflow_step(step_config, ctx)
|
|
231
|
+
elif step_config.plugin and step_config.step:
|
|
232
|
+
step_result = self._execute_plugin_step(step_config, ctx)
|
|
233
|
+
elif step_config.command:
|
|
234
|
+
step_result = self._execute_command_step(step_config, ctx)
|
|
235
|
+
else:
|
|
236
|
+
step_result = Error(f"Invalid step configuration for '{step_id}'.")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
step_result = Error(f"An unexpected error occurred in step '{step_name}': {e}", e)
|
|
239
|
+
|
|
240
|
+
# Handle step result
|
|
241
|
+
if is_error(step_result):
|
|
242
|
+
self._post_message_sync(
|
|
243
|
+
self.StepFailed(
|
|
244
|
+
step_index=step_index,
|
|
245
|
+
step_id=step_id,
|
|
246
|
+
step_name=step_name,
|
|
247
|
+
error_message=step_result.message,
|
|
248
|
+
on_error=step_config.on_error
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if step_config.on_error == "fail":
|
|
253
|
+
self._post_message_sync(
|
|
254
|
+
self.WorkflowFailed(
|
|
255
|
+
workflow_name=workflow.name,
|
|
256
|
+
step_name=step_name,
|
|
257
|
+
error_message=step_result.message
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
return Error(f"Workflow failed at step '{step_name}'", step_result.exception)
|
|
261
|
+
# else: on_error == "continue" - continue to next step
|
|
262
|
+
elif is_skip(step_result):
|
|
263
|
+
self._post_message_sync(
|
|
264
|
+
self.StepSkipped(
|
|
265
|
+
step_index=step_index,
|
|
266
|
+
step_id=step_id,
|
|
267
|
+
step_name=step_name
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
if step_result.metadata:
|
|
271
|
+
ctx.data.update(step_result.metadata)
|
|
272
|
+
else: # Success
|
|
273
|
+
self._post_message_sync(
|
|
274
|
+
self.StepCompleted(
|
|
275
|
+
step_index=step_index,
|
|
276
|
+
step_id=step_id,
|
|
277
|
+
step_name=step_name
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
if step_result.metadata:
|
|
281
|
+
ctx.data.update(step_result.metadata)
|
|
282
|
+
|
|
283
|
+
finally:
|
|
284
|
+
ctx.exit_workflow(workflow.name)
|
|
285
|
+
|
|
286
|
+
# Check if this is a nested workflow (called from another workflow)
|
|
287
|
+
is_nested = len(ctx._workflow_stack) > 0
|
|
288
|
+
|
|
289
|
+
# DEBUG: Log completion
|
|
290
|
+
# with open("/tmp/titan_debug.log", "a") as f:
|
|
291
|
+
# f.write(f"[{time.time():.3f}] Workflow '{workflow.name}' completed. is_nested={is_nested}, stack={ctx._workflow_stack}\n")
|
|
292
|
+
|
|
293
|
+
# Emit workflow completed event
|
|
294
|
+
self._post_message(
|
|
295
|
+
self.WorkflowCompleted(
|
|
296
|
+
workflow_name=workflow.name,
|
|
297
|
+
message=f"Workflow '{workflow.name}' finished.",
|
|
298
|
+
is_nested=is_nested
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# with open("/tmp/titan_debug.log", "a") as f:
|
|
303
|
+
# f.write(f"[{time.time():.3f}] WorkflowCompleted message posted\n")
|
|
304
|
+
|
|
305
|
+
return Success(f"Workflow '{workflow.name}' finished.", {})
|
|
306
|
+
|
|
307
|
+
def _execute_workflow_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
|
|
308
|
+
"""Execute a nested workflow as a step."""
|
|
309
|
+
workflow_name = step_config.workflow
|
|
310
|
+
if not workflow_name:
|
|
311
|
+
return Error("Workflow step is missing the 'workflow' name.")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
sub_workflow = self._workflow_registry.get_workflow(workflow_name)
|
|
315
|
+
if not sub_workflow:
|
|
316
|
+
return Error(f"Nested workflow '{workflow_name}' not found.")
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return Error(f"Failed to load workflow '{workflow_name}': {e}", e)
|
|
319
|
+
|
|
320
|
+
# Recursively execute the nested workflow
|
|
321
|
+
return self.execute(sub_workflow, ctx, params_override=step_config.params)
|
|
322
|
+
|
|
323
|
+
def _execute_plugin_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
|
|
324
|
+
"""Execute a plugin step."""
|
|
325
|
+
plugin_name = step_config.plugin
|
|
326
|
+
step_func_name = step_config.step
|
|
327
|
+
step_params = step_config.params
|
|
328
|
+
|
|
329
|
+
# Validate required context variables
|
|
330
|
+
required_vars = step_config.params.get("requires", [])
|
|
331
|
+
for var in required_vars:
|
|
332
|
+
if var not in ctx.data:
|
|
333
|
+
return Error(f"Step '{step_func_name}' is missing required context variable: '{var}'")
|
|
334
|
+
|
|
335
|
+
step_func = None
|
|
336
|
+
if plugin_name == "project":
|
|
337
|
+
# Handle virtual 'project' plugin for project-specific steps
|
|
338
|
+
step_func = self._workflow_registry.get_project_step(step_func_name)
|
|
339
|
+
if not step_func:
|
|
340
|
+
return Error(
|
|
341
|
+
f"Project step '{step_func_name}' not found in '.titan/steps/'.",
|
|
342
|
+
WorkflowExecutionError(f"Project step '{step_func_name}' not found")
|
|
343
|
+
)
|
|
344
|
+
elif plugin_name == "core":
|
|
345
|
+
# Handle virtual 'core' plugin for built-in core steps
|
|
346
|
+
step_func = self.CORE_STEPS.get(step_func_name)
|
|
347
|
+
if not step_func:
|
|
348
|
+
available = ", ".join(self.CORE_STEPS.keys())
|
|
349
|
+
return Error(
|
|
350
|
+
f"Core step '{step_func_name}' not found. Available: {available}",
|
|
351
|
+
WorkflowExecutionError(f"Core step '{step_func_name}' not found")
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
# Handle regular plugins
|
|
355
|
+
plugin_instance = self._plugin_registry.get_plugin(plugin_name)
|
|
356
|
+
if not plugin_instance:
|
|
357
|
+
return Error(
|
|
358
|
+
f"Plugin '{plugin_name}' not found or not initialized.",
|
|
359
|
+
WorkflowExecutionError(f"Plugin '{plugin_name}' not found")
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
step_functions = plugin_instance.get_steps()
|
|
363
|
+
step_func = step_functions.get(step_func_name)
|
|
364
|
+
if not step_func:
|
|
365
|
+
return Error(
|
|
366
|
+
f"Step '{step_func_name}' not found in plugin '{plugin_name}'.",
|
|
367
|
+
WorkflowExecutionError(f"Step '{step_func_name}' not found")
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Prepare parameters for the step function
|
|
371
|
+
resolved_params = self._resolve_parameters(step_params, ctx)
|
|
372
|
+
|
|
373
|
+
# Add resolved parameters to context data so step can access them via ctx.get()
|
|
374
|
+
ctx.data.update(resolved_params)
|
|
375
|
+
|
|
376
|
+
# Execute the step function
|
|
377
|
+
try:
|
|
378
|
+
if plugin_name == "core":
|
|
379
|
+
# Core steps receive (step: WorkflowStepModel, ctx: WorkflowContext)
|
|
380
|
+
return step_func(step_config, ctx)
|
|
381
|
+
else:
|
|
382
|
+
# Plugin and project steps receive only ctx (params are in ctx.data)
|
|
383
|
+
return step_func(ctx)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
error_source = f"plugin '{plugin_name}'" if plugin_name not in ("project", "core") else f"{plugin_name} step"
|
|
386
|
+
return Error(f"Error executing step '{step_func_name}' from {error_source}: {e}", e)
|
|
387
|
+
|
|
388
|
+
def _execute_command_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
|
|
389
|
+
"""Execute a shell command using the dedicated external function."""
|
|
390
|
+
return execute_external_command_step(step_config, ctx)
|
|
391
|
+
|
|
392
|
+
def _resolve_parameters(self, params: Dict[str, Any], ctx: WorkflowContext) -> Dict[str, Any]:
|
|
393
|
+
"""
|
|
394
|
+
Resolve parameter values by substituting placeholders from context data.
|
|
395
|
+
All workflow params are already in ctx.data.
|
|
396
|
+
"""
|
|
397
|
+
from titan_cli.engine.steps.command_step import resolve_parameters_in_string
|
|
398
|
+
|
|
399
|
+
resolved = {}
|
|
400
|
+
for key, value in params.items():
|
|
401
|
+
if isinstance(value, str):
|
|
402
|
+
resolved[key] = resolve_parameters_in_string(value, ctx)
|
|
403
|
+
else:
|
|
404
|
+
resolved[key] = value # Keep non-string parameters as is
|
|
405
|
+
return resolved
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Titan TUI Theme
|
|
3
|
+
|
|
4
|
+
Centralized theme configuration for the Textual UI.
|
|
5
|
+
Defines color variables, CSS utilities, and style constants.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Titan Theme CSS - Dracula Edition (Grises y Púrpuras)
|
|
9
|
+
TITAN_THEME_CSS = """
|
|
10
|
+
/* Color Variables */
|
|
11
|
+
$primary: #bd93f9; /* Purple (Dracula standard) */
|
|
12
|
+
$secondary: #50fa7b; /* Green */
|
|
13
|
+
$accent: #ff79c6; /* Pink */
|
|
14
|
+
$error: #ff5555; /* Red */
|
|
15
|
+
$warning: #f1fa8c; /* Yellow */
|
|
16
|
+
$success: #50fa7b; /* Green */
|
|
17
|
+
$info: #8be9fd; /* Cyan */
|
|
18
|
+
|
|
19
|
+
/* Backgrounds */
|
|
20
|
+
$surface: #282a36;
|
|
21
|
+
$surface-lighten-1: #343746;
|
|
22
|
+
$surface-lighten-2: #44475a;
|
|
23
|
+
|
|
24
|
+
/* Text Colors */
|
|
25
|
+
$text: #f8f8f2; /* Foreground (Almost white) */
|
|
26
|
+
$text-muted: #6272a4; /* Comment */
|
|
27
|
+
$text-disabled: #44475a; /* Disabled */
|
|
28
|
+
|
|
29
|
+
/* Banner gradient colors */
|
|
30
|
+
$banner-start: #6272a4;
|
|
31
|
+
$banner-mid: #bd93f9;
|
|
32
|
+
$banner-end: #ff79c6;
|
|
33
|
+
|
|
34
|
+
/* Base widget styles */
|
|
35
|
+
.title {
|
|
36
|
+
color: $primary;
|
|
37
|
+
text-style: bold;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.subtitle {
|
|
41
|
+
color: $secondary;
|
|
42
|
+
text-style: bold;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.body {
|
|
46
|
+
color: $text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.muted {
|
|
50
|
+
color: $text-muted;
|
|
51
|
+
text-style: italic;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.error-text {
|
|
55
|
+
color: $error;
|
|
56
|
+
text-style: bold;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.success-text {
|
|
60
|
+
color: $success;
|
|
61
|
+
text-style: bold;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.warning-text {
|
|
65
|
+
color: $warning;
|
|
66
|
+
text-style: bold;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.info-text {
|
|
70
|
+
color: $info;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Global scrollbar styles - applies to all widgets */
|
|
74
|
+
* {
|
|
75
|
+
scrollbar-background: $surface;
|
|
76
|
+
scrollbar-background-hover: $surface-lighten-1;
|
|
77
|
+
scrollbar-background-active: $surface-lighten-2;
|
|
78
|
+
scrollbar-color: $primary;
|
|
79
|
+
scrollbar-color-hover: $accent;
|
|
80
|
+
scrollbar-color-active: $accent;
|
|
81
|
+
scrollbar-corner-color: $surface;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Global OptionList styles - transparent to inherit parent background */
|
|
85
|
+
OptionList {
|
|
86
|
+
border: none;
|
|
87
|
+
background: transparent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
OptionList > .option-list--option {
|
|
91
|
+
background: transparent;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
OptionList > .option-list--option-highlighted {
|
|
95
|
+
background: $primary;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Screen {
|
|
99
|
+
background: $surface;
|
|
100
|
+
color: $text;
|
|
101
|
+
}
|
|
102
|
+
"""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Titan TUI Widgets
|
|
3
|
+
|
|
4
|
+
Reusable Textual widgets for the Titan TUI.
|
|
5
|
+
"""
|
|
6
|
+
from .status_bar import StatusBarWidget
|
|
7
|
+
from .header import HeaderWidget
|
|
8
|
+
from .panel import Panel
|
|
9
|
+
from .table import Table
|
|
10
|
+
from .button import Button
|
|
11
|
+
from .text import (
|
|
12
|
+
Text,
|
|
13
|
+
DimText,
|
|
14
|
+
BoldText,
|
|
15
|
+
PrimaryText,
|
|
16
|
+
BoldPrimaryText,
|
|
17
|
+
SuccessText,
|
|
18
|
+
ErrorText,
|
|
19
|
+
WarningText,
|
|
20
|
+
ItalicText,
|
|
21
|
+
DimItalicText,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"StatusBarWidget",
|
|
26
|
+
"HeaderWidget",
|
|
27
|
+
"Panel",
|
|
28
|
+
"Table",
|
|
29
|
+
"Button",
|
|
30
|
+
"Text",
|
|
31
|
+
"DimText",
|
|
32
|
+
"BoldText",
|
|
33
|
+
"PrimaryText",
|
|
34
|
+
"BoldPrimaryText",
|
|
35
|
+
"SuccessText",
|
|
36
|
+
"ErrorText",
|
|
37
|
+
"WarningText",
|
|
38
|
+
"ItalicText",
|
|
39
|
+
"DimItalicText",
|
|
40
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Button Widget
|
|
3
|
+
|
|
4
|
+
Reusable button widget with consistent styling and variants.
|
|
5
|
+
"""
|
|
6
|
+
from textual.widgets import Button as TextualButton
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Button(TextualButton):
|
|
10
|
+
"""
|
|
11
|
+
Custom Button widget with consistent styling.
|
|
12
|
+
|
|
13
|
+
Fixes the focus issue (black box around text) and provides consistent variants.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
Button("Click Me", variant="primary", id="my-button")
|
|
17
|
+
Button("Delete", variant="error")
|
|
18
|
+
Button("Cancel", variant="default")
|
|
19
|
+
|
|
20
|
+
Available variants:
|
|
21
|
+
- primary: Primary action button (blue/accent color)
|
|
22
|
+
- default: Default button (neutral)
|
|
23
|
+
- error: Destructive action (red)
|
|
24
|
+
- success: Success action (green)
|
|
25
|
+
- warning: Warning action (yellow)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
DEFAULT_CSS = """
|
|
29
|
+
Button {
|
|
30
|
+
min-width: 16;
|
|
31
|
+
height: 3;
|
|
32
|
+
margin: 0 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Button:focus {
|
|
36
|
+
text-style: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Button.-primary {
|
|
40
|
+
background: $primary;
|
|
41
|
+
color: $text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Button.-primary:hover {
|
|
45
|
+
background: $primary-lighten-1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Button.-primary:focus {
|
|
49
|
+
background: $primary;
|
|
50
|
+
text-style: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Button.-default {
|
|
54
|
+
background: $surface-lighten-2;
|
|
55
|
+
color: $text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Button.-default:hover {
|
|
59
|
+
background: $surface-lighten-3;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Button.-default:focus {
|
|
63
|
+
background: $surface-lighten-2;
|
|
64
|
+
text-style: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Button.-error {
|
|
68
|
+
background: $error;
|
|
69
|
+
color: $text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Button.-error:hover {
|
|
73
|
+
background: $error-lighten-1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Button.-error:focus {
|
|
77
|
+
background: $error;
|
|
78
|
+
text-style: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Button.-success {
|
|
82
|
+
background: $success;
|
|
83
|
+
color: $text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Button.-success:hover {
|
|
87
|
+
background: $success-lighten-1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Button.-success:focus {
|
|
91
|
+
background: $success;
|
|
92
|
+
text-style: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Button.-warning {
|
|
96
|
+
background: $warning;
|
|
97
|
+
color: $text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Button.-warning:hover {
|
|
101
|
+
background: $warning-lighten-1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Button.-warning:focus {
|
|
105
|
+
background: $warning;
|
|
106
|
+
text-style: none;
|
|
107
|
+
}
|
|
108
|
+
"""
|