comfyui-skill-cli 0.2.4__tar.gz → 0.2.5__tar.gz
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.
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/PKG-INFO +1 -1
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/PKG-INFO +1 -1
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/SOURCES.txt +5 -0
- comfyui_skill_cli-0.2.5/comfyui_skills_cli/__init__.py +1 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/client.py +36 -3
- comfyui_skill_cli-0.2.5/comfyui_skills_cli/commands/cancel.py +51 -0
- comfyui_skill_cli-0.2.5/comfyui_skills_cli/commands/free.py +46 -0
- comfyui_skill_cli-0.2.5/comfyui_skills_cli/commands/queue.py +82 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/run.py +9 -2
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/upload.py +6 -6
- comfyui_skill_cli-0.2.5/comfyui_skills_cli/error_hints.py +43 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/main.py +5 -2
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/pyproject.toml +1 -1
- comfyui_skill_cli-0.2.5/tests/test_client_new_apis.py +150 -0
- comfyui_skill_cli-0.2.4/comfyui_skills_cli/__init__.py +0 -1
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/LICENSE +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/README.md +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/dependency_links.txt +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/entry_points.txt +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/requires.txt +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/top_level.txt +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/__main__.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/__init__.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/config.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/deps.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/history.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/server.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/skill.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/workflow.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/config.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/output.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/storage.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/update_check.py +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/setup.cfg +0 -0
- {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/tests/test_update_check.py +0 -0
|
@@ -11,17 +11,22 @@ comfyui_skills_cli/__init__.py
|
|
|
11
11
|
comfyui_skills_cli/__main__.py
|
|
12
12
|
comfyui_skills_cli/client.py
|
|
13
13
|
comfyui_skills_cli/config.py
|
|
14
|
+
comfyui_skills_cli/error_hints.py
|
|
14
15
|
comfyui_skills_cli/main.py
|
|
15
16
|
comfyui_skills_cli/output.py
|
|
16
17
|
comfyui_skills_cli/storage.py
|
|
17
18
|
comfyui_skills_cli/update_check.py
|
|
18
19
|
comfyui_skills_cli/commands/__init__.py
|
|
20
|
+
comfyui_skills_cli/commands/cancel.py
|
|
19
21
|
comfyui_skills_cli/commands/config.py
|
|
20
22
|
comfyui_skills_cli/commands/deps.py
|
|
23
|
+
comfyui_skills_cli/commands/free.py
|
|
21
24
|
comfyui_skills_cli/commands/history.py
|
|
25
|
+
comfyui_skills_cli/commands/queue.py
|
|
22
26
|
comfyui_skills_cli/commands/run.py
|
|
23
27
|
comfyui_skills_cli/commands/server.py
|
|
24
28
|
comfyui_skills_cli/commands/skill.py
|
|
25
29
|
comfyui_skills_cli/commands/upload.py
|
|
26
30
|
comfyui_skills_cli/commands/workflow.py
|
|
31
|
+
tests/test_client_new_apis.py
|
|
27
32
|
tests/test_update_check.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.5"
|
|
@@ -126,13 +126,43 @@ class ComfyUIClient:
|
|
|
126
126
|
except (requests.RequestException, ValueError):
|
|
127
127
|
return None
|
|
128
128
|
|
|
129
|
-
# --
|
|
129
|
+
# -- Queue management --
|
|
130
130
|
|
|
131
|
-
def
|
|
131
|
+
def interrupt(self, prompt_id: str = "") -> dict[str, Any]:
|
|
132
|
+
payload = {"prompt_id": prompt_id} if prompt_id else {}
|
|
133
|
+
resp = self._post("/interrupt", json_data=payload)
|
|
134
|
+
resp.raise_for_status()
|
|
135
|
+
return {"success": True}
|
|
136
|
+
|
|
137
|
+
def queue_delete(self, prompt_ids: list[str]) -> dict[str, Any]:
|
|
138
|
+
resp = self._post("/queue", json_data={"delete": prompt_ids})
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
return {"success": True}
|
|
141
|
+
|
|
142
|
+
def queue_clear(self) -> dict[str, Any]:
|
|
143
|
+
resp = self._post("/queue", json_data={"clear": True})
|
|
144
|
+
resp.raise_for_status()
|
|
145
|
+
return {"success": True}
|
|
146
|
+
|
|
147
|
+
# -- Memory management --
|
|
148
|
+
|
|
149
|
+
def free_memory(self, unload_models: bool = False, free_memory: bool = False) -> dict[str, Any]:
|
|
150
|
+
payload: dict[str, Any] = {}
|
|
151
|
+
if unload_models:
|
|
152
|
+
payload["unload_models"] = True
|
|
153
|
+
if free_memory:
|
|
154
|
+
payload["free_memory"] = True
|
|
155
|
+
resp = self._post("/free", json_data=payload)
|
|
156
|
+
resp.raise_for_status()
|
|
157
|
+
return {"success": True}
|
|
158
|
+
|
|
159
|
+
# -- File upload --
|
|
160
|
+
|
|
161
|
+
def upload_file(self, filepath: str) -> dict[str, Any]:
|
|
132
162
|
import mimetypes
|
|
133
163
|
import os
|
|
134
164
|
filename = os.path.basename(filepath)
|
|
135
|
-
content_type = mimetypes.guess_type(filepath)[0] or "
|
|
165
|
+
content_type = mimetypes.guess_type(filepath)[0] or "application/octet-stream"
|
|
136
166
|
with open(filepath, "rb") as f:
|
|
137
167
|
content = f.read()
|
|
138
168
|
|
|
@@ -154,6 +184,9 @@ class ComfyUIClient:
|
|
|
154
184
|
resp.raise_for_status()
|
|
155
185
|
return resp.json()
|
|
156
186
|
|
|
187
|
+
def upload_image(self, filepath: str) -> dict[str, Any]:
|
|
188
|
+
return self.upload_file(filepath)
|
|
189
|
+
|
|
157
190
|
# -- ComfyUI Userdata API --
|
|
158
191
|
|
|
159
192
|
def list_userdata_workflows(self) -> list[str]:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""comfyui-skill cancel — cancel a running or queued job."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..client import ComfyUIClient
|
|
8
|
+
from ..config import get_base_dir, get_default_server_id, get_server, load_config
|
|
9
|
+
from ..output import output_error, output_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cancel_cmd(
|
|
13
|
+
ctx: typer.Context,
|
|
14
|
+
prompt_id: str = typer.Argument(help="Prompt ID to cancel"),
|
|
15
|
+
):
|
|
16
|
+
"""Cancel a running or queued job. Interrupts if running, removes from queue if pending."""
|
|
17
|
+
base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
|
|
18
|
+
config = load_config(base_dir)
|
|
19
|
+
server_id = ctx.obj.get("server") or get_default_server_id(config)
|
|
20
|
+
server_config = get_server(config, server_id)
|
|
21
|
+
|
|
22
|
+
if not server_config:
|
|
23
|
+
output_error(ctx, "SERVER_NOT_FOUND", f'Server "{server_id}" not found.')
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
client = ComfyUIClient(
|
|
27
|
+
server_url=server_config.get("url", "http://127.0.0.1:8188"),
|
|
28
|
+
auth=server_config.get("auth", ""),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
queue = client.get_queue()
|
|
33
|
+
|
|
34
|
+
# Check if running
|
|
35
|
+
for item in queue.get("queue_running", []):
|
|
36
|
+
if len(item) > 1 and item[1] == prompt_id:
|
|
37
|
+
client.interrupt(prompt_id)
|
|
38
|
+
output_result(ctx, {"status": "interrupted", "prompt_id": prompt_id})
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Check if pending
|
|
42
|
+
for item in queue.get("queue_pending", []):
|
|
43
|
+
if len(item) > 1 and item[1] == prompt_id:
|
|
44
|
+
client.queue_delete([prompt_id])
|
|
45
|
+
output_result(ctx, {"status": "removed", "prompt_id": prompt_id})
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
output_result(ctx, {"status": "not_found", "prompt_id": prompt_id})
|
|
49
|
+
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
output_error(ctx, "CANCEL_FAILED", f"Failed to cancel job: {exc}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""comfyui-skill free — release GPU memory and unload models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..client import ComfyUIClient
|
|
8
|
+
from ..config import get_base_dir, get_default_server_id, get_server, load_config
|
|
9
|
+
from ..output import output_error, output_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def free_cmd(
|
|
13
|
+
ctx: typer.Context,
|
|
14
|
+
models: bool = typer.Option(False, "--models", "-m", help="Unload all models from VRAM"),
|
|
15
|
+
memory: bool = typer.Option(False, "--memory", help="Free all cached memory"),
|
|
16
|
+
):
|
|
17
|
+
"""Release GPU memory. With no flags, unloads models and frees memory."""
|
|
18
|
+
base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
|
|
19
|
+
config = load_config(base_dir)
|
|
20
|
+
server_id = ctx.obj.get("server") or get_default_server_id(config)
|
|
21
|
+
server_config = get_server(config, server_id)
|
|
22
|
+
|
|
23
|
+
if not server_config:
|
|
24
|
+
output_error(ctx, "SERVER_NOT_FOUND", f'Server "{server_id}" not found.')
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
client = ComfyUIClient(
|
|
28
|
+
server_url=server_config.get("url", "http://127.0.0.1:8188"),
|
|
29
|
+
auth=server_config.get("auth", ""),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Default: both if no flag specified
|
|
33
|
+
if not models and not memory:
|
|
34
|
+
models = True
|
|
35
|
+
memory = True
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
client.free_memory(unload_models=models, free_memory=memory)
|
|
39
|
+
actions = []
|
|
40
|
+
if models:
|
|
41
|
+
actions.append("models_unloaded")
|
|
42
|
+
if memory:
|
|
43
|
+
actions.append("memory_freed")
|
|
44
|
+
output_result(ctx, {"status": "ok", "actions": actions, "server_id": server_id})
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
output_error(ctx, "FREE_FAILED", f"Failed to free memory: {exc}")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""comfyui-skill queue — view and manage the ComfyUI execution queue."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..client import ComfyUIClient
|
|
10
|
+
from ..config import get_base_dir, get_default_server_id, get_server, load_config
|
|
11
|
+
from ..output import output_error, output_result
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(no_args_is_help=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _build_client(ctx: typer.Context) -> tuple[ComfyUIClient, str]:
|
|
17
|
+
base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
|
|
18
|
+
config = load_config(base_dir)
|
|
19
|
+
server_id = ctx.obj.get("server") or get_default_server_id(config)
|
|
20
|
+
server_config = get_server(config, server_id)
|
|
21
|
+
|
|
22
|
+
if not server_config:
|
|
23
|
+
output_error(ctx, "SERVER_NOT_FOUND", f'Server "{server_id}" not found.')
|
|
24
|
+
|
|
25
|
+
client = ComfyUIClient(
|
|
26
|
+
server_url=server_config.get("url", "http://127.0.0.1:8188"),
|
|
27
|
+
auth=server_config.get("auth", ""),
|
|
28
|
+
)
|
|
29
|
+
return client, server_id
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command("list")
|
|
33
|
+
def queue_list(ctx: typer.Context):
|
|
34
|
+
"""Show running and pending jobs in the queue."""
|
|
35
|
+
client, server_id = _build_client(ctx)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
queue = client.get_queue()
|
|
39
|
+
running = [
|
|
40
|
+
{"prompt_id": item[1], "status": "running"}
|
|
41
|
+
for item in queue.get("queue_running", [])
|
|
42
|
+
if len(item) > 1
|
|
43
|
+
]
|
|
44
|
+
pending = [
|
|
45
|
+
{"prompt_id": item[1], "status": "pending", "position": i}
|
|
46
|
+
for i, item in enumerate(queue.get("queue_pending", []))
|
|
47
|
+
if len(item) > 1
|
|
48
|
+
]
|
|
49
|
+
output_result(ctx, {
|
|
50
|
+
"server_id": server_id,
|
|
51
|
+
"running": running,
|
|
52
|
+
"pending": pending,
|
|
53
|
+
})
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
output_error(ctx, "QUEUE_FAILED", f"Failed to get queue: {exc}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("clear")
|
|
59
|
+
def queue_clear(ctx: typer.Context):
|
|
60
|
+
"""Clear all pending jobs from the queue (does not stop running jobs)."""
|
|
61
|
+
client, server_id = _build_client(ctx)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
client.queue_clear()
|
|
65
|
+
output_result(ctx, {"status": "cleared", "server_id": server_id})
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
output_error(ctx, "QUEUE_FAILED", f"Failed to clear queue: {exc}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("delete")
|
|
71
|
+
def queue_delete(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
prompt_ids: List[str] = typer.Argument(help="Prompt IDs to remove from queue"),
|
|
74
|
+
):
|
|
75
|
+
"""Remove specific jobs from the pending queue."""
|
|
76
|
+
client, server_id = _build_client(ctx)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
client.queue_delete(prompt_ids)
|
|
80
|
+
output_result(ctx, {"status": "deleted", "prompt_ids": prompt_ids, "server_id": server_id})
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
output_error(ctx, "QUEUE_FAILED", f"Failed to delete from queue: {exc}")
|
|
@@ -18,6 +18,7 @@ import typer
|
|
|
18
18
|
from ..client import ComfyUIClient
|
|
19
19
|
from ..config import get_base_dir, get_default_server_id, get_server, load_config
|
|
20
20
|
from ..output import OutputFormat, get_output_format, is_machine_mode, output_error, output_event, output_result
|
|
21
|
+
from ..error_hints import match_error_hint
|
|
21
22
|
from ..storage import get_schema, get_workflow_data
|
|
22
23
|
|
|
23
24
|
_POLL_INITIAL = 1.0
|
|
@@ -85,8 +86,9 @@ def run_cmd(
|
|
|
85
86
|
|
|
86
87
|
if status_info.get("status_str") == "error":
|
|
87
88
|
error_msg = _format_errors(history)
|
|
89
|
+
hint = match_error_hint(error_msg)
|
|
88
90
|
output_event(ctx, "error", prompt_id=prompt_id, message=error_msg)
|
|
89
|
-
output_error(ctx, "EXECUTION_FAILED", error_msg)
|
|
91
|
+
output_error(ctx, "EXECUTION_FAILED", error_msg, hint=hint)
|
|
90
92
|
return
|
|
91
93
|
|
|
92
94
|
# Check queue position
|
|
@@ -166,7 +168,12 @@ def status_cmd(
|
|
|
166
168
|
output_result(ctx, {"status": "success", "prompt_id": prompt_id, "outputs": collected})
|
|
167
169
|
return
|
|
168
170
|
if status_info.get("status_str") == "error":
|
|
169
|
-
|
|
171
|
+
error_msg = _format_errors(history)
|
|
172
|
+
hint = match_error_hint(error_msg)
|
|
173
|
+
result: dict[str, Any] = {"status": "error", "prompt_id": prompt_id, "error": error_msg}
|
|
174
|
+
if hint:
|
|
175
|
+
result["hint"] = hint
|
|
176
|
+
output_result(ctx, result)
|
|
170
177
|
return
|
|
171
178
|
|
|
172
179
|
# Check queue
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""comfyui-skill upload — upload
|
|
1
|
+
"""comfyui-skill upload — upload files to ComfyUI server."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -13,11 +13,11 @@ from ..output import output_error, output_result
|
|
|
13
13
|
|
|
14
14
|
def upload_cmd(
|
|
15
15
|
ctx: typer.Context,
|
|
16
|
-
|
|
16
|
+
file_path: str = typer.Argument(help="Path to file (image, mask, audio, etc.)"),
|
|
17
17
|
):
|
|
18
|
-
"""Upload
|
|
19
|
-
if not os.path.isfile(
|
|
20
|
-
output_error(ctx, "FILE_NOT_FOUND", f'
|
|
18
|
+
"""Upload a file to ComfyUI for use in workflows (e.g., images, masks, audio)."""
|
|
19
|
+
if not os.path.isfile(file_path):
|
|
20
|
+
output_error(ctx, "FILE_NOT_FOUND", f'File not found: "{file_path}"')
|
|
21
21
|
return
|
|
22
22
|
|
|
23
23
|
base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
|
|
@@ -35,7 +35,7 @@ def upload_cmd(
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
try:
|
|
38
|
-
result = client.
|
|
38
|
+
result = client.upload_file(file_path)
|
|
39
39
|
output_result(ctx, {
|
|
40
40
|
"name": result.get("name", ""),
|
|
41
41
|
"subfolder": result.get("subfolder", ""),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Common error pattern matching — maps ComfyUI errors to actionable hints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Common error patterns → actionable hints
|
|
6
|
+
_ERROR_HINTS: list[tuple[str, str]] = [
|
|
7
|
+
(
|
|
8
|
+
"Unauthorized: Please login first",
|
|
9
|
+
"This workflow uses ComfyUI cloud API nodes. "
|
|
10
|
+
"Configure a ComfyUI API Key: (1) Go to https://platform.comfy.org to generate a key, "
|
|
11
|
+
'(2) Add it to server settings via Web UI or config.json "comfy_api_key" field.',
|
|
12
|
+
),
|
|
13
|
+
(
|
|
14
|
+
"ckpt",
|
|
15
|
+
"A required model file is missing. "
|
|
16
|
+
"Run `comfyui-skill deps check <workflow_id>` to identify missing models.",
|
|
17
|
+
),
|
|
18
|
+
(
|
|
19
|
+
"safetensors",
|
|
20
|
+
"A required model file is missing. "
|
|
21
|
+
"Run `comfyui-skill deps check <workflow_id>` to identify missing models.",
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
"Connection refused",
|
|
25
|
+
"ComfyUI server is not running. Start ComfyUI and try again.",
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"CUDA out of memory",
|
|
29
|
+
"GPU memory exhausted. Run `comfyui-skill free` to release VRAM, then retry.",
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
"MPS out of memory",
|
|
33
|
+
"GPU memory exhausted. Run `comfyui-skill free` to release VRAM, then retry.",
|
|
34
|
+
),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def match_error_hint(error_msg: str) -> str:
|
|
39
|
+
lower = error_msg.lower()
|
|
40
|
+
for pattern, hint in _ERROR_HINTS:
|
|
41
|
+
if pattern.lower() in lower:
|
|
42
|
+
return hint
|
|
43
|
+
return ""
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from . import __version__
|
|
8
|
-
from .commands import config, deps, history, run, server, skill, upload, workflow
|
|
8
|
+
from .commands import cancel, config, deps, free, history, queue, run, server, skill, upload, workflow
|
|
9
9
|
from .update_check import is_machine_output, maybe_notify_update
|
|
10
10
|
|
|
11
11
|
app = typer.Typer(
|
|
@@ -49,7 +49,7 @@ def main(
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
# Subcommand groups — each needs a callback to inherit parent context
|
|
52
|
-
for sub_app in [config.app, deps.app, history.app, server.app, workflow.app]:
|
|
52
|
+
for sub_app in [config.app, deps.app, history.app, queue.app, server.app, workflow.app]:
|
|
53
53
|
@sub_app.callback()
|
|
54
54
|
def _pass_context(ctx: typer.Context):
|
|
55
55
|
if ctx.parent and ctx.parent.obj:
|
|
@@ -59,6 +59,7 @@ for sub_app in [config.app, deps.app, history.app, server.app, workflow.app]:
|
|
|
59
59
|
app.add_typer(config.app, name="config", help="Import/export configuration")
|
|
60
60
|
app.add_typer(deps.app, name="deps", help="Manage dependencies")
|
|
61
61
|
app.add_typer(history.app, name="history", help="Execution history")
|
|
62
|
+
app.add_typer(queue.app, name="queue", help="View and manage execution queue")
|
|
62
63
|
app.add_typer(server.app, name="server", help="Manage servers")
|
|
63
64
|
app.add_typer(workflow.app, name="workflow", help="Manage workflows")
|
|
64
65
|
|
|
@@ -69,3 +70,5 @@ app.command("run")(run.run_cmd)
|
|
|
69
70
|
app.command("submit")(run.submit_cmd)
|
|
70
71
|
app.command("status")(run.status_cmd)
|
|
71
72
|
app.command("upload")(upload.upload_cmd)
|
|
73
|
+
app.command("cancel")(cancel.cancel_cmd)
|
|
74
|
+
app.command("free")(free.free_cmd)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Tests for new client methods and error hint matching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
from comfyui_skills_cli.client import ComfyUIClient
|
|
9
|
+
from comfyui_skills_cli.error_hints import match_error_hint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InterruptTests(unittest.TestCase):
|
|
13
|
+
def setUp(self) -> None:
|
|
14
|
+
self.client = ComfyUIClient("http://localhost:8188")
|
|
15
|
+
|
|
16
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
17
|
+
def test_interrupt_with_prompt_id(self, mock_post: MagicMock) -> None:
|
|
18
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
19
|
+
result = self.client.interrupt("abc-123")
|
|
20
|
+
self.assertTrue(result["success"])
|
|
21
|
+
call_kwargs = mock_post.call_args
|
|
22
|
+
self.assertIn("/interrupt", call_kwargs.args[0])
|
|
23
|
+
self.assertEqual(call_kwargs.kwargs["json"], {"prompt_id": "abc-123"})
|
|
24
|
+
|
|
25
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
26
|
+
def test_interrupt_without_prompt_id(self, mock_post: MagicMock) -> None:
|
|
27
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
28
|
+
result = self.client.interrupt()
|
|
29
|
+
self.assertTrue(result["success"])
|
|
30
|
+
call_kwargs = mock_post.call_args
|
|
31
|
+
self.assertEqual(call_kwargs.kwargs["json"], {})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QueueManagementTests(unittest.TestCase):
|
|
35
|
+
def setUp(self) -> None:
|
|
36
|
+
self.client = ComfyUIClient("http://localhost:8188")
|
|
37
|
+
|
|
38
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
39
|
+
def test_queue_clear(self, mock_post: MagicMock) -> None:
|
|
40
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
41
|
+
result = self.client.queue_clear()
|
|
42
|
+
self.assertTrue(result["success"])
|
|
43
|
+
call_kwargs = mock_post.call_args
|
|
44
|
+
self.assertEqual(call_kwargs.kwargs["json"], {"clear": True})
|
|
45
|
+
|
|
46
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
47
|
+
def test_queue_delete(self, mock_post: MagicMock) -> None:
|
|
48
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
49
|
+
result = self.client.queue_delete(["id-1", "id-2"])
|
|
50
|
+
self.assertTrue(result["success"])
|
|
51
|
+
call_kwargs = mock_post.call_args
|
|
52
|
+
self.assertEqual(call_kwargs.kwargs["json"], {"delete": ["id-1", "id-2"]})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FreeMemoryTests(unittest.TestCase):
|
|
56
|
+
def setUp(self) -> None:
|
|
57
|
+
self.client = ComfyUIClient("http://localhost:8188")
|
|
58
|
+
|
|
59
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
60
|
+
def test_free_both(self, mock_post: MagicMock) -> None:
|
|
61
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
62
|
+
result = self.client.free_memory(unload_models=True, free_memory=True)
|
|
63
|
+
self.assertTrue(result["success"])
|
|
64
|
+
call_kwargs = mock_post.call_args
|
|
65
|
+
self.assertEqual(call_kwargs.kwargs["json"], {"unload_models": True, "free_memory": True})
|
|
66
|
+
|
|
67
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
68
|
+
def test_free_models_only(self, mock_post: MagicMock) -> None:
|
|
69
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
70
|
+
self.client.free_memory(unload_models=True, free_memory=False)
|
|
71
|
+
call_kwargs = mock_post.call_args
|
|
72
|
+
self.assertEqual(call_kwargs.kwargs["json"], {"unload_models": True})
|
|
73
|
+
|
|
74
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
75
|
+
def test_free_memory_only(self, mock_post: MagicMock) -> None:
|
|
76
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
77
|
+
self.client.free_memory(unload_models=False, free_memory=True)
|
|
78
|
+
call_kwargs = mock_post.call_args
|
|
79
|
+
self.assertEqual(call_kwargs.kwargs["json"], {"free_memory": True})
|
|
80
|
+
|
|
81
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
82
|
+
def test_free_no_flags_sends_empty(self, mock_post: MagicMock) -> None:
|
|
83
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
84
|
+
self.client.free_memory(unload_models=False, free_memory=False)
|
|
85
|
+
call_kwargs = mock_post.call_args
|
|
86
|
+
self.assertEqual(call_kwargs.kwargs["json"], {})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class UploadFileTests(unittest.TestCase):
|
|
90
|
+
def setUp(self) -> None:
|
|
91
|
+
self.client = ComfyUIClient("http://localhost:8188")
|
|
92
|
+
|
|
93
|
+
@patch("comfyui_skills_cli.client.requests.post")
|
|
94
|
+
def test_upload_file_calls_upload_image_endpoint(self, mock_post: MagicMock) -> None:
|
|
95
|
+
mock_post.return_value = MagicMock(status_code=200)
|
|
96
|
+
mock_post.return_value.json.return_value = {"name": "test.png", "subfolder": "", "type": "input"}
|
|
97
|
+
|
|
98
|
+
import tempfile, os
|
|
99
|
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
|
100
|
+
f.write(b"fake png data")
|
|
101
|
+
tmp_path = f.name
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
result = self.client.upload_file(tmp_path)
|
|
105
|
+
self.assertEqual(result["name"], "test.png")
|
|
106
|
+
call_args = mock_post.call_args
|
|
107
|
+
self.assertIn("/upload/image", call_args.args[0])
|
|
108
|
+
finally:
|
|
109
|
+
os.unlink(tmp_path)
|
|
110
|
+
|
|
111
|
+
def test_upload_image_delegates_to_upload_file(self) -> None:
|
|
112
|
+
with patch.object(self.client, "upload_file", return_value={"name": "x.png"}) as mock:
|
|
113
|
+
result = self.client.upload_image("/fake/path.png")
|
|
114
|
+
mock.assert_called_once_with("/fake/path.png")
|
|
115
|
+
self.assertEqual(result["name"], "x.png")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ErrorHintTests(unittest.TestCase):
|
|
119
|
+
def test_unauthorized_hint(self) -> None:
|
|
120
|
+
hint = match_error_hint("Unauthorized: Please login first to use this node.")
|
|
121
|
+
self.assertIn("ComfyUI API Key", hint)
|
|
122
|
+
self.assertIn("platform.comfy.org", hint)
|
|
123
|
+
|
|
124
|
+
def test_cuda_oom_hint(self) -> None:
|
|
125
|
+
hint = match_error_hint("RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB")
|
|
126
|
+
self.assertIn("comfyui-skill free", hint)
|
|
127
|
+
|
|
128
|
+
def test_mps_oom_hint(self) -> None:
|
|
129
|
+
hint = match_error_hint("RuntimeError: MPS out of memory")
|
|
130
|
+
self.assertIn("comfyui-skill free", hint)
|
|
131
|
+
|
|
132
|
+
def test_connection_refused_hint(self) -> None:
|
|
133
|
+
hint = match_error_hint("ConnectionError: Connection refused")
|
|
134
|
+
self.assertIn("not running", hint)
|
|
135
|
+
|
|
136
|
+
def test_missing_model_ckpt_hint(self) -> None:
|
|
137
|
+
hint = match_error_hint("FileNotFoundError: model_v1.ckpt not found")
|
|
138
|
+
self.assertIn("deps check", hint)
|
|
139
|
+
|
|
140
|
+
def test_missing_model_safetensors_hint(self) -> None:
|
|
141
|
+
hint = match_error_hint("Could not load sdxl_base.safetensors")
|
|
142
|
+
self.assertIn("deps check", hint)
|
|
143
|
+
|
|
144
|
+
def test_no_hint_for_unknown_error(self) -> None:
|
|
145
|
+
hint = match_error_hint("Some random error message")
|
|
146
|
+
self.assertEqual(hint, "")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.4"
|
|
File without changes
|
|
File without changes
|
{comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|