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,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
from titan_cli.core.workflows import ParsedWorkflow
|
|
5
|
+
from titan_cli.core.workflows.workflow_exceptions import WorkflowExecutionError
|
|
6
|
+
from titan_cli.engine.context import WorkflowContext
|
|
7
|
+
from titan_cli.engine.results import WorkflowResult, Success, Error, is_error, is_skip
|
|
8
|
+
from titan_cli.core.workflows.workflow_registry import WorkflowRegistry
|
|
9
|
+
from titan_cli.core.plugins.plugin_registry import PluginRegistry
|
|
10
|
+
from titan_cli.core.workflows.models import WorkflowStepModel
|
|
11
|
+
from titan_cli.engine.steps.command_step import execute_command_step as execute_external_command_step
|
|
12
|
+
from titan_cli.engine.steps.ai_assistant_step import execute_ai_assistant_step
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowExecutor:
|
|
17
|
+
"""
|
|
18
|
+
Executes a ParsedWorkflow by iterating through its steps,
|
|
19
|
+
resolving plugins, and performing parameter substitution.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Core steps available to all workflows
|
|
23
|
+
CORE_STEPS = {
|
|
24
|
+
"ai_code_assistant": execute_ai_assistant_step,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def __init__(self, plugin_registry: PluginRegistry, workflow_registry: WorkflowRegistry):
|
|
28
|
+
self._plugin_registry = plugin_registry
|
|
29
|
+
self._workflow_registry = workflow_registry
|
|
30
|
+
|
|
31
|
+
def execute(self, workflow: ParsedWorkflow, ctx: WorkflowContext, params_override: Optional[Dict[str, Any]] = None) -> WorkflowResult:
|
|
32
|
+
"""
|
|
33
|
+
Executes the given ParsedWorkflow.
|
|
34
|
+
"""
|
|
35
|
+
# Merge workflow params into ctx.data with optional overrides
|
|
36
|
+
effective_params = {**workflow.params}
|
|
37
|
+
if params_override:
|
|
38
|
+
effective_params.update(params_override)
|
|
39
|
+
|
|
40
|
+
# Load params into ctx.data so steps can access them
|
|
41
|
+
ctx.data.update(effective_params)
|
|
42
|
+
|
|
43
|
+
# Inject workflow metadata into context
|
|
44
|
+
ctx.workflow_name = workflow.name
|
|
45
|
+
ctx.total_steps = len([s for s in workflow.steps if not s.get("hook")])
|
|
46
|
+
|
|
47
|
+
ctx.enter_workflow(workflow.name)
|
|
48
|
+
try:
|
|
49
|
+
step_index = 0
|
|
50
|
+
for step_data in workflow.steps:
|
|
51
|
+
step_config = WorkflowStepModel(**step_data)
|
|
52
|
+
|
|
53
|
+
# Hooks are resolved by the registry, so we just skip the placeholder.
|
|
54
|
+
# Check the parsed model instead of raw dict to handle auto-generated IDs
|
|
55
|
+
if step_config.hook:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
step_index += 1
|
|
59
|
+
ctx.current_step = step_index
|
|
60
|
+
|
|
61
|
+
step_id = step_config.id
|
|
62
|
+
step_name = step_config.name or step_id
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
if step_config.workflow:
|
|
66
|
+
step_result = self._execute_workflow_step(step_config, ctx)
|
|
67
|
+
elif step_config.plugin and step_config.step:
|
|
68
|
+
step_result = self._execute_plugin_step(step_config, ctx)
|
|
69
|
+
elif step_config.command:
|
|
70
|
+
step_result = self._execute_command_step(step_config, ctx)
|
|
71
|
+
else:
|
|
72
|
+
# This should be caught by model validation, but as a safeguard:
|
|
73
|
+
step_result = Error(f"Invalid step configuration for '{step_id}'.")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
step_result = Error(f"An unexpected error occurred in step '{step_name}': {e}", e)
|
|
76
|
+
|
|
77
|
+
# Handle step result
|
|
78
|
+
if is_error(step_result):
|
|
79
|
+
if step_config.on_error == "fail":
|
|
80
|
+
return Error(f"Workflow failed at step '{step_name}'", step_result.exception)
|
|
81
|
+
# else: on_error == "continue" - continue to next step
|
|
82
|
+
elif is_skip(step_result):
|
|
83
|
+
if step_result.metadata:
|
|
84
|
+
ctx.data.update(step_result.metadata)
|
|
85
|
+
else: # Success
|
|
86
|
+
if step_result.metadata:
|
|
87
|
+
ctx.data.update(step_result.metadata)
|
|
88
|
+
|
|
89
|
+
finally:
|
|
90
|
+
ctx.exit_workflow(workflow.name)
|
|
91
|
+
|
|
92
|
+
return Success(f"Workflow '{workflow.name}' finished.", {})
|
|
93
|
+
|
|
94
|
+
def _execute_workflow_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
|
|
95
|
+
"""Executes a nested workflow as a step."""
|
|
96
|
+
workflow_name = step_config.workflow
|
|
97
|
+
if not workflow_name:
|
|
98
|
+
return Error("Workflow step is missing the 'workflow' name.")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
sub_workflow = self._workflow_registry.get_workflow(workflow_name)
|
|
102
|
+
if not sub_workflow:
|
|
103
|
+
return Error(f"Nested workflow '{workflow_name}' not found.")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return Error(f"Failed to load workflow '{workflow_name}': {e}", e)
|
|
106
|
+
|
|
107
|
+
# We recursively call the main execute method.
|
|
108
|
+
# Pass a copy of the context data to isolate it if needed, but for now, we share it.
|
|
109
|
+
# The `enter_workflow` check will prevent infinite recursion.
|
|
110
|
+
return self.execute(sub_workflow, ctx, params_override=step_config.params)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _execute_plugin_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
|
|
114
|
+
plugin_name = step_config.plugin
|
|
115
|
+
step_func_name = step_config.step
|
|
116
|
+
step_params = step_config.params
|
|
117
|
+
|
|
118
|
+
# Validate required context variables
|
|
119
|
+
# This was part of `command` originally, but it's good practice for plugin steps too.
|
|
120
|
+
required_vars = step_config.params.get("requires", []) # Assuming 'requires' can be in params
|
|
121
|
+
for var in required_vars:
|
|
122
|
+
if var not in ctx.data:
|
|
123
|
+
return Error(f"Step '{step_func_name}' is missing required context variable: '{var}'")
|
|
124
|
+
|
|
125
|
+
step_func = None
|
|
126
|
+
if plugin_name == "project":
|
|
127
|
+
# Handle virtual 'project' plugin for project-specific steps
|
|
128
|
+
step_func = self._workflow_registry.get_project_step(step_func_name)
|
|
129
|
+
if not step_func:
|
|
130
|
+
return Error(f"Project step '{step_func_name}' not found in '.titan/steps/'.", WorkflowExecutionError(f"Project step '{step_func_name}' not found"))
|
|
131
|
+
elif plugin_name == "core":
|
|
132
|
+
# Handle virtual 'core' plugin for built-in core steps
|
|
133
|
+
step_func = self.CORE_STEPS.get(step_func_name)
|
|
134
|
+
if not step_func:
|
|
135
|
+
available = ", ".join(self.CORE_STEPS.keys())
|
|
136
|
+
return Error(f"Core step '{step_func_name}' not found. Available: {available}", WorkflowExecutionError(f"Core step '{step_func_name}' not found"))
|
|
137
|
+
else:
|
|
138
|
+
# Handle regular plugins
|
|
139
|
+
plugin_instance = self._plugin_registry.get_plugin(plugin_name)
|
|
140
|
+
if not plugin_instance:
|
|
141
|
+
return Error(f"Plugin '{plugin_name}' not found or not initialized.", WorkflowExecutionError(f"Plugin '{plugin_name}' not found"))
|
|
142
|
+
|
|
143
|
+
step_functions = plugin_instance.get_steps()
|
|
144
|
+
step_func = step_functions.get(step_func_name)
|
|
145
|
+
if not step_func:
|
|
146
|
+
return Error(f"Step '{step_func_name}' not found in plugin '{plugin_name}'.", WorkflowExecutionError(f"Step '{step_func_name}' not found"))
|
|
147
|
+
|
|
148
|
+
# Prepare parameters for the step function
|
|
149
|
+
resolved_params = self._resolve_parameters(step_params, ctx)
|
|
150
|
+
|
|
151
|
+
# Add resolved parameters to context data so step can access them via ctx.get()
|
|
152
|
+
ctx.data.update(resolved_params)
|
|
153
|
+
|
|
154
|
+
# Execute the step function
|
|
155
|
+
try:
|
|
156
|
+
if plugin_name == "core":
|
|
157
|
+
# Core steps receive (step: WorkflowStepModel, ctx: WorkflowContext)
|
|
158
|
+
return step_func(step_config, ctx)
|
|
159
|
+
else:
|
|
160
|
+
# Plugin and project steps receive only ctx (params are in ctx.data)
|
|
161
|
+
return step_func(ctx)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
error_source = f"plugin '{plugin_name}'" if plugin_name not in ("project", "core") else f"{plugin_name} step"
|
|
164
|
+
return Error(f"Error executing step '{step_func_name}' from {error_source}: {e}", e)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _execute_command_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult: # Changed type hint to WorkflowStepModel
|
|
168
|
+
"""
|
|
169
|
+
Executes a shell command using the dedicated external function.
|
|
170
|
+
"""
|
|
171
|
+
# Call the external function that handles command execution
|
|
172
|
+
return execute_external_command_step(step_config, ctx)
|
|
173
|
+
|
|
174
|
+
def _resolve_parameters(self, params: Dict[str, Any], ctx: WorkflowContext) -> Dict[str, Any]:
|
|
175
|
+
"""
|
|
176
|
+
Resolves parameter values by substituting placeholders from context data.
|
|
177
|
+
All workflow params are already in ctx.data.
|
|
178
|
+
"""
|
|
179
|
+
from titan_cli.engine.steps.command_step import resolve_parameters_in_string
|
|
180
|
+
|
|
181
|
+
resolved = {}
|
|
182
|
+
for key, value in params.items():
|
|
183
|
+
if isinstance(value, str):
|
|
184
|
+
resolved[key] = resolve_parameters_in_string(value, ctx)
|
|
185
|
+
else:
|
|
186
|
+
resolved[key] = value # Keep non-string parameters as is
|
|
187
|
+
return resolved
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# titan_cli/utils/cli_configs.py
|
|
2
|
+
"""
|
|
3
|
+
Centralized registry for external CLI tool configurations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
CLI_REGISTRY = {
|
|
7
|
+
"claude": {
|
|
8
|
+
"display_name": "Claude CLI",
|
|
9
|
+
"install_instructions": "Install: npm install -g @anthropic/claude-code",
|
|
10
|
+
"prompt_flag": None
|
|
11
|
+
},
|
|
12
|
+
"gemini": {
|
|
13
|
+
"display_name": "Gemini CLI",
|
|
14
|
+
"install_instructions": None,
|
|
15
|
+
"prompt_flag": "-i"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# titan_cli/utils/cli_launcher.py
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
class CLILauncher:
|
|
8
|
+
"""
|
|
9
|
+
Generic launcher for external CLI tools.
|
|
10
|
+
|
|
11
|
+
This class provides a standardized way to check for the availability of a CLI tool
|
|
12
|
+
and launch it, optionally passing an initial prompt. It abstracts away the
|
|
13
|
+
specific command-line arguments needed for interactive prompts for different tools.
|
|
14
|
+
|
|
15
|
+
To integrate a new CLI tool:
|
|
16
|
+
1. Ensure the tool is discoverable in the system's PATH.
|
|
17
|
+
2. Add its configuration to `titan_cli/utils/cli_configs.py` within the `CLI_REGISTRY`.
|
|
18
|
+
This configuration should include its `display_name`, `install_instructions` (optional),
|
|
19
|
+
and `prompt_flag` (e.g., "-i" for Gemini, or None if it takes a positional argument).
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
cli_name (str): The actual command to execute (e.g., "claude", "gemini").
|
|
23
|
+
install_instructions (Optional[str]): A message guiding the user on how to install the CLI.
|
|
24
|
+
prompt_flag (Optional[str]): The command-line flag used by the CLI to accept an
|
|
25
|
+
initial prompt while remaining interactive (e.g., "-i").
|
|
26
|
+
If the CLI accepts a positional argument for the prompt, set to None.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, cli_name: str, install_instructions: Optional[str] = None, prompt_flag: Optional[str] = None):
|
|
30
|
+
self.cli_name = cli_name
|
|
31
|
+
self.install_instructions = install_instructions
|
|
32
|
+
self.prompt_flag = prompt_flag
|
|
33
|
+
|
|
34
|
+
def is_available(self) -> bool:
|
|
35
|
+
"""Check if the CLI tool is installed."""
|
|
36
|
+
return shutil.which(self.cli_name) is not None
|
|
37
|
+
|
|
38
|
+
def launch(self, prompt: Optional[str] = None, cwd: Optional[str] = None) -> int:
|
|
39
|
+
"""
|
|
40
|
+
Launch the CLI tool in the current terminal.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
prompt: Optional initial prompt to send to the CLI
|
|
44
|
+
cwd: Working directory (default: current)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Exit code from the CLI tool
|
|
48
|
+
"""
|
|
49
|
+
cmd = [self.cli_name]
|
|
50
|
+
|
|
51
|
+
if prompt:
|
|
52
|
+
if self.prompt_flag:
|
|
53
|
+
cmd.extend([self.prompt_flag, prompt])
|
|
54
|
+
else:
|
|
55
|
+
cmd.append(prompt)
|
|
56
|
+
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
cmd,
|
|
59
|
+
stdin=sys.stdin,
|
|
60
|
+
stdout=sys.stdout,
|
|
61
|
+
stderr=sys.stderr,
|
|
62
|
+
cwd=cwd
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return result.returncode
|
titan_cli/messages.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized Messages for Titan CLI
|
|
3
|
+
|
|
4
|
+
All user-visible text should be defined here for:
|
|
5
|
+
- Consistency across the application
|
|
6
|
+
- Easy maintenance and updates
|
|
7
|
+
- Future i18n support if needed
|
|
8
|
+
- Clear organization by feature
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from titan_cli.messages import msg
|
|
12
|
+
|
|
13
|
+
typer.echo(msg.CLI.VERSION.format(version="0.1.0"))
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Messages:
|
|
18
|
+
"""All user-visible messages organized by category"""
|
|
19
|
+
|
|
20
|
+
# ═══════════════════════════════════════════════════════════════
|
|
21
|
+
# CLI Core Messages
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
class CLI:
|
|
25
|
+
"""Main CLI application messages"""
|
|
26
|
+
APP_NAME = "titan"
|
|
27
|
+
APP_DESCRIPTION = "Titan CLI - Development tools orchestrator"
|
|
28
|
+
VERSION = "Titan CLI v{version}"
|
|
29
|
+
|
|
30
|
+
# ═══════════════════════════════════════════════════════════════
|
|
31
|
+
# Workflow Engine
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
class Workflow:
|
|
35
|
+
"""Workflow execution messages"""
|
|
36
|
+
|
|
37
|
+
# Workflow lifecycle
|
|
38
|
+
TITLE = "{emoji} {name}"
|
|
39
|
+
STEP_INFO = "[{current_step}/{total_steps}] {step_name}"
|
|
40
|
+
STEP_EXCEPTION = "Step '{step_name}' raised an exception: {error}"
|
|
41
|
+
HALTED = "Workflow halted: {message}"
|
|
42
|
+
COMPLETED_SUCCESS = "{name} completed successfully"
|
|
43
|
+
COMPLETED_WITH_SKIPS = "{name} completed with skips"
|
|
44
|
+
|
|
45
|
+
# Step result logging
|
|
46
|
+
STEP_SUCCESS = " {symbol} {message}"
|
|
47
|
+
STEP_SKIPPED = " {symbol} {message}"
|
|
48
|
+
STEP_ERROR = " {symbol} {message}"
|
|
49
|
+
|
|
50
|
+
# Pre-flight checks
|
|
51
|
+
UNCOMMITTED_CHANGES_WARNING: str = "You have uncommitted changes."
|
|
52
|
+
UNCOMMITTED_CHANGES_PROMPT_TITLE: str = "Uncommitted Changes Detected"
|
|
53
|
+
WORKFLOW_STEPS_INFO: str = """This workflow will:
|
|
54
|
+
1. Prompt you for a commit message (or skip if you prefer)
|
|
55
|
+
2. Create and push the commit
|
|
56
|
+
3. Use AI to generate PR title and description automatically"""
|
|
57
|
+
CONTINUE_PROMPT: str = "Continue?"
|
|
58
|
+
|
|
59
|
+
# ═══════════════════════════════════════════════════════════════
|
|
60
|
+
# AI Assistant Step
|
|
61
|
+
# ═══════════════════════════════════════════════════════════════
|
|
62
|
+
|
|
63
|
+
class AIAssistant:
|
|
64
|
+
"""Messages for the AI Code Assistant step."""
|
|
65
|
+
UI_CONTEXT_NOT_AVAILABLE = "UI context is not available for this step."
|
|
66
|
+
CONTEXT_KEY_REQUIRED = "Parameter 'context_key' is required for ai_code_assistant step"
|
|
67
|
+
NO_DATA_IN_CONTEXT = "No data found in context key '{context_key}' - skipping AI assistance"
|
|
68
|
+
INVALID_PROMPT_TEMPLATE = "Invalid prompt_template: missing placeholder {e}"
|
|
69
|
+
FAILED_TO_BUILD_PROMPT = "Failed to build prompt: {e}"
|
|
70
|
+
CONFIRM_LAUNCH_ASSISTANT = "Would you like AI assistance to help fix these issues?"
|
|
71
|
+
SELECT_ASSISTANT_CLI = "Select which AI assistant to use"
|
|
72
|
+
DECLINED_ASSISTANCE_STOPPED = "User declined AI assistance - workflow stopped"
|
|
73
|
+
DECLINED_ASSISTANCE_SKIPPED = "User declined AI assistance"
|
|
74
|
+
NO_ASSISTANT_CLI_FOUND = "No AI coding assistant CLI found"
|
|
75
|
+
LAUNCHING_ASSISTANT = "Launching {cli_name}..."
|
|
76
|
+
PROMPT_PREVIEW = "Prompt: {prompt_preview}"
|
|
77
|
+
BACK_IN_TITAN = "Back in Titan workflow"
|
|
78
|
+
ASSISTANT_EXITED_WITH_CODE = "{cli_name} exited with code {exit_code}"
|
|
79
|
+
|
|
80
|
+
# ═══════════════════════════════════════════════════════════════
|
|
81
|
+
# Generic Error Messages
|
|
82
|
+
# ═══════════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
class Errors:
|
|
85
|
+
"""Generic error messages"""
|
|
86
|
+
|
|
87
|
+
# Plugin / Core Errors
|
|
88
|
+
PLUGIN_LOAD_FAILED = "Failed to load plugin '{plugin_name}': {error}"
|
|
89
|
+
PLUGIN_INIT_FAILED = "Failed to initialize plugin '{plugin_name}': {error}"
|
|
90
|
+
CONFIG_PARSE_ERROR = "Failed to parse configuration file at {file_path}: {error}"
|
|
91
|
+
|
|
92
|
+
# File system
|
|
93
|
+
FILE_NOT_FOUND = "File not found: {path}"
|
|
94
|
+
FILE_READ_ERROR = "Cannot read file: {path}"
|
|
95
|
+
FILE_WRITE_ERROR = "Cannot write file: {path}"
|
|
96
|
+
DIRECTORY_NOT_FOUND = "Directory not found: {path}"
|
|
97
|
+
PERMISSION_DENIED = "Permission denied: {path}"
|
|
98
|
+
|
|
99
|
+
# Input validation
|
|
100
|
+
INVALID_INPUT = "Invalid input: {value}"
|
|
101
|
+
MISSING_REQUIRED = "Missing required field: {field}"
|
|
102
|
+
INVALID_FORMAT = "Invalid format: {value}"
|
|
103
|
+
|
|
104
|
+
# Network
|
|
105
|
+
NETWORK_ERROR = "Network error: {error}"
|
|
106
|
+
TIMEOUT = "Operation timed out"
|
|
107
|
+
CONNECTION_FAILED = "Connection failed: {error}"
|
|
108
|
+
|
|
109
|
+
# General
|
|
110
|
+
UNKNOWN_ERROR = "An unknown error occurred: {error}"
|
|
111
|
+
NOT_IMPLEMENTED = "Feature not implemented yet"
|
|
112
|
+
OPERATION_CANCELLED = "Operation cancelled"
|
|
113
|
+
OPERATION_CANCELLED_NO_CHANGES = "Operation cancelled. No changes were made."
|
|
114
|
+
|
|
115
|
+
# Config specific
|
|
116
|
+
CONFIG_WRITE_FAILED = "Failed to write configuration file: {error}"
|
|
117
|
+
PROJECT_ROOT_NOT_SET = "Project root not set. Cannot discover projects."
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Singleton instance for easy access
|
|
121
|
+
msg = Messages()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Titan TUI Module
|
|
3
|
+
|
|
4
|
+
Textual-based Terminal User Interface for Titan CLI.
|
|
5
|
+
"""
|
|
6
|
+
from .app import TitanApp
|
|
7
|
+
|
|
8
|
+
__all__ = ["TitanApp"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def launch_tui():
|
|
12
|
+
"""
|
|
13
|
+
Launch the Titan TUI application.
|
|
14
|
+
|
|
15
|
+
This is the main entry point for running Titan in TUI mode.
|
|
16
|
+
|
|
17
|
+
Flow:
|
|
18
|
+
1. Check if global config exists (~/.titan/config.toml)
|
|
19
|
+
- If NO: Launch global setup wizard
|
|
20
|
+
- If YES: Continue
|
|
21
|
+
2. Check if project config exists (./.titan/config.toml)
|
|
22
|
+
- If NO: Launch project setup wizard
|
|
23
|
+
- If YES: Continue to main menu
|
|
24
|
+
"""
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from titan_cli.core.config import TitanConfig
|
|
27
|
+
from titan_cli.core.plugins.plugin_registry import PluginRegistry
|
|
28
|
+
from .screens import GlobalSetupWizardScreen, ProjectSetupWizardScreen, MainMenuScreen
|
|
29
|
+
|
|
30
|
+
# Check if global config exists
|
|
31
|
+
global_config_path = TitanConfig.GLOBAL_CONFIG
|
|
32
|
+
|
|
33
|
+
if not global_config_path.exists():
|
|
34
|
+
# First-time setup: Launch global setup wizard
|
|
35
|
+
plugin_registry = PluginRegistry()
|
|
36
|
+
config = TitanConfig(registry=plugin_registry)
|
|
37
|
+
|
|
38
|
+
# We'll create a special wrapper screen that handles the wizard flow
|
|
39
|
+
from .screens.base import BaseScreen
|
|
40
|
+
from textual.app import ComposeResult
|
|
41
|
+
from textual.containers import Container
|
|
42
|
+
|
|
43
|
+
class WizardFlowScreen(BaseScreen):
|
|
44
|
+
"""Temporary screen to manage wizard flow."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config, *args, **kwargs):
|
|
47
|
+
super().__init__(config, title="Setup", show_back=False, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def compose_content(self) -> ComposeResult:
|
|
50
|
+
# This won't be used, we push wizard immediately
|
|
51
|
+
yield Container()
|
|
52
|
+
|
|
53
|
+
def on_mount(self) -> None:
|
|
54
|
+
"""Push the global wizard on mount."""
|
|
55
|
+
def on_project_wizard_complete(_=None):
|
|
56
|
+
"""After project wizard completes, show main menu."""
|
|
57
|
+
# Reload project config without resetting plugins
|
|
58
|
+
from titan_cli.core.secrets import SecretManager
|
|
59
|
+
from titan_cli.core.models import TitanConfigModel
|
|
60
|
+
|
|
61
|
+
self.config.project_config_path = Path.cwd() / ".titan" / "config.toml"
|
|
62
|
+
self.config.project_config = self.config._load_toml(self.config.project_config_path)
|
|
63
|
+
|
|
64
|
+
# Merge configs and update
|
|
65
|
+
merged = self.config._merge_configs(self.config.global_config, self.config.project_config)
|
|
66
|
+
self.config.config = TitanConfigModel(**merged)
|
|
67
|
+
|
|
68
|
+
# Update secrets manager to use current project
|
|
69
|
+
self.config.secrets = SecretManager(project_path=Path.cwd())
|
|
70
|
+
|
|
71
|
+
# Initialize only the configured plugins (without reset)
|
|
72
|
+
self.config.registry.initialize_plugins(config=self.config, secrets=self.config.secrets)
|
|
73
|
+
|
|
74
|
+
# Reload workflow registry to reflect enabled/disabled plugins
|
|
75
|
+
self.config.workflows.reload()
|
|
76
|
+
|
|
77
|
+
# Pop all screens except the base one, then push main menu
|
|
78
|
+
# WizardFlowScreen is still there, so we need to pop it too
|
|
79
|
+
# Stack after project wizard completes: [WizardFlowScreen]
|
|
80
|
+
# We want: [MainMenuScreen]
|
|
81
|
+
self.app.pop_screen() # Remove WizardFlowScreen
|
|
82
|
+
self.app.push_screen(MainMenuScreen(self.config))
|
|
83
|
+
|
|
84
|
+
def on_global_wizard_complete(_=None):
|
|
85
|
+
"""After global wizard completes, check for project config."""
|
|
86
|
+
from titan_cli.core.models import TitanConfigModel
|
|
87
|
+
|
|
88
|
+
# Reload global config
|
|
89
|
+
self.config.global_config = self.config._load_toml(self.config._global_config_path)
|
|
90
|
+
|
|
91
|
+
# Update config.config with the new global config
|
|
92
|
+
# (merge with empty project config since we don't have one yet)
|
|
93
|
+
merged = self.config._merge_configs(self.config.global_config, {})
|
|
94
|
+
self.config.config = TitanConfigModel(**merged)
|
|
95
|
+
|
|
96
|
+
# Check if project config exists
|
|
97
|
+
project_config_path = Path.cwd() / ".titan" / "config.toml"
|
|
98
|
+
|
|
99
|
+
if not project_config_path.exists():
|
|
100
|
+
# Launch project setup wizard with callback to show main menu
|
|
101
|
+
self.app.push_screen(
|
|
102
|
+
ProjectSetupWizardScreen(self.config, Path.cwd()),
|
|
103
|
+
on_project_wizard_complete
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
# Project is configured, reload configs without resetting plugins
|
|
107
|
+
from titan_cli.core.secrets import SecretManager
|
|
108
|
+
from titan_cli.core.models import TitanConfigModel
|
|
109
|
+
|
|
110
|
+
self.config.project_config_path = project_config_path
|
|
111
|
+
self.config.project_config = self.config._load_toml(self.config.project_config_path)
|
|
112
|
+
|
|
113
|
+
# Merge configs and update
|
|
114
|
+
merged = self.config._merge_configs(self.config.global_config, self.config.project_config)
|
|
115
|
+
self.config.config = TitanConfigModel(**merged)
|
|
116
|
+
|
|
117
|
+
# Update secrets manager to use current project
|
|
118
|
+
self.config.secrets = SecretManager(project_path=Path.cwd())
|
|
119
|
+
|
|
120
|
+
# Initialize plugins with new config
|
|
121
|
+
self.config.registry.initialize_plugins(config=self.config, secrets=self.config.secrets)
|
|
122
|
+
|
|
123
|
+
# Reload workflow registry to reflect enabled/disabled plugins
|
|
124
|
+
self.config.workflows.reload()
|
|
125
|
+
|
|
126
|
+
# Stack: [WizardFlowScreen]
|
|
127
|
+
# We want: [MainMenuScreen]
|
|
128
|
+
self.app.pop_screen() # Remove WizardFlowScreen
|
|
129
|
+
self.app.push_screen(MainMenuScreen(self.config))
|
|
130
|
+
|
|
131
|
+
# Push global wizard
|
|
132
|
+
self.app.push_screen(GlobalSetupWizardScreen(self.config), on_global_wizard_complete)
|
|
133
|
+
|
|
134
|
+
# Create app with the flow screen
|
|
135
|
+
app = TitanApp(config=config, initial_screen=WizardFlowScreen(config))
|
|
136
|
+
app.run()
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Global config exists, initialize normally
|
|
140
|
+
plugin_registry = PluginRegistry()
|
|
141
|
+
config = TitanConfig(registry=plugin_registry)
|
|
142
|
+
|
|
143
|
+
# Check if project config exists in current directory
|
|
144
|
+
project_config_path = Path.cwd() / ".titan" / "config.toml"
|
|
145
|
+
|
|
146
|
+
if not project_config_path.exists():
|
|
147
|
+
# Project not configured: Launch project setup wizard
|
|
148
|
+
# Create a wrapper screen similar to global wizard flow
|
|
149
|
+
from .screens.base import BaseScreen
|
|
150
|
+
from textual.app import ComposeResult
|
|
151
|
+
from textual.containers import Container
|
|
152
|
+
|
|
153
|
+
class ProjectWizardFlowScreen(BaseScreen):
|
|
154
|
+
"""Temporary screen to manage project wizard flow."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, config, *args, **kwargs):
|
|
157
|
+
super().__init__(config, title="Project Setup", show_back=False, *args, **kwargs)
|
|
158
|
+
|
|
159
|
+
def compose_content(self) -> ComposeResult:
|
|
160
|
+
# This won't be used, we push wizard immediately
|
|
161
|
+
yield Container()
|
|
162
|
+
|
|
163
|
+
def on_mount(self) -> None:
|
|
164
|
+
"""Push the project wizard on mount."""
|
|
165
|
+
def on_project_wizard_complete(_=None):
|
|
166
|
+
"""After project wizard completes, show main menu."""
|
|
167
|
+
# Reload project config without resetting plugins
|
|
168
|
+
from pathlib import Path
|
|
169
|
+
from titan_cli.core.secrets import SecretManager
|
|
170
|
+
from titan_cli.core.models import TitanConfigModel
|
|
171
|
+
|
|
172
|
+
self.config.project_config_path = Path.cwd() / ".titan" / "config.toml"
|
|
173
|
+
self.config.project_config = self.config._load_toml(self.config.project_config_path)
|
|
174
|
+
|
|
175
|
+
# Merge configs and update
|
|
176
|
+
merged = self.config._merge_configs(self.config.global_config, self.config.project_config)
|
|
177
|
+
self.config.config = TitanConfigModel(**merged)
|
|
178
|
+
|
|
179
|
+
# Update secrets manager to use current project
|
|
180
|
+
self.config.secrets = SecretManager(project_path=Path.cwd())
|
|
181
|
+
|
|
182
|
+
# Initialize only the configured plugins (without reset)
|
|
183
|
+
self.config.registry.initialize_plugins(config=self.config, secrets=self.config.secrets)
|
|
184
|
+
|
|
185
|
+
# Reload workflow registry to reflect enabled/disabled plugins
|
|
186
|
+
self.config.workflows.reload()
|
|
187
|
+
|
|
188
|
+
# Pop this flow screen and show main menu
|
|
189
|
+
self.app.pop_screen() # Remove ProjectWizardFlowScreen
|
|
190
|
+
self.app.push_screen(MainMenuScreen(self.config))
|
|
191
|
+
|
|
192
|
+
# Push project wizard
|
|
193
|
+
self.app.push_screen(
|
|
194
|
+
ProjectSetupWizardScreen(self.config, Path.cwd()),
|
|
195
|
+
on_project_wizard_complete
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Create app with the flow screen
|
|
199
|
+
app = TitanApp(config=config, initial_screen=ProjectWizardFlowScreen(config))
|
|
200
|
+
app.run()
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Both global and project configs exist: Run normally
|
|
204
|
+
app = TitanApp(config=config)
|
|
205
|
+
app.run()
|