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,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()