Repository: OpenMined/PySyft

Issue:
In hagrid boolean arguments (e.g. `--cmd=True`) should be flags (just `--cmd`) instead
## Description
This has always been a gripe for me when working with hagrid. This and #7140 seem like small things but the main purpose of is to reduce friction between users and the entire Syft stack and would benefit from a smoother dev ex.

Boolean options like `--cmd=True`, `--tail=True`, `--headless=True` should have just been flags, *i.e.* `--cmd`, `--tail` and `--headless`. Reasons:

1. This is how most cli tools work.
2. `--cmd` achieves the same functionality and is also much shorter. Plus with `--cmd=True` users not familiar with the tools or those only use it once in a while will have a hard time remembering whether it's `--cmd`, `--cmd=True`, or `--cmd=true`, how about `--cmd=1`?
3. In hagrid there already exist other flags like `--silent`, `--tls`, `--test`, `--dev`, so this is both against cli best practice and inconsistent within hagrid itself.

## Are you interested in working on this improvement yourself?
Yes

@iamtrask gave his 👌 on fixing this and #7140. We're still deciding on the timeline. I know this is not originally planned as required for 0.7, and hagrid version is separated from syft, but with a new syft release a lot of folks are gonna use hagrid, some for the first time, so would be quite appropriate to make the changes available on 0.7 release.

My proposal is that I will work on these for the next week. If it takes more than that we can reassess.

## Additional Context
Add any other context or screenshots.



Discussion:
Totally agree. I think its worth doing some planning around rebuilding hagrid from scratch so that its much cleaner and more consistent and modular.

I think a new python cli code base could be started and the functionality we want ported over pretty quickly once we include the desired capabilities.

Lets discuss this post 0.7.

Resolving patch:
diff --git a/packages/hagrid/hagrid/cli.py b/packages/hagrid/hagrid/cli.py
--- a/packages/hagrid/hagrid/cli.py
+++ b/packages/hagrid/hagrid/cli.py
@@ -124,7 +124,8 @@ def cli() -> None:
 
 
 @click.command(
-    help="Restore some part of the hagrid installation or deployment to its initial/starting state."
+    help="Restore some part of the hagrid installation or deployment to its initial/starting state.",
+    context_settings={"show_default": True},
 )
 @click.argument("location", type=str, nargs=1)
 def clean(location: str) -> None:
@@ -145,89 +146,82 @@ def clean(location: str) -> None:
         subprocess.call("docker rmi $(docker images -q)", shell=True)  # nosec
 
 
-@click.command(help="Start a new PyGrid domain/network node!")
+@click.command(
+    help="Start a new PyGrid domain/network node!",
+    context_settings={"show_default": True},
+)
 @click.argument("args", type=str, nargs=-1)
 @click.option(
     "--username",
     default=None,
     required=False,
     type=str,
-    help="Optional: the username for provisioning the remote host",
+    help="Username for provisioning the remote host",
 )
 @click.option(
     "--key_path",
     default=None,
     required=False,
     type=str,
-    help="Optional: the path to the key file for provisioning the remote host",
+    help="Path to the key file for provisioning the remote host",
 )
 @click.option(
     "--password",
     default=None,
     required=False,
     type=str,
-    help="Optional: the password for provisioning the remote host",
+    help="Password for provisioning the remote host",
 )
 @click.option(
     "--repo",
     default=None,
     required=False,
     type=str,
-    help="Optional: repo to fetch source from",
+    help="Repo to fetch source from",
 )
 @click.option(
     "--branch",
     default=None,
     required=False,
     type=str,
-    help="Optional: branch to monitor for updates",
+    help="Branch to monitor for updates",
 )
 @click.option(
     "--tail",
-    default="false",
-    required=False,
-    type=str,
-    help="Optional: don't tail logs on launch",
+    is_flag=True,
+    help="Tail logs on launch",
 )
 @click.option(
     "--headless",
-    default="false",
-    required=False,
-    type=str,
-    help="Optional: don't start the frontend container",
+    is_flag=True,
+    help="Start the frontend container",
 )
 @click.option(
     "--cmd",
-    default="false",
-    required=False,
-    type=str,
-    help="Optional: print the cmd without running it",
+    is_flag=True,
+    help="Print the cmd without running it",
 )
 @click.option(
     "--jupyter",
     is_flag=True,
-    help="Optional: enable Jupyter Notebooks",
+    help="Enable Jupyter Notebooks",
 )
 @click.option(
     "--build",
-    default=None,
-    required=False,
-    type=str,
-    help="Optional: enable or disable forcing re-build",
+    is_flag=True,
+    help="Disable forcing re-build",
 )
 @click.option(
-    "--provision",
-    default="true",
-    required=False,
-    type=str,
-    help="Optional: enable or disable provisioning VMs",
+    "--no_provision",
+    is_flag=True,
+    help="Disable provisioning VMs",
 )
 @click.option(
     "--node_count",
     default=1,
     required=False,
     type=click.IntRange(1, 250),
-    help="Optional: number of independent nodes/VMs to launch",
+    help="Number of independent nodes/VMs to launch",
 )
 @click.option(
     "--auth_type",
@@ -247,89 +241,81 @@ def clean(location: str) -> None:
     default="production",
     required=False,
     type=click.Choice(["production", "development"], case_sensitive=False),
-    help="Optional: choose between production and development release",
+    help="Choose between production and development release",
 )
 @click.option(
     "--cert_store_path",
     default="/home/om/certs",
     required=False,
     type=str,
-    help="Optional: remote path to store and load TLS cert and key",
+    help="Remote path to store and load TLS cert and key",
 )
 @click.option(
     "--upload_tls_cert",
     default="",
     required=False,
     type=str,
-    help="Optional: local path to TLS cert to upload and store at --cert_store_path",
+    help="Local path to TLS cert to upload and store at --cert_store_path",
 )
 @click.option(
     "--upload_tls_key",
     default="",
     required=False,
     type=str,
-    help="Optional: local path to TLS private key to upload and store at --cert_store_path",
+    help="Local path to TLS private key to upload and store at --cert_store_path",
 )
 @click.option(
-    "--use_blob_storage",
-    default=None,
-    required=False,
-    type=str,
-    help="Optional: flag to use blob storage",
+    "--no_blob_storage",
+    is_flag=True,
+    help="Disable blob storage",
 )
 @click.option(
     "--image_name",
     default=None,
     required=False,
     type=str,
-    help="Optional: image to use for the VM",
+    help="Image to use for the VM",
 )
 @click.option(
     "--tag",
     default=None,
     required=False,
     type=str,
-    help="Optional: container image tag to use",
+    help="Container image tag to use",
 )
 @click.option(
     "--build_src",
     default=DEFAULT_BRANCH,
     required=False,
     type=str,
-    help="Optional: git branch to use for launch / build operations",
+    help="Git branch to use for launch / build operations",
 )
 @click.option(
     "--platform",
     default=None,
     required=False,
     type=str,
-    help="Optional: run docker with a different platform like linux/arm64",
+    help="Run docker with a different platform like linux/arm64",
 )
 @click.option(
-    "--vpn",
-    default="true",
-    required=False,
-    type=str,
-    help="Optional: turn tailscale vpn container on or off",
+    "--no_vpn",
+    is_flag=True,
+    help="Disable tailscale vpn container",
 )
 @click.option(
     "--silent",
     is_flag=True,
-    help="Optional: prevent lots of launch output",
+    help="Suppress extra launch outputs",
 )
 @click.option(
     "--from_template",
-    default="false",
-    required=False,
-    type=str,
-    help="Optional: launch node using the manifest template",
+    is_flag=True,
+    help="Launch node using the manifest template",
 )
 @click.option(
-    "--health_checks",
-    default="true",
-    required=False,
-    type=str,
-    help="Optional: turn on or off auto health checks post node launch",
+    "--no_health_checks",
+    is_flag=True,
+    help="Turn off auto health checks post node launch",
 )
 def launch(args: TypeTuple[str], **kwargs: Any) -> None:
     verb = get_launch_verb()
@@ -351,19 +337,15 @@ def launch(args: TypeTuple[str], **kwargs: Any) -> None:
         print(f"Error: {e}\n\n")
         return
 
-    dry_run = True
-    if "cmd" not in kwargs or str_to_bool(cast(str, kwargs["cmd"])) is False:
-        dry_run = False
+    dry_run = bool(kwargs["cmd"])
 
-    health_checks = str_to_bool(cast(str, kwargs["health_checks"]))
+    health_checks = not bool(kwargs["no_health_checks"])
 
     try:
-        tail = False if "tail" in kwargs and not str_to_bool(kwargs["tail"]) else True
+        tail = bool(kwargs["tail"])
         silent = not tail
 
-        from_rendered_dir = (
-            str_to_bool(cast(str, kwargs["from_template"])) and EDITABLE_MODE
-        )
+        from_rendered_dir = bool(kwargs["from_template"]) and EDITABLE_MODE
 
         execute_commands(
             cmds, dry_run=dry_run, silent=silent, from_rendered_dir=from_rendered_dir
@@ -1054,20 +1036,13 @@ def create_launch_cmd(
     host = host_term.host
     auth: Optional[AuthCredentials] = None
 
-    tail = True
-    if "tail" in kwargs and not str_to_bool(kwargs["tail"]):
-        tail = False
+    tail = bool(kwargs["tail"])
 
     parsed_kwargs = {}
 
-    if "build" in kwargs and kwargs["build"] is not None:
-        parsed_kwargs["build"] = str_to_bool(cast(str, kwargs["build"]))
-    else:
-        parsed_kwargs["build"] = None
+    parsed_kwargs["build"] = bool(kwargs["build"])
 
-    parsed_kwargs["use_blob_storage"] = (
-        kwargs["use_blob_storage"] if "use_blob_storage" in kwargs else None
-    )
+    parsed_kwargs["use_blob_storage"] = not bool(kwargs["no_blob_storage"])
 
     parsed_kwargs["node_count"] = (
         int(kwargs["node_count"]) if "node_count" in kwargs else 1
@@ -1079,19 +1054,15 @@ def create_launch_cmd(
         # Default to detached mode if running more than one nodes
         tail = False if parsed_kwargs["node_count"] > 1 else tail
 
-    headless = False
-    if "headless" in kwargs and str_to_bool(cast(str, kwargs["headless"])):
-        headless = True
+    headless = bool(kwargs["headless"])
     parsed_kwargs["headless"] = headless
 
-    parsed_kwargs["tls"] = bool(kwargs["tls"]) if "tls" in kwargs else False
-    parsed_kwargs["test"] = bool(kwargs["test"]) if "test" in kwargs else False
-    parsed_kwargs["dev"] = bool(kwargs["dev"]) if "dev" in kwargs else False
+    parsed_kwargs["tls"] = bool(kwargs["tls"])
+    parsed_kwargs["test"] = bool(kwargs["test"])
+    parsed_kwargs["dev"] = bool(kwargs["dev"])
 
-    parsed_kwargs["silent"] = bool(kwargs["silent"]) if "silent" in kwargs else False
-    parsed_kwargs["from_template"] = (
-        str_to_bool(kwargs["from_template"]) if "from_template" in kwargs else False
-    )
+    parsed_kwargs["silent"] = bool(kwargs["silent"])
+    parsed_kwargs["from_template"] = bool(kwargs["from_template"])
 
     parsed_kwargs["release"] = "production"
     if "release" in kwargs and kwargs["release"] != "production":
@@ -1107,8 +1078,8 @@ def create_launch_cmd(
         parsed_kwargs["upload_tls_cert"] = kwargs["upload_tls_cert"]
     if "upload_tls_key" in kwargs:
         parsed_kwargs["upload_tls_key"] = kwargs["upload_tls_key"]
-    if "provision" in kwargs:
-        parsed_kwargs["provision"] = str_to_bool(cast(str, kwargs["provision"]))
+
+    parsed_kwargs["provision"] = not bool(kwargs["no_provision"])
 
     if "image_name" in kwargs and kwargs["image_name"] is not None:
         parsed_kwargs["image_name"] = kwargs["image_name"]
@@ -1125,14 +1096,13 @@ def create_launch_cmd(
     else:
         parsed_kwargs["jupyter"] = False
 
-    if "vpn" in kwargs and kwargs["vpn"] is not None:
-        parsed_kwargs["vpn"] = str_to_bool(cast(str, kwargs["vpn"]))
-    else:
-        parsed_kwargs["vpn"] = True
+    parsed_kwargs["vpn"] = not bool(kwargs["no_vpn"])
 
     # allows changing docker platform to other cpu architectures like arm64
     parsed_kwargs["platform"] = kwargs["platform"] if "platform" in kwargs else None
 
+    parsed_kwargs["tail"] = tail
+
     if parsed_kwargs["from_template"] and host is not None:
         # Setup the files from the manifest_template.yml
         kwargs = setup_from_manifest_template(host_type=host)
@@ -1693,14 +1663,11 @@ def create_launch_docker_cmd(
             version_string = GRID_SRC_VERSION[0]
         version_string += "-dev"
         version_hash = GRID_SRC_VERSION[1]
-        if build is None:
-            build = True
+        build = True
     else:
         # whereas if in production mode and tag == "local" use the local VERSION file
         # or if its not set somehow, which should never happen, use stable
         # otherwise use the kwargs["tag"] from above
-        if build is None:
-            build = False
 
         # during production the default would be stable
         if version_string == "local":
@@ -1725,11 +1692,9 @@ def create_launch_docker_cmd(
 
     print("\n")
 
-    use_blob_storage = "True"
-    if str(node_type.input) == "network":
-        use_blob_storage = "False"
-    elif "use_blob_storage" in kwargs and kwargs["use_blob_storage"] is not None:
-        use_blob_storage = str(str_to_bool(kwargs["use_blob_storage"]))
+    use_blob_storage = (
+        False if str(node_type.input) == "network" else bool(kwargs["use_blob_storage"])
+    )
 
     envs = {
         "RELEASE": "production",
@@ -1743,7 +1708,7 @@ def create_launch_docker_cmd(
         "TRAEFIK_PUBLIC_NETWORK_IS_EXTERNAL": "False",
         "VERSION": version_string,
         "VERSION_HASH": version_hash,
-        "USE_BLOB_STORAGE": use_blob_storage,
+        "USE_BLOB_STORAGE": str(use_blob_storage),
         "FRONTEND_TARGET": "grid-ui-production",
         "STACK_API_KEY": str(
             generate_sec_random_password(length=48, special_chars=False)
@@ -1794,17 +1759,17 @@ def create_launch_docker_cmd(
 
     cmd += " docker compose -p " + snake_name
 
-    if "vpn" in kwargs and kwargs["vpn"]:
+    if bool(kwargs["vpn"]):
         cmd += " --profile vpn"
 
     if str(node_type.input) == "network":
         cmd += " --profile network"
 
-    if str_to_bool(use_blob_storage):
+    if use_blob_storage:
         cmd += " --profile blob-storage"
 
     # no frontend container so expect bad gateway on the / route
-    if kwargs["headless"] is False:
+    if not bool(kwargs["headless"]):
         cmd += " --profile frontend"
 
     # new docker compose regression work around
@@ -2132,7 +2097,7 @@ def create_launch_gcp_cmd(
     if not host_up:
         raise Exception(f"Something went wrong launching the VM at IP: {host_ip}.")
 
-    if "provision" in kwargs and not kwargs["provision"]:
+    if not bool(kwargs["provision"]):
         print("Skipping automatic provisioning.")
         print("VM created with:")
         print(f"IP: {host_ip}")
@@ -2282,7 +2247,7 @@ def create_launch_azure_cmd(
         host_term.parse_input(host_ip)
         verb.set_named_term_type(name="host", new_term=host_term)
 
-        if "provision" in kwargs and not kwargs["provision"]:
+        if not bool(kwargs["provision"]):
             print("Skipping automatic provisioning.")
             print("VM created with:")
             print(f"Name: {snake_name}")
@@ -2579,14 +2544,15 @@ def create_land_docker_cmd(verb: GrammarVerb) -> str:
     return cmd
 
 
-@click.command(help="Stop a running PyGrid domain/network node.")
+@click.command(
+    help="Stop a running PyGrid domain/network node.",
+    context_settings={"show_default": True},
+)
 @click.argument("args", type=str, nargs=-1)
 @click.option(
     "--cmd",
-    default="false",
-    required=False,
-    type=str,
-    help="Optional: print the cmd without running it",
+    is_flag=True,
+    help="Print the cmd without running it",
 )
 @click.option(
     "--ansible_extras",
@@ -2598,22 +2564,22 @@ def create_land_docker_cmd(verb: GrammarVerb) -> str:
     default=DEFAULT_BRANCH,
     required=False,
     type=str,
-    help="Optional: git branch to use for launch / build operations",
+    help="Git branch to use for launch / build operations",
 )
 @click.option(
     "--silent",
     is_flag=True,
-    help="Optional: prevent lots of land output",
+    help="Suppress extra outputs",
 )
 @click.option(
     "--force",
     is_flag=True,
-    help="Optional: bypass the prompt during hagrid land ",
+    help="Bypass the prompt during hagrid land",
 )
 def land(args: TypeTuple[str], **kwargs: Any) -> None:
     verb = get_land_verb()
-    silent = bool(kwargs["silent"]) if "silent" in kwargs else False
-    force = bool(kwargs["force"]) if "force" in kwargs else False
+    silent = bool(kwargs["silent"])
+    force = bool(kwargs["force"])
     try:
         grammar = parse_grammar(args=args, verb=verb)
         verb.load_grammar(grammar=grammar)
@@ -2645,7 +2611,7 @@ def land(args: TypeTuple[str], **kwargs: Any) -> None:
         )
 
     if force or _land_domain == "y":
-        if "cmd" not in kwargs or str_to_bool(cast(str, kwargs["cmd"])) is False:
+        if not bool(kwargs["cmd"]):
             if not silent:
                 print("Running: \n", cmd)
             try:
@@ -2673,7 +2639,9 @@ def land(args: TypeTuple[str], **kwargs: Any) -> None:
 cli.add_command(clean)
 
 
-@click.command(help="Show HAGrid debug information")
+@click.command(
+    help="Show HAGrid debug information", context_settings={"show_default": True}
+)
 @click.argument("args", type=str, nargs=-1)
 def debug(args: TypeTuple[str], **kwargs: TypeDict[str, Any]) -> None:
     debug_info = gather_debug()
@@ -2882,22 +2850,25 @@ def get_syft_install_status(host_name: str) -> bool:
         return True
 
 
-@click.command(help="Check health of an IP address/addresses or a resource group")
+@click.command(
+    help="Check health of an IP address/addresses or a resource group",
+    context_settings={"show_default": True},
+)
 @click.argument("ip_addresses", type=str, nargs=-1)
 @click.option(
     "--timeout",
     default=300,
-    help="Timeout for hagrid check command,Default: 300 seconds",
+    help="Timeout for hagrid check command",
 )
 @click.option(
-    "--silent",
-    default=True,
-    help="Optional: don't refresh output,Defaults True",
+    "--verbose",
+    is_flag=True,
+    help="Refresh output",
 )
 def check(
-    ip_addresses: TypeList[str], silent: bool = True, timeout: Union[int, str] = 300
+    ip_addresses: TypeList[str], verbose: bool = False, timeout: Union[int, str] = 300
 ) -> None:
-    check_status(ip_addresses=ip_addresses, silent=silent, timeout=timeout)
+    check_status(ip_addresses=ip_addresses, silent=not verbose, timeout=timeout)
 
 
 def _check_status(
@@ -3021,7 +2992,7 @@ def check_status(
 
 
 # add Hagrid info to the cli
-@click.command(help="Show HAGrid info")
+@click.command(help="Show HAGrid info", context_settings={"show_default": True})
 def version() -> None:
     print(f"HAGRID_VERSION: {get_version_string()}")
     if EDITABLE_MODE:
@@ -3189,12 +3160,14 @@ def enqueue_output(out: Any, queue: Queue) -> None:
         raise e
 
 
-@click.command(help="Launch a Syft + Jupyter Session with a Notebook URL / Path")
+@click.command(
+    help="Launch a Syft + Jupyter Session with a Notebook URL / Path",
+    context_settings={"show_default": True},
+)
 @click.argument("url", type=str, required=False)
 @click.option(
     "--reset",
     is_flag=True,
-    show_default=True,
     default=False,
     help="Force hagrid quickstart to setup a fresh virtualenv",
 )
@@ -3206,15 +3179,11 @@ def enqueue_output(out: Any, queue: Queue) -> None:
 @click.option(
     "--quiet",
     is_flag=True,
-    show_default=True,
-    default=False,
     help="Silence confirmation prompts",
 )
 @click.option(
     "--pre",
     is_flag=True,
-    show_default=True,
-    default=False,
     help="Install pre-release versions of syft",
 )
 @click.option(
@@ -3224,7 +3193,6 @@ def enqueue_output(out: Any, queue: Queue) -> None:
 )
 @click.option(
     "--test",
-    default=False,
     is_flag=True,
     help="CI Test Mode, don't hang on Jupyter",
 )
@@ -3416,7 +3384,7 @@ def add_intro_notebook(directory: str, reset: bool = False) -> str:
     return os.path.abspath(f"{directory}/{filename}")
 
 
-@click.command(help="Walk the Path")
+@click.command(help="Walk the Path", context_settings={"show_default": True})
 @click.option(
     "--repo",
     help="Obi-Wan will guide you to Dagobah",
@@ -3468,7 +3436,10 @@ def ssh_into_remote_machine(
         raise e
 
 
-@click.command(help="SSH into the IP address or a resource group")
+@click.command(
+    help="SSH into the IP address or a resource group",
+    context_settings={"show_default": True},
+)
 @click.argument("ip_address", type=str)
 @click.option(
     "--cmd",
@@ -3530,7 +3501,9 @@ def ssh(ip_address: str, cmd: str) -> None:
 
 
 # Add hagrid logs command to the CLI
-@click.command(help="Get the logs of the HAGrid node")
+@click.command(
+    help="Get the logs of the HAGrid node", context_settings={"show_default": True}
+)
 @click.argument("domain_name", type=str)
 def logs(domain_name: str) -> None:  # nosec
 


Test patch:
diff --git a/packages/hagrid/tests/hagrid/cli_test.py b/packages/hagrid/tests/hagrid/cli_test.py
--- a/packages/hagrid/tests/hagrid/cli_test.py
+++ b/packages/hagrid/tests/hagrid/cli_test.py
@@ -1,4 +1,5 @@
 # stdlib
+from collections import defaultdict
 from typing import List
 from typing import Tuple
 
@@ -18,7 +19,9 @@ def test_hagrid_launch() -> None:
     verb = cli.get_launch_verb()
     grammar = cli.parse_grammar(args=tuple(args), verb=verb)
     verb.load_grammar(grammar=grammar)
-    cmd = cli.create_launch_cmd(verb=verb, kwargs={}, ignore_docker_version_check=True)
+    cmd = cli.create_launch_cmd(
+        verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True
+    )
 
     cmd = cmd["Launching"][0]  # type: ignore
 
@@ -56,7 +59,9 @@ def test_hagrid_launch_without_name_with_preposition() -> None:
     verb = cli.get_launch_verb()
     grammar = cli.parse_grammar(args=tuple(args), verb=verb)
     verb.load_grammar(grammar=grammar)
-    cmd = cli.create_launch_cmd(verb=verb, kwargs={}, ignore_docker_version_check=True)
+    cmd = cli.create_launch_cmd(
+        verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True
+    )
 
     cmd = cmd["Launching"][0]  # type: ignore
     # check that it's a domain by default
@@ -93,7 +98,9 @@ def test_launch_with_multiword_domain_name() -> None:
     verb = cli.get_launch_verb()
     grammar = cli.parse_grammar(args=tuple(args), verb=verb)
     verb.load_grammar(grammar=grammar)
-    cmd = cli.create_launch_cmd(verb=verb, kwargs={}, ignore_docker_version_check=True)
+    cmd = cli.create_launch_cmd(
+        verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True
+    )
 
     cmd = cmd["Launching"][0]  # type: ignore
 
@@ -117,7 +124,9 @@ def test_launch_with_longer_multiword_domain_name() -> None:
     verb = cli.get_launch_verb()
     grammar = cli.parse_grammar(args=tuple(args), verb=verb)
     verb.load_grammar(grammar=grammar)
-    cmd = cli.create_launch_cmd(verb=verb, kwargs={}, ignore_docker_version_check=True)
+    cmd = cli.create_launch_cmd(
+        verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True
+    )
 
     cmd = cmd["Launching"][0]  # type: ignore
 
@@ -144,7 +153,9 @@ def test_launch_with_longer_multiword_domain_name_with_preposition() -> None:
     verb = cli.get_launch_verb()
     grammar = cli.parse_grammar(args=tuple(args), verb=verb)
     verb.load_grammar(grammar=grammar)
-    cmd = cli.create_launch_cmd(verb=verb, kwargs={}, ignore_docker_version_check=True)
+    cmd = cli.create_launch_cmd(
+        verb=verb, kwargs=defaultdict(lambda: None), ignore_docker_version_check=True
+    )
 
     cmd = cmd["Launching"][0]  # type: ignore