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,686 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Setup Wizard Screen
|
|
3
|
+
|
|
4
|
+
Wizard for configuring a new Titan project in the current directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.widgets import Static, Input, SelectionList
|
|
10
|
+
from textual.widgets.selection_list import Selection
|
|
11
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from titan_cli.ui.tui.icons import Icons
|
|
16
|
+
from titan_cli.ui.tui.widgets import Text, DimText, Button, BoldText
|
|
17
|
+
from titan_cli.utils.autoupdate import is_dev_install
|
|
18
|
+
from .base import BaseScreen
|
|
19
|
+
|
|
20
|
+
# Setup debug logging (only in development)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
if is_dev_install():
|
|
23
|
+
debug_log = Path("/tmp/titan_wizard_debug.log")
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
filename=str(debug_log),
|
|
26
|
+
level=logging.DEBUG,
|
|
27
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
28
|
+
filemode='w'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StepIndicator(Static):
|
|
33
|
+
"""Widget showing a single step with status indicator."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, step_number: int, title: str, status: str = "pending"):
|
|
36
|
+
self.step_number = step_number
|
|
37
|
+
self.title = title
|
|
38
|
+
self.status = status
|
|
39
|
+
super().__init__()
|
|
40
|
+
|
|
41
|
+
def render(self) -> str:
|
|
42
|
+
"""Render the step with appropriate icon."""
|
|
43
|
+
if self.status == "completed":
|
|
44
|
+
icon = Icons.SUCCESS
|
|
45
|
+
style = "dim"
|
|
46
|
+
elif self.status == "in_progress":
|
|
47
|
+
icon = Icons.RUNNING
|
|
48
|
+
style = "bold cyan"
|
|
49
|
+
else: # pending
|
|
50
|
+
icon = Icons.PENDING
|
|
51
|
+
style = "dim"
|
|
52
|
+
|
|
53
|
+
return f"[{style}]{icon} {self.step_number}. {self.title}[/{style}]"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProjectSetupWizardScreen(BaseScreen):
|
|
57
|
+
"""
|
|
58
|
+
Project setup wizard for configuring Titan in a project.
|
|
59
|
+
|
|
60
|
+
This wizard runs when Titan is launched from a directory that doesn't
|
|
61
|
+
have a .titan/config.toml file.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
BINDINGS = [
|
|
65
|
+
Binding("escape", "cancel", "Cancel"),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
CSS = """
|
|
69
|
+
ProjectSetupWizardScreen {
|
|
70
|
+
align: center middle;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#wizard-container {
|
|
74
|
+
width: 100%;
|
|
75
|
+
height: 1fr;
|
|
76
|
+
background: $surface-lighten-1;
|
|
77
|
+
padding: 0 2 1 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#steps-panel {
|
|
81
|
+
width: 20%;
|
|
82
|
+
height: 100%;
|
|
83
|
+
border: round $primary;
|
|
84
|
+
border-title-align: center;
|
|
85
|
+
background: $surface-lighten-1;
|
|
86
|
+
padding: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#steps-content {
|
|
90
|
+
padding: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
StepIndicator {
|
|
94
|
+
height: auto;
|
|
95
|
+
margin-bottom: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#content-panel {
|
|
99
|
+
width: 80%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
border: round $primary;
|
|
102
|
+
border-title-align: center;
|
|
103
|
+
background: $surface-lighten-1;
|
|
104
|
+
padding: 0;
|
|
105
|
+
layout: vertical;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#content-scroll {
|
|
109
|
+
height: 1fr;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#content-area {
|
|
113
|
+
padding: 1;
|
|
114
|
+
height: auto;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#content-title {
|
|
118
|
+
color: $accent;
|
|
119
|
+
text-style: bold;
|
|
120
|
+
margin-bottom: 2;
|
|
121
|
+
height: auto;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#content-body {
|
|
125
|
+
height: auto;
|
|
126
|
+
margin-bottom: 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#button-container {
|
|
130
|
+
height: auto;
|
|
131
|
+
padding: 1 2;
|
|
132
|
+
background: $surface-lighten-1;
|
|
133
|
+
border-top: solid $primary;
|
|
134
|
+
align: right middle;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#plugins-selection {
|
|
138
|
+
height: auto;
|
|
139
|
+
margin-top: 1;
|
|
140
|
+
margin-bottom: 2;
|
|
141
|
+
border: solid $accent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#plugins-selection > .selection-list--option {
|
|
145
|
+
padding: 1 2;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#plugins-selection > .selection-list--option-highlighted {
|
|
149
|
+
padding: 1 2;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Input {
|
|
153
|
+
width: 100%;
|
|
154
|
+
margin-top: 1;
|
|
155
|
+
border: solid $accent;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Input:focus {
|
|
159
|
+
border: solid $primary;
|
|
160
|
+
}
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, config, project_path: Path):
|
|
164
|
+
# Debug: check registry state at init
|
|
165
|
+
logger.debug(f"ProjectSetupWizard.__init__ - Registry has {len(config.registry._plugins)} plugins: {list(config.registry._plugins.keys())}")
|
|
166
|
+
|
|
167
|
+
super().__init__(
|
|
168
|
+
config,
|
|
169
|
+
title=f"{Icons.SETTINGS} Project Setup",
|
|
170
|
+
show_back=False,
|
|
171
|
+
show_status_bar=False
|
|
172
|
+
)
|
|
173
|
+
self.project_path = project_path
|
|
174
|
+
self.current_step = 0
|
|
175
|
+
self.wizard_data = {}
|
|
176
|
+
self._mounted = False
|
|
177
|
+
|
|
178
|
+
# Detect if it's a git repository
|
|
179
|
+
self.is_git_repo = (project_path / ".git").exists()
|
|
180
|
+
|
|
181
|
+
# Define all wizard steps
|
|
182
|
+
self.steps = [
|
|
183
|
+
{"id": "welcome", "title": "Welcome"},
|
|
184
|
+
{"id": "project_name", "title": "Project Name"},
|
|
185
|
+
{"id": "select_plugins", "title": "Select Plugins"},
|
|
186
|
+
{"id": "complete", "title": "Complete"},
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def compose_content(self) -> ComposeResult:
|
|
190
|
+
"""Compose the wizard screen with two panels."""
|
|
191
|
+
with Container(id="wizard-container"):
|
|
192
|
+
with Horizontal():
|
|
193
|
+
# Left panel: Steps
|
|
194
|
+
left_panel = VerticalScroll(id="steps-panel")
|
|
195
|
+
left_panel.border_title = "Setup Steps"
|
|
196
|
+
with left_panel:
|
|
197
|
+
with Container(id="steps-content"):
|
|
198
|
+
for i, step in enumerate(self.steps, 1):
|
|
199
|
+
status = "in_progress" if i == 1 else "pending"
|
|
200
|
+
yield StepIndicator(i, step["title"], status=status)
|
|
201
|
+
|
|
202
|
+
# Right panel: Content
|
|
203
|
+
right_panel = Container(id="content-panel")
|
|
204
|
+
right_panel.border_title = "Project Configuration"
|
|
205
|
+
with right_panel:
|
|
206
|
+
with VerticalScroll(id="content-scroll"):
|
|
207
|
+
with Container(id="content-area"):
|
|
208
|
+
yield Static("", id="content-title")
|
|
209
|
+
yield Container(id="content-body")
|
|
210
|
+
|
|
211
|
+
# Bottom buttons
|
|
212
|
+
with Horizontal(id="button-container"):
|
|
213
|
+
yield Button("Back", variant="default", id="back-button", disabled=True)
|
|
214
|
+
yield Button("Next", variant="primary", id="next-button")
|
|
215
|
+
yield Button("Cancel", variant="default", id="cancel-button")
|
|
216
|
+
|
|
217
|
+
def on_mount(self) -> None:
|
|
218
|
+
"""Load the first step when mounted."""
|
|
219
|
+
if not self._mounted:
|
|
220
|
+
self._mounted = True
|
|
221
|
+
self.load_step(0)
|
|
222
|
+
|
|
223
|
+
def load_step(self, step_index: int) -> None:
|
|
224
|
+
"""Load content for the given step."""
|
|
225
|
+
self.current_step = step_index
|
|
226
|
+
step = self.steps[step_index]
|
|
227
|
+
|
|
228
|
+
# Update step indicators
|
|
229
|
+
for i, indicator in enumerate(self.query(StepIndicator)):
|
|
230
|
+
if i < step_index:
|
|
231
|
+
indicator.status = "completed"
|
|
232
|
+
elif i == step_index:
|
|
233
|
+
indicator.status = "in_progress"
|
|
234
|
+
else:
|
|
235
|
+
indicator.status = "pending"
|
|
236
|
+
indicator.refresh()
|
|
237
|
+
|
|
238
|
+
# Update buttons
|
|
239
|
+
back_button = self.query_one("#back-button", Button)
|
|
240
|
+
back_button.disabled = (step_index == 0)
|
|
241
|
+
|
|
242
|
+
# Change Next button label based on step
|
|
243
|
+
next_button = self.query_one("#next-button", Button)
|
|
244
|
+
if step_index == len(self.steps) - 1:
|
|
245
|
+
next_button.label = "Finish"
|
|
246
|
+
else:
|
|
247
|
+
next_button.label = "Next"
|
|
248
|
+
|
|
249
|
+
# Load step content
|
|
250
|
+
content_title = self.query_one("#content-title", Static)
|
|
251
|
+
content_body = self.query_one("#content-body", Container)
|
|
252
|
+
|
|
253
|
+
if step["id"] == "welcome":
|
|
254
|
+
self.load_welcome_step(content_title, content_body)
|
|
255
|
+
elif step["id"] == "project_name":
|
|
256
|
+
self.load_project_name_step(content_title, content_body)
|
|
257
|
+
elif step["id"] == "select_plugins":
|
|
258
|
+
self.load_select_plugins_step(content_title, content_body)
|
|
259
|
+
elif step["id"] == "complete":
|
|
260
|
+
self.load_complete_step(content_title, content_body)
|
|
261
|
+
|
|
262
|
+
def load_welcome_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
263
|
+
"""Load Welcome step."""
|
|
264
|
+
title_widget.update("Configure This Project")
|
|
265
|
+
|
|
266
|
+
# Clear previous content
|
|
267
|
+
body_widget.remove_children()
|
|
268
|
+
|
|
269
|
+
# Add welcome message
|
|
270
|
+
project_dir = self.project_path.name
|
|
271
|
+
welcome_text = Text(
|
|
272
|
+
f"Welcome to project setup for '{project_dir}'!\n\n"
|
|
273
|
+
"This wizard will help you configure Titan for this project.\n\n"
|
|
274
|
+
)
|
|
275
|
+
body_widget.mount(welcome_text)
|
|
276
|
+
|
|
277
|
+
# Detect project type
|
|
278
|
+
project_type_info = BoldText("Detected Project Type:\n")
|
|
279
|
+
body_widget.mount(project_type_info)
|
|
280
|
+
|
|
281
|
+
if self.is_git_repo:
|
|
282
|
+
git_info = Text(
|
|
283
|
+
" ✓ Git Repository\n\n"
|
|
284
|
+
"Titan can help you with:\n"
|
|
285
|
+
" • AI-powered commit messages\n"
|
|
286
|
+
" • Branch management\n"
|
|
287
|
+
" • GitHub integration (PRs, issues)\n"
|
|
288
|
+
" • Jira integration (issue tracking)\n"
|
|
289
|
+
)
|
|
290
|
+
body_widget.mount(git_info)
|
|
291
|
+
else:
|
|
292
|
+
no_git_info = DimText(
|
|
293
|
+
" ✗ Not a Git repository\n\n"
|
|
294
|
+
"You can still use Titan for workflows and AI features.\n"
|
|
295
|
+
)
|
|
296
|
+
body_widget.mount(no_git_info)
|
|
297
|
+
|
|
298
|
+
# Add next steps
|
|
299
|
+
next_steps = Text(
|
|
300
|
+
"\nIn the next steps, you'll:\n"
|
|
301
|
+
" 1. Name your project\n"
|
|
302
|
+
" 2. Select plugins to enable\n"
|
|
303
|
+
)
|
|
304
|
+
body_widget.mount(next_steps)
|
|
305
|
+
|
|
306
|
+
def load_project_name_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
307
|
+
"""Load Project Name step."""
|
|
308
|
+
title_widget.update("Project Name")
|
|
309
|
+
body_widget.remove_children()
|
|
310
|
+
|
|
311
|
+
# Add description
|
|
312
|
+
description = Text(
|
|
313
|
+
"Enter a name for this project.\n\n"
|
|
314
|
+
"This helps identify the project in Titan's configuration."
|
|
315
|
+
)
|
|
316
|
+
body_widget.mount(description)
|
|
317
|
+
|
|
318
|
+
# Default to directory name
|
|
319
|
+
default_name = self.wizard_data.get("project_name", self.project_path.name)
|
|
320
|
+
|
|
321
|
+
# Add example
|
|
322
|
+
example = DimText(
|
|
323
|
+
f"\nExamples:\n"
|
|
324
|
+
f" • {self.project_path.name}\n"
|
|
325
|
+
f" • my-awesome-project\n"
|
|
326
|
+
f" • company-backend-api"
|
|
327
|
+
)
|
|
328
|
+
body_widget.mount(example)
|
|
329
|
+
|
|
330
|
+
# Add input field
|
|
331
|
+
input_widget = Input(
|
|
332
|
+
value=default_name,
|
|
333
|
+
placeholder="Enter project name...",
|
|
334
|
+
id="project-name-input"
|
|
335
|
+
)
|
|
336
|
+
input_widget.styles.margin = (2, 0, 0, 0)
|
|
337
|
+
body_widget.mount(input_widget)
|
|
338
|
+
|
|
339
|
+
# Focus the input
|
|
340
|
+
self.call_after_refresh(lambda: input_widget.focus())
|
|
341
|
+
|
|
342
|
+
def load_select_plugins_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
343
|
+
"""Load Select Plugins step."""
|
|
344
|
+
title_widget.update("Select Plugins")
|
|
345
|
+
body_widget.remove_children()
|
|
346
|
+
|
|
347
|
+
# Add description
|
|
348
|
+
description = Text(
|
|
349
|
+
"Select which plugins to enable for this project.\n"
|
|
350
|
+
)
|
|
351
|
+
body_widget.mount(description)
|
|
352
|
+
|
|
353
|
+
# Required plugins info
|
|
354
|
+
required_info = BoldText(
|
|
355
|
+
"\nRequired: git, github (mandatory for all projects)\n"
|
|
356
|
+
)
|
|
357
|
+
body_widget.mount(required_info)
|
|
358
|
+
|
|
359
|
+
optional_info = DimText(
|
|
360
|
+
"Optional: jira (configure if you use Jira)\n"
|
|
361
|
+
)
|
|
362
|
+
body_widget.mount(optional_info)
|
|
363
|
+
|
|
364
|
+
# Get available plugins
|
|
365
|
+
logger.debug(f"load_select_plugins_step - Registry has {len(self.config.registry._plugins)} plugins: {list(self.config.registry._plugins.keys())}")
|
|
366
|
+
installed_plugins = self.config.registry.list_discovered()
|
|
367
|
+
logger.debug(f"Discovered plugins: {installed_plugins}")
|
|
368
|
+
|
|
369
|
+
if not installed_plugins:
|
|
370
|
+
no_plugins = DimText(
|
|
371
|
+
"\nNo plugins are currently installed.\n"
|
|
372
|
+
"You can install plugins later from the main menu:\n"
|
|
373
|
+
" Main Menu → Plugin Management → Install a new Plugin"
|
|
374
|
+
)
|
|
375
|
+
body_widget.mount(no_plugins)
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# Required plugins
|
|
379
|
+
required_plugins = ["git", "github"]
|
|
380
|
+
|
|
381
|
+
# Create a SelectionList with checkboxes
|
|
382
|
+
selections = []
|
|
383
|
+
for plugin_name in installed_plugins:
|
|
384
|
+
# Pre-select required plugins
|
|
385
|
+
initial_state = plugin_name in required_plugins
|
|
386
|
+
selections.append(Selection(plugin_name, plugin_name, initial_state))
|
|
387
|
+
|
|
388
|
+
if selections:
|
|
389
|
+
selection_list = SelectionList(*selections, id="plugins-selection")
|
|
390
|
+
body_widget.mount(selection_list)
|
|
391
|
+
|
|
392
|
+
# Add instructions
|
|
393
|
+
instructions = DimText(
|
|
394
|
+
"\n\nUse Space to toggle optional plugins, Enter to continue.\n"
|
|
395
|
+
"Note: git and github must remain selected."
|
|
396
|
+
)
|
|
397
|
+
body_widget.mount(instructions)
|
|
398
|
+
|
|
399
|
+
# Focus the selection list
|
|
400
|
+
self.call_after_refresh(lambda: selection_list.focus())
|
|
401
|
+
|
|
402
|
+
def load_complete_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
403
|
+
"""Load Setup Complete step."""
|
|
404
|
+
title_widget.update("Project Configured!")
|
|
405
|
+
body_widget.remove_children()
|
|
406
|
+
|
|
407
|
+
# Add completion message
|
|
408
|
+
project_name = self.wizard_data.get("project_name", self.project_path.name)
|
|
409
|
+
completion_text = BoldText(
|
|
410
|
+
f"Project '{project_name}' has been configured successfully!\n"
|
|
411
|
+
)
|
|
412
|
+
body_widget.mount(completion_text)
|
|
413
|
+
|
|
414
|
+
# Show selected plugins
|
|
415
|
+
enabled_plugins = self.wizard_data.get("enabled_plugins", [])
|
|
416
|
+
if enabled_plugins:
|
|
417
|
+
plugins_text = Text(
|
|
418
|
+
f"\n\nEnabled Plugins ({len(enabled_plugins)}):\n"
|
|
419
|
+
)
|
|
420
|
+
body_widget.mount(plugins_text)
|
|
421
|
+
|
|
422
|
+
for plugin in enabled_plugins:
|
|
423
|
+
plugin_item = DimText(f" • {plugin}")
|
|
424
|
+
body_widget.mount(plugin_item)
|
|
425
|
+
else:
|
|
426
|
+
no_plugins_text = DimText(
|
|
427
|
+
"\n\nNo plugins were enabled.\n"
|
|
428
|
+
"You can enable them later from the main menu."
|
|
429
|
+
)
|
|
430
|
+
body_widget.mount(no_plugins_text)
|
|
431
|
+
|
|
432
|
+
# Add next stepsbr
|
|
433
|
+
next_steps = Text(
|
|
434
|
+
"\n\nYou can now:\n"
|
|
435
|
+
" • Run workflows\n"
|
|
436
|
+
" • Configure plugin settings\n"
|
|
437
|
+
" • Install additional plugins\n"
|
|
438
|
+
)
|
|
439
|
+
body_widget.mount(next_steps)
|
|
440
|
+
|
|
441
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
442
|
+
"""Handle Enter key in input fields - auto-advance to next step."""
|
|
443
|
+
self.handle_next()
|
|
444
|
+
|
|
445
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
446
|
+
"""Handle button presses."""
|
|
447
|
+
if event.button.id == "next-button":
|
|
448
|
+
self.handle_next()
|
|
449
|
+
elif event.button.id == "back-button":
|
|
450
|
+
self.handle_back()
|
|
451
|
+
elif event.button.id == "cancel-button":
|
|
452
|
+
self.action_cancel()
|
|
453
|
+
|
|
454
|
+
def handle_next(self) -> None:
|
|
455
|
+
"""Move to next step or complete setup."""
|
|
456
|
+
# Validate and save current step data
|
|
457
|
+
if not self.validate_and_save_step():
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
# If on last step, complete setup
|
|
461
|
+
if self.current_step == len(self.steps) - 1:
|
|
462
|
+
self.complete_setup()
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
# If on select_plugins step, configure each selected plugin
|
|
466
|
+
if self.steps[self.current_step]["id"] == "select_plugins":
|
|
467
|
+
enabled_plugins = self.wizard_data.get("enabled_plugins", [])
|
|
468
|
+
logger.debug(f"Type of enabled_plugins: {type(enabled_plugins)}, Content: {enabled_plugins}")
|
|
469
|
+
if enabled_plugins:
|
|
470
|
+
# Launch plugin configuration wizards
|
|
471
|
+
self._configure_plugins(enabled_plugins)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# Move to next step
|
|
475
|
+
if self.current_step < len(self.steps) - 1:
|
|
476
|
+
self.load_step(self.current_step + 1)
|
|
477
|
+
|
|
478
|
+
def validate_and_save_step(self) -> bool:
|
|
479
|
+
"""Validate and save data from current step."""
|
|
480
|
+
step = self.steps[self.current_step]
|
|
481
|
+
|
|
482
|
+
if step["id"] == "welcome":
|
|
483
|
+
# No validation needed for welcome step
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
elif step["id"] == "project_name":
|
|
487
|
+
# Get project name from input
|
|
488
|
+
try:
|
|
489
|
+
input_widget = self.query_one("#project-name-input", Input)
|
|
490
|
+
project_name = input_widget.value.strip()
|
|
491
|
+
|
|
492
|
+
# Validate project name
|
|
493
|
+
if not project_name:
|
|
494
|
+
self.app.notify("Please enter a project name", severity="warning")
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
self.wizard_data["project_name"] = project_name
|
|
498
|
+
return True
|
|
499
|
+
except Exception:
|
|
500
|
+
self.app.notify("Please enter a valid project name", severity="error")
|
|
501
|
+
return False
|
|
502
|
+
|
|
503
|
+
elif step["id"] == "select_plugins":
|
|
504
|
+
# Get enabled plugins from SelectionList
|
|
505
|
+
try:
|
|
506
|
+
selection_list = self.query_one("#plugins-selection", SelectionList)
|
|
507
|
+
# Get the selected values (plugin names)
|
|
508
|
+
raw_selected = selection_list.selected
|
|
509
|
+
logger.debug(f"Raw selected type: {type(raw_selected)}, value: {raw_selected}")
|
|
510
|
+
|
|
511
|
+
enabled_plugins = [str(item) for item in raw_selected]
|
|
512
|
+
|
|
513
|
+
# Validate that required plugins are selected
|
|
514
|
+
required_plugins = ["git", "github"]
|
|
515
|
+
missing_required = [p for p in required_plugins if p not in enabled_plugins]
|
|
516
|
+
|
|
517
|
+
if missing_required:
|
|
518
|
+
self.app.notify(
|
|
519
|
+
f"Required plugins must be selected: {', '.join(missing_required)}",
|
|
520
|
+
severity="warning"
|
|
521
|
+
)
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
self.wizard_data["enabled_plugins"] = enabled_plugins
|
|
525
|
+
logger.debug(f"Selected {len(enabled_plugins)} plugins: {enabled_plugins}")
|
|
526
|
+
|
|
527
|
+
return True
|
|
528
|
+
except Exception as e:
|
|
529
|
+
logger.error(f"Error getting plugins: {e}")
|
|
530
|
+
self.app.notify("Error selecting plugins", severity="error")
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
return True
|
|
534
|
+
|
|
535
|
+
def handle_back(self) -> None:
|
|
536
|
+
"""Move to previous step."""
|
|
537
|
+
if self.current_step > 0:
|
|
538
|
+
self.load_step(self.current_step - 1)
|
|
539
|
+
|
|
540
|
+
def _configure_plugins(self, plugins_to_configure: list):
|
|
541
|
+
"""Configure each selected plugin one by one."""
|
|
542
|
+
import tomli_w
|
|
543
|
+
|
|
544
|
+
logger.debug(f"Configuring {len(plugins_to_configure)} plugins: {plugins_to_configure}")
|
|
545
|
+
|
|
546
|
+
if not plugins_to_configure:
|
|
547
|
+
logger.debug("No plugins to configure, skipping")
|
|
548
|
+
self.load_step(self.current_step + 1)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
# Create .titan directory and config file BEFORE configuring plugins
|
|
552
|
+
try:
|
|
553
|
+
titan_dir = self.project_path / ".titan"
|
|
554
|
+
titan_dir.mkdir(exist_ok=True)
|
|
555
|
+
|
|
556
|
+
project_config_path = titan_dir / "config.toml"
|
|
557
|
+
project_name = self.wizard_data.get("project_name", self.project_path.name)
|
|
558
|
+
|
|
559
|
+
# Build initial project config structure
|
|
560
|
+
project_config_data = {
|
|
561
|
+
"project": {
|
|
562
|
+
"name": project_name,
|
|
563
|
+
},
|
|
564
|
+
"plugins": {}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Get all available plugins
|
|
568
|
+
all_plugins = self.config.registry.list_discovered()
|
|
569
|
+
|
|
570
|
+
# Add enabled plugins and disable non-selected ones
|
|
571
|
+
for plugin_name in all_plugins:
|
|
572
|
+
if plugin_name in plugins_to_configure:
|
|
573
|
+
project_config_data["plugins"][plugin_name] = {
|
|
574
|
+
"enabled": True,
|
|
575
|
+
"config": {}
|
|
576
|
+
}
|
|
577
|
+
else:
|
|
578
|
+
# Explicitly disable plugins not selected for this project
|
|
579
|
+
project_config_data["plugins"][plugin_name] = {
|
|
580
|
+
"enabled": False
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Save initial project config
|
|
584
|
+
with open(project_config_path, "wb") as f:
|
|
585
|
+
tomli_w.dump(project_config_data, f)
|
|
586
|
+
|
|
587
|
+
# Update config.project_config_path so plugins can save to it
|
|
588
|
+
self.config.project_config_path = project_config_path
|
|
589
|
+
|
|
590
|
+
except Exception as e:
|
|
591
|
+
self.app.notify(f"Failed to create project config: {e}", severity="error")
|
|
592
|
+
self.load_step(self.current_step + 1)
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
# Store list of plugins to configure
|
|
596
|
+
self.wizard_data["plugins_to_configure"] = list(plugins_to_configure)
|
|
597
|
+
self.wizard_data["current_plugin_index"] = 0
|
|
598
|
+
|
|
599
|
+
# Start configuring first plugin
|
|
600
|
+
self._configure_next_plugin()
|
|
601
|
+
|
|
602
|
+
def _configure_next_plugin(self):
|
|
603
|
+
"""Configure the next plugin in the queue."""
|
|
604
|
+
plugins_to_configure = self.wizard_data.get("plugins_to_configure", [])
|
|
605
|
+
current_index = self.wizard_data.get("current_plugin_index", 0)
|
|
606
|
+
|
|
607
|
+
if current_index >= len(plugins_to_configure):
|
|
608
|
+
# All plugins configured, move to next step
|
|
609
|
+
self.load_step(self.current_step + 1)
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
plugin_name = plugins_to_configure[current_index]
|
|
613
|
+
|
|
614
|
+
# Debug: check registry state before launching wizard
|
|
615
|
+
available_plugins = list(self.config.registry._plugins.keys())
|
|
616
|
+
logger.debug(f"Before launching wizard for '{plugin_name}': Registry has {available_plugins}")
|
|
617
|
+
|
|
618
|
+
def on_plugin_config_complete(_=None):
|
|
619
|
+
"""Callback when plugin configuration completes."""
|
|
620
|
+
# Move to next plugin
|
|
621
|
+
self.wizard_data["current_plugin_index"] = current_index + 1
|
|
622
|
+
self._configure_next_plugin()
|
|
623
|
+
|
|
624
|
+
# Launch plugin configuration wizard
|
|
625
|
+
from .plugin_config_wizard import PluginConfigWizardScreen
|
|
626
|
+
self.app.push_screen(
|
|
627
|
+
PluginConfigWizardScreen(self.config, plugin_name),
|
|
628
|
+
on_plugin_config_complete
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def complete_setup(self) -> None:
|
|
632
|
+
"""Complete the project setup."""
|
|
633
|
+
import tomli_w
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
# .titan directory and config should already exist if plugins were configured
|
|
637
|
+
titan_dir = self.project_path / ".titan"
|
|
638
|
+
titan_dir.mkdir(exist_ok=True)
|
|
639
|
+
|
|
640
|
+
project_config_path = titan_dir / "config.toml"
|
|
641
|
+
project_name = self.wizard_data.get("project_name", self.project_path.name)
|
|
642
|
+
enabled_plugins = self.wizard_data.get("enabled_plugins", [])
|
|
643
|
+
|
|
644
|
+
# If config file already exists (plugins were configured), just verify it
|
|
645
|
+
if project_config_path.exists():
|
|
646
|
+
# Config already created and populated by plugin wizards
|
|
647
|
+
logger.debug(f"Project config already exists at {project_config_path}")
|
|
648
|
+
else:
|
|
649
|
+
# No plugins were configured, create basic config
|
|
650
|
+
project_config_data = {
|
|
651
|
+
"project": {
|
|
652
|
+
"name": project_name,
|
|
653
|
+
},
|
|
654
|
+
"plugins": {}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
# Get all available plugins
|
|
658
|
+
all_plugins = self.config.registry.list_discovered()
|
|
659
|
+
|
|
660
|
+
# Add enabled/disabled plugins
|
|
661
|
+
for plugin_name in all_plugins:
|
|
662
|
+
if plugin_name in enabled_plugins:
|
|
663
|
+
project_config_data["plugins"][plugin_name] = {
|
|
664
|
+
"enabled": True
|
|
665
|
+
}
|
|
666
|
+
else:
|
|
667
|
+
# Explicitly disable plugins not selected
|
|
668
|
+
project_config_data["plugins"][plugin_name] = {
|
|
669
|
+
"enabled": False
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# Save project config
|
|
673
|
+
with open(project_config_path, "wb") as f:
|
|
674
|
+
tomli_w.dump(project_config_data, f)
|
|
675
|
+
|
|
676
|
+
self.app.notify(f"Project '{project_name}' configured successfully!", severity="information")
|
|
677
|
+
|
|
678
|
+
# Close wizard - the callback will handle navigation
|
|
679
|
+
self.dismiss()
|
|
680
|
+
|
|
681
|
+
except Exception as e:
|
|
682
|
+
self.app.notify(f"Failed to complete project setup: {e}", severity="error")
|
|
683
|
+
|
|
684
|
+
def action_cancel(self) -> None:
|
|
685
|
+
"""Cancel the setup wizard."""
|
|
686
|
+
self.app.exit(message="Project setup cancelled. Run Titan again to configure this project.")
|