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,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.")