titan-cli 0.1.4__py3-none-any.whl → 0.1.6__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/core/config.py +3 -1
- titan_cli/core/workflows/__init__.py +2 -1
- titan_cli/core/workflows/project_step_source.py +95 -32
- titan_cli/core/workflows/workflow_filter_service.py +16 -8
- titan_cli/core/workflows/workflow_registry.py +12 -1
- titan_cli/core/workflows/workflow_sources.py +1 -1
- titan_cli/engine/__init__.py +5 -1
- titan_cli/engine/results.py +31 -1
- titan_cli/engine/steps/ai_assistant_step.py +47 -12
- titan_cli/engine/workflow_executor.py +13 -3
- titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
- titan_cli/ui/tui/screens/workflow_execution.py +28 -50
- titan_cli/ui/tui/screens/workflows.py +8 -4
- titan_cli/ui/tui/textual_components.py +342 -185
- titan_cli/ui/tui/textual_workflow_executor.py +39 -3
- titan_cli/ui/tui/theme.py +34 -5
- titan_cli/ui/tui/widgets/__init__.py +17 -0
- titan_cli/ui/tui/widgets/multiline_input.py +32 -0
- titan_cli/ui/tui/widgets/prompt_choice.py +138 -0
- titan_cli/ui/tui/widgets/prompt_input.py +74 -0
- titan_cli/ui/tui/widgets/prompt_selection_list.py +150 -0
- titan_cli/ui/tui/widgets/prompt_textarea.py +87 -0
- titan_cli/ui/tui/widgets/step_container.py +70 -0
- titan_cli/ui/tui/widgets/styled_option_list.py +107 -0
- titan_cli/ui/tui/widgets/text.py +51 -130
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/METADATA +3 -5
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/RECORD +61 -46
- titan_plugin_git/clients/git_client.py +140 -5
- titan_plugin_git/plugin.py +13 -0
- titan_plugin_git/steps/ai_commit_message_step.py +39 -34
- titan_plugin_git/steps/branch_steps.py +18 -37
- titan_plugin_git/steps/checkout_step.py +66 -0
- titan_plugin_git/steps/commit_step.py +18 -22
- titan_plugin_git/steps/create_branch_step.py +131 -0
- titan_plugin_git/steps/diff_summary_step.py +180 -0
- titan_plugin_git/steps/pull_step.py +70 -0
- titan_plugin_git/steps/push_step.py +27 -11
- titan_plugin_git/steps/restore_original_branch_step.py +97 -0
- titan_plugin_git/steps/save_current_branch_step.py +82 -0
- titan_plugin_git/steps/status_step.py +32 -25
- titan_plugin_git/workflows/commit-ai.yaml +9 -3
- titan_plugin_github/agents/pr_agent.py +15 -2
- titan_plugin_github/steps/ai_pr_step.py +99 -40
- titan_plugin_github/steps/create_pr_step.py +18 -8
- titan_plugin_github/steps/github_prompt_steps.py +53 -1
- titan_plugin_github/steps/issue_steps.py +31 -18
- titan_plugin_github/steps/preview_step.py +15 -4
- titan_plugin_github/utils.py +5 -4
- titan_plugin_github/workflows/create-pr-ai.yaml +6 -11
- titan_plugin_jira/messages.py +12 -0
- titan_plugin_jira/plugin.py +4 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +12 -7
- titan_plugin_jira/steps/get_issue_step.py +17 -13
- titan_plugin_jira/steps/list_versions_step.py +133 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +20 -8
- titan_plugin_jira/steps/search_jql_step.py +191 -0
- titan_plugin_jira/steps/search_saved_query_step.py +26 -24
- titan_plugin_jira/utils/__init__.py +1 -1
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/LICENSE +0 -0
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/WHEEL +0 -0
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search JIRA issues using custom JQL query
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from titan_cli.ui.tui.widgets import Table
|
|
7
|
+
from ..exceptions import JiraAPIError
|
|
8
|
+
from ..messages import msg
|
|
9
|
+
from ..utils import IssueSorter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def search_jql_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
13
|
+
"""
|
|
14
|
+
Search JIRA issues using a custom JQL query.
|
|
15
|
+
|
|
16
|
+
This is a generic search step that accepts raw JQL and allows variable substitution.
|
|
17
|
+
Use this when you need a specific query that isn't covered by saved queries.
|
|
18
|
+
|
|
19
|
+
Inputs (from ctx.data):
|
|
20
|
+
jql (str): JQL query string (supports variable substitution with ${var_name})
|
|
21
|
+
max_results (int, optional): Maximum number of results (default: 100)
|
|
22
|
+
|
|
23
|
+
Outputs (saved to ctx.data):
|
|
24
|
+
jira_issues (list): List of JiraTicket objects
|
|
25
|
+
jira_issue_count (int): Number of issues found
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Success: Issues found
|
|
29
|
+
Error: Search failed or JQL not provided
|
|
30
|
+
|
|
31
|
+
Variable substitution:
|
|
32
|
+
You can use ${variable_name} in the JQL and it will be replaced with values from ctx.data.
|
|
33
|
+
Example: "project = ${project_key} AND fixVersion = ${fix_version}"
|
|
34
|
+
|
|
35
|
+
Example usage in workflow:
|
|
36
|
+
```yaml
|
|
37
|
+
- id: search_issues
|
|
38
|
+
plugin: jira
|
|
39
|
+
step: search_jql
|
|
40
|
+
params:
|
|
41
|
+
jql: "project = MYPROJECT AND status = 'In Progress' ORDER BY created DESC"
|
|
42
|
+
max_results: 50
|
|
43
|
+
|
|
44
|
+
# With variable substitution:
|
|
45
|
+
- id: search_release
|
|
46
|
+
plugin: jira
|
|
47
|
+
step: search_jql
|
|
48
|
+
params:
|
|
49
|
+
jql: "project = ${project_key} AND fixVersion = ${fix_version} ORDER BY created DESC"
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
if not ctx.textual:
|
|
53
|
+
return Error("Textual UI context is not available for this step.")
|
|
54
|
+
|
|
55
|
+
# Begin step container
|
|
56
|
+
ctx.textual.begin_step("Search JIRA Issues")
|
|
57
|
+
|
|
58
|
+
if not ctx.jira:
|
|
59
|
+
ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
60
|
+
ctx.textual.end_step("error")
|
|
61
|
+
return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
62
|
+
|
|
63
|
+
# Get JQL query
|
|
64
|
+
jql = ctx.get("jql")
|
|
65
|
+
if not jql:
|
|
66
|
+
error_msg = "JQL query is required but not provided"
|
|
67
|
+
ctx.textual.error_text(error_msg)
|
|
68
|
+
ctx.textual.dim_text("Provide 'jql' parameter in workflow step")
|
|
69
|
+
ctx.textual.end_step("error")
|
|
70
|
+
return Error(error_msg)
|
|
71
|
+
|
|
72
|
+
# Perform variable substitution: replace ${var} with ctx.data values
|
|
73
|
+
# This allows dynamic queries like "project = ${project_key}"
|
|
74
|
+
import re
|
|
75
|
+
|
|
76
|
+
def replace_var(match):
|
|
77
|
+
var_name = match.group(1)
|
|
78
|
+
value = ctx.get(var_name)
|
|
79
|
+
if value is None:
|
|
80
|
+
# Variable not found in context, keep original placeholder
|
|
81
|
+
return match.group(0)
|
|
82
|
+
return str(value)
|
|
83
|
+
|
|
84
|
+
# Replace ${variable} patterns
|
|
85
|
+
jql = re.sub(r'\$\{([^}]+)\}', replace_var, jql)
|
|
86
|
+
|
|
87
|
+
# Show which query is being executed
|
|
88
|
+
ctx.textual.text("")
|
|
89
|
+
ctx.textual.bold_text("Executing JQL Query:")
|
|
90
|
+
ctx.textual.dim_text(f" {jql}")
|
|
91
|
+
ctx.textual.text("")
|
|
92
|
+
|
|
93
|
+
# Get max results
|
|
94
|
+
max_results = ctx.get("max_results", 100)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Execute search with loading indicator
|
|
98
|
+
# Request ALL fields including custom fields
|
|
99
|
+
with ctx.textual.loading("Searching JIRA issues..."):
|
|
100
|
+
issues = ctx.jira.search_tickets(jql=jql, max_results=max_results, fields=["*all"])
|
|
101
|
+
|
|
102
|
+
if not issues:
|
|
103
|
+
ctx.textual.dim_text("No issues found")
|
|
104
|
+
ctx.textual.end_step("success")
|
|
105
|
+
return Success(
|
|
106
|
+
"No issues found",
|
|
107
|
+
metadata={
|
|
108
|
+
"jira_issues": [],
|
|
109
|
+
"issues": [], # Alias for compatibility
|
|
110
|
+
"jira_issue_count": 0
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Show results
|
|
115
|
+
ctx.textual.text("") # spacing
|
|
116
|
+
ctx.textual.success_text(f"Found {len(issues)} issues")
|
|
117
|
+
ctx.textual.text("")
|
|
118
|
+
|
|
119
|
+
# Show detailed table
|
|
120
|
+
ctx.textual.bold_text("Found Issues:")
|
|
121
|
+
ctx.textual.text("")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# Sort issues intelligently
|
|
125
|
+
sorter = IssueSorter()
|
|
126
|
+
sorted_issues = sorter.sort(issues)
|
|
127
|
+
|
|
128
|
+
# Prepare table data with row numbers for selection
|
|
129
|
+
headers = ["#", "Key", "Status", "Summary", "Assignee", "Type", "Priority"]
|
|
130
|
+
rows = []
|
|
131
|
+
for i, issue in enumerate(sorted_issues, 1):
|
|
132
|
+
assignee = issue.assignee or "Unassigned"
|
|
133
|
+
status = issue.status or "Unknown"
|
|
134
|
+
priority = issue.priority or "Unknown"
|
|
135
|
+
issue_type = issue.issue_type or "Unknown"
|
|
136
|
+
summary = (issue.summary or "No summary")[:60]
|
|
137
|
+
|
|
138
|
+
rows.append([
|
|
139
|
+
str(i),
|
|
140
|
+
issue.key,
|
|
141
|
+
status,
|
|
142
|
+
summary,
|
|
143
|
+
assignee,
|
|
144
|
+
issue_type,
|
|
145
|
+
priority
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
# Render table using textual widget
|
|
149
|
+
ctx.textual.mount(
|
|
150
|
+
Table(
|
|
151
|
+
headers=headers,
|
|
152
|
+
rows=rows,
|
|
153
|
+
title=f"Issues (sorted by {sorter.get_sort_description()})"
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Use sorted issues for downstream steps
|
|
158
|
+
issues = sorted_issues
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# If table rendering fails, show error but continue with raw issue list
|
|
161
|
+
ctx.textual.error_text(f"Error rendering table: {e}")
|
|
162
|
+
ctx.textual.primary_text(f"Found {len(issues)} issues (showing raw data)")
|
|
163
|
+
for i, issue in enumerate(issues, 1):
|
|
164
|
+
ctx.textual.text(f"{i}. {issue.key} - {getattr(issue, 'summary', 'N/A')}")
|
|
165
|
+
ctx.textual.text("")
|
|
166
|
+
|
|
167
|
+
ctx.textual.end_step("success")
|
|
168
|
+
return Success(
|
|
169
|
+
f"Found {len(issues)} issues",
|
|
170
|
+
metadata={
|
|
171
|
+
"jira_issues": issues,
|
|
172
|
+
"issues": issues, # Alias for compatibility
|
|
173
|
+
"jira_issue_count": len(issues)
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
except JiraAPIError as e:
|
|
178
|
+
error_msg = f"JIRA search failed: {e}"
|
|
179
|
+
ctx.textual.error_text(error_msg)
|
|
180
|
+
ctx.textual.end_step("error")
|
|
181
|
+
return Error(error_msg)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
import traceback
|
|
184
|
+
error_detail = traceback.format_exc()
|
|
185
|
+
error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
|
|
186
|
+
ctx.textual.error_text(error_msg)
|
|
187
|
+
ctx.textual.end_step("error")
|
|
188
|
+
return Error(error_msg)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["search_jql_step"]
|
|
@@ -3,7 +3,7 @@ Search JIRA issues using saved query from utils registry
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
-
from titan_cli.ui.tui.widgets import
|
|
6
|
+
from titan_cli.ui.tui.widgets import Table
|
|
7
7
|
from ..exceptions import JiraAPIError
|
|
8
8
|
from ..messages import msg
|
|
9
9
|
from ..utils import SAVED_QUERIES, IssueSorter
|
|
@@ -57,14 +57,19 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
57
57
|
if not ctx.textual:
|
|
58
58
|
return Error("Textual UI context is not available for this step.")
|
|
59
59
|
|
|
60
|
+
# Begin step container
|
|
61
|
+
ctx.textual.begin_step("Search Open Issues")
|
|
62
|
+
|
|
60
63
|
if not ctx.jira:
|
|
61
|
-
ctx.textual.
|
|
64
|
+
ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
65
|
+
ctx.textual.end_step("error")
|
|
62
66
|
return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
63
67
|
|
|
64
68
|
# Get query name
|
|
65
69
|
query_name = ctx.get("query_name")
|
|
66
70
|
if not query_name:
|
|
67
|
-
ctx.textual.
|
|
71
|
+
ctx.textual.error_text(msg.Steps.Search.QUERY_NAME_REQUIRED)
|
|
72
|
+
ctx.textual.end_step("error")
|
|
68
73
|
return Error(msg.Steps.Search.QUERY_NAME_REQUIRED)
|
|
69
74
|
|
|
70
75
|
# Get all predefined queries from utils
|
|
@@ -102,7 +107,8 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
102
107
|
error_msg += "\n\n" + msg.Steps.Search.ADD_CUSTOM_HINT + "\n"
|
|
103
108
|
error_msg += msg.Steps.Search.CUSTOM_QUERY_EXAMPLE
|
|
104
109
|
|
|
105
|
-
ctx.textual.
|
|
110
|
+
ctx.textual.error_text(error_msg)
|
|
111
|
+
ctx.textual.end_step("error")
|
|
106
112
|
return Error(error_msg)
|
|
107
113
|
|
|
108
114
|
jql = all_queries[query_name]
|
|
@@ -119,7 +125,8 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
119
125
|
|
|
120
126
|
if not project:
|
|
121
127
|
error_msg = msg.Steps.Search.PROJECT_REQUIRED.format(query_name=query_name, jql=jql)
|
|
122
|
-
ctx.textual.
|
|
128
|
+
ctx.textual.error_text(error_msg)
|
|
129
|
+
ctx.textual.end_step("error")
|
|
123
130
|
return Error(error_msg)
|
|
124
131
|
|
|
125
132
|
jql = jql.format(project=project)
|
|
@@ -129,8 +136,8 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
129
136
|
source_label = "Custom" if is_custom else "Predefined"
|
|
130
137
|
|
|
131
138
|
ctx.textual.text("")
|
|
132
|
-
ctx.textual.
|
|
133
|
-
ctx.textual.
|
|
139
|
+
ctx.textual.bold_text(f"Using {source_label} Query: {query_name}")
|
|
140
|
+
ctx.textual.dim_text(f" JQL: {jql}")
|
|
134
141
|
ctx.textual.text("")
|
|
135
142
|
|
|
136
143
|
# Get max results
|
|
@@ -142,12 +149,8 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
142
149
|
issues = ctx.jira.search_tickets(jql=jql, max_results=max_results)
|
|
143
150
|
|
|
144
151
|
if not issues:
|
|
145
|
-
ctx.textual.
|
|
146
|
-
|
|
147
|
-
text=f"No issues found for query: {query_name}",
|
|
148
|
-
panel_type="info"
|
|
149
|
-
)
|
|
150
|
-
)
|
|
152
|
+
ctx.textual.dim_text(f"No issues found for query: {query_name}")
|
|
153
|
+
ctx.textual.end_step("success")
|
|
151
154
|
return Success(
|
|
152
155
|
"No issues found",
|
|
153
156
|
metadata={
|
|
@@ -158,16 +161,12 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
158
161
|
)
|
|
159
162
|
|
|
160
163
|
# Show results
|
|
161
|
-
ctx.textual.
|
|
162
|
-
|
|
163
|
-
text=f"Found {len(issues)} issues",
|
|
164
|
-
panel_type="success"
|
|
165
|
-
)
|
|
166
|
-
)
|
|
164
|
+
ctx.textual.text("") # spacing
|
|
165
|
+
ctx.textual.success_text(f"Found {len(issues)} issues")
|
|
167
166
|
ctx.textual.text("")
|
|
168
167
|
|
|
169
168
|
# Show detailed table
|
|
170
|
-
ctx.textual.
|
|
169
|
+
ctx.textual.bold_text("Found Issues:")
|
|
171
170
|
ctx.textual.text("")
|
|
172
171
|
|
|
173
172
|
try:
|
|
@@ -208,12 +207,13 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
208
207
|
issues = sorted_issues
|
|
209
208
|
except Exception as e:
|
|
210
209
|
# If table rendering fails, show error but continue with raw issue list
|
|
211
|
-
ctx.textual.
|
|
212
|
-
ctx.textual.
|
|
210
|
+
ctx.textual.error_text(f"Error rendering table: {e}")
|
|
211
|
+
ctx.textual.primary_text(f"Found {len(issues)} issues (showing raw data)")
|
|
213
212
|
for i, issue in enumerate(issues, 1):
|
|
214
213
|
ctx.textual.text(f"{i}. {issue.key} - {getattr(issue, 'summary', 'N/A')}")
|
|
215
214
|
ctx.textual.text("")
|
|
216
215
|
|
|
216
|
+
ctx.textual.end_step("success")
|
|
217
217
|
return Success(
|
|
218
218
|
f"Found {len(issues)} issues using query: {query_name}",
|
|
219
219
|
metadata={
|
|
@@ -225,13 +225,15 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
225
225
|
|
|
226
226
|
except JiraAPIError as e:
|
|
227
227
|
error_msg = f"JIRA search failed: {e}"
|
|
228
|
-
ctx.textual.
|
|
228
|
+
ctx.textual.error_text(error_msg)
|
|
229
|
+
ctx.textual.end_step("error")
|
|
229
230
|
return Error(error_msg)
|
|
230
231
|
except Exception as e:
|
|
231
232
|
import traceback
|
|
232
233
|
error_detail = traceback.format_exc()
|
|
233
234
|
error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
|
|
234
|
-
ctx.textual.
|
|
235
|
+
ctx.textual.error_text(error_msg)
|
|
236
|
+
ctx.textual.end_step("error")
|
|
235
237
|
return Error(error_msg)
|
|
236
238
|
|
|
237
239
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|