Skip to content

Commit 8637f6a

Browse files
feat: add generic --feature-flag CLI arg and --list-feature-flags registry
Add --feature-flag KEY=VALUE CLI argument that allows setting arbitrary server feature flags at startup. Values are auto-converted to appropriate Python types (bool, int, float, string). CLI flags are merged into SERVER_FEATURE_FLAGS but cannot overwrite core flags. Add --list-feature-flags which prints the registry of known CLI-settable feature flags as JSON and exits, enabling launchers to discover valid flags for a specific ComfyUI version. Part of Comfy-Org/ComfyUI-Desktop-2.0-Beta#415 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d9386-54d3-74d9-a661-97e0a8d37b6b
1 parent 1de83f9 commit 8637f6a

File tree

4 files changed

+115
-3
lines changed

4 files changed

+115
-3
lines changed

comfy/cli_args.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ def is_valid_directory(path: str) -> str:
238238
)
239239
parser.add_argument("--database-url", type=str, default=f"sqlite:///{database_default_path}", help="Specify the database URL, e.g. for an in-memory database you can use 'sqlite:///:memory:'.")
240240
parser.add_argument("--enable-assets", action="store_true", help="Enable the assets system (API routes, database synchronization, and background scanning).")
241+
parser.add_argument("--feature-flag", type=str, action='append', default=[], metavar="KEY=VALUE", help="Set a server feature flag as a key=value pair. Can be specified multiple times. Boolean values (true/false) and numbers are auto-converted. Example: --feature-flag show_signin_button=true")
242+
parser.add_argument("--list-feature-flags", action="store_true", help="Print the registry of known CLI-settable feature flags as JSON and exit.")
241243

242244
if comfy.options.args_parsing:
243245
args = parser.parse_args()

comfy_api/feature_flags.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,78 @@
55
allowing graceful protocol evolution while maintaining backward compatibility.
66
"""
77

8-
from typing import Any
8+
from typing import Any, TypedDict
99

1010
from comfy.cli_args import args
1111

12+
13+
class FeatureFlagInfo(TypedDict):
14+
type: str
15+
default: Any
16+
description: str
17+
18+
19+
# Registry of known CLI-settable feature flags.
20+
# Launchers can query this via --list-feature-flags to discover valid flags.
21+
CLI_FEATURE_FLAG_REGISTRY: dict[str, FeatureFlagInfo] = {
22+
"show_signin_button": {
23+
"type": "bool",
24+
"default": False,
25+
"description": "Show the sign-in button in the frontend even when not signed in",
26+
},
27+
}
28+
29+
30+
def get_cli_feature_flag_registry() -> dict[str, FeatureFlagInfo]:
31+
"""Return the registry of known CLI-settable feature flags."""
32+
return {k: dict(v) for k, v in CLI_FEATURE_FLAG_REGISTRY.items()}
33+
34+
35+
_COERCE_FNS: dict[str, Any] = {
36+
"bool": lambda v: v.lower() == "true",
37+
"int": lambda v: int(v),
38+
"float": lambda v: float(v),
39+
}
40+
41+
42+
def _coerce_flag_value(key: str, raw_value: str) -> Any:
43+
"""Coerce a raw string value using the registry type, or keep as string."""
44+
info = CLI_FEATURE_FLAG_REGISTRY.get(key)
45+
if info is None:
46+
return raw_value
47+
coerce = _COERCE_FNS.get(info["type"])
48+
if coerce is None:
49+
return raw_value
50+
return coerce(raw_value)
51+
52+
53+
def _parse_cli_feature_flags() -> dict[str, Any]:
54+
"""Parse --feature-flag key=value pairs from CLI args into a dict."""
55+
result: dict[str, Any] = {}
56+
for item in getattr(args, "feature_flag", []):
57+
if "=" not in item:
58+
continue
59+
key, _, raw_value = item.partition("=")
60+
key = key.strip()
61+
if key:
62+
result[key] = _coerce_flag_value(key, raw_value.strip())
63+
return result
64+
65+
1266
# Default server capabilities
13-
SERVER_FEATURE_FLAGS: dict[str, Any] = {
67+
_CORE_FEATURE_FLAGS: dict[str, Any] = {
1468
"supports_preview_metadata": True,
1569
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
1670
"extension": {"manager": {"supports_v4": True}},
1771
"node_replacements": True,
1872
"assets": args.enable_assets,
1973
}
2074

75+
# CLI-provided flags cannot overwrite core flags
76+
_cli_flags = {k: v for k, v in _parse_cli_feature_flags().items() if k not in _CORE_FEATURE_FLAGS}
77+
78+
SERVER_FEATURE_FLAGS: dict[str, Any] = {**_CORE_FEATURE_FLAGS, **_cli_flags}
79+
2180

2281
def get_connection_feature(
2382
sockets_metadata: dict[str, dict[str, Any]],

main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import comfy.options
22
comfy.options.enable_args_parsing()
33

4+
from comfy.cli_args import args
5+
6+
if args.list_feature_flags:
7+
import json
8+
from comfy_api.feature_flags import get_cli_feature_flag_registry
9+
print(json.dumps(get_cli_feature_flag_registry(), indent=2)) # noqa: T201
10+
raise SystemExit(0)
11+
412
import os
513
import importlib.util
614
import shutil
715
import importlib.metadata
816
import folder_paths
917
import time
10-
from comfy.cli_args import args, enables_dynamic_vram
18+
from comfy.cli_args import enables_dynamic_vram
1119
from app.logger import setup_logger
1220
from app.assets.seeder import asset_seeder
1321
from app.assets.services import register_output_files

tests-unit/feature_flags_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
get_connection_feature,
55
supports_feature,
66
get_server_features,
7+
get_cli_feature_flag_registry,
78
SERVER_FEATURE_FLAGS,
9+
_coerce_flag_value,
10+
_parse_cli_feature_flags,
811
)
912

1013

@@ -96,3 +99,43 @@ def test_empty_feature_flags_dict(self):
9699
result = get_connection_feature(sockets_metadata, "sid1", "any_feature")
97100
assert result is False
98101
assert supports_feature(sockets_metadata, "sid1", "any_feature") is False
102+
103+
104+
class TestCoerceFlagValue:
105+
"""Test suite for _coerce_flag_value."""
106+
107+
def test_registered_bool_true(self):
108+
assert _coerce_flag_value("show_signin_button", "true") is True
109+
assert _coerce_flag_value("show_signin_button", "True") is True
110+
111+
def test_registered_bool_false(self):
112+
assert _coerce_flag_value("show_signin_button", "false") is False
113+
assert _coerce_flag_value("show_signin_button", "FALSE") is False
114+
115+
def test_unregistered_key_stays_string(self):
116+
assert _coerce_flag_value("unknown_flag", "true") == "true"
117+
assert _coerce_flag_value("unknown_flag", "42") == "42"
118+
119+
120+
class TestParseCliFeatureFlags:
121+
"""Test suite for _parse_cli_feature_flags."""
122+
123+
def test_single_flag(self, monkeypatch):
124+
monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["show_signin_button=true"]})())
125+
result = _parse_cli_feature_flags()
126+
assert result == {"show_signin_button": True}
127+
128+
def test_missing_equals_skipped(self, monkeypatch):
129+
monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["noequals", "valid=1"]})())
130+
result = _parse_cli_feature_flags()
131+
assert result == {"valid": "1"}
132+
133+
134+
class TestCliFeatureFlagRegistry:
135+
"""Test suite for the CLI feature flag registry."""
136+
137+
def test_registry_entries_have_required_fields(self):
138+
for key, info in get_cli_feature_flag_registry().items():
139+
assert "type" in info, f"{key} missing 'type'"
140+
assert "default" in info, f"{key} missing 'default'"
141+
assert "description" in info, f"{key} missing 'description'"

0 commit comments

Comments
 (0)