crackerjack 0.19.8__py3-none-any.whl → 0.20.1__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.
- crackerjack/.gitignore +2 -0
- crackerjack/.pre-commit-config.yaml +2 -2
- crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
- crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
- crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
- crackerjack/__init__.py +38 -1
- crackerjack/__main__.py +39 -3
- crackerjack/crackerjack.py +474 -66
- crackerjack/errors.py +176 -0
- crackerjack/interactive.py +487 -0
- crackerjack/py313.py +221 -0
- crackerjack/pyproject.toml +106 -106
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.1.dist-info}/METADATA +96 -4
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.1.dist-info}/RECORD +17 -12
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.1.dist-info}/licenses/LICENSE +0 -0
crackerjack/errors.py
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
"""Error handling module for Crackerjack.
|
2
|
+
|
3
|
+
This module defines a structured error system with error codes, detailed error messages,
|
4
|
+
and recovery suggestions. It provides a consistent way to handle errors throughout
|
5
|
+
the application.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import sys
|
9
|
+
import typing as t
|
10
|
+
from enum import Enum
|
11
|
+
from pathlib import Path
|
12
|
+
|
13
|
+
from rich.console import Console
|
14
|
+
from rich.panel import Panel
|
15
|
+
|
16
|
+
|
17
|
+
class ErrorCode(Enum):
|
18
|
+
CONFIG_FILE_NOT_FOUND = 1001
|
19
|
+
CONFIG_PARSE_ERROR = 1002
|
20
|
+
INVALID_CONFIG = 1003
|
21
|
+
MISSING_CONFIG_FIELD = 1004
|
22
|
+
|
23
|
+
COMMAND_EXECUTION_ERROR = 2001
|
24
|
+
COMMAND_TIMEOUT = 2002
|
25
|
+
EXTERNAL_TOOL_ERROR = 2003
|
26
|
+
PDM_INSTALL_ERROR = 2004
|
27
|
+
PRE_COMMIT_ERROR = 2005
|
28
|
+
|
29
|
+
TEST_EXECUTION_ERROR = 3001
|
30
|
+
TEST_FAILURE = 3002
|
31
|
+
BENCHMARK_REGRESSION = 3003
|
32
|
+
|
33
|
+
BUILD_ERROR = 4001
|
34
|
+
PUBLISH_ERROR = 4002
|
35
|
+
VERSION_BUMP_ERROR = 4003
|
36
|
+
AUTHENTICATION_ERROR = 4004
|
37
|
+
|
38
|
+
GIT_COMMAND_ERROR = 5001
|
39
|
+
PULL_REQUEST_ERROR = 5002
|
40
|
+
COMMIT_ERROR = 5003
|
41
|
+
|
42
|
+
FILE_NOT_FOUND = 6001
|
43
|
+
PERMISSION_ERROR = 6002
|
44
|
+
FILE_READ_ERROR = 6003
|
45
|
+
FILE_WRITE_ERROR = 6004
|
46
|
+
|
47
|
+
CODE_CLEANING_ERROR = 7001
|
48
|
+
FORMATTING_ERROR = 7002
|
49
|
+
|
50
|
+
UNKNOWN_ERROR = 9001
|
51
|
+
NOT_IMPLEMENTED = 9002
|
52
|
+
UNEXPECTED_ERROR = 9999
|
53
|
+
|
54
|
+
|
55
|
+
class CrackerjackError(Exception):
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
message: str,
|
59
|
+
error_code: ErrorCode,
|
60
|
+
details: str | None = None,
|
61
|
+
recovery: str | None = None,
|
62
|
+
exit_code: int = 1,
|
63
|
+
) -> None:
|
64
|
+
self.message = message
|
65
|
+
self.error_code = error_code
|
66
|
+
self.details = details
|
67
|
+
self.recovery = recovery
|
68
|
+
self.exit_code = exit_code
|
69
|
+
super().__init__(message)
|
70
|
+
|
71
|
+
|
72
|
+
class ConfigError(CrackerjackError):
|
73
|
+
pass
|
74
|
+
|
75
|
+
|
76
|
+
class ExecutionError(CrackerjackError):
|
77
|
+
pass
|
78
|
+
|
79
|
+
|
80
|
+
class TestError(CrackerjackError):
|
81
|
+
pass
|
82
|
+
|
83
|
+
|
84
|
+
class PublishError(CrackerjackError):
|
85
|
+
pass
|
86
|
+
|
87
|
+
|
88
|
+
class GitError(CrackerjackError):
|
89
|
+
pass
|
90
|
+
|
91
|
+
|
92
|
+
class FileError(CrackerjackError):
|
93
|
+
pass
|
94
|
+
|
95
|
+
|
96
|
+
class CleaningError(CrackerjackError):
|
97
|
+
pass
|
98
|
+
|
99
|
+
|
100
|
+
def handle_error(
|
101
|
+
error: CrackerjackError,
|
102
|
+
console: Console,
|
103
|
+
verbose: bool = False,
|
104
|
+
ai_agent: bool = False,
|
105
|
+
exit_on_error: bool = True,
|
106
|
+
) -> None:
|
107
|
+
if ai_agent:
|
108
|
+
import json
|
109
|
+
|
110
|
+
error_data = {
|
111
|
+
"status": "error",
|
112
|
+
"error_code": error.error_code.name,
|
113
|
+
"code": error.error_code.value,
|
114
|
+
"message": error.message,
|
115
|
+
}
|
116
|
+
if verbose and error.details:
|
117
|
+
error_data["details"] = error.details
|
118
|
+
if error.recovery:
|
119
|
+
error_data["recovery"] = error.recovery
|
120
|
+
formatted_json = json.dumps(error_data)
|
121
|
+
console.print(f"[json]{formatted_json}[/json]")
|
122
|
+
else:
|
123
|
+
title = f"❌ Error {error.error_code.value}: {error.error_code.name}"
|
124
|
+
content = [error.message]
|
125
|
+
|
126
|
+
if verbose and error.details:
|
127
|
+
content.extend(("\n[bold]Details:[/bold]", str(error.details)))
|
128
|
+
|
129
|
+
if error.recovery:
|
130
|
+
content.extend(
|
131
|
+
("\n[bold green]Recovery suggestion:[/bold green]", str(error.recovery))
|
132
|
+
)
|
133
|
+
|
134
|
+
console.print(
|
135
|
+
Panel(
|
136
|
+
"\n".join(content),
|
137
|
+
title=title,
|
138
|
+
border_style="red",
|
139
|
+
title_align="left",
|
140
|
+
expand=False,
|
141
|
+
)
|
142
|
+
)
|
143
|
+
|
144
|
+
if exit_on_error:
|
145
|
+
sys.exit(error.exit_code)
|
146
|
+
|
147
|
+
|
148
|
+
def check_file_exists(path: Path, error_message: str) -> None:
|
149
|
+
if not path.exists():
|
150
|
+
raise FileError(
|
151
|
+
message=error_message,
|
152
|
+
error_code=ErrorCode.FILE_NOT_FOUND,
|
153
|
+
details=f"The file at {path} does not exist.",
|
154
|
+
recovery="Check the file path and ensure the file exists.",
|
155
|
+
)
|
156
|
+
|
157
|
+
|
158
|
+
def check_command_result(
|
159
|
+
result: t.Any,
|
160
|
+
command: str,
|
161
|
+
error_message: str,
|
162
|
+
error_code: ErrorCode = ErrorCode.COMMAND_EXECUTION_ERROR,
|
163
|
+
recovery: str | None = None,
|
164
|
+
) -> None:
|
165
|
+
if getattr(result, "returncode", 0) != 0:
|
166
|
+
stderr = getattr(result, "stderr", "")
|
167
|
+
details = f"Command '{command}' failed with return code {result.returncode}."
|
168
|
+
if stderr:
|
169
|
+
details += f"\nStandard error output:\n{stderr}"
|
170
|
+
|
171
|
+
raise ExecutionError(
|
172
|
+
message=error_message,
|
173
|
+
error_code=error_code,
|
174
|
+
details=details,
|
175
|
+
recovery=recovery,
|
176
|
+
)
|
@@ -0,0 +1,487 @@
|
|
1
|
+
"""Interactive CLI module using Rich for enhanced user experience.
|
2
|
+
|
3
|
+
This module provides an enhanced interactive CLI experience using Rich library components.
|
4
|
+
It includes progress tracking, interactive prompts, and detailed error reporting.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import time
|
8
|
+
import typing as t
|
9
|
+
from enum import Enum, auto
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
from rich.box import ROUNDED
|
13
|
+
from rich.console import Console
|
14
|
+
from rich.layout import Layout
|
15
|
+
from rich.live import Live
|
16
|
+
from rich.panel import Panel
|
17
|
+
from rich.progress import (
|
18
|
+
BarColumn,
|
19
|
+
Progress,
|
20
|
+
SpinnerColumn,
|
21
|
+
TextColumn,
|
22
|
+
TimeElapsedColumn,
|
23
|
+
)
|
24
|
+
from rich.prompt import Confirm, Prompt
|
25
|
+
from rich.table import Table
|
26
|
+
from rich.text import Text
|
27
|
+
from rich.tree import Tree
|
28
|
+
|
29
|
+
from .errors import CrackerjackError, ErrorCode, handle_error
|
30
|
+
|
31
|
+
|
32
|
+
class TaskStatus(Enum):
|
33
|
+
PENDING = auto()
|
34
|
+
RUNNING = auto()
|
35
|
+
SUCCESS = auto()
|
36
|
+
FAILED = auto()
|
37
|
+
SKIPPED = auto()
|
38
|
+
|
39
|
+
|
40
|
+
class Task:
|
41
|
+
def __init__(
|
42
|
+
self,
|
43
|
+
name: str,
|
44
|
+
description: str,
|
45
|
+
dependencies: list["Task"] | None = None,
|
46
|
+
) -> None:
|
47
|
+
self.name = name
|
48
|
+
self.description = description
|
49
|
+
self.dependencies = dependencies or []
|
50
|
+
self.status = TaskStatus.PENDING
|
51
|
+
self.start_time: float | None = None
|
52
|
+
self.end_time: float | None = None
|
53
|
+
self.error: CrackerjackError | None = None
|
54
|
+
|
55
|
+
@property
|
56
|
+
def duration(self) -> float | None:
|
57
|
+
if self.start_time is None:
|
58
|
+
return None
|
59
|
+
end = self.end_time or time.time()
|
60
|
+
return end - self.start_time
|
61
|
+
|
62
|
+
def start(self) -> None:
|
63
|
+
self.status = TaskStatus.RUNNING
|
64
|
+
self.start_time = time.time()
|
65
|
+
|
66
|
+
def complete(self, success: bool = True) -> None:
|
67
|
+
self.end_time = time.time()
|
68
|
+
self.status = TaskStatus.SUCCESS if success else TaskStatus.FAILED
|
69
|
+
|
70
|
+
def skip(self) -> None:
|
71
|
+
self.status = TaskStatus.SKIPPED
|
72
|
+
|
73
|
+
def fail(self, error: CrackerjackError) -> None:
|
74
|
+
self.end_time = time.time()
|
75
|
+
self.status = TaskStatus.FAILED
|
76
|
+
self.error = error
|
77
|
+
|
78
|
+
def can_run(self) -> bool:
|
79
|
+
return all(
|
80
|
+
dep.status in (TaskStatus.SUCCESS, TaskStatus.SKIPPED)
|
81
|
+
for dep in self.dependencies
|
82
|
+
)
|
83
|
+
|
84
|
+
def __str__(self) -> str:
|
85
|
+
return f"{self.name} ({self.status.name})"
|
86
|
+
|
87
|
+
|
88
|
+
class WorkflowManager:
|
89
|
+
def __init__(self, console: Console) -> None:
|
90
|
+
self.console = console
|
91
|
+
self.tasks: dict[str, Task] = {}
|
92
|
+
self.current_task: Task | None = None
|
93
|
+
|
94
|
+
def add_task(
|
95
|
+
self,
|
96
|
+
name: str,
|
97
|
+
description: str,
|
98
|
+
dependencies: list[str] | None = None,
|
99
|
+
) -> Task:
|
100
|
+
dep_tasks = []
|
101
|
+
if dependencies:
|
102
|
+
for dep_name in dependencies:
|
103
|
+
if dep_name not in self.tasks:
|
104
|
+
raise ValueError(f"Dependency task '{dep_name}' not found")
|
105
|
+
dep_tasks.append(self.tasks[dep_name])
|
106
|
+
|
107
|
+
task = Task(name, description, dep_tasks)
|
108
|
+
self.tasks[name] = task
|
109
|
+
return task
|
110
|
+
|
111
|
+
def get_next_task(self) -> Task | None:
|
112
|
+
for task in self.tasks.values():
|
113
|
+
if (
|
114
|
+
task.status == TaskStatus.PENDING
|
115
|
+
and task.can_run()
|
116
|
+
and task != self.current_task
|
117
|
+
):
|
118
|
+
return task
|
119
|
+
return None
|
120
|
+
|
121
|
+
def all_tasks_completed(self) -> bool:
|
122
|
+
return all(
|
123
|
+
task.status in (TaskStatus.SUCCESS, TaskStatus.FAILED, TaskStatus.SKIPPED)
|
124
|
+
for task in self.tasks.values()
|
125
|
+
)
|
126
|
+
|
127
|
+
def run_task(self, task: Task, func: t.Callable[[], t.Any]) -> bool:
|
128
|
+
self.current_task = task
|
129
|
+
task.start()
|
130
|
+
|
131
|
+
try:
|
132
|
+
func()
|
133
|
+
task.complete()
|
134
|
+
return True
|
135
|
+
except CrackerjackError as e:
|
136
|
+
task.fail(e)
|
137
|
+
return False
|
138
|
+
except Exception as e:
|
139
|
+
from .errors import ExecutionError
|
140
|
+
|
141
|
+
error = ExecutionError(
|
142
|
+
message=f"Unexpected error in task '{task.name}'",
|
143
|
+
error_code=ErrorCode.UNEXPECTED_ERROR,
|
144
|
+
details=str(e),
|
145
|
+
recovery=f"This is an unexpected error in task '{task.name}'. Please report this issue.",
|
146
|
+
)
|
147
|
+
task.fail(error)
|
148
|
+
return False
|
149
|
+
finally:
|
150
|
+
self.current_task = None
|
151
|
+
|
152
|
+
def display_task_tree(self) -> None:
|
153
|
+
tree = Tree("Workflow")
|
154
|
+
|
155
|
+
for task in self.tasks.values():
|
156
|
+
if not task.dependencies:
|
157
|
+
self._add_task_to_tree(task, tree)
|
158
|
+
|
159
|
+
self.console.print(tree)
|
160
|
+
|
161
|
+
def _add_task_to_tree(self, task: Task, parent: Tree) -> None:
|
162
|
+
if task.status == TaskStatus.SUCCESS:
|
163
|
+
status = "[green]✅[/green]"
|
164
|
+
elif task.status == TaskStatus.FAILED:
|
165
|
+
status = "[red]❌[/red]"
|
166
|
+
elif task.status == TaskStatus.RUNNING:
|
167
|
+
status = "[yellow]⏳[/yellow]"
|
168
|
+
elif task.status == TaskStatus.SKIPPED:
|
169
|
+
status = "[blue]⏩[/blue]"
|
170
|
+
else:
|
171
|
+
status = "[grey]⏸️[/grey]"
|
172
|
+
|
173
|
+
branch = parent.add(f"{status} {task.name} - {task.description}")
|
174
|
+
|
175
|
+
for dependent in self.tasks.values():
|
176
|
+
if task in dependent.dependencies:
|
177
|
+
self._add_task_to_tree(dependent, branch)
|
178
|
+
|
179
|
+
|
180
|
+
class InteractiveCLI:
|
181
|
+
def __init__(self, console: Console | None = None) -> None:
|
182
|
+
self.console = console or Console()
|
183
|
+
self.workflow = WorkflowManager(self.console)
|
184
|
+
|
185
|
+
def show_banner(self, version: str) -> None:
|
186
|
+
title = Text("Crackerjack", style="bold cyan")
|
187
|
+
version_text = Text(f"v{version}", style="dim cyan")
|
188
|
+
subtitle = Text("Your Python project management toolkit", style="italic")
|
189
|
+
|
190
|
+
panel = Panel(
|
191
|
+
f"{title} {version_text}\n{subtitle}",
|
192
|
+
box=ROUNDED,
|
193
|
+
border_style="cyan",
|
194
|
+
expand=False,
|
195
|
+
)
|
196
|
+
|
197
|
+
self.console.print(panel)
|
198
|
+
self.console.print()
|
199
|
+
|
200
|
+
def create_standard_workflow(self) -> None:
|
201
|
+
self.workflow.add_task("setup", "Initialize project structure")
|
202
|
+
self.workflow.add_task(
|
203
|
+
"config", "Update configuration files", dependencies=["setup"]
|
204
|
+
)
|
205
|
+
self.workflow.add_task(
|
206
|
+
"clean", "Clean code (remove docstrings, comments)", dependencies=["config"]
|
207
|
+
)
|
208
|
+
self.workflow.add_task("hooks", "Run pre-commit hooks", dependencies=["clean"])
|
209
|
+
self.workflow.add_task("test", "Run tests", dependencies=["hooks"])
|
210
|
+
self.workflow.add_task("version", "Bump version", dependencies=["test"])
|
211
|
+
self.workflow.add_task("publish", "Publish package", dependencies=["version"])
|
212
|
+
self.workflow.add_task("commit", "Commit changes", dependencies=["publish"])
|
213
|
+
|
214
|
+
def setup_layout(self) -> Layout:
|
215
|
+
layout = Layout()
|
216
|
+
|
217
|
+
layout.split(
|
218
|
+
Layout(name="header", size=3),
|
219
|
+
Layout(name="main"),
|
220
|
+
Layout(name="footer", size=3),
|
221
|
+
)
|
222
|
+
|
223
|
+
layout["main"].split_row(
|
224
|
+
Layout(name="tasks", ratio=1),
|
225
|
+
Layout(name="details", ratio=2),
|
226
|
+
)
|
227
|
+
|
228
|
+
return layout
|
229
|
+
|
230
|
+
def show_task_status(self, task: Task) -> Panel:
|
231
|
+
if task.status == TaskStatus.RUNNING:
|
232
|
+
status = "[yellow]⏳ Running[/yellow]"
|
233
|
+
style = "yellow"
|
234
|
+
elif task.status == TaskStatus.SUCCESS:
|
235
|
+
status = "[green]✅ Success[/green]"
|
236
|
+
style = "green"
|
237
|
+
elif task.status == TaskStatus.FAILED:
|
238
|
+
status = "[red]❌ Failed[/red]"
|
239
|
+
style = "red"
|
240
|
+
elif task.status == TaskStatus.SKIPPED:
|
241
|
+
status = "[blue]⏩ Skipped[/blue]"
|
242
|
+
style = "blue"
|
243
|
+
else:
|
244
|
+
status = "[grey]⏸️ Pending[/grey]"
|
245
|
+
style = "dim"
|
246
|
+
|
247
|
+
duration = task.duration
|
248
|
+
duration_text = f"Duration: {duration:.2f}s" if duration else ""
|
249
|
+
|
250
|
+
content = f"{task.name}: {task.description}\nStatus: {status}\n{duration_text}"
|
251
|
+
|
252
|
+
if task.error:
|
253
|
+
content += f"\n[red]Error: {task.error.message}[/red]"
|
254
|
+
if task.error.details:
|
255
|
+
content += f"\n[dim red]Details: {task.error.details}[/dim red]"
|
256
|
+
if task.error.recovery:
|
257
|
+
content += f"\n[yellow]Recovery: {task.error.recovery}[/yellow]"
|
258
|
+
|
259
|
+
return Panel(
|
260
|
+
content,
|
261
|
+
title=task.name,
|
262
|
+
border_style=style,
|
263
|
+
expand=False,
|
264
|
+
)
|
265
|
+
|
266
|
+
def show_task_table(self) -> Table:
|
267
|
+
table = Table(
|
268
|
+
title="Workflow Tasks",
|
269
|
+
box=ROUNDED,
|
270
|
+
show_header=True,
|
271
|
+
header_style="bold cyan",
|
272
|
+
)
|
273
|
+
|
274
|
+
table.add_column("Task", style="cyan")
|
275
|
+
table.add_column("Status")
|
276
|
+
table.add_column("Duration")
|
277
|
+
table.add_column("Dependencies")
|
278
|
+
|
279
|
+
for task in self.workflow.tasks.values():
|
280
|
+
if task.status == TaskStatus.RUNNING:
|
281
|
+
status = "[yellow]⏳ Running[/yellow]"
|
282
|
+
elif task.status == TaskStatus.SUCCESS:
|
283
|
+
status = "[green]✅ Success[/green]"
|
284
|
+
elif task.status == TaskStatus.FAILED:
|
285
|
+
status = "[red]❌ Failed[/red]"
|
286
|
+
elif task.status == TaskStatus.SKIPPED:
|
287
|
+
status = "[blue]⏩ Skipped[/blue]"
|
288
|
+
else:
|
289
|
+
status = "[grey]⏸️ Pending[/grey]"
|
290
|
+
|
291
|
+
duration = task.duration
|
292
|
+
duration_text = f"{duration:.2f}s" if duration else "-"
|
293
|
+
|
294
|
+
deps = ", ".join(dep.name for dep in task.dependencies) or "-"
|
295
|
+
|
296
|
+
table.add_row(task.name, status, duration_text, deps)
|
297
|
+
|
298
|
+
return table
|
299
|
+
|
300
|
+
def run_interactive(self) -> None:
|
301
|
+
self.console.clear()
|
302
|
+
layout = self.setup_layout()
|
303
|
+
|
304
|
+
layout["header"].update(
|
305
|
+
Panel(
|
306
|
+
"Crackerjack Interactive Mode",
|
307
|
+
style="bold cyan",
|
308
|
+
box=ROUNDED,
|
309
|
+
)
|
310
|
+
)
|
311
|
+
|
312
|
+
layout["footer"].update(
|
313
|
+
Panel(
|
314
|
+
"Press Ctrl+C to exit",
|
315
|
+
style="dim",
|
316
|
+
box=ROUNDED,
|
317
|
+
)
|
318
|
+
)
|
319
|
+
|
320
|
+
progress = Progress(
|
321
|
+
SpinnerColumn(),
|
322
|
+
TextColumn("[bold blue]{task.description}"),
|
323
|
+
BarColumn(),
|
324
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
325
|
+
TimeElapsedColumn(),
|
326
|
+
)
|
327
|
+
|
328
|
+
total_tasks = len(self.workflow.tasks)
|
329
|
+
progress_task = progress.add_task("Running workflow", total=total_tasks)
|
330
|
+
completed_tasks = 0
|
331
|
+
|
332
|
+
with Live(layout, refresh_per_second=4, screen=True) as live:
|
333
|
+
try:
|
334
|
+
while not self.workflow.all_tasks_completed():
|
335
|
+
layout["tasks"].update(self.show_task_table())
|
336
|
+
|
337
|
+
next_task = self.workflow.get_next_task()
|
338
|
+
if not next_task:
|
339
|
+
break
|
340
|
+
|
341
|
+
layout["details"].update(self.show_task_status(next_task))
|
342
|
+
|
343
|
+
live.stop()
|
344
|
+
should_run = Confirm.ask(
|
345
|
+
f"Run task '{next_task.name}'?",
|
346
|
+
default=True,
|
347
|
+
)
|
348
|
+
live.start()
|
349
|
+
|
350
|
+
if not should_run:
|
351
|
+
next_task.skip()
|
352
|
+
continue
|
353
|
+
|
354
|
+
next_task.start()
|
355
|
+
layout["details"].update(self.show_task_status(next_task))
|
356
|
+
time.sleep(1)
|
357
|
+
|
358
|
+
import random
|
359
|
+
|
360
|
+
success = random.choice([True, True, True, False])
|
361
|
+
|
362
|
+
if success:
|
363
|
+
next_task.complete(True)
|
364
|
+
completed_tasks += 1
|
365
|
+
else:
|
366
|
+
from .errors import ExecutionError
|
367
|
+
|
368
|
+
error = ExecutionError(
|
369
|
+
message=f"Task '{next_task.name}' failed",
|
370
|
+
error_code=ErrorCode.COMMAND_EXECUTION_ERROR,
|
371
|
+
details="This is a simulated failure for demonstration.",
|
372
|
+
recovery=f"Retry the '{next_task.name}' task.",
|
373
|
+
)
|
374
|
+
next_task.fail(error)
|
375
|
+
|
376
|
+
progress.update(progress_task, completed=completed_tasks)
|
377
|
+
|
378
|
+
layout["details"].update(self.show_task_status(next_task))
|
379
|
+
|
380
|
+
layout["tasks"].update(self.show_task_table())
|
381
|
+
|
382
|
+
successful = sum(
|
383
|
+
1
|
384
|
+
for task in self.workflow.tasks.values()
|
385
|
+
if task.status == TaskStatus.SUCCESS
|
386
|
+
)
|
387
|
+
failed = sum(
|
388
|
+
1
|
389
|
+
for task in self.workflow.tasks.values()
|
390
|
+
if task.status == TaskStatus.FAILED
|
391
|
+
)
|
392
|
+
skipped = sum(
|
393
|
+
1
|
394
|
+
for task in self.workflow.tasks.values()
|
395
|
+
if task.status == TaskStatus.SKIPPED
|
396
|
+
)
|
397
|
+
|
398
|
+
summary = Panel(
|
399
|
+
f"Workflow completed!\n\n"
|
400
|
+
f"[green]✅ Successful tasks: {successful}[/green]\n"
|
401
|
+
f"[red]❌ Failed tasks: {failed}[/red]\n"
|
402
|
+
f"[blue]⏩ Skipped tasks: {skipped}[/blue]",
|
403
|
+
title="Summary",
|
404
|
+
border_style="cyan",
|
405
|
+
)
|
406
|
+
layout["details"].update(summary)
|
407
|
+
|
408
|
+
except KeyboardInterrupt:
|
409
|
+
layout["footer"].update(
|
410
|
+
Panel(
|
411
|
+
"Interrupted by user",
|
412
|
+
style="yellow",
|
413
|
+
box=ROUNDED,
|
414
|
+
)
|
415
|
+
)
|
416
|
+
|
417
|
+
self.console.print("\nWorkflow Status:")
|
418
|
+
self.workflow.display_task_tree()
|
419
|
+
|
420
|
+
def ask_for_file(
|
421
|
+
self, prompt: str, directory: Path, default: str | None = None
|
422
|
+
) -> Path:
|
423
|
+
self.console.print(f"\n[bold]{prompt}[/bold]")
|
424
|
+
|
425
|
+
files = list(directory.iterdir())
|
426
|
+
files.sort()
|
427
|
+
|
428
|
+
table = Table(title=f"Files in {directory}", box=ROUNDED)
|
429
|
+
table.add_column("#", style="cyan")
|
430
|
+
table.add_column("Filename", style="green")
|
431
|
+
table.add_column("Size", style="blue")
|
432
|
+
table.add_column("Modified", style="yellow")
|
433
|
+
|
434
|
+
for i, file in enumerate(files, 1):
|
435
|
+
if file.is_file():
|
436
|
+
size = f"{file.stat().st_size / 1024:.1f} KB"
|
437
|
+
mtime = time.strftime(
|
438
|
+
"%Y-%m-%d %H:%M:%S",
|
439
|
+
time.localtime(file.stat().st_mtime),
|
440
|
+
)
|
441
|
+
table.add_row(str(i), file.name, size, mtime)
|
442
|
+
|
443
|
+
self.console.print(table)
|
444
|
+
|
445
|
+
selection = Prompt.ask(
|
446
|
+
"Enter file number or name",
|
447
|
+
default=default or "",
|
448
|
+
)
|
449
|
+
|
450
|
+
if selection.isdigit() and 1 <= int(selection) <= len(files):
|
451
|
+
return files[int(selection) - 1]
|
452
|
+
else:
|
453
|
+
for file in files:
|
454
|
+
if file.name == selection:
|
455
|
+
return file
|
456
|
+
|
457
|
+
return directory / selection
|
458
|
+
|
459
|
+
def confirm_dangerous_action(self, action: str, details: str) -> bool:
|
460
|
+
panel = Panel(
|
461
|
+
f"[bold red]WARNING: {action}[/bold red]\n\n{details}\n\n"
|
462
|
+
"This action cannot be undone. Please type the action name to confirm.",
|
463
|
+
title="Confirmation Required",
|
464
|
+
border_style="red",
|
465
|
+
)
|
466
|
+
|
467
|
+
self.console.print(panel)
|
468
|
+
|
469
|
+
confirmation = Prompt.ask("Type the action name to confirm")
|
470
|
+
|
471
|
+
return confirmation.lower() == action.lower()
|
472
|
+
|
473
|
+
def show_error(self, error: CrackerjackError, verbose: bool = False) -> None:
|
474
|
+
handle_error(error, self.console, verbose, exit_on_error=False)
|
475
|
+
|
476
|
+
|
477
|
+
def launch_interactive_cli(version: str) -> None:
|
478
|
+
console = Console()
|
479
|
+
cli = InteractiveCLI(console)
|
480
|
+
|
481
|
+
cli.show_banner(version)
|
482
|
+
cli.create_standard_workflow()
|
483
|
+
cli.run_interactive()
|
484
|
+
|
485
|
+
|
486
|
+
if __name__ == "__main__":
|
487
|
+
launch_interactive_cli("0.19.8")
|