Repository navigation
Expand file tree
/
Copy pathprestartup_script.py
More file actions
451 lines (362 loc) · 15.6 KB
/
Copy pathprestartup_script.py
File metadata and controls
451 lines (362 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
"""
ComfyUI_Viewer Extension Auto-Installer
This script runs at ComfyUI startup before nodes are imported.
It automatically installs View Extensions from .zip files placed in the extensions folder.
Usage:
1. Download an extension from GitHub as a .zip file
2. Place the .zip in ComfyUI_Viewer/extensions/
3. Restart ComfyUI - the extension will be automatically installed
The script will:
- Extract nodes/, web/views/, and modules/parsers/ to ComfyUI_Viewer
- Install requirements.txt using ComfyUI's Python interpreter
- Create a log file to track installation (skip if already installed)
"""
import sys
import zipfile
import shutil
import subprocess
import datetime
import json
from pathlib import Path
# Get paths
SCRIPT_DIR = Path(__file__).parent.resolve()
EXTENSIONS_DIR = SCRIPT_DIR / "extensions"
LOGS_DIR = EXTENSIONS_DIR / "logs"
EXTENSION_VIEWS_JSON = SCRIPT_DIR / "web" / "views" / "extension_views.json"
# Folders to extract from extension zips
EXTRACT_FOLDERS = {
"nodes": SCRIPT_DIR / "nodes",
"web/views": SCRIPT_DIR / "web" / "views",
"modules/parsers": SCRIPT_DIR / "modules" / "parsers",
"routes": SCRIPT_DIR / "routes",
"apps": SCRIPT_DIR / "apps",
}
def print_progress(message: str, level: str = "INFO"):
"""Print progress message."""
prefix = {
"INFO": "\033[94m•\033[0m",
"OK": "\033[92m✓\033[0m",
"WARN": "\033[93m!\033[0m",
"ERROR": "\033[91m✗\033[0m",
"PROGRESS": "\033[96m→\033[0m",
"HEADER": "\033[95m■\033[0m",
}.get(level, "•")
print(f" {prefix} {message}")
def get_python_executable() -> str:
"""Get the Python executable used by ComfyUI."""
return sys.executable
def ensure_directories():
"""Ensure extensions and logs directories exist."""
EXTENSIONS_DIR.mkdir(exist_ok=True)
LOGS_DIR.mkdir(exist_ok=True)
# Create .gitkeep to preserve empty folders
gitkeep = EXTENSIONS_DIR / ".gitkeep"
if not gitkeep.exists():
gitkeep.write_text("# Place extension .zip files here\n")
def get_log_path(zip_name: str) -> Path:
"""Get the log file path for an extension."""
base_name = zip_name.rsplit(".", 1)[0]
return LOGS_DIR / f"{base_name}_install.log"
def is_installed(zip_name: str) -> bool:
"""Check if an extension is already installed (log file exists)."""
return get_log_path(zip_name).exists()
def find_extension_root(zip_ref: zipfile.ZipFile) -> str:
"""
Find the root folder inside the zip (GitHub adds branch suffix).
e.g., ComfyUI_Viewer_Image_Search-main/
"""
names = zip_ref.namelist()
# Find the common root folder
roots = set()
for name in names:
parts = name.split("/")
if len(parts) > 1:
roots.add(parts[0])
if len(roots) == 1:
return list(roots)[0]
return ""
def extract_extension(zip_path: Path, log_lines: list) -> bool:
"""
Extract extension folders from zip to ComfyUI_Viewer.
Returns True if successful.
"""
try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
root = find_extension_root(zip_ref)
if not root:
log_lines.append(
"ERROR: Could not determine extension root folder in zip"
)
return False
log_lines.append(f"Extension root: {root}")
extracted_count = 0
for source_folder, dest_folder in EXTRACT_FOLDERS.items():
source_prefix = f"{root}/{source_folder}/"
# Find matching files
matching_files = [
name
for name in zip_ref.namelist()
if name.startswith(source_prefix) and not name.endswith("/")
]
if not matching_files:
log_lines.append(f" No files found in {source_folder}/")
continue
log_lines.append(
f" Extracting {len(matching_files)} files from {source_folder}/"
)
for file_path in matching_files:
# Calculate relative path and destination
rel_path = file_path[len(source_prefix) :]
dest_path = dest_folder / rel_path
# Ensure parent directory exists
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Extract file
with zip_ref.open(file_path) as src:
with open(dest_path, "wb") as dst:
dst.write(src.read())
extracted_count += 1
log_lines.append(f" -> {dest_path.relative_to(SCRIPT_DIR)}")
log_lines.append(f"Extracted {extracted_count} files total")
return extracted_count > 0
except zipfile.BadZipFile:
log_lines.append("ERROR: Invalid or corrupted zip file")
return False
except Exception as e:
log_lines.append(f"ERROR: Extraction failed: {e}")
return False
def extract_and_install_requirements(zip_path: Path, log_lines: list) -> bool:
"""
Extract requirements.txt from zip and install using pip.
Returns True if successful (or no requirements).
"""
try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
root = find_extension_root(zip_ref)
requirements_path = f"{root}/requirements.txt"
# Check if requirements.txt exists in zip
if requirements_path not in zip_ref.namelist():
log_lines.append("No requirements.txt found - skipping pip install")
return True
# Extract to extensions folder with unique name
base_name = zip_path.stem
dest_requirements = EXTENSIONS_DIR / f"{base_name}_requirements.txt"
with zip_ref.open(requirements_path) as src:
content = src.read().decode("utf-8")
dest_requirements.write_text(content)
log_lines.append(f"Extracted requirements to: {dest_requirements.name}")
log_lines.append(f"Requirements content:\n{content}")
# Install using pip
python_exe = get_python_executable()
log_lines.append(f"Installing requirements with: {python_exe}")
result = subprocess.run(
[python_exe, "-m", "pip", "install", "-r", str(dest_requirements)],
capture_output=True,
text=True,
timeout=300, # 5 minute timeout
)
log_lines.append(f"pip stdout:\n{result.stdout}")
if result.stderr:
log_lines.append(f"pip stderr:\n{result.stderr}")
if result.returncode != 0:
log_lines.append(
f"ERROR: pip install failed with code {result.returncode}"
)
return False
log_lines.append("Requirements installed successfully")
return True
except subprocess.TimeoutExpired:
log_lines.append("ERROR: pip install timed out after 5 minutes")
return False
except Exception as e:
log_lines.append(f"ERROR: Requirements installation failed: {e}")
return False
def install_extension(zip_path: Path) -> bool:
"""
Install a single extension from a zip file.
Returns True if successful.
"""
zip_name = zip_path.name
# Extract friendly name (remove -main/-master suffix and .zip)
friendly_name = (
zip_name.replace("-main.zip", "").replace("-master.zip", "").replace(".zip", "")
)
log_lines = []
log_lines.append("=" * 60)
log_lines.append("ComfyUI_Viewer Extension Installation Log")
log_lines.append(f"Extension: {zip_name}")
log_lines.append(f"Date: {datetime.datetime.now().isoformat()}")
log_lines.append("=" * 60)
log_lines.append("")
print_progress(f"Installing: {friendly_name}", "HEADER")
# Step 1: Extract folders
print_progress("Extracting files...", "PROGRESS")
log_lines.append("Step 1: Extracting files")
log_lines.append("-" * 40)
if not extract_extension(zip_path, log_lines):
log_lines.append("\nINSTALLATION FAILED: Extraction error")
print_progress("Extraction failed", "ERROR")
return False
log_lines.append("")
# Step 2: Install requirements
print_progress("Installing dependencies...", "PROGRESS")
log_lines.append("Step 2: Installing requirements")
log_lines.append("-" * 40)
if not extract_and_install_requirements(zip_path, log_lines):
log_lines.append("\nINSTALLATION FAILED: Requirements error")
print_progress("Dependency installation failed", "ERROR")
return False
log_lines.append("")
log_lines.append("=" * 60)
log_lines.append("INSTALLATION COMPLETED SUCCESSFULLY")
log_lines.append("=" * 60)
# Write log file (marks as installed)
log_path = get_log_path(zip_name)
log_path.write_text("\n".join(log_lines))
print_progress("Installed successfully", "OK")
return True
def sync_extension_directories():
"""
Sync sibling ComfyUI_Viewer_* extension directories into ComfyUI_Viewer.
Extensions can be installed either from .zip files (handled by run_extension_installer)
or placed as sibling directories alongside ComfyUI_Viewer. This function handles the
latter by copying all extension assets into ComfyUI_Viewer so the node loader, parser
loader, and view loader can discover them.
Runs every startup to keep files in sync. Uses the same folder mapping as
EXTRACT_FOLDERS to stay consistent with zip-based installation.
"""
workspace_dir = SCRIPT_DIR.parent
if not workspace_dir.is_dir():
return
for entry in sorted(workspace_dir.iterdir()):
if not entry.name.startswith("ComfyUI_Viewer_") or not entry.is_dir():
continue
for source_folder, dest_folder in EXTRACT_FOLDERS.items():
ext_source = entry / source_folder
if not ext_source.is_dir():
continue
dest_folder.mkdir(parents=True, exist_ok=True)
# Walk the source tree and copy all files
for src_file in ext_source.rglob("*"):
if src_file.is_dir() or src_file.name.startswith("_"):
continue
rel = src_file.relative_to(ext_source)
dest_path = dest_folder / rel
try:
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dest_path)
except Exception as e:
print_progress(
f"Failed to copy {rel} from {entry.name}: {e}", "WARN"
)
def update_extension_views_json():
"""
Update extension_views.json with list of installed extension view files.
This allows the view_loader to dynamically discover extension views.
Scans two locations:
1. Installed views in ComfyUI_Viewer/web/views/ (from zip installs)
2. Sibling ComfyUI_Viewer_* directories for dev/unzipped extensions
"""
views_dir = SCRIPT_DIR / "web" / "views"
# Core views from view_manifest.js (these are not extension views)
core_views = {
"canvas.js",
"html.js",
"svg.js",
"markdown.js",
"json.js",
"csv.js",
"yaml.js",
"ansi.js",
"python.js",
"javascript.js",
"css.js",
"object.js",
"text.js",
}
skip_files = {
"view_manifest.js",
"view_loader.js",
"base_view.js",
"code_scripts.js",
}
# Find all .js files in views folder that aren't core views or utility files
extension_views = []
seen = set()
if views_dir.exists():
for js_file in views_dir.glob("*.js"):
filename = js_file.name
if filename in core_views or filename in skip_files:
continue
extension_views.append(filename)
seen.add(filename)
# Scan sibling ComfyUI_Viewer_* extension directories for additional views
workspace_dir = SCRIPT_DIR.parent
if workspace_dir.is_dir():
for entry in sorted(workspace_dir.iterdir()):
if not entry.name.startswith("ComfyUI_Viewer_") or not entry.is_dir():
continue
ext_views_dir = entry / "web" / "views"
if not ext_views_dir.is_dir():
continue
for js_file in ext_views_dir.glob("*.js"):
filename = js_file.name
if filename in core_views or filename in skip_files or filename in seen:
continue
# Copy the view file into our views directory so the frontend can load it
dest = views_dir / filename
try:
shutil.copy2(js_file, dest)
extension_views.append(filename)
seen.add(filename)
except Exception as e:
print_progress(
f"Failed to copy extension view {filename} from {entry.name}: {e}",
"WARN",
)
# Write to JSON file
try:
EXTENSION_VIEWS_JSON.write_text(json.dumps(sorted(extension_views), indent=2))
except Exception as e:
print_progress(f"Failed to update extension_views.json: {e}", "WARN")
def run_extension_installer():
"""Main entry point - scan and install extensions."""
ensure_directories()
# Find all .zip files in extensions folder
zip_files = list(EXTENSIONS_DIR.glob("*.zip"))
if not zip_files:
return # No extensions to install, silent exit
# Filter to only uninstalled extensions
to_install = [z for z in zip_files if not is_installed(z.name)]
if not to_install:
return # All installed, silent exit
print("")
print("\033[95m ╔══════════════════════════════════════════════════╗\033[0m")
print(
"\033[95m ║\033[0m \033[1mComfyUI_Viewer Extension Installer\033[0m \033[95m║\033[0m"
)
print("\033[95m ╚══════════════════════════════════════════════════╝\033[0m")
print("")
print_progress(f"Found {len(to_install)} new extension(s) to install", "INFO")
print("")
success_count = 0
fail_count = 0
for i, zip_path in enumerate(to_install, 1):
if install_extension(zip_path):
success_count += 1
else:
fail_count += 1
print("")
if fail_count == 0:
print_progress(f"Done! {success_count} extension(s) installed", "OK")
else:
print_progress(f"Installed: {success_count}, Failed: {fail_count}", "WARN")
print("")
# Update extension views manifest for view_loader auto-discovery
if success_count > 0:
update_extension_views_json()
# Run when imported by ComfyUI
run_extension_installer()
# Sync sibling extension directories (nodes, parsers, web assets) into ComfyUI_Viewer
sync_extension_directories()
# Update extension views manifest for view_loader auto-discovery
update_extension_views_json()