krita-cli 1.0.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.
- krita_cli/__init__.py +5 -0
- krita_cli/_shared.py +94 -0
- krita_cli/app.py +149 -0
- krita_cli/cli.py +59 -0
- krita_cli/commands/__init__.py +1 -0
- krita_cli/commands/batch.py +86 -0
- krita_cli/commands/brush.py +58 -0
- krita_cli/commands/call.py +49 -0
- krita_cli/commands/canvas.py +77 -0
- krita_cli/commands/color.py +42 -0
- krita_cli/commands/config.py +48 -0
- krita_cli/commands/file_ops.py +27 -0
- krita_cli/commands/health.py +32 -0
- krita_cli/commands/history_cmd.py +64 -0
- krita_cli/commands/introspect.py +46 -0
- krita_cli/commands/layers.py +112 -0
- krita_cli/commands/navigation.py +33 -0
- krita_cli/commands/replay.py +126 -0
- krita_cli/commands/rollback.py +39 -0
- krita_cli/commands/selection.py +364 -0
- krita_cli/commands/stroke.py +101 -0
- krita_cli/config_cmd.py +68 -0
- krita_cli/history.py +198 -0
- krita_cli-1.0.0.dist-info/METADATA +103 -0
- krita_cli-1.0.0.dist-info/RECORD +35 -0
- krita_cli-1.0.0.dist-info/WHEEL +4 -0
- krita_cli-1.0.0.dist-info/entry_points.txt +2 -0
- krita_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- krita_client/__init__.py +68 -0
- krita_client/client.py +690 -0
- krita_client/config.py +53 -0
- krita_client/models.py +507 -0
- krita_client/schema.py +109 -0
- krita_mcp/__init__.py +1 -0
- krita_mcp/server.py +1022 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""File operation CLI commands: open-file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from typer import Context
|
|
9
|
+
|
|
10
|
+
from krita_cli import _shared
|
|
11
|
+
from krita_client import KritaError
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("open-file")
|
|
17
|
+
def open_file(
|
|
18
|
+
ctx: Context,
|
|
19
|
+
path: Annotated[str, typer.Argument(help="Full file path to open")],
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Open an existing file in Krita (.kra, .png, .jpg, etc)."""
|
|
22
|
+
try:
|
|
23
|
+
client = _shared._get_client(ctx)
|
|
24
|
+
result = client.open_file(path=path)
|
|
25
|
+
_shared._format_result(result)
|
|
26
|
+
except KritaError as exc:
|
|
27
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Health check CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from typer import Context
|
|
8
|
+
|
|
9
|
+
from krita_cli import _shared
|
|
10
|
+
from krita_client import KritaError
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
app = typer.Typer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def health(ctx: Context) -> None:
|
|
19
|
+
"""Check if Krita is running with the MCP plugin active."""
|
|
20
|
+
try:
|
|
21
|
+
client = _shared._get_client(ctx)
|
|
22
|
+
result = client.health()
|
|
23
|
+
plugin = result.get("plugin", "unknown")
|
|
24
|
+
status = result.get("status", "unknown")
|
|
25
|
+
version = result.get("version", "unknown")
|
|
26
|
+
protocol_version = result.get("protocol_version", "unknown")
|
|
27
|
+
console.print(
|
|
28
|
+
f"[green]Krita is running.[/green] Plugin: [bold]{plugin}[/bold] "
|
|
29
|
+
f"v{version} (protocol v{protocol_version}, {status})"
|
|
30
|
+
)
|
|
31
|
+
except KritaError as exc:
|
|
32
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""History command CLI subcommand."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated, Any, cast
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from typer import Context
|
|
12
|
+
|
|
13
|
+
from krita_cli import _shared
|
|
14
|
+
from krita_client import KritaError
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
app = typer.Typer()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def history(
|
|
23
|
+
ctx: Context,
|
|
24
|
+
limit: Annotated[int, typer.Option("--limit", "-n", help="Number of history entries to show")] = 20,
|
|
25
|
+
as_json: Annotated[bool, typer.Option("--json", "-j", help="Output as raw JSON")] = False, # noqa: FBT002
|
|
26
|
+
) -> None:
|
|
27
|
+
"""View recent command execution history."""
|
|
28
|
+
try:
|
|
29
|
+
client = _shared._get_client(ctx)
|
|
30
|
+
result = client.get_command_history(limit=limit)
|
|
31
|
+
|
|
32
|
+
if as_json:
|
|
33
|
+
console.print(json.dumps(result, indent=2, default=str))
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
records_raw = result.get("history", [])
|
|
37
|
+
if not isinstance(records_raw, list):
|
|
38
|
+
records_raw = []
|
|
39
|
+
records = cast("list[dict[str, Any]]", records_raw)
|
|
40
|
+
|
|
41
|
+
if not records:
|
|
42
|
+
console.print("[dim]No command history recorded.[/dim]")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
table = Table(title=f"Command History ({len(records)} entries)")
|
|
46
|
+
table.add_column("#", style="dim")
|
|
47
|
+
table.add_column("Action")
|
|
48
|
+
table.add_column("Status")
|
|
49
|
+
table.add_column("Duration (ms)")
|
|
50
|
+
table.add_column("Error", style="red")
|
|
51
|
+
|
|
52
|
+
for i, rec in enumerate(records, 1):
|
|
53
|
+
table.add_row(
|
|
54
|
+
str(i),
|
|
55
|
+
rec.get("action", "?"),
|
|
56
|
+
rec.get("status", "?"),
|
|
57
|
+
f"{rec.get('duration_ms', 0):.1f}",
|
|
58
|
+
rec.get("error", "") or "",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print(table)
|
|
62
|
+
|
|
63
|
+
except KritaError as exc:
|
|
64
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Canvas introspection CLI commands: canvas-info, current-color, current-brush."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer import Context
|
|
7
|
+
|
|
8
|
+
from krita_cli import _shared
|
|
9
|
+
from krita_client import KritaError
|
|
10
|
+
|
|
11
|
+
console = _shared.console
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("canvas-info")
|
|
17
|
+
def canvas_info(ctx: Context) -> None:
|
|
18
|
+
"""Get information about the current canvas."""
|
|
19
|
+
try:
|
|
20
|
+
client = _shared._get_client(ctx)
|
|
21
|
+
result = client.get_canvas_info()
|
|
22
|
+
_shared._format_result(result)
|
|
23
|
+
except KritaError as exc:
|
|
24
|
+
_shared._handle_error(exc)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("current-color")
|
|
28
|
+
def current_color(ctx: Context) -> None:
|
|
29
|
+
"""Get the current foreground and background colors."""
|
|
30
|
+
try:
|
|
31
|
+
client = _shared._get_client(ctx)
|
|
32
|
+
result = client.get_current_color()
|
|
33
|
+
_shared._format_result(result)
|
|
34
|
+
except KritaError as exc:
|
|
35
|
+
_shared._handle_error(exc)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("current-brush")
|
|
39
|
+
def current_brush(ctx: Context) -> None:
|
|
40
|
+
"""Get the current brush preset and properties."""
|
|
41
|
+
try:
|
|
42
|
+
client = _shared._get_client(ctx)
|
|
43
|
+
result = client.get_current_brush()
|
|
44
|
+
_shared._format_result(result)
|
|
45
|
+
except KritaError as exc:
|
|
46
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Layer-related CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from typer import Context
|
|
9
|
+
|
|
10
|
+
from krita_cli import _shared
|
|
11
|
+
from krita_client import KritaError
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_layers(ctx: Context) -> None:
|
|
18
|
+
"""List all layers in the current document."""
|
|
19
|
+
try:
|
|
20
|
+
client = _shared._get_client(ctx)
|
|
21
|
+
result = client.list_layers()
|
|
22
|
+
_shared._format_result(result)
|
|
23
|
+
except KritaError as exc:
|
|
24
|
+
_shared._handle_error(exc)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("create")
|
|
28
|
+
def create_layer(
|
|
29
|
+
ctx: Context,
|
|
30
|
+
name: Annotated[str, typer.Option("--name", "-n", help="Layer name")] = "New Layer",
|
|
31
|
+
layer_type: Annotated[str, typer.Option("--type", "-t", help="Layer type")] = "paintlayer",
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Create a new layer in the current document."""
|
|
34
|
+
try:
|
|
35
|
+
client = _shared._get_client(ctx)
|
|
36
|
+
result = client.create_layer(name=name, layer_type=layer_type)
|
|
37
|
+
_shared._format_result(result)
|
|
38
|
+
except KritaError as exc:
|
|
39
|
+
_shared._handle_error(exc)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("select")
|
|
43
|
+
def select_layer(
|
|
44
|
+
ctx: Context,
|
|
45
|
+
name: Annotated[str, typer.Argument(help="Layer name to select")],
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Select a layer by name."""
|
|
48
|
+
try:
|
|
49
|
+
client = _shared._get_client(ctx)
|
|
50
|
+
result = client.select_layer(name=name)
|
|
51
|
+
_shared._format_result(result)
|
|
52
|
+
except KritaError as exc:
|
|
53
|
+
_shared._handle_error(exc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("delete")
|
|
57
|
+
def delete_layer(
|
|
58
|
+
ctx: Context,
|
|
59
|
+
name: Annotated[str, typer.Argument(help="Layer name to delete")],
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Delete a layer by name."""
|
|
62
|
+
try:
|
|
63
|
+
client = _shared._get_client(ctx)
|
|
64
|
+
result = client.delete_layer(name=name)
|
|
65
|
+
_shared._format_result(result)
|
|
66
|
+
except KritaError as exc:
|
|
67
|
+
_shared._handle_error(exc)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("rename")
|
|
71
|
+
def rename_layer(
|
|
72
|
+
ctx: Context,
|
|
73
|
+
old_name: Annotated[str, typer.Argument(help="Current layer name")],
|
|
74
|
+
new_name: Annotated[str, typer.Argument(help="New layer name")],
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Rename a layer."""
|
|
77
|
+
try:
|
|
78
|
+
client = _shared._get_client(ctx)
|
|
79
|
+
result = client.rename_layer(old_name=old_name, new_name=new_name)
|
|
80
|
+
_shared._format_result(result)
|
|
81
|
+
except KritaError as exc:
|
|
82
|
+
_shared._handle_error(exc)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("set-opacity")
|
|
86
|
+
def set_layer_opacity(
|
|
87
|
+
ctx: Context,
|
|
88
|
+
name: Annotated[str, typer.Argument(help="Layer name")],
|
|
89
|
+
opacity: Annotated[float, typer.Option("--opacity", "-o", help="Opacity (0.0 to 1.0)")] = 1.0,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Set the opacity of a layer."""
|
|
92
|
+
try:
|
|
93
|
+
client = _shared._get_client(ctx)
|
|
94
|
+
result = client.set_layer_opacity(name=name, opacity=opacity)
|
|
95
|
+
_shared._format_result(result)
|
|
96
|
+
except KritaError as exc:
|
|
97
|
+
_shared._handle_error(exc)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command("set-visibility")
|
|
101
|
+
def set_layer_visibility(
|
|
102
|
+
ctx: Context,
|
|
103
|
+
name: Annotated[str, typer.Argument(help="Layer name")],
|
|
104
|
+
visible: Annotated[bool, typer.Option("--visible/--hidden", "-v/-h", help="Layer visibility")] = True,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Toggle the visibility of a layer."""
|
|
107
|
+
try:
|
|
108
|
+
client = _shared._get_client(ctx)
|
|
109
|
+
result = client.set_layer_visibility(name=name, visible=visible)
|
|
110
|
+
_shared._format_result(result)
|
|
111
|
+
except KritaError as exc:
|
|
112
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Navigation-related CLI commands: undo, redo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer import Context
|
|
7
|
+
|
|
8
|
+
from krita_cli import _shared
|
|
9
|
+
from krita_client import KritaError
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def undo(ctx: Context) -> None:
|
|
16
|
+
"""Undo the last action."""
|
|
17
|
+
try:
|
|
18
|
+
client = _shared._get_client(ctx)
|
|
19
|
+
result = client.undo()
|
|
20
|
+
_shared._format_result(result)
|
|
21
|
+
except KritaError as exc:
|
|
22
|
+
_shared._handle_error(exc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command()
|
|
26
|
+
def redo(ctx: Context) -> None:
|
|
27
|
+
"""Redo the last undone action."""
|
|
28
|
+
try:
|
|
29
|
+
client = _shared._get_client(ctx)
|
|
30
|
+
result = client.redo()
|
|
31
|
+
_shared._format_result(result)
|
|
32
|
+
except KritaError as exc:
|
|
33
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Replay command CLI subcommand."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
import time
|
|
8
|
+
from typing import Annotated, Any, cast
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from typer import Context
|
|
13
|
+
|
|
14
|
+
from krita_cli import _shared
|
|
15
|
+
from krita_client import KritaError
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
app = typer.Typer()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_records(records: list[dict]) -> None:
|
|
23
|
+
"""Validate that all records have required 'action' field."""
|
|
24
|
+
for i, rec in enumerate(records, 1):
|
|
25
|
+
action = rec.get("action")
|
|
26
|
+
if not action:
|
|
27
|
+
console.print(f" [red]#{i}: Missing 'action' field[/red]")
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _execute_replay(
|
|
32
|
+
ctx: Context,
|
|
33
|
+
records: list[dict],
|
|
34
|
+
speed: float,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Execute recorded commands via the client."""
|
|
37
|
+
client = _shared._get_client(ctx)
|
|
38
|
+
ok_count = 0
|
|
39
|
+
err_count = 0
|
|
40
|
+
|
|
41
|
+
for i, rec in enumerate(records, 1):
|
|
42
|
+
action = rec.get("action")
|
|
43
|
+
params = rec.get("params", {})
|
|
44
|
+
|
|
45
|
+
if not action:
|
|
46
|
+
console.print(f" [red]#{i}: Skipping — missing 'action'[/red]")
|
|
47
|
+
err_count += 1
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if speed > 0:
|
|
51
|
+
original_ms = rec.get("duration_ms", 0) / 1000.0
|
|
52
|
+
delay = original_ms / speed
|
|
53
|
+
if delay > 0:
|
|
54
|
+
time.sleep(delay)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = client.send_command(action, params)
|
|
58
|
+
if "error" in result:
|
|
59
|
+
err_raw = result.get("error", {})
|
|
60
|
+
if isinstance(err_raw, dict):
|
|
61
|
+
err_dict = cast("dict[str, Any]", err_raw)
|
|
62
|
+
err_msg = str(err_dict.get("message", str(err_raw)))
|
|
63
|
+
else:
|
|
64
|
+
err_msg = str(err_raw)
|
|
65
|
+
console.print(f" [red]#{i}: {action} — {err_msg}[/red]")
|
|
66
|
+
err_count += 1
|
|
67
|
+
else:
|
|
68
|
+
ok_count += 1
|
|
69
|
+
except KritaError as exc:
|
|
70
|
+
console.print(f" [red]#{i}: {action} — {exc}[/red]")
|
|
71
|
+
err_count += 1
|
|
72
|
+
|
|
73
|
+
console.print(f"\nReplay complete: {ok_count} succeeded, {err_count} failed out of {len(records)}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command()
|
|
77
|
+
def replay(
|
|
78
|
+
ctx: Context,
|
|
79
|
+
file: Annotated[
|
|
80
|
+
str,
|
|
81
|
+
typer.Argument(help="JSON file with recorded commands"),
|
|
82
|
+
],
|
|
83
|
+
speed: Annotated[
|
|
84
|
+
float,
|
|
85
|
+
typer.Option("--speed", "-s", help="Playback speed (0.0=instant, 1.0=original)"),
|
|
86
|
+
] = 1.0,
|
|
87
|
+
dry_run: Annotated[ # noqa: FBT002
|
|
88
|
+
bool,
|
|
89
|
+
typer.Option("--dry-run", "-n", help="Validate without executing"),
|
|
90
|
+
] = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Replay commands from a JSON file.
|
|
93
|
+
|
|
94
|
+
The JSON file should contain an array of command records from
|
|
95
|
+
'krita history --json', each with an "action" and optional "params" key.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
krita history --json > history.json
|
|
99
|
+
krita replay history.json
|
|
100
|
+
krita replay history.json --speed 0.5
|
|
101
|
+
krita replay history.json --dry-run
|
|
102
|
+
"""
|
|
103
|
+
path = pathlib.Path(file)
|
|
104
|
+
try:
|
|
105
|
+
records = json.loads(path.read_text())
|
|
106
|
+
except json.JSONDecodeError as exc:
|
|
107
|
+
console.print(f"[red]Error:[/red] Invalid JSON in {path}: {exc}")
|
|
108
|
+
raise typer.Exit(code=1) from exc
|
|
109
|
+
except OSError as exc:
|
|
110
|
+
console.print(f"[red]Error:[/red] Cannot read {path}: {exc}")
|
|
111
|
+
raise typer.Exit(code=1) from exc
|
|
112
|
+
|
|
113
|
+
if not isinstance(records, list):
|
|
114
|
+
console.print("[red]Error:[/red] JSON file must contain an array.")
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
|
|
117
|
+
if dry_run:
|
|
118
|
+
console.print(f"[dim]Dry run: validating {len(records)} records[/dim]")
|
|
119
|
+
_validate_records(records)
|
|
120
|
+
console.print(f"[green]All {len(records)} records are valid.[/green]")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
_execute_replay(ctx, records, speed)
|
|
125
|
+
except KritaError as exc:
|
|
126
|
+
_shared._handle_error(exc)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Rollback CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from typer import Context
|
|
10
|
+
|
|
11
|
+
from krita_cli import _shared
|
|
12
|
+
from krita_client import KritaError
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def rollback(
|
|
21
|
+
ctx: Context,
|
|
22
|
+
batch_id: Annotated[str, typer.Argument(help="ID of the batch to roll back")],
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Roll back a previously executed batch.
|
|
25
|
+
|
|
26
|
+
This restores the canvas to its state before the specified batch was executed.
|
|
27
|
+
Note that snapshots are kept in memory and are lost if the Krita plugin is restarted.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
client = _shared._get_client(ctx)
|
|
31
|
+
result = client.rollback(batch_id=batch_id)
|
|
32
|
+
status = result.get("status", "unknown")
|
|
33
|
+
msg = result.get("message", "")
|
|
34
|
+
if status == "ok":
|
|
35
|
+
console.print(f"[green]Rollback successful: {msg}[/green]")
|
|
36
|
+
else:
|
|
37
|
+
console.print(f"[red]Rollback failed: {msg}[/red]")
|
|
38
|
+
except KritaError as exc:
|
|
39
|
+
_shared._handle_error(exc)
|