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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
from titan_cli.engine.context import WorkflowContext
|
|
3
3
|
from titan_cli.engine.results import WorkflowResult, Success, Error, Skip
|
|
4
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
5
4
|
from ..agents.issue_generator import IssueGeneratorAgent
|
|
5
|
+
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
8
8
|
"""
|
|
@@ -12,15 +12,21 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
12
12
|
if not ctx.textual:
|
|
13
13
|
return Error("Textual UI context is not available for this step.")
|
|
14
14
|
|
|
15
|
+
# Begin step container
|
|
16
|
+
ctx.textual.begin_step("Categorize and Generate Issue")
|
|
17
|
+
|
|
15
18
|
if not ctx.ai:
|
|
19
|
+
ctx.textual.dim_text("AI client not available")
|
|
20
|
+
ctx.textual.end_step("skip")
|
|
16
21
|
return Skip("AI client not available")
|
|
17
22
|
|
|
18
23
|
issue_body_prompt = ctx.get("issue_body")
|
|
19
24
|
if not issue_body_prompt:
|
|
25
|
+
ctx.textual.error_text("issue_body not found in context")
|
|
26
|
+
ctx.textual.end_step("error")
|
|
20
27
|
return Error("issue_body not found in context")
|
|
21
28
|
|
|
22
|
-
ctx.textual.
|
|
23
|
-
|
|
29
|
+
ctx.textual.dim_text("Using AI to categorize and generate issue...")
|
|
24
30
|
try:
|
|
25
31
|
# Get available labels from repository for smart mapping
|
|
26
32
|
available_labels = None
|
|
@@ -34,7 +40,6 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
34
40
|
# Get template directory from repo path
|
|
35
41
|
template_dir = None
|
|
36
42
|
if ctx.git:
|
|
37
|
-
from pathlib import Path
|
|
38
43
|
template_dir = Path(ctx.git.repo_path) / ".github" / "ISSUE_TEMPLATE"
|
|
39
44
|
|
|
40
45
|
issue_generator = IssueGeneratorAgent(ctx.ai, template_dir=template_dir)
|
|
@@ -46,25 +51,24 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
46
51
|
category = result["category"]
|
|
47
52
|
template_used = result.get("template_used", False)
|
|
48
53
|
|
|
49
|
-
ctx.textual.
|
|
50
|
-
|
|
51
|
-
text=f"Category detected: {category}",
|
|
52
|
-
panel_type="success"
|
|
53
|
-
)
|
|
54
|
-
)
|
|
54
|
+
ctx.textual.text("") # spacing
|
|
55
|
+
ctx.textual.success_text(f"Category detected: {category}")
|
|
55
56
|
|
|
56
57
|
if template_used:
|
|
57
|
-
ctx.textual.
|
|
58
|
+
ctx.textual.success_text(f"Using template: {category}.md")
|
|
58
59
|
else:
|
|
59
|
-
ctx.textual.
|
|
60
|
+
ctx.textual.warning_text(f"No template found for {category}, using default structure")
|
|
60
61
|
|
|
61
62
|
ctx.set("issue_title", result["title"])
|
|
62
63
|
ctx.set("issue_body", result["body"])
|
|
63
64
|
ctx.set("issue_category", category)
|
|
64
65
|
ctx.set("labels", result["labels"])
|
|
65
66
|
|
|
67
|
+
ctx.textual.end_step("success")
|
|
66
68
|
return Success(f"AI-generated issue ({category}) created successfully")
|
|
67
69
|
except Exception as e:
|
|
70
|
+
ctx.textual.error_text(f"Failed to generate issue: {e}")
|
|
71
|
+
ctx.textual.end_step("error")
|
|
68
72
|
return Error(f"Failed to generate issue: {e}")
|
|
69
73
|
|
|
70
74
|
|
|
@@ -75,7 +79,12 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
75
79
|
if not ctx.textual:
|
|
76
80
|
return Error("Textual UI context is not available for this step.")
|
|
77
81
|
|
|
82
|
+
# Begin step container
|
|
83
|
+
ctx.textual.begin_step("Create Issue")
|
|
84
|
+
|
|
78
85
|
if not ctx.github:
|
|
86
|
+
ctx.textual.error_text("GitHub client not available")
|
|
87
|
+
ctx.textual.end_step("error")
|
|
79
88
|
return Error("GitHub client not available")
|
|
80
89
|
|
|
81
90
|
issue_title = ctx.get("issue_title")
|
|
@@ -103,9 +112,13 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
103
112
|
labels = []
|
|
104
113
|
|
|
105
114
|
if not issue_title:
|
|
115
|
+
ctx.textual.error_text("issue_title not found in context")
|
|
116
|
+
ctx.textual.end_step("error")
|
|
106
117
|
return Error("issue_title not found in context")
|
|
107
118
|
|
|
108
119
|
if not issue_body:
|
|
120
|
+
ctx.textual.error_text("issue_body not found in context")
|
|
121
|
+
ctx.textual.end_step("error")
|
|
109
122
|
return Error("issue_body not found in context")
|
|
110
123
|
|
|
111
124
|
# Filter labels to only those that exist in the repository
|
|
@@ -123,21 +136,21 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
123
136
|
pass
|
|
124
137
|
|
|
125
138
|
try:
|
|
139
|
+
ctx.textual.dim_text(f"Creating issue: {issue_title}...")
|
|
126
140
|
issue = ctx.github.create_issue(
|
|
127
141
|
title=issue_title,
|
|
128
142
|
body=issue_body,
|
|
129
143
|
assignees=assignees,
|
|
130
144
|
labels=labels,
|
|
131
145
|
)
|
|
132
|
-
ctx.textual.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
panel_type="success"
|
|
136
|
-
)
|
|
137
|
-
)
|
|
146
|
+
ctx.textual.text("") # spacing
|
|
147
|
+
ctx.textual.success_text(f"Successfully created issue #{issue.number}")
|
|
148
|
+
ctx.textual.end_step("success")
|
|
138
149
|
return Success(
|
|
139
150
|
f"Successfully created issue #{issue.number}",
|
|
140
151
|
metadata={"issue": issue}
|
|
141
152
|
)
|
|
142
153
|
except Exception as e:
|
|
154
|
+
ctx.textual.error_text(f"Failed to create issue: {e}")
|
|
155
|
+
ctx.textual.end_step("error")
|
|
143
156
|
return Error(f"Failed to create issue: {e}")
|
|
@@ -8,24 +8,29 @@ def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
8
8
|
if not ctx.textual:
|
|
9
9
|
return Error("Textual UI context is not available for this step.")
|
|
10
10
|
|
|
11
|
+
# Begin step container
|
|
12
|
+
ctx.textual.begin_step("Preview and Confirm Issue")
|
|
13
|
+
|
|
11
14
|
issue_title = ctx.get("issue_title")
|
|
12
15
|
issue_body = ctx.get("issue_body")
|
|
13
16
|
|
|
14
17
|
if not issue_title or not issue_body:
|
|
18
|
+
ctx.textual.error_text("issue_title or issue_body not found in context")
|
|
19
|
+
ctx.textual.end_step("error")
|
|
15
20
|
return Error("issue_title or issue_body not found in context")
|
|
16
21
|
|
|
17
22
|
# Show preview header
|
|
18
23
|
ctx.textual.text("") # spacing
|
|
19
|
-
ctx.textual.
|
|
24
|
+
ctx.textual.bold_text("AI-Generated Issue Preview")
|
|
20
25
|
ctx.textual.text("") # spacing
|
|
21
26
|
|
|
22
27
|
# Show title
|
|
23
|
-
ctx.textual.
|
|
24
|
-
ctx.textual.
|
|
28
|
+
ctx.textual.bold_text("Title:")
|
|
29
|
+
ctx.textual.primary_text(f" {issue_title}")
|
|
25
30
|
ctx.textual.text("") # spacing
|
|
26
31
|
|
|
27
32
|
# Show description
|
|
28
|
-
ctx.textual.
|
|
33
|
+
ctx.textual.bold_text("Description:")
|
|
29
34
|
# Render markdown in a scrollable container
|
|
30
35
|
ctx.textual.markdown(issue_body)
|
|
31
36
|
|
|
@@ -33,8 +38,14 @@ def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
33
38
|
|
|
34
39
|
try:
|
|
35
40
|
if not ctx.textual.ask_confirm("Use this AI-generated issue?", default=True):
|
|
41
|
+
ctx.textual.warning_text("User rejected AI-generated issue")
|
|
42
|
+
ctx.textual.end_step("error")
|
|
36
43
|
return Error("User rejected AI-generated issue")
|
|
37
44
|
except (KeyboardInterrupt, EOFError):
|
|
45
|
+
ctx.textual.error_text("User cancelled operation")
|
|
46
|
+
ctx.textual.end_step("error")
|
|
38
47
|
return Error("User cancelled operation")
|
|
39
48
|
|
|
49
|
+
ctx.textual.success_text("User confirmed AI-generated issue")
|
|
50
|
+
ctx.textual.end_step("success")
|
|
40
51
|
return Success("User confirmed AI-generated issue")
|
titan_plugin_github/utils.py
CHANGED
|
@@ -54,21 +54,22 @@ def calculate_pr_size(diff: str) -> PRSizeEstimation:
|
|
|
54
54
|
files_changed = len(re.findall(file_pattern, diff, re.MULTILINE))
|
|
55
55
|
|
|
56
56
|
# Dynamic character limit based on PR size
|
|
57
|
+
# Increased limits to accommodate PR templates with images/GIFs
|
|
57
58
|
if files_changed <= 3 and diff_lines < 100:
|
|
58
59
|
# Small PR: bug fix, doc update, small feature
|
|
59
|
-
max_chars =
|
|
60
|
+
max_chars = 1500
|
|
60
61
|
pr_size = "small"
|
|
61
62
|
elif files_changed <= 10 and diff_lines < 500:
|
|
62
63
|
# Medium PR: feature, moderate refactor
|
|
63
|
-
max_chars =
|
|
64
|
+
max_chars = 2500
|
|
64
65
|
pr_size = "medium"
|
|
65
66
|
elif files_changed <= 30 and diff_lines < 2000:
|
|
66
67
|
# Large PR: architectural changes, new modules
|
|
67
|
-
max_chars =
|
|
68
|
+
max_chars = 4000
|
|
68
69
|
pr_size = "large"
|
|
69
70
|
else:
|
|
70
71
|
# Very large PR: major refactor, breaking changes
|
|
71
|
-
max_chars =
|
|
72
|
+
max_chars = 6000
|
|
72
73
|
pr_size = "very large"
|
|
73
74
|
|
|
74
75
|
return PRSizeEstimation(
|
|
@@ -19,7 +19,13 @@ steps:
|
|
|
19
19
|
plugin: git
|
|
20
20
|
step: get_current_branch
|
|
21
21
|
|
|
22
|
+
- id: branch_diff_summary
|
|
23
|
+
name: "Show Branch Changes Summary"
|
|
24
|
+
plugin: git
|
|
25
|
+
step: show_branch_diff_summary
|
|
26
|
+
|
|
22
27
|
# AI generates PR title and description from branch commits
|
|
28
|
+
# User can choose to use as-is, edit, or reject
|
|
23
29
|
- id: ai_pr_description
|
|
24
30
|
name: "AI PR Description"
|
|
25
31
|
plugin: github
|
|
@@ -27,17 +33,6 @@ steps:
|
|
|
27
33
|
|
|
28
34
|
- hook: before_push
|
|
29
35
|
|
|
30
|
-
# Fallback to manual prompts if AI was rejected
|
|
31
|
-
- id: prompt_pr_title
|
|
32
|
-
name: "Prompt for PR Title"
|
|
33
|
-
plugin: github
|
|
34
|
-
step: prompt_for_pr_title
|
|
35
|
-
|
|
36
|
-
- id: prompt_pr_body
|
|
37
|
-
name: "Prompt for PR Body"
|
|
38
|
-
plugin: github
|
|
39
|
-
step: prompt_for_pr_body
|
|
40
|
-
|
|
41
36
|
- id: create_pr
|
|
42
37
|
name: "Create Pull Request"
|
|
43
38
|
plugin: github
|
titan_plugin_jira/messages.py
CHANGED
|
@@ -101,6 +101,18 @@ class Messages:
|
|
|
101
101
|
ISSUE_SELECTION_CONFIRM: str = "Selected: {key} - {summary}"
|
|
102
102
|
SELECT_SUCCESS: str = "Selected issue: {key}"
|
|
103
103
|
|
|
104
|
+
class ReleaseNotes:
|
|
105
|
+
"""Release notes generation step messages"""
|
|
106
|
+
AI_NOT_AVAILABLE: str = "AI not available, using original JIRA summaries"
|
|
107
|
+
AI_GENERATION_FAILED: str = "AI generation failed: {error}"
|
|
108
|
+
USING_FALLBACK_SUMMARIES: str = "Using original JIRA summaries as fallback"
|
|
109
|
+
PROCESSING_ISSUES: str = "Processing {count} issues for version {version}"
|
|
110
|
+
GROUPED_BRANDS: str = "Grouped into {count} brands"
|
|
111
|
+
GENERATING_AI_DESCRIPTIONS: str = "Generating AI descriptions..."
|
|
112
|
+
SUCCESS: str = "Release notes generated successfully!"
|
|
113
|
+
COPY_INSTRUCTIONS: str = "Copy the markdown above and paste it into your release notes"
|
|
114
|
+
NO_ISSUES_FOUND: str = "No JIRA issues found to generate release notes"
|
|
115
|
+
|
|
104
116
|
class JIRA:
|
|
105
117
|
"""JIRA-specific messages"""
|
|
106
118
|
AUTHENTICATION_FAILED: str = "JIRA authentication failed. Check your API token."
|
titan_plugin_jira/plugin.py
CHANGED
|
@@ -243,14 +243,18 @@ class JiraPlugin(TitanPlugin):
|
|
|
243
243
|
Returns a dictionary of available workflow steps.
|
|
244
244
|
"""
|
|
245
245
|
from .steps.search_saved_query_step import search_saved_query_step
|
|
246
|
+
from .steps.search_jql_step import search_jql_step
|
|
246
247
|
from .steps.prompt_select_issue_step import prompt_select_issue_step
|
|
247
248
|
from .steps.get_issue_step import get_issue_step
|
|
248
249
|
from .steps.ai_analyze_issue_step import ai_analyze_issue_requirements_step
|
|
250
|
+
from .steps.list_versions_step import list_versions_step
|
|
249
251
|
return {
|
|
250
252
|
"search_saved_query": search_saved_query_step,
|
|
253
|
+
"search_jql": search_jql_step,
|
|
251
254
|
"prompt_select_issue": prompt_select_issue_step,
|
|
252
255
|
"get_issue": get_issue_step,
|
|
253
256
|
"ai_analyze_issue_requirements": ai_analyze_issue_requirements_step,
|
|
257
|
+
"list_versions": list_versions_step,
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
@property
|
|
@@ -3,7 +3,6 @@ AI-powered JIRA issue analysis step
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
|
|
6
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
7
6
|
from ..messages import msg
|
|
8
7
|
from ..agents import JiraAgent
|
|
9
8
|
from ..formatters import IssueAnalysisMarkdownFormatter
|
|
@@ -32,15 +31,20 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
32
31
|
if not ctx.textual:
|
|
33
32
|
return Error("Textual UI context is not available for this step.")
|
|
34
33
|
|
|
34
|
+
# Begin step container
|
|
35
|
+
ctx.textual.begin_step("AI Analyze Issue")
|
|
36
|
+
|
|
35
37
|
# Check if AI is available
|
|
36
38
|
if not ctx.ai or not ctx.ai.is_available():
|
|
37
|
-
ctx.textual.
|
|
39
|
+
ctx.textual.dim_text(msg.Steps.AIIssue.AI_NOT_CONFIGURED_SKIP)
|
|
40
|
+
ctx.textual.end_step("skip")
|
|
38
41
|
return Skip(msg.Steps.AIIssue.AI_NOT_CONFIGURED)
|
|
39
42
|
|
|
40
43
|
# Get issue to analyze
|
|
41
44
|
issue = ctx.get("jira_issue") or ctx.get("selected_issue")
|
|
42
45
|
if not issue:
|
|
43
|
-
ctx.textual.
|
|
46
|
+
ctx.textual.error_text(msg.Steps.AIIssue.NO_ISSUE_FOUND)
|
|
47
|
+
ctx.textual.end_step("error")
|
|
44
48
|
return Error(msg.Steps.AIIssue.NO_ISSUE_FOUND)
|
|
45
49
|
|
|
46
50
|
# Create JiraAgent instance and analyze issue with loading indicator
|
|
@@ -61,12 +65,12 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
61
65
|
|
|
62
66
|
# Display analysis
|
|
63
67
|
ctx.textual.text("")
|
|
64
|
-
ctx.textual.
|
|
68
|
+
ctx.textual.bold_primary_text("AI Analysis Results")
|
|
65
69
|
ctx.textual.text("")
|
|
66
70
|
|
|
67
71
|
# Show issue header
|
|
68
|
-
ctx.textual.
|
|
69
|
-
ctx.textual.
|
|
72
|
+
ctx.textual.bold_text(f"{issue.key}: {issue.summary}")
|
|
73
|
+
ctx.textual.dim_text(f"Type: {issue.issue_type} | Status: {issue.status} | Priority: {issue.priority}")
|
|
70
74
|
ctx.textual.text("")
|
|
71
75
|
|
|
72
76
|
# Show AI analysis as markdown
|
|
@@ -74,7 +78,7 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
74
78
|
|
|
75
79
|
# Show token usage
|
|
76
80
|
if analysis.total_tokens_used > 0:
|
|
77
|
-
ctx.textual.
|
|
81
|
+
ctx.textual.dim_text(f"Tokens used: {analysis.total_tokens_used}")
|
|
78
82
|
|
|
79
83
|
# Save structured analysis to context
|
|
80
84
|
ctx.set("ai_analysis_structured", {
|
|
@@ -90,6 +94,7 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
90
94
|
"estimated_effort": analysis.estimated_effort
|
|
91
95
|
})
|
|
92
96
|
|
|
97
|
+
ctx.textual.end_step("success")
|
|
93
98
|
return Success(
|
|
94
99
|
"AI analysis completed",
|
|
95
100
|
metadata={
|
|
@@ -3,7 +3,6 @@ Get JIRA issue details step
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
7
6
|
from ..exceptions import JiraAPIError
|
|
8
7
|
from ..messages import msg
|
|
9
8
|
|
|
@@ -26,15 +25,20 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
26
25
|
if not ctx.textual:
|
|
27
26
|
return Error("Textual UI context is not available for this step.")
|
|
28
27
|
|
|
28
|
+
# Begin step container
|
|
29
|
+
ctx.textual.begin_step("Get Full Issue Details")
|
|
30
|
+
|
|
29
31
|
# Check if JIRA client is available
|
|
30
32
|
if not ctx.jira:
|
|
31
|
-
ctx.textual.
|
|
33
|
+
ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
34
|
+
ctx.textual.end_step("error")
|
|
32
35
|
return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
33
36
|
|
|
34
37
|
# Get issue key
|
|
35
38
|
issue_key = ctx.get("jira_issue_key")
|
|
36
39
|
if not issue_key:
|
|
37
|
-
ctx.textual.
|
|
40
|
+
ctx.textual.error_text("JIRA issue key is required")
|
|
41
|
+
ctx.textual.end_step("error")
|
|
38
42
|
return Error("JIRA issue key is required")
|
|
39
43
|
|
|
40
44
|
# Get optional expand fields
|
|
@@ -46,20 +50,17 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
46
50
|
issue = ctx.jira.get_ticket(ticket_key=issue_key, expand=expand)
|
|
47
51
|
|
|
48
52
|
# Show success
|
|
49
|
-
ctx.textual.
|
|
50
|
-
|
|
51
|
-
text=msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
|
|
52
|
-
panel_type="success"
|
|
53
|
-
)
|
|
54
|
-
)
|
|
53
|
+
ctx.textual.text("") # spacing
|
|
54
|
+
ctx.textual.success_text(msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key))
|
|
55
55
|
|
|
56
56
|
# Show issue details
|
|
57
|
-
ctx.textual.
|
|
57
|
+
ctx.textual.primary_text(f" Title: {issue.summary}")
|
|
58
58
|
ctx.textual.text(f" Status: {issue.status}")
|
|
59
59
|
ctx.textual.text(f" Type: {issue.issue_type}")
|
|
60
60
|
ctx.textual.text(f" Assignee: {issue.assignee or 'Unassigned'}")
|
|
61
61
|
ctx.textual.text("")
|
|
62
62
|
|
|
63
|
+
ctx.textual.end_step("success")
|
|
63
64
|
return Success(
|
|
64
65
|
msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
|
|
65
66
|
metadata={"jira_issue": issue}
|
|
@@ -68,14 +69,17 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
68
69
|
except JiraAPIError as e:
|
|
69
70
|
if e.status_code == 404:
|
|
70
71
|
error_msg = msg.Steps.GetIssue.ISSUE_NOT_FOUND.format(issue_key=issue_key)
|
|
71
|
-
ctx.textual.
|
|
72
|
+
ctx.textual.error_text(error_msg)
|
|
73
|
+
ctx.textual.end_step("error")
|
|
72
74
|
return Error(error_msg)
|
|
73
75
|
error_msg = msg.Steps.GetIssue.GET_FAILED.format(e=e)
|
|
74
|
-
ctx.textual.
|
|
76
|
+
ctx.textual.error_text(error_msg)
|
|
77
|
+
ctx.textual.end_step("error")
|
|
75
78
|
return Error(error_msg)
|
|
76
79
|
except Exception as e:
|
|
77
80
|
error_msg = f"Unexpected error getting issue: {e}"
|
|
78
|
-
ctx.textual.
|
|
81
|
+
ctx.textual.error_text(error_msg)
|
|
82
|
+
ctx.textual.end_step("error")
|
|
79
83
|
return Error(error_msg)
|
|
80
84
|
|
|
81
85
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
List available versions (fixVersions) for a JIRA project
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from ..exceptions import JiraAPIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def list_versions_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
10
|
+
"""
|
|
11
|
+
List unreleased versions for a JIRA project.
|
|
12
|
+
|
|
13
|
+
Filters and returns only versions that are not yet released,
|
|
14
|
+
sorted by name in descending order (most recent first).
|
|
15
|
+
|
|
16
|
+
Inputs (from ctx.data):
|
|
17
|
+
project_key (str, optional): Project key. If not provided, uses default_project from JIRA plugin config.
|
|
18
|
+
|
|
19
|
+
Outputs (saved to ctx.data):
|
|
20
|
+
versions (list): List of unreleased version names
|
|
21
|
+
versions_full (list): List of full unreleased version objects
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Success: Unreleased versions listed
|
|
25
|
+
Error: Failed to fetch versions or project_key not configured
|
|
26
|
+
|
|
27
|
+
Example usage in workflow:
|
|
28
|
+
```yaml
|
|
29
|
+
- id: list_versions
|
|
30
|
+
plugin: jira
|
|
31
|
+
step: list_versions
|
|
32
|
+
params:
|
|
33
|
+
project_key: "MYPROJECT"
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
if not ctx.textual:
|
|
37
|
+
return Error("Textual UI context is not available for this step.")
|
|
38
|
+
|
|
39
|
+
if not ctx.jira:
|
|
40
|
+
return Error("JIRA client not available in context")
|
|
41
|
+
|
|
42
|
+
# Begin step container
|
|
43
|
+
ctx.textual.begin_step("List Project Versions")
|
|
44
|
+
|
|
45
|
+
# Get project key from context or fall back to default_project from JIRA client
|
|
46
|
+
project_key = ctx.get("project_key")
|
|
47
|
+
if not project_key:
|
|
48
|
+
# Try to use default project from JIRA client config
|
|
49
|
+
if hasattr(ctx.jira, 'project_key') and ctx.jira.project_key:
|
|
50
|
+
project_key = ctx.jira.project_key
|
|
51
|
+
ctx.textual.dim_text(f"Using default project from JIRA config: {project_key}")
|
|
52
|
+
else:
|
|
53
|
+
ctx.textual.error_text("project_key is required but not provided")
|
|
54
|
+
ctx.textual.dim_text("Set project_key in workflow params or configure default_project in JIRA plugin")
|
|
55
|
+
ctx.textual.end_step("error")
|
|
56
|
+
return Error("project_key is required. Provide it in workflow params or configure default_project in JIRA plugin.")
|
|
57
|
+
|
|
58
|
+
# Show fetching message
|
|
59
|
+
ctx.textual.dim_text(f"Fetching versions for project: {project_key}")
|
|
60
|
+
ctx.textual.text("")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Get project details which includes versions
|
|
64
|
+
project = ctx.jira.get_project(project_key)
|
|
65
|
+
versions = project.get("versions", [])
|
|
66
|
+
|
|
67
|
+
if not versions:
|
|
68
|
+
ctx.textual.panel(
|
|
69
|
+
f"No versions found for project {project_key}", panel_type="info"
|
|
70
|
+
)
|
|
71
|
+
ctx.textual.end_step("skip")
|
|
72
|
+
return Success(
|
|
73
|
+
"No versions found", metadata={"versions": [], "versions_full": []}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Filter only unreleased versions for release notes workflow
|
|
77
|
+
unreleased_versions = [v for v in versions if not v.get("released", False)]
|
|
78
|
+
|
|
79
|
+
# Sort unreleased by name descending (most recent first)
|
|
80
|
+
unreleased_versions.sort(key=lambda v: v.get("name", ""), reverse=True)
|
|
81
|
+
|
|
82
|
+
# Use only unreleased versions
|
|
83
|
+
sorted_versions = unreleased_versions
|
|
84
|
+
|
|
85
|
+
# Extract version names
|
|
86
|
+
version_names = [v.get("name", "") for v in sorted_versions]
|
|
87
|
+
|
|
88
|
+
# Show success panel
|
|
89
|
+
ctx.textual.text("")
|
|
90
|
+
ctx.textual.panel(
|
|
91
|
+
f"Found {len(sorted_versions)} unreleased versions",
|
|
92
|
+
panel_type="success",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Show versions list
|
|
96
|
+
ctx.textual.text("")
|
|
97
|
+
ctx.textual.bold_primary_text("Unreleased Versions:")
|
|
98
|
+
ctx.textual.text("")
|
|
99
|
+
|
|
100
|
+
for v in sorted_versions[:20]: # Show first 20
|
|
101
|
+
name = v.get("name", "")
|
|
102
|
+
description = v.get("description", "")
|
|
103
|
+
desc_text = f" - {description[:50]}" if description else ""
|
|
104
|
+
ctx.textual.primary_text(f" • {name}{desc_text}")
|
|
105
|
+
|
|
106
|
+
if len(sorted_versions) > 20:
|
|
107
|
+
ctx.textual.dim_text(
|
|
108
|
+
f" ... and {len(sorted_versions) - 20} more"
|
|
109
|
+
)
|
|
110
|
+
ctx.textual.text("")
|
|
111
|
+
|
|
112
|
+
ctx.textual.end_step("success")
|
|
113
|
+
return Success(
|
|
114
|
+
f"Found {len(sorted_versions)} unreleased versions",
|
|
115
|
+
metadata={"versions": version_names, "versions_full": sorted_versions},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
except JiraAPIError as e:
|
|
119
|
+
error_msg = f"Failed to fetch versions: {e}"
|
|
120
|
+
ctx.textual.panel(error_msg, panel_type="error")
|
|
121
|
+
ctx.textual.end_step("error")
|
|
122
|
+
return Error(error_msg)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
import traceback
|
|
125
|
+
|
|
126
|
+
error_detail = traceback.format_exc()
|
|
127
|
+
error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
|
|
128
|
+
ctx.textual.panel(error_msg, panel_type="error")
|
|
129
|
+
ctx.textual.end_step("error")
|
|
130
|
+
return Error(error_msg)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = ["list_versions_step"]
|
|
@@ -3,7 +3,6 @@ Prompt user to select an issue from search results
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
7
6
|
from ..messages import msg
|
|
8
7
|
|
|
9
8
|
|
|
@@ -21,12 +20,19 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
21
20
|
if not ctx.textual:
|
|
22
21
|
return Error("Textual UI context is not available for this step.")
|
|
23
22
|
|
|
23
|
+
# Begin step container
|
|
24
|
+
ctx.textual.begin_step("Select Issue to Analyze")
|
|
25
|
+
|
|
24
26
|
# Get issues from previous search
|
|
25
27
|
issues = ctx.get("jira_issues")
|
|
26
28
|
if not issues:
|
|
29
|
+
ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
|
|
30
|
+
ctx.textual.end_step("error")
|
|
27
31
|
return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
|
|
28
32
|
|
|
29
33
|
if len(issues) == 0:
|
|
34
|
+
ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
|
|
35
|
+
ctx.textual.end_step("error")
|
|
30
36
|
return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
|
|
31
37
|
|
|
32
38
|
# Prompt user to select issue (issues already displayed in table from previous step)
|
|
@@ -40,32 +46,36 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
40
46
|
)
|
|
41
47
|
|
|
42
48
|
if not response or not response.strip():
|
|
49
|
+
ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
|
|
50
|
+
ctx.textual.end_step("error")
|
|
43
51
|
return Error(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
|
|
44
52
|
|
|
45
53
|
# Validate it's a number
|
|
46
54
|
try:
|
|
47
55
|
selected_index = int(response.strip())
|
|
48
56
|
except ValueError:
|
|
57
|
+
ctx.textual.error_text(f"Invalid input: '{response}' is not a number")
|
|
58
|
+
ctx.textual.end_step("error")
|
|
49
59
|
return Error(f"Invalid input: '{response}' is not a number")
|
|
50
60
|
|
|
51
61
|
# Validate it's in range
|
|
52
62
|
if selected_index < 1 or selected_index > len(issues):
|
|
63
|
+
ctx.textual.error_text(f"Invalid selection: must be between 1 and {len(issues)}")
|
|
64
|
+
ctx.textual.end_step("error")
|
|
53
65
|
return Error(f"Invalid selection: must be between 1 and {len(issues)}")
|
|
54
66
|
|
|
55
67
|
# Convert to 0-based index
|
|
56
68
|
selected_issue = issues[selected_index - 1]
|
|
57
69
|
|
|
58
70
|
ctx.textual.text("")
|
|
59
|
-
ctx.textual.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
summary=selected_issue.summary
|
|
64
|
-
),
|
|
65
|
-
panel_type="success"
|
|
71
|
+
ctx.textual.success_text(
|
|
72
|
+
msg.Steps.PromptSelectIssue.ISSUE_SELECTION_CONFIRM.format(
|
|
73
|
+
key=selected_issue.key,
|
|
74
|
+
summary=selected_issue.summary
|
|
66
75
|
)
|
|
67
76
|
)
|
|
68
77
|
|
|
78
|
+
ctx.textual.end_step("success")
|
|
69
79
|
return Success(
|
|
70
80
|
msg.Steps.PromptSelectIssue.SELECT_SUCCESS.format(key=selected_issue.key),
|
|
71
81
|
metadata={
|
|
@@ -74,6 +84,8 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
74
84
|
}
|
|
75
85
|
)
|
|
76
86
|
except (KeyboardInterrupt, EOFError):
|
|
87
|
+
ctx.textual.error_text("User cancelled issue selection")
|
|
88
|
+
ctx.textual.end_step("error")
|
|
77
89
|
return Error("User cancelled issue selection")
|
|
78
90
|
|
|
79
91
|
|