Repository: nvidia-holoscan/holohub
Pull request #723: Consolidate infrastructure scripts in `holohub` CLI
Author: login: tbirdso
Created: 2025-03-10T17:39:30Z | Merged: 2025-04-15T20:44:21Z

 Add the `holohub` script providing a consolidated entrypoint to HoloHub
    development infrastructure.
    
 Notable changes:
- Consolidates most command functionality from the `dev_container` and `run` scripts into a single `holohub` entrypoint
- Updates default behavior to execute prior steps in each app command chain for simplified new dev experience, similar to `dev_container build_and_run`. For instance, `holohub run <project>` will build the project container, launch the project container, build the project, and  run the project
- Organizes Python utilities into reusable modules under the `utilities` folder
- Adds basic unit tests
    
Migrated command coverage:
    - `run setup`
    - `run lint`
    - `run build`
    - `run launch`
    - `dev_container build`
    - `dev_container launch`
    - `dev_container build_and_run`
    
Packaging and installation commands are not yet fully  migrated.
    
Tested primarily with projects:
    - Endoscopy Tool Tracking
    - Multi-AI Ultrasound
    - Body Pose Estimation
    - Object Detection Torch


Diff:
diff --git a/.github/workflows/check_metadata.yml b/.github/workflows/check_metadata.yml
index 573e9a2767..89b37e5a08 100644
--- a/.github/workflows/check_metadata.yml
+++ b/.github/workflows/check_metadata.yml
@@ -24,4 +24,4 @@ jobs:
           python -m pip install -r ./utilities/requirements.txt
 
       - name: Run metadata_validator.py
-        run: python utilities/metadata_validator.py
+        run: python utilities/metadata/metadata_validator.py
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d0ffa6cb0..f548aadac2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -85,3 +85,9 @@ add_subdirectory(operators)
 
 # Build the extensions
 add_subdirectory(gxf_extensions)
+
+option(BUILD_HOLOHUB_TESTING OFF)
+if(BUILD_TESTING AND BUILD_HOLOHUB_TESTING)
+  enable_testing()
+  add_subdirectory(utilities/cli/tests)
+endif()
\ No newline at end of file
diff --git a/README.md b/README.md
index 0b3e5ac9b7..810e914df4 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,7 @@ To build and run in a containerized environment you will need:
   - the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) (v1.12.2 or later)
   - [Docker](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository), including the buildx plugin (`docker-buildx-plugin`)
   - `git` version control
+  - (optional) A [Python3](https://www.python.org/downloads/) base installation to run the `holohub` infrastructure script
 
 You will also need to set up your NVIDIA NGC credentials at [ngc.nvidia.com](https://catalog.ngc.nvidia.com/).
 
diff --git a/holohub b/holohub
new file mode 100755
index 0000000000..fe523577d8
--- /dev/null
+++ b/holohub
@@ -0,0 +1,18 @@
+#!/usr/bin/bash
+
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+PYTHONPATH=${PYTHONPATH}:$(pwd) python3 ./utilities/cli/holohub.py "$@"
diff --git a/utilities/cli/__init__.py b/utilities/cli/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/utilities/cli/container.py b/utilities/cli/container.py
new file mode 100644
index 0000000000..41761068c4
--- /dev/null
+++ b/utilities/cli/container.py
@@ -0,0 +1,387 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import subprocess
+import sys
+from pathlib import Path
+from typing import List, Optional
+
+from .util import (
+    check_nvidia_ctk,
+    fatal,
+    get_compute_capacity,
+    get_group_id,
+    get_host_gpu,
+    normalize_language,
+    run_command,
+)
+
+
+class HoloHubContainer:
+    """
+    Describes the container environment for a HoloHub project.
+
+    This class is responsible for common container operations and environment configuration,
+    which may differ across different projects.
+
+    Default attributes may be overridden by a project-specific implementation.
+    """
+
+    CONTAINER_PREFIX = "holohub"
+    HOLOHUB_ROOT = Path(__file__).parent.parent.parent
+
+    @classmethod
+    def default_base_image(cls) -> str:
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v3.1.0-{get_host_gpu()}"
+
+    @classmethod
+    def default_image(cls) -> str:
+        return f"{cls.CONTAINER_PREFIX}:ngc-v3.1.0-{get_host_gpu()}"
+
+    @staticmethod
+    def default_dockerfile() -> Path:
+        return HoloHubContainer.HOLOHUB_ROOT / "Dockerfile"
+
+    @staticmethod
+    def ucx_args() -> List[str]:
+        """UCX-related docker run arguments"""
+        return [
+            "--ipc=host",
+            "--cap-add=CAP_SYS_PTRACE",
+            "--ulimit=memlock=-1",
+            "--ulimit=stack=67108864",
+        ]
+
+    @staticmethod
+    def device_args() -> List[str]:
+        """Get docker run arguments for mounting devices in the container"""
+        options = []
+
+        # Add device mounts
+        device_paths = [
+            "/usr/lib/libvideomasterhd.so",
+            "/opt/deltacast/videomaster/Include",
+            "/opt/yuan/qcap/include",
+            "/opt/yuan/qcap/lib",
+            "/usr/lib/aarch64-linux-gnu/tegra",
+        ]
+
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f"--volume={path}:{path}")
+                else:
+                    options.append(f"--volume={path}:{path}")
+        return options
+
+    @staticmethod
+    def group_args() -> List[str]:
+        """Get docker run arguments for adding groups to the container"""
+        options = []
+        for group in ["video", "render", "docker"]:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f"--group-add={gid}")
+        return options
+
+    @staticmethod
+    def runtime_args() -> List[str]:
+        """Get docker run arguments for the runtime"""
+        return ["--runtime=nvidia", "--gpus", "all"]
+
+    def get_conditional_options(
+        self, use_tini: bool = False, persistent: bool = False
+    ) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append("--init")
+        if not persistent:
+            options.append("--rm")
+
+        return options
+
+    @property
+    def image_name(self) -> str:
+        if self.dockerfile_path != HoloHubContainer.default_dockerfile():
+            return f"{self.CONTAINER_PREFIX}:{self.project_metadata.get('project_name', '')}"
+        return HoloHubContainer.default_image()
+
+    @property
+    def dockerfile_path(self) -> Path:
+        """
+        Get Dockerfile path for the project according to the search strategy:
+        1. As specified in metadata.json
+        2. <app_source>/Dockerfile
+        3. <app_source>/<language>/Dockerfile
+        4. holohub/Dockerfile
+        """
+        if not self.project_metadata:
+            return HoloHubContainer.default_dockerfile()
+
+        if self.project_metadata.get("metadata", {}).get("dockerfile"):
+            dockerfile = self.project_metadata["metadata"]["dockerfile"]
+            dockerfile = dockerfile.replace(
+                "<holohub_app_source>", str(self.project_metadata["source_folder"])
+            )
+            dockerfile = dockerfile.replace("<holohub_root>", str(HoloHubContainer.HOLOHUB_ROOT))
+            return Path(dockerfile)
+
+        source_folder = self.project_metadata.get("source_folder", "")
+        if source_folder:
+            dockerfile_path = self.project_metadata.get("source_folder", "") / "Dockerfile"
+            if (source_folder / "Dockerfile").exists():
+                return dockerfile_path
+
+            language = normalize_language(
+                self.project_metadata.get("metadata", {}).get("language", "")
+            )
+            if (source_folder / language / "Dockerfile").exists():
+                return source_folder / language / "Dockerfile"
+
+        return HoloHubContainer.HOLOHUB_ROOT / "Dockerfile"
+
+    def __init__(self, project_metadata: Optional[dict[str, any]]):
+        if not project_metadata:
+            print("No project provided, proceeding with default container")
+        elif not isinstance(project_metadata, dict):
+            print("No project provided, proceeding with default container")
+
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get("HOLOSCAN_PY_EXE", "python3")
+        self.holoscan_docker_exe = os.environ.get("HOLOSCAN_DOCKER_EXE", "docker")
+
+        self.project_metadata = project_metadata
+
+        self.holoscan_sdk_version = "sdk-latest"
+        self.holohub_container_base_name = "holohub"
+
+        self.dryrun = False
+
+    def build(
+        self,
+        docker_file: Optional[str] = None,
+        base_img: Optional[str] = None,
+        img: Optional[str] = None,
+        no_cache: bool = False,
+        build_args: Optional[str] = None,
+    ) -> None:
+        """Build the container image"""
+
+        # Get Dockerfile path
+        docker_file_path = docker_file or self.dockerfile_path
+        base_img = base_img or self.default_base_image()
+        img = img or self.image_name
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        # Check if buildx exists
+        try:
+            run_command(["docker", "buildx", "version"], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal(
+                "docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository"
+            )
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ["DOCKER_BUILDKIT"] = "1"
+
+        cmd = [
+            "docker",
+            "build",
+            "--build-arg",
+            "BUILDKIT_INLINE_CACHE=1",
+            "--build-arg",
+            f"BASE_IMAGE={base_img}",
+            "--build-arg",
+            f"GPU_TYPE={gpu_type}",
+            "--build-arg",
+            f"COMPUTE_CAPACITY={compute_capacity}",
+            "--network=host",
+        ]
+
+        if no_cache:
+            cmd.append("--no-cache")
+
+        if build_args:
+            cmd.extend(build_args.split())
+
+        cmd.extend(["-f", str(docker_file_path), "-t", img, str(HoloHubContainer.HOLOHUB_ROOT)])
+
+        run_command(cmd, dry_run=self.dryrun)
+
+    def run(
+        self,
+        img: Optional[str] = None,
+        local_sdk_root: Optional[Path] = None,
+        ssh_x11: bool = False,
+        use_tini: bool = False,
+        persistent: bool = False,
+        nsys_profile: bool = False,
+        nsys_location: str = "",
+        as_root: bool = False,
+        docker_opts: str = "",
+        add_volumes: List[str] = None,
+        verbose: bool = False,
+        extra_args: List[str] = None,
+    ) -> None:
+        """Launch the container"""
+
+        check_nvidia_ctk()
+
+        img = img or self.image_name
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, "run", "--net", "host"]
+
+        # Basic options
+        cmd.extend(["--interactive"])
+        if sys.stdout.isatty():
+            cmd.append("--tty")
+
+        # User permissions
+        if not as_root:
+            cmd.extend(["-u", f"{os.getuid()}:{os.getgid()}"])
+        cmd.extend(["-v", "/etc/group:/etc/group:ro", "-v", "/etc/passwd:/etc/passwd:ro"])
+
+        # Workspace mounting
+        cmd.extend(
+            [
+                "-v",
+                f"{HoloHubContainer.HOLOHUB_ROOT}:/workspace/holohub",
+                "-w",
+                "/workspace/holohub",
+            ]
+        )
+
+        # GPU and device access
+        cmd.extend(
+            [
+                "--runtime=nvidia",
+                "--gpus",
+                "all",
+                "--cap-add",
+                "CAP_SYS_PTRACE",
+                "--ipc=host",
+                "-v",
+                "/dev:/dev",
+                "--device-cgroup-rule",
+                "c 81:* rmw",
+                "--device-cgroup-rule",
+                "c 189:* rmw",
+            ]
+        )
+
+        # Environment variables
+        cmd.extend(
+            [
+                "-e",
+                "NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display",
+                "-e",
+                "HOME=/workspace/holohub",
+                "-e",
+                "HOLOHUB_BUILD_LOCAL=1",
+            ]
+        )
+
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(["-v", f"{volume}:/workspace/volumes/{base}"])
+
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+
+        # Add UCX options
+        cmd.extend(self.ucx_args())
+
+        # Add display server options
+        cmd.extend(self.get_display_options(ssh_x11))
+
+        # Add nsys options
+        cmd.extend(self.get_nsys_options(nsys_profile, nsys_location))
+
+        # Add local SDK options if provided
+        pythonpath = "/workspace/holohub/benchmarks/holoscan_flow_benchmarking"
+        if local_sdk_root:
+            cmd.extend(self.get_local_sdk_options(local_sdk_root))
+            cmd.extend(["-e", f"PYTHONPATH=/workspace/holoscan-sdk/build/python/lib:{pythonpath}"])
+        else:
+            cmd.extend(["-e", f"PYTHONPATH=/opt/nvidia/holoscan/python/lib:{pythonpath}"])
+
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+
+        # Add the image name
+        cmd.append(img)
+
+        # Add any extra arguments
+        cmd.extend(extra_args)
+
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+
+        run_command(cmd, dry_run=self.dryrun)
+
+    def get_display_options(self, ssh_x11: bool) -> List[str]:
+        """Get display-related options"""
+        options = []
+        if "XDG_SESSION_TYPE" in os.environ:
+            options.extend(["-e", "XDG_SESSION_TYPE"])
+            if os.environ["XDG_SESSION_TYPE"] == "wayland":
+                options.extend(["-e", "WAYLAND_DISPLAY"])
+
+        if "XDG_RUNTIME_DIR" in os.environ:
+            options.extend(["-e", "XDG_RUNTIME_DIR"])
+            if os.path.isdir(os.environ["XDG_RUNTIME_DIR"]):
+                options.extend(
+                    ["-v", f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"]
+                )
+
+        if ssh_x11:
+            options.extend(["-v", "/tmp/.X11-unix:/tmp/.X11-unix", "-e", "DISPLAY"])
+        return options
+
+    def get_nsys_options(self, nsys_profile: bool, nsys_location: str) -> List[str]:
+        """Get nsys-related options"""
+        options = []
+        if nsys_profile:
+            options.extend(["--nvtx", "--capture-range", "0"])
+
+        if nsys_location:
+            options.extend(["--output", nsys_location])
+
+        return options
+
+    def get_local_sdk_options(self, local_sdk_root: Path) -> List[str]:
+        """Get Holoscan SDK-related options"""
+        return [
+            "-v",
+            f"{local_sdk_root}:/workspace/holoscan-sdk",
+            "-e",
+            "HOLOSCAN_LIB_PATH=/workspace/holoscan-sdk/build/lib",
+            "-e",
+            "HOLOSCAN_SAMPLE_DATA_PATH=/workspace/holoscan-sdk/data",
+            "-e",
+            "HOLOSCAN_TESTS_DATA_PATH=/workspace/holoscan-sdk/tests/data",
+        ]
diff --git a/utilities/cli/holohub.py b/utilities/cli/holohub.py
new file mode 100755
index 0000000000..c23622d2aa
--- /dev/null
+++ b/utilities/cli/holohub.py
@@ -0,0 +1,1019 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+from collections import defaultdict
+from pathlib import Path
+from typing import List, Optional
+
+import utilities.cli.util as holohub_cli_util
+import utilities.metadata.gather_metadata as metadata_util
+from utilities.cli.container import HoloHubContainer
+
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob("CMakeLists.txt"):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split("(")[1].split(")")[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+
+    HOLOHUB_ROOT = Path(__file__).parent.parent.parent
+    DEFAULT_BUILD_PARENT_DIR = HOLOHUB_ROOT / "build"
+    DEFAULT_DATA_DIR = HOLOHUB_ROOT / "data"
+    DEFAULT_SDK_DIR = "/opt/nvidia/holoscan/lib"
+
+    def __init__(self):
+        self.parser = self._create_parser()
+        self._collect_metadata()
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(
+            description="HoloHub CLI tool for managing HoloHub applications and containers"
+        )
+        subparsers = parser.add_subparsers(dest="command", required=True)
+
+        # build-container command
+        build_container = subparsers.add_parser(
+            "build-container", help="Build the development container"
+        )
+        build_container.add_argument("project", nargs="?", help="Project to build container for")
+        build_container.add_argument("--base_img", help="Fully qualified base image name")
+        build_container.add_argument("--docker_file", help="Path to Dockerfile to use")
+        build_container.add_argument("--img", help="Specify fully qualified container name")
+        build_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker build command"
+        )
+        build_container.add_argument(
+            "--no-cache", action="store_true", help="Do not use cache when building the image"
+        )
+        build_container.add_argument(
+            "--build-args", help="Provides extra arguments to docker build command"
+        )
+        build_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser(
+            "run-container", help="Launch the development container"
+        )
+        run_container.add_argument("project", nargs="?", help="Project to run container for")
+        run_container.add_argument("--img", help="Fully qualified image name")
+        run_container.add_argument(
+            "--local_sdk_root",
+            help="Path to Holoscan SDK used for building local Holoscan SDK container",
+        )
+        run_container.add_argument(
+            "--ssh_x11",
+            action="store_true",
+            help="Enable X11 forwarding of graphical HoloHub applications over SSH",
+        )
+        run_container.add_argument("--init", action="store_true", help="Support tini entry point")
+        run_container.add_argument(
+            "--persistent", action="store_true", help="Does not delete container after it is run"
+        )
+        run_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker run command"
+        )
+        run_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run_container.add_argument("--add-volume", action="append", help="Mount additional volume")
+        run_container.add_argument(
+            "--as_root", action="store_true", help="Run the container with root permissions"
+        )
+        run_container.add_argument(
+            "--docker_opts", help="Additional options to pass to the Docker launch"
+        )
+        run_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser("build", help="Build a project")
+        build.add_argument("project", help="Project to build")
+        build.add_argument(
+            "--local", action="store_true", help="Build locally instead of in container"
+        )
+        build.add_argument("--verbose", action="store_true", help="Print extra output")
+        build.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        build.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser("run", help="Run a project")
+        run.add_argument("project", help="Project to run")
+        run.add_argument("--local", action="store_true", help="Run locally instead of in container")
+        run.add_argument("--verbose", action="store_true", help="Print extra output")
+        run.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run.add_argument(
+            "--nsys-profile", action="store_true", help="Enable Nsight Systems profiling"
+        )
+        run.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser("list", help="List all available targets")
+        list_cmd.set_defaults(func=self.handle_list)
+
+        # lint command
+        lint = subparsers.add_parser("lint", help="Run linting tools")
+        lint.add_argument("path", nargs="?", default=".", help="Path to lint")
+        lint.add_argument("--fix", action="store_true", help="Fix linting issues")
+        lint.add_argument(
+            "--install-dependencies", action="store_true", help="Install linting dependencies"
+        )
+        lint.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        lint.set_defaults(func=self.handle_lint)
+
+        # setup command
+        setup = subparsers.add_parser("setup", help="Install HoloHub main required packages")
+        setup.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser("install", help="Install a project")
+        install.add_argument("project", help="Project to install")
+        install.add_argument(
+            "--local", action="store_true", help="Install locally instead of in container"
+        )
+        install.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        install.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser("clear-cache", help="Clear cache folders")
+        clear_cache.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def _collect_metadata(self) -> None:
+        """Create an unstructured database of metadata for all projects"""
+
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            HoloHubCLI.HOLOHUB_ROOT / "applications",
+            HoloHubCLI.HOLOHUB_ROOT / "benchmarks",
+            HoloHubCLI.HOLOHUB_ROOT / "gxf_extensions",
+            HoloHubCLI.HOLOHUB_ROOT / "operators",
+            HoloHubCLI.HOLOHUB_ROOT / "pkg",
+            HoloHubCLI.HOLOHUB_ROOT / "workflows",
+        )
+        self.projects = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+
+    def _find_project(self, project_name: str, language: Optional[str] = None) -> dict:
+        """Find a project by name"""
+        normalized_language = holohub_cli_util.normalize_language(language) if language else None
+        for project in self.projects:
+            if project["project_name"] == project_name:
+                if (
+                    normalized_language
+                    and holohub_cli_util.normalize_language(project["metadata"]["language"])
+                    != normalized_language
+                ):
+                    continue
+                return project
+        return None
+
+    def _make_project_container(
+        self, project_name: Optional[str] = None, language: Optional[str] = None
+    ) -> HoloHubContainer:
+        """Define a project container"""
+        if not project_name:
+            return HoloHubContainer(project_metadata=None)
+
+        project_data = self._find_project(project_name=project_name, language=language)
+        if not project_data:
+            holohub_cli_util.fatal(
+                f"Project '{project_name}' {f'({language})' if language else ''} not found"
+            )
+        return HoloHubContainer(project_metadata=project_data)
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build()
+        container.run(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root,
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts,
+            add_volumes=args.add_volume,
+            verbose=args.verbose,
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get("CMAKE_BUILD_TYPE", "Release")
+
+        build_type_map = {
+            "debug": "Debug",
+            "release": "Release",
+            "rel-debug": "RelWithDebInfo",
+            "relwithdebinfo": "RelWithDebInfo",
+        }
+        return build_type_map.get(build_type.lower(), "Release")
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_type = self.get_buildtype_str(args.build_type)
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={build_type}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", build_type], dry_run=args.dryrun
+            )
+
+        else:
+            # Build in container
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            container.run(
+                docker_opts="--entrypoint=bash",
+                extra_args=["-c", f"./holohub build {args.project} --local"],
+                verbose=args.verbose,
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            BUILD_TYPE = "Release"
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={BUILD_TYPE}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", BUILD_TYPE], dry_run=args.dryrun
+            )
+
+            language = holohub_cli_util.normalize_language(
+                project_data.get("metadata", {}).get("language", None)
+            )
+
+            run_config = project_data.get("metadata", {}).get("run", {})
+            if not run_config:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' does not have a run configuration"
+                )
+
+            app_source_path = project_data.get("source_folder", "")
+
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(HoloHubCLI.DEFAULT_DATA_DIR))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source_path))
+
+            cmd = cmd.replace("<holohub_bin>", str(build_dir))
+
+            app_build_dir = build_dir / app_source_path.relative_to(HoloHubCLI.HOLOHUB_ROOT)
+            cmd = cmd.replace("<holohub_app_bin>", str(app_build_dir))
+
+            if language == "cpp":
+                if not build_dir.is_dir() and not args.dryrun:
+                    holohub_cli_util.fatal(
+                        f"The build directory for this application does not exist.\n"
+                        f"Did you forget to './holohub build {args.project}'?"
+                    )
+
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(project_data.get("source_folder", ""))
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {project_data.get('source_folder', '')}\033[0m"
+                )
+            elif workdir == "holohub_bin":
+                if not args.dryrun:
+                    os.chdir(build_dir)
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {build_dir}\033[0m"
+                )
+            else:  # default to app binary directory
+                target_dir = (
+                    build_dir if language == "cpp" else project_data.get("source_folder", "")
+                )
+                if not args.dryrun:
+                    os.chdir(target_dir)
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {target_dir}\033[0m"
+                )
+
+            # Set up environment
+            env = os.environ.copy()
+            env["PYTHONPATH"] = (
+                f"{HoloHubCLI.DEFAULT_SDK_DIR}/../python/lib:{build_dir}/python/lib:{HoloHubCLI.HOLOHUB_ROOT}"
+            )
+            env["HOLOHUB_DATA_PATH"] = str(HoloHubCLI.DEFAULT_DATA_DIR)
+            env["HOLOSCAN_INPUT_PATH"] = os.environ.get(
+                "HOLOSCAN_INPUT_PATH", str(HoloHubCLI.DEFAULT_DATA_DIR)
+            )
+
+            # Print environment setup
+            if args.verbose or args.dryrun:
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport PYTHONPATH={env['PYTHONPATH']}\033[0m"
+                )
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport HOLOHUB_DATA_PATH={env['HOLOHUB_DATA_PATH']}\033[0m"
+                )
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport HOLOSCAN_INPUT_PATH={env['HOLOSCAN_INPUT_PATH']}\033[0m"
+                )
+
+            # Handle Nsight Systems profiling
+            if args.nsys_profile:
+                if (
+                    not shutil.which("nsys")
+                    and not os.path.isdir("/opt/nvidia/nsys-host")
+                    and not args.dryrun
+                ):
+                    holohub_cli_util.fatal(
+                        "Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted."
+                    )
+                nsys_cmd = "/opt/nvidia/nsys-host/bin/nsys" if not shutil.which("nsys") else "nsys"
+
+                # Check perf_event_paranoid level
+                if not args.dryrun:
+                    try:
+                        with open("/proc/sys/kernel/perf_event_paranoid") as f:
+                            if int(f.read()) > 2:
+                                holohub_cli_util.fatal(
+                                    "For Nsight Systems profiling the Linux operating system's perf_event_paranoid level must be 2 or less."
+                                )
+                    except (IOError, ValueError):
+                        pass
+
+                cmd = f"{nsys_cmd} profile --trace=cuda,vulkan,nvtx,osrt {cmd}"
+
+            holohub_cli_util.run_command(cmd.split(), env=env, dry_run=args.dryrun)
+        else:
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            # Get language before launching container
+            language = holohub_cli_util.normalize_language(
+                container.project_metadata.get("metadata", {}).get("language", None)
+            )
+
+            # Build command with all necessary arguments
+            run_cmd = f"./holohub run {args.project} --language {language} --local"
+            if args.dryrun:
+                run_cmd += " --dryrun"
+            if args.verbose:
+                run_cmd += " --verbose"
+            if args.nsys_profile:
+                run_cmd += " --nsys-profile"
+
+            container.run(
+                docker_opts="--entrypoint=bash", extra_args=["-c", run_cmd], verbose=args.verbose
+            )
+
+    def handle_list(self, args: argparse.Namespace) -> None:
+        """Handle list command"""
+        LIST_TYPES = [
+            "application",
+            "benchmark",
+            "gxf_extension",
+            "package",
+            "operator",
+            "tutorial",
+            "workflow",
+        ]
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            HoloHubCLI.HOLOHUB_ROOT / "applications",
+            HoloHubCLI.HOLOHUB_ROOT / "benchmarks",
+            HoloHubCLI.HOLOHUB_ROOT / "gxf_extensions",
+            HoloHubCLI.HOLOHUB_ROOT / "operators",
+            HoloHubCLI.HOLOHUB_ROOT / "pkg",
+            HoloHubCLI.HOLOHUB_ROOT / "workflows",
+        )
+        metadata = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+        grouped_metadata = defaultdict(list)
+        for project in metadata:
+            grouped_metadata[project.get("project_type", "")].append(project)
+
+        for project_type in LIST_TYPES:
+            if project_type not in grouped_metadata:
+                continue
+            print(f"\n\033[1;37m== {project_type.upper()}S =================\033[0m\n")
+            for project in sorted(grouped_metadata[project_type], key=lambda x: x["project_name"]):
+                language = project.get("metadata", {}).get("language", "")
+                language = f"({language})" if language else ""
+                print(f'{project["project_name"]} {language}')
+
+        print("\n\033[1;37m=================================\033[0m\n")
+
+    def handle_lint(self, args: argparse.Namespace) -> None:
+        """Handle lint command"""
+        if args.install_dependencies:
+            self._install_lint_deps(args.dryrun)
+            return
+
+        exit_code = 0
+
+        # Change to script directory
+        os.chdir(HoloHubCLI.HOLOHUB_ROOT)
+
+        if args.fix:
+            # Fix Python
+            holohub_cli_util.run_command(
+                ["ruff", "check", "--fix", "--ignore", "E712", args.path],
+                check=False,
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(["isort", args.path], check=False, dry_run=args.dryrun)
+            holohub_cli_util.run_command(["black", args.path], check=False, dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                [
+                    "codespell",
+                    "-w",
+                    "-i",
+                    "3",
+                    args.path,
+                    "--ignore-words",
+                    "codespell_ignore_words.txt",
+                    "--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data",
+                ],
+                check=False,
+                dry_run=args.dryrun,
+            )
+
+            # Fix C++ with clang-format
+            try:
+                cmd = [
+                    sys.executable,
+                    "-m",
+                    "cpplint",
+                    "--exclude",
+                    "build",
+                    "--exclude",
+                    "install",
+                    "--exclude",
+                    "build-*",
+                    "--exclude",
+                    "install-*",
+                    "--exclude",
+                    "applications/holoviz/template/cookiecutter*",
+                    "--recursive",
+                    args.path,
+                ]
+                if args.dryrun:
+                    holohub_cli_util.run_command(cmd, dry_run=True)
+                    cpp_files = ""
+                else:
+                    cpp_files = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
+
+                for file in cpp_files.splitlines():
+                    if ":" in file:  # Only process files with issues
+                        file = file.split(":")[0]
+                        holohub_cli_util.run_command(
+                            [
+                                "clang-format",
+                                "--style=file",
+                                "--sort-includes=0",
+                                "--lines=20:10000",
+                                "-i",
+                                file,
+                            ],
+                            check=False,
+                            dry_run=args.dryrun,
+                        )
+            except subprocess.CalledProcessError:
+                pass
+
+        else:
+            # Run linting checks
+            print("Linting Python")
+            if (
+                holohub_cli_util.run_command(
+                    ["ruff", "check", "--ignore", "E712", args.path],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+            if (
+                holohub_cli_util.run_command(
+                    ["isort", "-c", args.path], check=False, dry_run=args.dryrun
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+            if (
+                holohub_cli_util.run_command(
+                    ["black", "--check", args.path], check=False, dry_run=args.dryrun
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Linting C++")
+            if (
+                holohub_cli_util.run_command(
+                    [
+                        "python",
+                        "-m",
+                        "cpplint",
+                        "--exclude",
+                        "build",
+                        "--exclude",
+                        "install",
+                        "--exclude",
+                        "build-*",
+                        "--exclude",
+                        "install-*",
+                        "--exclude",
+                        ".vscode-server",
+                        "--exclude",
+                        "applications/holoviz/template/cookiecutter*",
+                        "--recursive",
+                        args.path,
+                    ],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Code spelling")
+            if (
+                holohub_cli_util.run_command(
+                    [
+                        "codespell",
+                        args.path,
+                        "--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data",
+                        "--ignore-words",
+                        "codespell_ignore_words.txt",
+                        "--exclude-file",
+                        "codespell.txt",
+                    ],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Linting CMake")
+            cmake_files = list(Path(args.path).rglob("CMakeLists.txt"))
+            cmake_files.extend(Path(args.path).rglob("*.cmake"))
+            if cmake_files:
+                if (
+                    holohub_cli_util.run_command(
+                        [
+                            "cmakelint",
+                            "--filter=-whitespace/indent,-linelength,-readability/wonkycase,-convention/filename,-package/stdargs",
+                            *[str(f) for f in cmake_files],
+                        ],
+                        check=False,
+                        dry_run=args.dryrun,
+                    ).returncode
+                    != 0
+                ):
+                    exit_code = 1
+
+        if exit_code == 0:
+            print("Everything looks good!")
+        sys.exit(exit_code)
+
+    def _install_lint_deps(self, dry_run: bool = False) -> None:
+        """Install linting dependencies"""
+        os.chdir(HoloHubCLI.HOLOHUB_ROOT)
+
+        print("Install Lint Dependencies for Python")
+        holohub_cli_util.run_command(
+            [
+                sys.executable,
+                "-m",
+                "pip",
+                "install",
+                "-r",
+                str(HoloHubCLI.HOLOHUB_ROOT / "utilities/requirements.lint.txt"),
+            ],
+            dry_run=dry_run,
+        )
+        holohub_cli_util.run_command(
+            ["apt", "install", "--no-install-recommends", "-y", "clang-format=1:14.0*"],
+            dry_run=dry_run,
+        )
+
+    def handle_setup(self, args: argparse.Namespace) -> None:
+        """Handle setup command"""
+        # Install system dependencies
+        holohub_cli_util.run_command(["sudo", "apt-get", "update"], dry_run=args.dryrun)
+
+        # Install wget if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "-y", "wget"], dry_run=args.dryrun
+        )
+
+        # Install xvfb for running tests/examples headless
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "-y", "xvfb"], dry_run=args.dryrun
+        )
+
+        # Check and install CMake if needed
+        cmake_version = subprocess.check_output(
+            ["dpkg", "--status", "cmake"], text=True, stderr=subprocess.DEVNULL
+        )
+        ubuntu_codename = subprocess.check_output(["cat", "/etc/os-release"], text=True)
+        ubuntu_codename = re.search(r"UBUNTU_CODENAME=(\w+)", ubuntu_codename).group(1)
+
+        if not cmake_version or "3.24.0" > cmake_version:
+            holohub_cli_util.run_command(
+                ["sudo", "apt", "install", "--no-install-recommends", "-y", "gpg"],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                [
+                    "wget",
+                    "-O",
+                    "-",
+                    "https://apt.kitware.com/keys/kitware-archive-latest.asc",
+                    "2>/dev/null",
+                    "|",
+                    "gpg",
+                    "--dearmor",
+                    "-",
+                    "|",
+                    "sudo",
+                    "tee",
+                    "/usr/share/keyrings/kitware-archive-keyring.gpg",
+                    ">/dev/null",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                [
+                    "echo",
+                    f'"deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ {ubuntu_codename} main"',
+                    "|",
+                    "sudo",
+                    "tee",
+                    "/etc/apt/sources.list.d/kitware.list",
+                    ">/dev/null",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(["sudo", "apt-get", "update"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                [
+                    "sudo",
+                    "apt",
+                    "install",
+                    "--no-install-recommends",
+                    "-y",
+                    "cmake",
+                    "cmake-curses-gui",
+                ],
+                dry_run=args.dryrun,
+            )
+
+        # Install Ninja
+        holohub_cli_util.run_command(
+            ["sudo", "apt", "install", "--no-install-recommends", "-y", "ninja-build"],
+            dry_run=args.dryrun,
+        )
+
+        # Install Python dev
+        python3_dev_version = subprocess.check_output(
+            ["dpkg", "--status", "python3-dev"], text=True, stderr=subprocess.DEVNULL
+        )
+        if not python3_dev_version or "3.9.0" > python3_dev_version:
+            holohub_cli_util.run_command(
+                [
+                    "sudo",
+                    "apt",
+                    "install",
+                    "--no-install-recommends",
+                    "-y",
+                    "python3",
+                    "python3-dev",
+                ],
+                dry_run=args.dryrun,
+            )
+
+        # Install ffmpeg
+        holohub_cli_util.run_command(
+            ["sudo", "apt", "install", "--no-install-recommends", "-y", "ffmpeg"],
+            dry_run=args.dryrun,
+        )
+
+        # Install libv4l-dev
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "libv4l-dev"],
+            dry_run=args.dryrun,
+        )
+
+        # Install git if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "git"],
+            dry_run=args.dryrun,
+        )
+
+        # Install unzip if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "unzip"],
+            dry_run=args.dryrun,
+        )
+
+        # Install ngc-cli if not present
+        try:
+            subprocess.check_output(["ngc", "--version"], stderr=subprocess.DEVNULL)
+        except (subprocess.CalledProcessError, FileNotFoundError):
+            if platform.machine() == "aarch64":
+                holohub_cli_util.run_command(
+                    [
+                        "wget",
+                        "--content-disposition",
+                        "https://ngc.nvidia.com/downloads/ngccli_arm64.zip",
+                    ],
+                    dry_run=args.dryrun,
+                )
+                holohub_cli_util.run_command(["unzip", "ngccli_arm64.zip"], dry_run=args.dryrun)
+            else:
+                holohub_cli_util.run_command(
+                    [
+                        "wget",
+                        "--content-disposition",
+                        "https://ngc.nvidia.com/downloads/ngccli_linux.zip",
+                    ],
+                    dry_run=args.dryrun,
+                )
+                holohub_cli_util.run_command(["unzip", "ngccli_linux.zip"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(["chmod", "u+x", "ngc-cli/ngc"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                ["sudo", "ln", "-s", f"{os.getcwd()}/ngc-cli/ngc", "/usr/local/bin/"],
+                dry_run=args.dryrun,
+            )
+
+        # Install CUDA dependencies
+        for version in [
+            "12-6",
+            "12-5",
+            "12-4",
+            "12-3",
+            "12-2",
+            "12-1",
+            "12-0",
+            "11-8",
+            "11-7",
+            "11-6",
+            "11-4",
+        ]:
+            try:
+                cuda_version = subprocess.check_output(
+                    ["dpkg", "--status", f"cuda-cudart-{version}"],
+                    text=True,
+                    stderr=subprocess.DEVNULL,
+                )
+                if cuda_version:
+                    break
+            except subprocess.CalledProcessError:
+                continue
+
+        if cuda_version:
+            short_cuda_version = cuda_version.split(".")[0]
+
+            # Install cudnn9 first
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libcudnn9-cuda-12", "9.*", optional=True, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libcudnn9-dev-cuda-12", "9.*", optional=True, dry_run=args.dryrun
+            )
+
+            # Check if cudnn9 is installed, if not install cudnn8
+            installed_cudnn9_version = subprocess.check_output(
+                ["apt", "list", "--installed", "libcudnn9-cuda-12"],
+                text=True,
+                stderr=subprocess.DEVNULL,
+            )
+            if not installed_cudnn9_version:
+                holohub_cli_util.install_cuda_dependencies_package(
+                    "libcudnn8", f"cuda{short_cuda_version}", dry_run=args.dryrun
+                )
+                holohub_cli_util.install_cuda_dependencies_package(
+                    "libcudnn8-dev", f"cuda{short_cuda_version}", dry_run=args.dryrun
+                )
+
+            # Install TensorRT dependencies
+            installed_libnvinferbin = subprocess.check_output(
+                ["dpkg", "--status", "libnvinfer-bin"], text=True, stderr=subprocess.DEVNULL
+            )
+            # Extract version string using regex
+            version_match = re.search(
+                r"Version: (\d+\.\d+\.\d+)\.\d+-\d+\+cuda\d+\.\d+", installed_libnvinferbin
+            )
+            if version_match:
+                installed_libnvinferbin_version = version_match.group(1)
+            else:
+                holohub_cli_util.fatal("Could not determine libnvinfer-bin version")
+
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-headers-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-plugin-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvonnxparsers-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+
+        # Install the autocomplete
+        holohub_cli_util.run_command(
+            [
+                "sudo",
+                "cp",
+                f"{HoloHubCLI.HOLOHUB_ROOT}/utilities/holohub_autocomplete",
+                "/etc/bash_completion.d/",
+            ],
+            dry_run=args.dryrun,
+        )
+
+        print("Setup for HoloHub is ready. Happy Holocoding!")
+
+    def handle_install(self, args: argparse.Namespace) -> None:
+        """Handle install command"""
+        raise NotImplementedError("Install command not yet implemented")
+
+    def handle_clear_cache(self, args: argparse.Namespace) -> None:
+        """Handle clear-cache command"""
+        if args.dryrun:
+            print("Would clear cache folders:")
+            for pattern in ["build", "build-*", "install"]:
+                for path in HoloHubCLI.HOLOHUB_ROOT.glob(pattern):
+                    if path.is_dir():
+                        print(f"  Would remove: {path}")
+        else:
+            print("Clearing cache...")
+            for pattern in ["build", "build-*", "install"]:
+                for path in HoloHubCLI.HOLOHUB_ROOT.glob(pattern):
+                    if path.is_dir():
+                        shutil.rmtree(path)
+
+    def run(self) -> None:
+        """Main entry point for the CLI"""
+        args = self.parser.parse_args()
+        if hasattr(args, "func"):
+            args.func(args)
+        else:
+            self.parser.print_help()
+            sys.exit(1)
+
+
+def main():
+    cli = HoloHubCLI()
+    cli.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/utilities/cli/tests/CMakeLists.txt b/utilities/cli/tests/CMakeLists.txt
new file mode 100644
index 0000000000..50f6c29d6d
--- /dev/null
+++ b/utilities/cli/tests/CMakeLists.txt
@@ -0,0 +1,79 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+include(CTest)
+find_package(Python3 REQUIRED COMPONENTS Interpreter)
+
+# Add functional tests for holohub script
+add_test(
+    NAME test_holohub_list
+    COMMAND ${CMAKE_SOURCE_DIR}/holohub list
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
+set_property(TEST test_holohub_list PROPERTY
+    PASS_REGULAR_EXPRESSION "APPLICATIONS"
+    PASS_REGULAR_EXPRESSION "BENCHMARKS"
+    PASS_REGULAR_EXPRESSION "PACKAGES"
+    PASS_REGULAR_EXPRESSION "OPERATORS"
+    PASS_REGULAR_EXPRESSION "WORKFLOWS"
+)
+
+add_test(
+    NAME test_holohub_lint
+    COMMAND ${CMAKE_SOURCE_DIR}/holohub lint --dryrun
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
+add_test(
+    NAME test_holohub_lint_fix
+    COMMAND ${CMAKE_SOURCE_DIR}/holohub lint --fix --dryrun
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
+foreach(lint_test "test_holohub_lint;test_holohub_lint_fix")
+    set_property(TEST test_holohub_lint PROPERTY
+        PASS_REGULAR_EXPRESSION "ruff"
+        PASS_REGULAR_EXPRESSION "isort"
+        PASS_REGULAR_EXPRESSION "cpplint"
+        PASS_REGULAR_EXPRESSION "codespell"
+        PASS_REGULAR_EXPRESSION "cmakelint"
+    )
+endforeach()
+
+add_test(
+    NAME test_holohub_setup
+    COMMAND ${CMAKE_SOURCE_DIR}/holohub setup --dryrun
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
+set_property(TEST test_holohub_setup PROPERTY
+    PASS_REGULAR_EXPRESSION "libcudnn"
+    PASS_REGULAR_EXPRESSION "libnvinfer"
+    FAIL_REGULAR_EXPRESSION "Error checking available versions"
+)
+
+add_test(
+    NAME test_holohub_clear_cache
+    COMMAND ${CMAKE_SOURCE_DIR}/holohub clear-cache --dryrun
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
+set_property(TEST test_holohub_clear_cache PROPERTY
+    PASS_REGULAR_EXPRESSION "Would remove: ${CMAKE_SOURCE_DIR}/build"
+    PASS_REGULAR_EXPRESSION "Would remove: ${CMAKE_SOURCE_DIR}/install"
+)
+
+# Add test for container module
+add_test(
+    NAME test_container
+    COMMAND ${Python3_EXECUTABLE} -m unittest discover -s ${CMAKE_CURRENT_SOURCE_DIR} -p "test_container.py"
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)
diff --git a/utilities/cli/tests/test_container.py b/utilities/cli/tests/test_container.py
new file mode 100644
index 0000000000..43df84f8ad
--- /dev/null
+++ b/utilities/cli/tests/test_container.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+# Add the utilities directory to the Python path
+sys.path.append(str(Path(os.getcwd()) / "utilities"))
+
+from utilities.cli.container import HoloHubContainer
+
+
+class TestHoloHubContainer(unittest.TestCase):
+    def setUp(self):
+        self.project_metadata = {
+            "project_name": "test_project",
+            "source_folder": Path("/test/path"),
+            "metadata": {"language": "cpp", "dockerfile": "<holohub_app_source>/Dockerfile"},
+        }
+        self.container = HoloHubContainer(project_metadata=self.project_metadata)
+
+    def test_default_base_image(self):
+        """Test that default base image is correctly formatted"""
+        base_image = HoloHubContainer.default_base_image()
+        self.assertTrue(base_image.startswith("nvcr.io/nvidia/clara-holoscan/holoscan:"))
+
+    def test_default_image(self):
+        """Test that default image name is correctly formatted"""
+        image_name = HoloHubContainer.default_image()
+        self.assertTrue(image_name.startswith("holohub:ngc-"))
+
+    def test_dockerfile_path(self):
+        """Test that Dockerfile path is correctly determined"""
+        dockerfile_path = self.container.dockerfile_path
+        self.assertEqual(str(dockerfile_path), "/test/path/Dockerfile")
+
+    def test_image_name(self):
+        """Test that image name is correctly determined"""
+        image_name = self.container.image_name
+        self.assertEqual(image_name, "holohub:test_project")
+
+    @patch("subprocess.run")
+    def test_build(self, mock_run):
+        """Test container build command"""
+        self.container.build()
+        self.assertGreater(mock_run.call_count, 0)
+        cmd = mock_run.call_args[0][0]
+        self.assertTrue("docker" in cmd)
+        self.assertTrue("build" in cmd)
+        self.assertTrue(
+            str(self.container.dockerfile_path) in " ".join(cmd),
+            f"Dockerfile path {self.container.dockerfile_path} not found in command: {cmd}",
+        )
+        self.assertTrue(
+            self.container.image_name in " ".join(cmd),
+            f"Image name {self.container.image_name} not found in command: {cmd}",
+        )
+
+    @patch("subprocess.run")
+    @patch("subprocess.check_output")
+    def test_run(self, mock_check_output, mock_run):
+        """Test container run command"""
+        mock_check_output.return_value = ""
+        self.container.run()
+        mock_run.assert_called_once()
+        cmd = mock_run.call_args[0][0]
+        self.assertTrue("docker" in cmd)
+        self.assertTrue("run" in cmd)
+        self.assertTrue("--runtime=nvidia" in cmd)
+        self.assertTrue(self.container.image_name in cmd)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/utilities/cli/util.py b/utilities/cli/util.py
new file mode 100644
index 0000000000..24ba9e7e0c
--- /dev/null
+++ b/utilities/cli/util.py
@@ -0,0 +1,290 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import grp
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
+
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+
+def run_command(
+    cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs
+) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = format_long_command(cmd) if dry_run else " ".join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+
+    if not shutil.which("nvidia-ctk"):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+
+    try:
+        output = subprocess.check_output(["nvidia-ctk", "--version"], text=True)
+        import re
+
+        match = re.search(r"(\d+\.\d+\.\d+)", output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(
+                    f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended)."
+                )
+        else:
+            print(f"Failed to parse available nvidia-ctk version: {output}")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which("nvidia-smi"):
+        print(
+            "Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.",
+            file=sys.stderr,
+        )
+        return "dgpu"
+
+    try:
+        output = subprocess.check_output(
+            ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]
+        )
+        if not output or "Orin (nvgpu)" in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+
+    return "dgpu"
+
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which("nvidia-smi"):
+        return "0.0"
+    try:
+        output = subprocess.check_output(
+            ["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader"]
+        )
+        return output.decode().strip().split("\n")[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+
+def normalize_language(language: str) -> Optional[str]:
+    """Normalize language name"""
+    if language.lower() == "cpp" or language.lower() == "c++":
+        return "cpp"
+    elif language.lower() == "python" or language.lower() == "py":
+        return "python"
+    return None
+
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob("metadata.json"):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+
+            if "{{" in dir_name and "}}" in dir_name:
+                continue  # Skip templates
+
+            if dir_name in ["cpp", "python"]:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+
+            results.append((name, language))
+
+    return sorted(results)
+
+
+def install_cuda_dependencies_package(
+    package_name: str, preferred_version: str, optional: bool = False, dry_run: bool = False
+) -> bool:
+    """Install CUDA dependencies package with version checking
+
+    Args:
+        package_name: Name of the package to install
+        preferred_version: Preferred version string to match
+        optional: Whether the package is optional (default: False)
+
+    Returns:
+        bool: True if package was installed or already present, False if optional package was skipped
+
+    Raises:
+        SystemExit: If non-optional package cannot be installed
+    """
+    # Check if package is already installed
+    try:
+        output = subprocess.check_output(
+            ["apt", "list", "--installed", package_name], text=True, stderr=subprocess.DEVNULL
+        )
+        # Extract installed version from apt list output using regex
+        # Example output: "libcudnn9-cuda-12/unknown,now 9.5.1.17-1 amd64 [installed,upgradable to: 9.8.0.87-1]"
+        installed_version = re.search(rf"{package_name}/.*?now\s+([\d\.-]+)", output)
+        if installed_version:
+            installed_version = installed_version.group(1)
+            print(f"Package {package_name} found with version {installed_version}")
+            return True
+    except subprocess.CalledProcessError:
+        pass
+
+    # Check available versions
+    try:
+        available_versions = subprocess.check_output(
+            ["apt", "list", "-a", package_name], text=True, stderr=subprocess.DEVNULL
+        )
+
+        # Find matching version
+        matching_version = None
+        for line in available_versions.splitlines():
+            if preferred_version in line and package_name in line:
+                matching_version = line.split()[1]  # Get version from second column
+                break
+
+        if not matching_version:
+            if optional:
+                print(f"Package {package_name} {preferred_version} not found. Skipping.")
+                return False
+            else:
+                fatal(
+                    f"{package_name} {preferred_version} is not installable.\n"
+                    f"You might want to try to install a newer version manually and rerun the setup:\n"
+                    f"  sudo apt install {package_name}"
+                )
+
+        # Install the package
+        print(f"Installing {package_name}={matching_version}")
+        run_command(
+            [
+                "sudo",
+                "apt",
+                "install",
+                "--no-install-recommends",
+                "-y",
+                f"{package_name}={matching_version}",
+            ],
+            dry_run=dry_run,
+        )
+        return True
+
+    except subprocess.CalledProcessError as e:
+        if optional:
+            print(f"Error checking available versions for {package_name}: {e}")
+            return False
+        else:
+            fatal(f"Error checking available versions for {package_name}: {e}")
+
+
+def format_long_command(cmd: List[str], max_line_length: int = 80) -> str:
+    """Format a long command into multiple lines for better readability
+
+    Args:
+        cmd: Command to format as a list of strings
+        max_line_length: Maximum line length before wrapping
+
+    Returns:
+        Formatted command string with line continuations
+    """
+    if not cmd:
+        return ""
+
+    # Start with the first command
+    formatted = cmd[0]
+    current_line = cmd[0]
+
+    # Common patterns that suggest good break points
+    break_patterns = {
+        "--",  # Long options
+        "-",  # Short options
+        "&&",  # Command chaining
+        "||",  # Command chaining
+        "|",  # Pipes
+        ";",  # Command separator
+        ">",  # Output redirection
+        "<",  # Input redirection
+        ">>",  # Append redirection
+        "2>",  # Error redirection
+    }
+
+    for i, arg in enumerate(cmd[1:]):
+        # Check if this is a good place to break
+        should_break = (
+            # Break if we exceed max length
+            len(current_line) + len(arg) + 1 > max_line_length
+            or
+            # Break before common command separators
+            any(arg.startswith(pattern) for pattern in break_patterns)
+            or
+            # Break after common command separators
+            any(cmd[i].endswith(pattern) for pattern in break_patterns)
+        )
+
+        if should_break:
+            formatted += " \\\n    " + arg
+            current_line = arg
+        else:
+            formatted += " " + arg
+            current_line += " " + arg
+
+    return formatted
diff --git a/utilities/gather_metadata.py b/utilities/metadata/gather_metadata.py
similarity index 66%
rename from utilities/gather_metadata.py
rename to utilities/metadata/gather_metadata.py
index 2de0b1a1b0..e1fff59ece 100644
--- a/utilities/gather_metadata.py
+++ b/utilities/metadata/gather_metadata.py
@@ -20,6 +20,7 @@
 import json
 import logging
 import os
+from pathlib import Path
 
 logger = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
@@ -53,10 +54,10 @@ def extract_readme(file_path):
             return ""
 
 
-def extract_application_name(metadata_filepath: str) -> str:
-    """Extract the application or workflow name from the README file path.
+def extract_project_name(metadata_filepath: str) -> str:
+    """Extract the project name from the metadata.json file path.
 
-    HoloHub convention is such that an application/workflow `metadata.json` file
+    HoloHub convention is such that a `metadata.json` file
     must be located at either:
     - the named project folder; or
     - a language subfolder one level below the project folder.
@@ -85,36 +86,66 @@ def generate_build_and_run_command(metadata: dict) -> str:
         return f'./dev_container build_and_run {metadata["application_name"]}'
 
 
-def gather_metadata(repo_path, exclude_files: None) -> dict:
-    """Collect project metadata from JSON files into a single dictionary"""
-    SCHEMA_TYPES = ["workflow", "application", "operator", "gxf_extension", "tutorial"]
+def gather_metadata(repo_paths: list[str], exclude_paths: list[str] = None) -> list[dict]:
+    """
+    Collect project metadata from JSON files into a single dictionary
+
+    This function will return a list of dictionaries, each containing metadata for a project.
 
-    metadata_files = find_metadata_files(repo_path)
+    :input:
+        repo_path: str
+            The path to the repository to collect metadata from.
+        exclude_files: list
+            A list of files to exclude from metadata collection.
+    :return:
+        A list of dictionaries, each containing metadata for a project.
+    """
+    SCHEMA_TYPES = [
+        "application",
+        "benchmark",
+        "gxf_extension",
+        "package",
+        "operator",
+        "tutorial",
+        "workflow",
+    ]
+
+    metadata_files = find_metadata_files(repo_paths)
     metadata = []
-    exclude_files = exclude_files or []
+    exclude_paths = exclude_paths or []
 
     # Iterate over the found metadata files
     for file_path in metadata_files:
-        if file_path in exclude_files:
+        if any(exclude_path in file_path for exclude_path in exclude_paths):
             continue
         with open(file_path, "r") as file:
             try:
-                data = json.load(file)
-
-                for schema_type in SCHEMA_TYPES:
-                    if schema_type in data:
-                        data["metadata"] = data.pop(schema_type)
-                        break
-
-                readme = extract_readme(file_path)
-                application_name = extract_application_name(file_path)
-                source_folder = os.path.normpath(file_path).split("/")[0]
-                data["readme"] = readme
-                data["application_name"] = application_name
-                data["source_folder"] = source_folder
-                if source_folder in ["applications", "workflows"]:
-                    data["build_and_run"] = generate_build_and_run_command(data)
-                metadata.append(data)
+                entries = json.load(file)
+                entries = entries if type(entries) is list else [entries]
+
+                for data in entries:
+                    try:
+                        schema_type = next(key for key in data.keys() if key in SCHEMA_TYPES)
+                    except StopIteration:
+                        logger.error(
+                            'No valid schema type found in metadata file "%s". Available keys: %s',
+                            file_path,
+                            ", ".join(data.keys()),
+                        )
+                        continue
+
+                    data["project_type"] = schema_type
+                    data["metadata"] = data.pop(schema_type)
+
+                    readme = extract_readme(file_path)
+                    project_name = extract_project_name(file_path)
+                    source_folder = Path(file_path).parent
+                    data["readme"] = readme
+                    data["project_name"] = project_name
+                    data["source_folder"] = source_folder
+                    if source_folder in ["applications", "benchmarks", "workflows"]:
+                        data["build_and_run"] = generate_build_and_run_command(data)
+                    metadata.append(data)
             except json.decoder.JSONDecodeError as e:
                 logger.error('Error parsing JSON file "%s": %s', file_path, e)
                 continue
diff --git a/utilities/metadata_validator.py b/utilities/metadata/metadata_validator.py
similarity index 100%
rename from utilities/metadata_validator.py
rename to utilities/metadata/metadata_validator.py
diff --git a/utilities/summarize_metadata.py b/utilities/metadata/summarize_metadata.py
similarity index 100%
rename from utilities/summarize_metadata.py
rename to utilities/metadata/summarize_metadata.py


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @tbirdso, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request is a work-in-progress to consolidate the `dev_container` and `run` script infrastructure into a single `holohub` CLI script. It introduces a new `holohub` script that aims to provide a unified interface for building and running HoloHub applications and containers. The script includes functionalities for building container images, launching containers with various options, building and running projects locally or within containers, linting, and managing dependencies.

### Highlights
* **New `holohub` CLI**: Introduces a new Python script `holohub` that consolidates functionalities from previous separate scripts.
* **Container Management**: Provides commands for building and running HoloHub development containers with options for GPU support, networking, and volume mounting.
* **Project Build and Run**: Enables building and running HoloHub projects either locally or within a container, supporting both C++ and Python projects.
* **Linting and Setup**: Includes commands for linting code, installing dependencies, and setting up the HoloHub environment.

### Changelog
* **holohub**
  * Introduces utility functions for timestamping, error handling, and command execution (lines 31-57).
  * Adds functions to check NVIDIA Container Toolkit version and determine host GPU type (lines 58-91).
  * Implements functions to locate application and package source directories based on HoloHub conventions (lines 110-159).
  * Introduces `HoloHubContainer` class to manage container operations, including building and launching containers (lines 208-497).
  * Adds `HoloHubCLI` class to provide a command-line interface with subcommands for building, running, listing, linting, and setting up HoloHub projects (lines 498-1060).
  * Implements argument parsing using `argparse` to handle various command-line options (lines 512-640).
  * Adds functions to handle different commands such as `build-container`, `run-container`, `build`, `run`, `list`, `lint`, `setup`, `install`, and `clear-cache` (lines 642-1045).


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/review-github-code).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

**Did you know?**

The first version of CMake was written in 1999 to build software for medical imaging.

state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T17:39:40Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request consolidates infrastructure scripts into a single `holohub` CLI script, which is a great step towards simplifying the HoloHub development and deployment process. The script is well-structured and includes various functionalities for building and running containers, managing projects, and more. However, there are several areas that could be improved to enhance the script's robustness, maintainability, and user-friendliness.

### Summary of Findings
* **Error Handling**: The script uses `sys.exit()` for error handling, which can abruptly terminate the program. Consider using exceptions or more graceful error handling mechanisms to allow for cleanup or recovery.
* **Code Duplication**: There are instances of code duplication, such as the repeated checks for directory existence. Refactor these into reusable functions to improve maintainability.
* **Argument Parsing**: The argument parsing logic could be improved by using more descriptive help messages and providing better validation for input values.
* **Path Handling**: The script uses string concatenation for path manipulation, which can be error-prone. Use `pathlib` for more robust and platform-independent path handling.

### Merge Readiness
While the consolidation of scripts is a positive step, the identified issues need to be addressed before merging. Specifically, the error handling and code duplication issues should be resolved to ensure the script is robust and maintainable. I am unable to approve this pull request, and recommend that it not be merged until the critical and high severity issues are addressed, and that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-10T17:39:56Z
user: login: wyli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T08:51:53Z
user: login: wyli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:55:16Z
user: login: wyli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:02:52Z
user: login: wyli
body: thanks, looks good to me!

One possible future direction is to let an AI agent to automatically call these subroutines such as `HoloHubCLI().handle_run(project, language, ...)` and receive any application outcomes. 

The current implementation is not far from achieving that... just need to make the methods in `HoloHubCLI` less coupled with the argparser input and `util.run_command` to return the stdout/stderr outputs in a python string.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:19:16Z
user: login: tbirdso
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T20:35:02Z
user: login: tbirdso
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T20:36:55Z
user: login: tbirdso
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T20:37:39Z

Review comments:
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider logging the traceback information instead of just printing it to stderr. This would allow for better debugging and monitoring in production environments.

```
    import logging
    logging.error(f"{message}\n{traceback.format_exc()}")
    sys.exit(1)
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Instead of exiting directly, consider raising a custom exception that can be caught and handled by the calling function. This would allow for more graceful error handling and potential cleanup operations.

```
    except subprocess.CalledProcessError as e:
        print(f"Error running command: {cmd_str}")
        print(f"Exit code: {e.returncode}")
        raise Exception(f"Command failed with exit code {e.returncode}") from e
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a more specific exception type, such as `ValueError`, to provide more context about the error.

```
        raise ValueError("Missing app name argument for get_app_source_root_dir")
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider using `next` with a default value to simplify the logic and avoid potential `IndexError` if `sub_app_paths` is empty.

```
        holohub_app_source = next((p for p in script_dir.glob(f"applications/*/{app_name}")), None)
        if holohub_app_source:
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a custom exception with a more descriptive error message.

```
        raise FileNotFoundError(f"Could not find project {app_name}")
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
+        
+    return holohub_app_source
+
+def get_app_source_lang_dir(app_name: str, language: str, script_dir: Path) -> Path:
+    """Get source path for a given application language implementation"""
+    holohub_app_source = get_app_source_root_dir(app_name, script_dir)
+    lang_dir = holohub_app_source / language
+    
+    if lang_dir.is_dir():
+        return lang_dir
+    return holohub_app_source
+
+def get_pkg_dir(pkg_name: str, script_dir: Path) -> Path:
+    """Get source path for a given package according to HoloHub convention"""
+    if not pkg_name:
+        fatal("Missing package name argument for get_pkg_dir")
+        
+    holohub_pkg_source = script_dir / "pkg" / pkg_name
+    
+    # Check if the package is in a subdirectory
+    if not holohub_pkg_source.is_dir():
+        sub_pkg_paths = list(script_dir.glob(f"pkg/*/{pkg_name}"))
+        if sub_pkg_paths:
+            holohub_pkg_source = sub_pkg_paths[0]
+            
+    if not holohub_pkg_source.is_dir():
+        fatal(f"Could not find package {pkg_name}")
+        
+    return holohub_pkg_source
+
+def get_metadata(path: Path) -> Dict:
+    """Read and parse metadata.json file"""
+    try:
+        with open(path / "metadata.json") as f:
+            return json.load(f)
+    except FileNotFoundError:
+        fatal(f"The metadata file does not exist: {path}/metadata.json")
+    except json.JSONDecodeError:
+        fatal(f"Invalid JSON in metadata file: {path}/metadata.json")
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob('metadata.json'):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+            
+            if '{{' in dir_name and '}}' in dir_name:
+                continue  # Skip templates
+                
+            if dir_name in ['cpp', 'python']:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+                
+            results.append((name, language))
+    
+    return sorted(results)
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob('CMakeLists.txt'):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split('(')[1].split(')')[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+class HoloHubContainer:
+    """Manages container operations for HoloHub"""
+    
+    def __init__(self):
+        self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+        self.holohub_root = self.script_dir
+        
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get('HOLOSCAN_PY_EXE', 'python3')
+        self.holoscan_docker_exe = os.environ.get('HOLOSCAN_DOCKER_EXE', 'docker')
+        self.holoscan_sdk_version = 'sdk-latest'
+        self.holohub_container_base_name = 'holohub'
+        
+        self.do_dry_run = False
+
+    def get_default_base_img(self) -> str:
+        """Get default base image name"""
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v2.9.0-{get_host_gpu()}"
+
+    def get_default_img(self) -> str:
+        """Get default image name"""
+        return f"holohub:ngc-v2.9.0-{get_host_gpu()}"
+
+    def get_ucx_options(self) -> List[str]:
+        """Get UCX-related docker options"""
+        return [
+            '--ipc=host',
+            '--cap-add=CAP_SYS_PTRACE',
+            '--ulimit=memlock=-1',
+            '--ulimit=stack=67108864'
+        ]
+
+    def get_conditional_options(self, use_tini: bool = False, persistent: bool = False) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+        
+        # Add device mounts
+        device_paths = [
+            '/usr/lib/libvideomasterhd.so',
+            '/opt/deltacast/videomaster/Include',
+            '/opt/yuan/qcap/include',
+            '/opt/yuan/qcap/lib',
+            '/usr/lib/aarch64-linux-gnu/tegra'
+        ]
+        
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f'--volume={path}:{path}')
+                else:
+                    options.append(f'--volume={path}:{path}')
+
+        # Add group permissions
+        for group in ['video', 'render', 'docker']:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f'--group-add={gid}')
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append('--init')
+        if not persistent:
+            options.append('--rm')
+
+        return options
+
+    def get_dockerfile_path(self, app_name: str = None, language: Optional[str] = 'cpp') -> Path:
+        """Get Dockerfile path for an application or default"""
+        if not app_name:
+            return self.script_dir / "Dockerfile"
+            
+        app_source = get_app_source_lang_dir(app_name, language, self.script_dir)
+        
+        # Check metadata.json for dockerfile entry
+        try:
+            metadata = get_metadata(app_source)
+            if "dockerfile" in metadata:
+                dockerfile = metadata["dockerfile"].replace("<holohub_app_source>", str(app_source))
+                return Path(dockerfile)
+        except (FileNotFoundError, KeyError):
+            pass
+            
+        # Check for Dockerfile in application directory
+        app_dockerfile = app_source / "Dockerfile"
+        if app_dockerfile.exists():
+            return app_dockerfile
+        
+        # Check for Dockerfile in language directory
+        lang_dockerfile = app_source / "Dockerfile"
+        if lang_dockerfile.exists():
+            return lang_dockerfile
+            
+        # Use default Dockerfile
+        return self.script_dir / "Dockerfile"
+
+    def build(self, 
+              docker_file: Optional[str] = None,
+              base_img: Optional[str] = None,
+              img: Optional[str] = None,
+              no_cache: bool = False,
+              build_args: Optional[str] = None,
+              project: Optional[str] = None,
+              language: Optional[str] = 'cpp',
+              verbose: bool = False) -> None:
+        """Build the container image"""
+        
+        # Get Dockerfile path
+        if docker_file:
+            docker_file_path = Path(docker_file)
+        else:
+            docker_file_path = self.get_dockerfile_path(project, language)
+            
+        base_img = base_img or self.get_default_base_img()
+        img = img or self.get_default_img()
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        if verbose:
+            print(f"Build (HOLOHUB_ROOT: {self.holohub_root})")
+            print(f"Build (gpu_type: {gpu_type})")
+            print(f"Build (base_img: {base_img})")
+            print(f"Build (docker_file: {docker_file_path})")
+            print(f"Build (img: {img})")
+
+        # Check if buildx exists
+        try:
+            run_command(['docker', 'buildx', 'version'], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal("docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                  "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository")
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ['DOCKER_BUILDKIT'] = '1'
+        
+        cmd = [
+            'docker', 'build',
+            '--build-arg', f'BUILDKIT_INLINE_CACHE=1',
+            '--build-arg', f'BASE_IMAGE={base_img}',
+            '--build-arg', f'GPU_TYPE={gpu_type}',
+            '--build-arg', f'COMPUTE_CAPACITY={compute_capacity}',
+            '--network=host',
+        ]
+        
+        if no_cache:
+            cmd.append('--no-cache')
+            
+        if build_args:
+            cmd.extend(build_args.split())
+            
+        cmd.extend([
+            '-f', str(docker_file_path),
+            '-t', img,
+            str(self.holohub_root)
+        ])
+        
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def launch(self,
+               img: Optional[str] = None,
+               local_sdk_root: str = "undefined",
+               ssh_x11: bool = False,
+               use_tini: bool = False,
+               persistent: bool = False,
+               nsys_profile: bool = False,
+               nsys_location: str = "",
+               as_root: bool = False,
+               docker_opts: str = "",
+               add_volumes: List[str] = None,
+               verbose: bool = False,
+               dryrun: bool = False,
+               extra_args: List[str] = None) -> None:
+        """Launch the container"""
+        
+        check_nvidia_ctk()
+        
+        img = img or self.get_default_img()
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, 'run', '--net', 'host']
+        
+        # Basic options
+        cmd.extend(['--interactive'])
+        if sys.stdout.isatty():
+            cmd.append('--tty')
+            
+        # User permissions
+        if not as_root:
+            cmd.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
+        cmd.extend([
+            '-v', '/etc/group:/etc/group:ro',
+            '-v', '/etc/passwd:/etc/passwd:ro'
+        ])
+        
+        # Workspace mounting
+        cmd.extend([
+            '-v', f'{self.holohub_root}:/workspace/holohub',
+            '-w', '/workspace/holohub'
+        ])
+        
+        # GPU and device access
+        cmd.extend([
+            '--runtime=nvidia',
+            '--gpus', 'all',
+            '--cap-add', 'CAP_SYS_PTRACE',
+            '--ipc=host',
+            '-v', '/dev:/dev',
+            '--device-cgroup-rule', 'c 81:* rmw',
+            '--device-cgroup-rule', 'c 189:* rmw',
+        ])
+        
+        # Environment variables
+        cmd.extend([
+            '-e', 'NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display',
+            '-e', 'HOME=/workspace/holohub'
+        ])
+        
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(['-v', f'{volume}:/workspace/volumes/{base}'])
+            
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+        
+        # Add UCX options
+        cmd.extend(self.get_ucx_options())
+        
+        # Add display server options
+        self._add_display_options(cmd, ssh_x11)
+        
+        # Add local SDK options if provided
+        if local_sdk_root != "undefined":
+            self._add_local_sdk_options(cmd, local_sdk_root)
+            
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+            
+        # Add the image name
+        cmd.append(img)
+        
+        # Add any extra arguments
+        cmd.extend(extra_args)
+        
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+            
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def _add_display_options(self, cmd: List[str], ssh_x11: bool) -> None:
+        """Add display-related options to docker command"""
+        if 'XDG_SESSION_TYPE' in os.environ:
+            cmd.extend(['-e', 'XDG_SESSION_TYPE'])
+            if os.environ['XDG_SESSION_TYPE'] == 'wayland':
+                cmd.extend(['-e', 'WAYLAND_DISPLAY'])
+                
+        if 'XDG_RUNTIME_DIR' in os.environ:
+            cmd.extend(['-e', 'XDG_RUNTIME_DIR'])
+            if os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
+                cmd.extend(['-v', f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"])
+                
+        # Handle X11
+        session_type = os.environ.get('XDG_SESSION_TYPE', '')
+        if not session_type or session_type in ['x11', 'tty']:
+            if 'DISPLAY' in os.environ and shutil.which('xhost'):
+                run_command(['xhost', '+local:docker'])
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Instead of directly calling `run_command`, consider creating a wrapper function that handles the `xhost` command and logs any errors that occur.

```
            try:
                run_command(['xhost', '+local:docker'])
            except subprocess.CalledProcessError as e:
                print(f"Error running xhost command: {e}", file=sys.stderr)
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
+        
+    return holohub_app_source
+
+def get_app_source_lang_dir(app_name: str, language: str, script_dir: Path) -> Path:
+    """Get source path for a given application language implementation"""
+    holohub_app_source = get_app_source_root_dir(app_name, script_dir)
+    lang_dir = holohub_app_source / language
+    
+    if lang_dir.is_dir():
+        return lang_dir
+    return holohub_app_source
+
+def get_pkg_dir(pkg_name: str, script_dir: Path) -> Path:
+    """Get source path for a given package according to HoloHub convention"""
+    if not pkg_name:
+        fatal("Missing package name argument for get_pkg_dir")
+        
+    holohub_pkg_source = script_dir / "pkg" / pkg_name
+    
+    # Check if the package is in a subdirectory
+    if not holohub_pkg_source.is_dir():
+        sub_pkg_paths = list(script_dir.glob(f"pkg/*/{pkg_name}"))
+        if sub_pkg_paths:
+            holohub_pkg_source = sub_pkg_paths[0]
+            
+    if not holohub_pkg_source.is_dir():
+        fatal(f"Could not find package {pkg_name}")
+        
+    return holohub_pkg_source
+
+def get_metadata(path: Path) -> Dict:
+    """Read and parse metadata.json file"""
+    try:
+        with open(path / "metadata.json") as f:
+            return json.load(f)
+    except FileNotFoundError:
+        fatal(f"The metadata file does not exist: {path}/metadata.json")
+    except json.JSONDecodeError:
+        fatal(f"Invalid JSON in metadata file: {path}/metadata.json")
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob('metadata.json'):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+            
+            if '{{' in dir_name and '}}' in dir_name:
+                continue  # Skip templates
+                
+            if dir_name in ['cpp', 'python']:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+                
+            results.append((name, language))
+    
+    return sorted(results)
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob('CMakeLists.txt'):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split('(')[1].split(')')[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+class HoloHubContainer:
+    """Manages container operations for HoloHub"""
+    
+    def __init__(self):
+        self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+        self.holohub_root = self.script_dir
+        
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get('HOLOSCAN_PY_EXE', 'python3')
+        self.holoscan_docker_exe = os.environ.get('HOLOSCAN_DOCKER_EXE', 'docker')
+        self.holoscan_sdk_version = 'sdk-latest'
+        self.holohub_container_base_name = 'holohub'
+        
+        self.do_dry_run = False
+
+    def get_default_base_img(self) -> str:
+        """Get default base image name"""
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v2.9.0-{get_host_gpu()}"
+
+    def get_default_img(self) -> str:
+        """Get default image name"""
+        return f"holohub:ngc-v2.9.0-{get_host_gpu()}"
+
+    def get_ucx_options(self) -> List[str]:
+        """Get UCX-related docker options"""
+        return [
+            '--ipc=host',
+            '--cap-add=CAP_SYS_PTRACE',
+            '--ulimit=memlock=-1',
+            '--ulimit=stack=67108864'
+        ]
+
+    def get_conditional_options(self, use_tini: bool = False, persistent: bool = False) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+        
+        # Add device mounts
+        device_paths = [
+            '/usr/lib/libvideomasterhd.so',
+            '/opt/deltacast/videomaster/Include',
+            '/opt/yuan/qcap/include',
+            '/opt/yuan/qcap/lib',
+            '/usr/lib/aarch64-linux-gnu/tegra'
+        ]
+        
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f'--volume={path}:{path}')
+                else:
+                    options.append(f'--volume={path}:{path}')
+
+        # Add group permissions
+        for group in ['video', 'render', 'docker']:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f'--group-add={gid}')
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append('--init')
+        if not persistent:
+            options.append('--rm')
+
+        return options
+
+    def get_dockerfile_path(self, app_name: str = None, language: Optional[str] = 'cpp') -> Path:
+        """Get Dockerfile path for an application or default"""
+        if not app_name:
+            return self.script_dir / "Dockerfile"
+            
+        app_source = get_app_source_lang_dir(app_name, language, self.script_dir)
+        
+        # Check metadata.json for dockerfile entry
+        try:
+            metadata = get_metadata(app_source)
+            if "dockerfile" in metadata:
+                dockerfile = metadata["dockerfile"].replace("<holohub_app_source>", str(app_source))
+                return Path(dockerfile)
+        except (FileNotFoundError, KeyError):
+            pass
+            
+        # Check for Dockerfile in application directory
+        app_dockerfile = app_source / "Dockerfile"
+        if app_dockerfile.exists():
+            return app_dockerfile
+        
+        # Check for Dockerfile in language directory
+        lang_dockerfile = app_source / "Dockerfile"
+        if lang_dockerfile.exists():
+            return lang_dockerfile
+            
+        # Use default Dockerfile
+        return self.script_dir / "Dockerfile"
+
+    def build(self, 
+              docker_file: Optional[str] = None,
+              base_img: Optional[str] = None,
+              img: Optional[str] = None,
+              no_cache: bool = False,
+              build_args: Optional[str] = None,
+              project: Optional[str] = None,
+              language: Optional[str] = 'cpp',
+              verbose: bool = False) -> None:
+        """Build the container image"""
+        
+        # Get Dockerfile path
+        if docker_file:
+            docker_file_path = Path(docker_file)
+        else:
+            docker_file_path = self.get_dockerfile_path(project, language)
+            
+        base_img = base_img or self.get_default_base_img()
+        img = img or self.get_default_img()
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        if verbose:
+            print(f"Build (HOLOHUB_ROOT: {self.holohub_root})")
+            print(f"Build (gpu_type: {gpu_type})")
+            print(f"Build (base_img: {base_img})")
+            print(f"Build (docker_file: {docker_file_path})")
+            print(f"Build (img: {img})")
+
+        # Check if buildx exists
+        try:
+            run_command(['docker', 'buildx', 'version'], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal("docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                  "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository")
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ['DOCKER_BUILDKIT'] = '1'
+        
+        cmd = [
+            'docker', 'build',
+            '--build-arg', f'BUILDKIT_INLINE_CACHE=1',
+            '--build-arg', f'BASE_IMAGE={base_img}',
+            '--build-arg', f'GPU_TYPE={gpu_type}',
+            '--build-arg', f'COMPUTE_CAPACITY={compute_capacity}',
+            '--network=host',
+        ]
+        
+        if no_cache:
+            cmd.append('--no-cache')
+            
+        if build_args:
+            cmd.extend(build_args.split())
+            
+        cmd.extend([
+            '-f', str(docker_file_path),
+            '-t', img,
+            str(self.holohub_root)
+        ])
+        
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def launch(self,
+               img: Optional[str] = None,
+               local_sdk_root: str = "undefined",
+               ssh_x11: bool = False,
+               use_tini: bool = False,
+               persistent: bool = False,
+               nsys_profile: bool = False,
+               nsys_location: str = "",
+               as_root: bool = False,
+               docker_opts: str = "",
+               add_volumes: List[str] = None,
+               verbose: bool = False,
+               dryrun: bool = False,
+               extra_args: List[str] = None) -> None:
+        """Launch the container"""
+        
+        check_nvidia_ctk()
+        
+        img = img or self.get_default_img()
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, 'run', '--net', 'host']
+        
+        # Basic options
+        cmd.extend(['--interactive'])
+        if sys.stdout.isatty():
+            cmd.append('--tty')
+            
+        # User permissions
+        if not as_root:
+            cmd.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
+        cmd.extend([
+            '-v', '/etc/group:/etc/group:ro',
+            '-v', '/etc/passwd:/etc/passwd:ro'
+        ])
+        
+        # Workspace mounting
+        cmd.extend([
+            '-v', f'{self.holohub_root}:/workspace/holohub',
+            '-w', '/workspace/holohub'
+        ])
+        
+        # GPU and device access
+        cmd.extend([
+            '--runtime=nvidia',
+            '--gpus', 'all',
+            '--cap-add', 'CAP_SYS_PTRACE',
+            '--ipc=host',
+            '-v', '/dev:/dev',
+            '--device-cgroup-rule', 'c 81:* rmw',
+            '--device-cgroup-rule', 'c 189:* rmw',
+        ])
+        
+        # Environment variables
+        cmd.extend([
+            '-e', 'NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display',
+            '-e', 'HOME=/workspace/holohub'
+        ])
+        
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(['-v', f'{volume}:/workspace/volumes/{base}'])
+            
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+        
+        # Add UCX options
+        cmd.extend(self.get_ucx_options())
+        
+        # Add display server options
+        self._add_display_options(cmd, ssh_x11)
+        
+        # Add local SDK options if provided
+        if local_sdk_root != "undefined":
+            self._add_local_sdk_options(cmd, local_sdk_root)
+            
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+            
+        # Add the image name
+        cmd.append(img)
+        
+        # Add any extra arguments
+        cmd.extend(extra_args)
+        
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+            
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def _add_display_options(self, cmd: List[str], ssh_x11: bool) -> None:
+        """Add display-related options to docker command"""
+        if 'XDG_SESSION_TYPE' in os.environ:
+            cmd.extend(['-e', 'XDG_SESSION_TYPE'])
+            if os.environ['XDG_SESSION_TYPE'] == 'wayland':
+                cmd.extend(['-e', 'WAYLAND_DISPLAY'])
+                
+        if 'XDG_RUNTIME_DIR' in os.environ:
+            cmd.extend(['-e', 'XDG_RUNTIME_DIR'])
+            if os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
+                cmd.extend(['-v', f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"])
+                
+        # Handle X11
+        session_type = os.environ.get('XDG_SESSION_TYPE', '')
+        if not session_type or session_type in ['x11', 'tty']:
+            if 'DISPLAY' in os.environ and shutil.which('xhost'):
+                run_command(['xhost', '+local:docker'])
+            cmd.extend([
+                '-v', '/tmp/.X11-unix:/tmp/.X11-unix',
+                '-e', 'DISPLAY'
+            ])
+
+    def _add_local_sdk_options(self, cmd: List[str], local_sdk_root: str) -> None:
+        """Add Holoscan SDK-related options to docker command"""
+        pythonpath = "/workspace/holohub/benchmarks/holoscan_flow_benchmarking"
+        
+        if os.path.isdir(local_sdk_root):
+            cmd.extend(['-v', f'{local_sdk_root}:/workspace/holoscan-sdk'])
+            cmd.extend([
+                '-e', 'HOLOSCAN_LIB_PATH=/workspace/holoscan-sdk/build/lib',
+                '-e', 'HOLOSCAN_SAMPLE_DATA_PATH=/workspace/holoscan-sdk/data',
+                '-e', 'HOLOSCAN_TESTS_DATA_PATH=/workspace/holoscan-sdk/tests/data'
+            ])
+            pythonpath = f"/workspace/holoscan-sdk/build/python/lib:{pythonpath}"
+        else:
+            pythonpath = f"/opt/nvidia/holoscan/python/lib:{pythonpath}"
+            
+        cmd.extend(['-e', f'PYTHONPATH={pythonpath}'])
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+    
+    def __init__(self):
+        self.container = HoloHubContainer()
+        self.parser = self._create_parser()
+        
+        # Add build configuration
+        self.build_dir = self.container.script_dir / "build"
+        self.data_dir = os.environ.get('HOLOHUB_DATA_PATH', 
+                                     self.container.script_dir / "data")
+        self.holoscan_sdk_install = os.environ.get('HOLOSCAN_LIB_PATH',
+                                                  '/opt/nvidia/holoscan/lib')
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(description='HoloHub CLI tool')
+        subparsers = parser.add_subparsers(dest='command', required=True)
+        
+        # build-container command
+        build_container = subparsers.add_parser('build-container', 
+            help='Build the development container')
+        build_container.add_argument('project', nargs='?',
+            help='Project to build container for')
+        build_container.add_argument('--base_img',
+            help='Fully qualified base image name')
+        build_container.add_argument('--docker_file',
+            help='Path to Dockerfile to use')
+        build_container.add_argument('--img',
+            help='Specify fully qualified container name')
+        build_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker build command')
+        build_container.add_argument('--no-cache', action='store_true',
+            help='Do not use cache when building the image')
+        build_container.add_argument('--build-args',
+            help='Provides extra arguments to docker build command')
+        build_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser('run-container',
+            help='Launch the development container')
+        run_container.add_argument('--img',
+            help='Fully qualified image name')
+        run_container.add_argument('--local_sdk_root',
+            help='Path to Holoscan SDK used for building local Holoscan SDK container')
+        run_container.add_argument('--ssh_x11', action='store_true',
+            help='Enable X11 forwarding of graphical HoloHub applications over SSH')
+        run_container.add_argument('--init', action='store_true',
+            help='Support tini entry point')
+        run_container.add_argument('--persistent', action='store_true',
+            help='Does not delete container after it is run')
+        run_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker run command')
+        run_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run_container.add_argument('--add-volume', action='append',
+            help='Mount additional volume')
+        run_container.add_argument('--as_root', action='store_true',
+            help='Run the container with root permissions')
+        run_container.add_argument('--docker_opts',
+            help='Additional options to pass to the Docker launch')
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser('build',
+            help='Build a project')
+        build.add_argument('project',
+            help='Project to build')
+        build.add_argument('--local', action='store_true',
+            help='Build locally instead of in container')
+        build.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        build.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        build.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser('run',
+            help='Run a project')
+        run.add_argument('project',
+            help='Project to run')
+        run.add_argument('--local', action='store_true',
+            help='Run locally instead of in container')
+        run.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        run.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run.add_argument('--nsys-profile', action='store_true',
+            help='Enable Nsight Systems profiling')
+        run.add_argument('--language', choices=['cpp', 'python'],
+            help='Specify language implementation')
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser('list',
+            help='List all available targets')
+        list_cmd.set_defaults(func=self.handle_list)
+        
+        # lint command
+        lint = subparsers.add_parser('lint',
+            help='Run linting tools')
+        lint.add_argument('path', nargs='?', default='.',
+            help='Path to lint')
+        lint.add_argument('--fix', action='store_true',
+            help='Fix linting issues')
+        lint.add_argument('--install-dependencies', action='store_true',
+            help='Install linting dependencies')
+        lint.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        lint.set_defaults(func=self.handle_lint)
+        
+        # setup command
+        setup = subparsers.add_parser('setup',
+            help='Install HoloHub main required packages')
+        setup.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser('install',
+            help='Install a project')
+        install.add_argument('project',
+            help='Project to install')
+        install.add_argument('--local', action='store_true',
+            help='Install locally instead of in container')
+        install.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        install.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser('clear-cache',
+            help='Clear cache folders')
+        clear_cache.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+            project=args.project,
+            verbose=args.verbose
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.launch(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root or "undefined",
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts or "",
+            add_volumes=args.add_volume,
+            verbose=args.verbose
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get('CMAKE_BUILD_TYPE', 'Release')
+            
+        build_type_map = {
+            'debug': 'Debug',
+            'Debug': 'Debug',
+            'release': 'Release',
+            'Release': 'Release',
+            'rel-debug': 'RelWithDebInfo',
+            'RelWithDebInfo': 'RelWithDebInfo'
+        }
+        return build_type_map.get(build_type, 'Release')
+
+    def get_app_default_language(self, app_name: str) -> str:
+        """Get default language if multiple are available"""
+        apps = list_metadata_json_dir(
+            self.container.script_dir / "applications",
+            self.container.script_dir / "benchmarks"
+        )
+        for name, lang in apps:
+            if name == app_name:
+                return lang.strip('()') if lang else ''
+        return ''
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        language = (hasattr(args, 'language') and args.language) or self.get_app_default_language(args.project)
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a more specific exception type, such as `ValueError`, to provide more context about the error.

```
                raise ValueError("Could not determine project language. Please specify with --language")
```
created_at: 2025-03-10T17:39:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
+        
+    return holohub_app_source
+
+def get_app_source_lang_dir(app_name: str, language: str, script_dir: Path) -> Path:
+    """Get source path for a given application language implementation"""
+    holohub_app_source = get_app_source_root_dir(app_name, script_dir)
+    lang_dir = holohub_app_source / language
+    
+    if lang_dir.is_dir():
+        return lang_dir
+    return holohub_app_source
+
+def get_pkg_dir(pkg_name: str, script_dir: Path) -> Path:
+    """Get source path for a given package according to HoloHub convention"""
+    if not pkg_name:
+        fatal("Missing package name argument for get_pkg_dir")
+        
+    holohub_pkg_source = script_dir / "pkg" / pkg_name
+    
+    # Check if the package is in a subdirectory
+    if not holohub_pkg_source.is_dir():
+        sub_pkg_paths = list(script_dir.glob(f"pkg/*/{pkg_name}"))
+        if sub_pkg_paths:
+            holohub_pkg_source = sub_pkg_paths[0]
+            
+    if not holohub_pkg_source.is_dir():
+        fatal(f"Could not find package {pkg_name}")
+        
+    return holohub_pkg_source
+
+def get_metadata(path: Path) -> Dict:
+    """Read and parse metadata.json file"""
+    try:
+        with open(path / "metadata.json") as f:
+            return json.load(f)
+    except FileNotFoundError:
+        fatal(f"The metadata file does not exist: {path}/metadata.json")
+    except json.JSONDecodeError:
+        fatal(f"Invalid JSON in metadata file: {path}/metadata.json")
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob('metadata.json'):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+            
+            if '{{' in dir_name and '}}' in dir_name:
+                continue  # Skip templates
+                
+            if dir_name in ['cpp', 'python']:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+                
+            results.append((name, language))
+    
+    return sorted(results)
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob('CMakeLists.txt'):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split('(')[1].split(')')[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+class HoloHubContainer:
+    """Manages container operations for HoloHub"""
+    
+    def __init__(self):
+        self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+        self.holohub_root = self.script_dir
+        
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get('HOLOSCAN_PY_EXE', 'python3')
+        self.holoscan_docker_exe = os.environ.get('HOLOSCAN_DOCKER_EXE', 'docker')
+        self.holoscan_sdk_version = 'sdk-latest'
+        self.holohub_container_base_name = 'holohub'
+        
+        self.do_dry_run = False
+
+    def get_default_base_img(self) -> str:
+        """Get default base image name"""
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v2.9.0-{get_host_gpu()}"
+
+    def get_default_img(self) -> str:
+        """Get default image name"""
+        return f"holohub:ngc-v2.9.0-{get_host_gpu()}"
+
+    def get_ucx_options(self) -> List[str]:
+        """Get UCX-related docker options"""
+        return [
+            '--ipc=host',
+            '--cap-add=CAP_SYS_PTRACE',
+            '--ulimit=memlock=-1',
+            '--ulimit=stack=67108864'
+        ]
+
+    def get_conditional_options(self, use_tini: bool = False, persistent: bool = False) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+        
+        # Add device mounts
+        device_paths = [
+            '/usr/lib/libvideomasterhd.so',
+            '/opt/deltacast/videomaster/Include',
+            '/opt/yuan/qcap/include',
+            '/opt/yuan/qcap/lib',
+            '/usr/lib/aarch64-linux-gnu/tegra'
+        ]
+        
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f'--volume={path}:{path}')
+                else:
+                    options.append(f'--volume={path}:{path}')
+
+        # Add group permissions
+        for group in ['video', 'render', 'docker']:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f'--group-add={gid}')
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append('--init')
+        if not persistent:
+            options.append('--rm')
+
+        return options
+
+    def get_dockerfile_path(self, app_name: str = None, language: Optional[str] = 'cpp') -> Path:
+        """Get Dockerfile path for an application or default"""
+        if not app_name:
+            return self.script_dir / "Dockerfile"
+            
+        app_source = get_app_source_lang_dir(app_name, language, self.script_dir)
+        
+        # Check metadata.json for dockerfile entry
+        try:
+            metadata = get_metadata(app_source)
+            if "dockerfile" in metadata:
+                dockerfile = metadata["dockerfile"].replace("<holohub_app_source>", str(app_source))
+                return Path(dockerfile)
+        except (FileNotFoundError, KeyError):
+            pass
+            
+        # Check for Dockerfile in application directory
+        app_dockerfile = app_source / "Dockerfile"
+        if app_dockerfile.exists():
+            return app_dockerfile
+        
+        # Check for Dockerfile in language directory
+        lang_dockerfile = app_source / "Dockerfile"
+        if lang_dockerfile.exists():
+            return lang_dockerfile
+            
+        # Use default Dockerfile
+        return self.script_dir / "Dockerfile"
+
+    def build(self, 
+              docker_file: Optional[str] = None,
+              base_img: Optional[str] = None,
+              img: Optional[str] = None,
+              no_cache: bool = False,
+              build_args: Optional[str] = None,
+              project: Optional[str] = None,
+              language: Optional[str] = 'cpp',
+              verbose: bool = False) -> None:
+        """Build the container image"""
+        
+        # Get Dockerfile path
+        if docker_file:
+            docker_file_path = Path(docker_file)
+        else:
+            docker_file_path = self.get_dockerfile_path(project, language)
+            
+        base_img = base_img or self.get_default_base_img()
+        img = img or self.get_default_img()
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        if verbose:
+            print(f"Build (HOLOHUB_ROOT: {self.holohub_root})")
+            print(f"Build (gpu_type: {gpu_type})")
+            print(f"Build (base_img: {base_img})")
+            print(f"Build (docker_file: {docker_file_path})")
+            print(f"Build (img: {img})")
+
+        # Check if buildx exists
+        try:
+            run_command(['docker', 'buildx', 'version'], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal("docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                  "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository")
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ['DOCKER_BUILDKIT'] = '1'
+        
+        cmd = [
+            'docker', 'build',
+            '--build-arg', f'BUILDKIT_INLINE_CACHE=1',
+            '--build-arg', f'BASE_IMAGE={base_img}',
+            '--build-arg', f'GPU_TYPE={gpu_type}',
+            '--build-arg', f'COMPUTE_CAPACITY={compute_capacity}',
+            '--network=host',
+        ]
+        
+        if no_cache:
+            cmd.append('--no-cache')
+            
+        if build_args:
+            cmd.extend(build_args.split())
+            
+        cmd.extend([
+            '-f', str(docker_file_path),
+            '-t', img,
+            str(self.holohub_root)
+        ])
+        
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def launch(self,
+               img: Optional[str] = None,
+               local_sdk_root: str = "undefined",
+               ssh_x11: bool = False,
+               use_tini: bool = False,
+               persistent: bool = False,
+               nsys_profile: bool = False,
+               nsys_location: str = "",
+               as_root: bool = False,
+               docker_opts: str = "",
+               add_volumes: List[str] = None,
+               verbose: bool = False,
+               dryrun: bool = False,
+               extra_args: List[str] = None) -> None:
+        """Launch the container"""
+        
+        check_nvidia_ctk()
+        
+        img = img or self.get_default_img()
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, 'run', '--net', 'host']
+        
+        # Basic options
+        cmd.extend(['--interactive'])
+        if sys.stdout.isatty():
+            cmd.append('--tty')
+            
+        # User permissions
+        if not as_root:
+            cmd.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
+        cmd.extend([
+            '-v', '/etc/group:/etc/group:ro',
+            '-v', '/etc/passwd:/etc/passwd:ro'
+        ])
+        
+        # Workspace mounting
+        cmd.extend([
+            '-v', f'{self.holohub_root}:/workspace/holohub',
+            '-w', '/workspace/holohub'
+        ])
+        
+        # GPU and device access
+        cmd.extend([
+            '--runtime=nvidia',
+            '--gpus', 'all',
+            '--cap-add', 'CAP_SYS_PTRACE',
+            '--ipc=host',
+            '-v', '/dev:/dev',
+            '--device-cgroup-rule', 'c 81:* rmw',
+            '--device-cgroup-rule', 'c 189:* rmw',
+        ])
+        
+        # Environment variables
+        cmd.extend([
+            '-e', 'NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display',
+            '-e', 'HOME=/workspace/holohub'
+        ])
+        
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(['-v', f'{volume}:/workspace/volumes/{base}'])
+            
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+        
+        # Add UCX options
+        cmd.extend(self.get_ucx_options())
+        
+        # Add display server options
+        self._add_display_options(cmd, ssh_x11)
+        
+        # Add local SDK options if provided
+        if local_sdk_root != "undefined":
+            self._add_local_sdk_options(cmd, local_sdk_root)
+            
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+            
+        # Add the image name
+        cmd.append(img)
+        
+        # Add any extra arguments
+        cmd.extend(extra_args)
+        
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+            
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def _add_display_options(self, cmd: List[str], ssh_x11: bool) -> None:
+        """Add display-related options to docker command"""
+        if 'XDG_SESSION_TYPE' in os.environ:
+            cmd.extend(['-e', 'XDG_SESSION_TYPE'])
+            if os.environ['XDG_SESSION_TYPE'] == 'wayland':
+                cmd.extend(['-e', 'WAYLAND_DISPLAY'])
+                
+        if 'XDG_RUNTIME_DIR' in os.environ:
+            cmd.extend(['-e', 'XDG_RUNTIME_DIR'])
+            if os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
+                cmd.extend(['-v', f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"])
+                
+        # Handle X11
+        session_type = os.environ.get('XDG_SESSION_TYPE', '')
+        if not session_type or session_type in ['x11', 'tty']:
+            if 'DISPLAY' in os.environ and shutil.which('xhost'):
+                run_command(['xhost', '+local:docker'])
+            cmd.extend([
+                '-v', '/tmp/.X11-unix:/tmp/.X11-unix',
+                '-e', 'DISPLAY'
+            ])
+
+    def _add_local_sdk_options(self, cmd: List[str], local_sdk_root: str) -> None:
+        """Add Holoscan SDK-related options to docker command"""
+        pythonpath = "/workspace/holohub/benchmarks/holoscan_flow_benchmarking"
+        
+        if os.path.isdir(local_sdk_root):
+            cmd.extend(['-v', f'{local_sdk_root}:/workspace/holoscan-sdk'])
+            cmd.extend([
+                '-e', 'HOLOSCAN_LIB_PATH=/workspace/holoscan-sdk/build/lib',
+                '-e', 'HOLOSCAN_SAMPLE_DATA_PATH=/workspace/holoscan-sdk/data',
+                '-e', 'HOLOSCAN_TESTS_DATA_PATH=/workspace/holoscan-sdk/tests/data'
+            ])
+            pythonpath = f"/workspace/holoscan-sdk/build/python/lib:{pythonpath}"
+        else:
+            pythonpath = f"/opt/nvidia/holoscan/python/lib:{pythonpath}"
+            
+        cmd.extend(['-e', f'PYTHONPATH={pythonpath}'])
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+    
+    def __init__(self):
+        self.container = HoloHubContainer()
+        self.parser = self._create_parser()
+        
+        # Add build configuration
+        self.build_dir = self.container.script_dir / "build"
+        self.data_dir = os.environ.get('HOLOHUB_DATA_PATH', 
+                                     self.container.script_dir / "data")
+        self.holoscan_sdk_install = os.environ.get('HOLOSCAN_LIB_PATH',
+                                                  '/opt/nvidia/holoscan/lib')
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(description='HoloHub CLI tool')
+        subparsers = parser.add_subparsers(dest='command', required=True)
+        
+        # build-container command
+        build_container = subparsers.add_parser('build-container', 
+            help='Build the development container')
+        build_container.add_argument('project', nargs='?',
+            help='Project to build container for')
+        build_container.add_argument('--base_img',
+            help='Fully qualified base image name')
+        build_container.add_argument('--docker_file',
+            help='Path to Dockerfile to use')
+        build_container.add_argument('--img',
+            help='Specify fully qualified container name')
+        build_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker build command')
+        build_container.add_argument('--no-cache', action='store_true',
+            help='Do not use cache when building the image')
+        build_container.add_argument('--build-args',
+            help='Provides extra arguments to docker build command')
+        build_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser('run-container',
+            help='Launch the development container')
+        run_container.add_argument('--img',
+            help='Fully qualified image name')
+        run_container.add_argument('--local_sdk_root',
+            help='Path to Holoscan SDK used for building local Holoscan SDK container')
+        run_container.add_argument('--ssh_x11', action='store_true',
+            help='Enable X11 forwarding of graphical HoloHub applications over SSH')
+        run_container.add_argument('--init', action='store_true',
+            help='Support tini entry point')
+        run_container.add_argument('--persistent', action='store_true',
+            help='Does not delete container after it is run')
+        run_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker run command')
+        run_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run_container.add_argument('--add-volume', action='append',
+            help='Mount additional volume')
+        run_container.add_argument('--as_root', action='store_true',
+            help='Run the container with root permissions')
+        run_container.add_argument('--docker_opts',
+            help='Additional options to pass to the Docker launch')
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser('build',
+            help='Build a project')
+        build.add_argument('project',
+            help='Project to build')
+        build.add_argument('--local', action='store_true',
+            help='Build locally instead of in container')
+        build.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        build.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        build.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser('run',
+            help='Run a project')
+        run.add_argument('project',
+            help='Project to run')
+        run.add_argument('--local', action='store_true',
+            help='Run locally instead of in container')
+        run.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        run.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run.add_argument('--nsys-profile', action='store_true',
+            help='Enable Nsight Systems profiling')
+        run.add_argument('--language', choices=['cpp', 'python'],
+            help='Specify language implementation')
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser('list',
+            help='List all available targets')
+        list_cmd.set_defaults(func=self.handle_list)
+        
+        # lint command
+        lint = subparsers.add_parser('lint',
+            help='Run linting tools')
+        lint.add_argument('path', nargs='?', default='.',
+            help='Path to lint')
+        lint.add_argument('--fix', action='store_true',
+            help='Fix linting issues')
+        lint.add_argument('--install-dependencies', action='store_true',
+            help='Install linting dependencies')
+        lint.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        lint.set_defaults(func=self.handle_lint)
+        
+        # setup command
+        setup = subparsers.add_parser('setup',
+            help='Install HoloHub main required packages')
+        setup.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser('install',
+            help='Install a project')
+        install.add_argument('project',
+            help='Project to install')
+        install.add_argument('--local', action='store_true',
+            help='Install locally instead of in container')
+        install.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        install.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser('clear-cache',
+            help='Clear cache folders')
+        clear_cache.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+            project=args.project,
+            verbose=args.verbose
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.launch(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root or "undefined",
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts or "",
+            add_volumes=args.add_volume,
+            verbose=args.verbose
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get('CMAKE_BUILD_TYPE', 'Release')
+            
+        build_type_map = {
+            'debug': 'Debug',
+            'Debug': 'Debug',
+            'release': 'Release',
+            'Release': 'Release',
+            'rel-debug': 'RelWithDebInfo',
+            'RelWithDebInfo': 'RelWithDebInfo'
+        }
+        return build_type_map.get(build_type, 'Release')
+
+    def get_app_default_language(self, app_name: str) -> str:
+        """Get default language if multiple are available"""
+        apps = list_metadata_json_dir(
+            self.container.script_dir / "applications",
+            self.container.script_dir / "benchmarks"
+        )
+        for name, lang in apps:
+            if name == app_name:
+                return lang.strip('()') if lang else ''
+        return ''
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        language = (hasattr(args, 'language') and args.language) or self.get_app_default_language(args.project)
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            build_type = self.get_buildtype_str(args.build_type)
+            
+            # Local build
+            build_dir = self.build_dir / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+            
+            if language == 'cpp':
+                # Build C++ project
+                run_command([
+                    'cmake', '-B', str(build_dir),
+                    '-S', str(app_source),
+                    f'-DCMAKE_BUILD_TYPE={build_type}',
+                    f'-DCMAKE_PREFIX_PATH={self.holoscan_sdk_install}'
+                ], dry_run=args.dryrun)
+                run_command(['cmake', '--build', str(build_dir), '--config', build_type],
+                          dry_run=args.dryrun)
+            else:
+                # For Python projects, just create the directory
+                pass
+        else:
+            # Build in container
+            self.container.do_dry_run = args.dryrun
+            self.container.build(project=args.project, verbose=args.verbose)
+            self.container.launch(
+                docker_opts="--entrypoint=bash",
+                extra_args=['-c', f'holohub build {args.project} --language {language} --local'],
+                verbose=args.verbose
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            language = args.language or self.get_app_default_language(args.project)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a more specific exception type, such as `ValueError`, to provide more context about the error.

```
                raise ValueError("Could not determine project language. Please specify with --language")
```
created_at: 2025-03-10T17:39:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
+        
+    return holohub_app_source
+
+def get_app_source_lang_dir(app_name: str, language: str, script_dir: Path) -> Path:
+    """Get source path for a given application language implementation"""
+    holohub_app_source = get_app_source_root_dir(app_name, script_dir)
+    lang_dir = holohub_app_source / language
+    
+    if lang_dir.is_dir():
+        return lang_dir
+    return holohub_app_source
+
+def get_pkg_dir(pkg_name: str, script_dir: Path) -> Path:
+    """Get source path for a given package according to HoloHub convention"""
+    if not pkg_name:
+        fatal("Missing package name argument for get_pkg_dir")
+        
+    holohub_pkg_source = script_dir / "pkg" / pkg_name
+    
+    # Check if the package is in a subdirectory
+    if not holohub_pkg_source.is_dir():
+        sub_pkg_paths = list(script_dir.glob(f"pkg/*/{pkg_name}"))
+        if sub_pkg_paths:
+            holohub_pkg_source = sub_pkg_paths[0]
+            
+    if not holohub_pkg_source.is_dir():
+        fatal(f"Could not find package {pkg_name}")
+        
+    return holohub_pkg_source
+
+def get_metadata(path: Path) -> Dict:
+    """Read and parse metadata.json file"""
+    try:
+        with open(path / "metadata.json") as f:
+            return json.load(f)
+    except FileNotFoundError:
+        fatal(f"The metadata file does not exist: {path}/metadata.json")
+    except json.JSONDecodeError:
+        fatal(f"Invalid JSON in metadata file: {path}/metadata.json")
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob('metadata.json'):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+            
+            if '{{' in dir_name and '}}' in dir_name:
+                continue  # Skip templates
+                
+            if dir_name in ['cpp', 'python']:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+                
+            results.append((name, language))
+    
+    return sorted(results)
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob('CMakeLists.txt'):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split('(')[1].split(')')[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+class HoloHubContainer:
+    """Manages container operations for HoloHub"""
+    
+    def __init__(self):
+        self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+        self.holohub_root = self.script_dir
+        
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get('HOLOSCAN_PY_EXE', 'python3')
+        self.holoscan_docker_exe = os.environ.get('HOLOSCAN_DOCKER_EXE', 'docker')
+        self.holoscan_sdk_version = 'sdk-latest'
+        self.holohub_container_base_name = 'holohub'
+        
+        self.do_dry_run = False
+
+    def get_default_base_img(self) -> str:
+        """Get default base image name"""
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v2.9.0-{get_host_gpu()}"
+
+    def get_default_img(self) -> str:
+        """Get default image name"""
+        return f"holohub:ngc-v2.9.0-{get_host_gpu()}"
+
+    def get_ucx_options(self) -> List[str]:
+        """Get UCX-related docker options"""
+        return [
+            '--ipc=host',
+            '--cap-add=CAP_SYS_PTRACE',
+            '--ulimit=memlock=-1',
+            '--ulimit=stack=67108864'
+        ]
+
+    def get_conditional_options(self, use_tini: bool = False, persistent: bool = False) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+        
+        # Add device mounts
+        device_paths = [
+            '/usr/lib/libvideomasterhd.so',
+            '/opt/deltacast/videomaster/Include',
+            '/opt/yuan/qcap/include',
+            '/opt/yuan/qcap/lib',
+            '/usr/lib/aarch64-linux-gnu/tegra'
+        ]
+        
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f'--volume={path}:{path}')
+                else:
+                    options.append(f'--volume={path}:{path}')
+
+        # Add group permissions
+        for group in ['video', 'render', 'docker']:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f'--group-add={gid}')
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append('--init')
+        if not persistent:
+            options.append('--rm')
+
+        return options
+
+    def get_dockerfile_path(self, app_name: str = None, language: Optional[str] = 'cpp') -> Path:
+        """Get Dockerfile path for an application or default"""
+        if not app_name:
+            return self.script_dir / "Dockerfile"
+            
+        app_source = get_app_source_lang_dir(app_name, language, self.script_dir)
+        
+        # Check metadata.json for dockerfile entry
+        try:
+            metadata = get_metadata(app_source)
+            if "dockerfile" in metadata:
+                dockerfile = metadata["dockerfile"].replace("<holohub_app_source>", str(app_source))
+                return Path(dockerfile)
+        except (FileNotFoundError, KeyError):
+            pass
+            
+        # Check for Dockerfile in application directory
+        app_dockerfile = app_source / "Dockerfile"
+        if app_dockerfile.exists():
+            return app_dockerfile
+        
+        # Check for Dockerfile in language directory
+        lang_dockerfile = app_source / "Dockerfile"
+        if lang_dockerfile.exists():
+            return lang_dockerfile
+            
+        # Use default Dockerfile
+        return self.script_dir / "Dockerfile"
+
+    def build(self, 
+              docker_file: Optional[str] = None,
+              base_img: Optional[str] = None,
+              img: Optional[str] = None,
+              no_cache: bool = False,
+              build_args: Optional[str] = None,
+              project: Optional[str] = None,
+              language: Optional[str] = 'cpp',
+              verbose: bool = False) -> None:
+        """Build the container image"""
+        
+        # Get Dockerfile path
+        if docker_file:
+            docker_file_path = Path(docker_file)
+        else:
+            docker_file_path = self.get_dockerfile_path(project, language)
+            
+        base_img = base_img or self.get_default_base_img()
+        img = img or self.get_default_img()
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        if verbose:
+            print(f"Build (HOLOHUB_ROOT: {self.holohub_root})")
+            print(f"Build (gpu_type: {gpu_type})")
+            print(f"Build (base_img: {base_img})")
+            print(f"Build (docker_file: {docker_file_path})")
+            print(f"Build (img: {img})")
+
+        # Check if buildx exists
+        try:
+            run_command(['docker', 'buildx', 'version'], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal("docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                  "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository")
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ['DOCKER_BUILDKIT'] = '1'
+        
+        cmd = [
+            'docker', 'build',
+            '--build-arg', f'BUILDKIT_INLINE_CACHE=1',
+            '--build-arg', f'BASE_IMAGE={base_img}',
+            '--build-arg', f'GPU_TYPE={gpu_type}',
+            '--build-arg', f'COMPUTE_CAPACITY={compute_capacity}',
+            '--network=host',
+        ]
+        
+        if no_cache:
+            cmd.append('--no-cache')
+            
+        if build_args:
+            cmd.extend(build_args.split())
+            
+        cmd.extend([
+            '-f', str(docker_file_path),
+            '-t', img,
+            str(self.holohub_root)
+        ])
+        
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def launch(self,
+               img: Optional[str] = None,
+               local_sdk_root: str = "undefined",
+               ssh_x11: bool = False,
+               use_tini: bool = False,
+               persistent: bool = False,
+               nsys_profile: bool = False,
+               nsys_location: str = "",
+               as_root: bool = False,
+               docker_opts: str = "",
+               add_volumes: List[str] = None,
+               verbose: bool = False,
+               dryrun: bool = False,
+               extra_args: List[str] = None) -> None:
+        """Launch the container"""
+        
+        check_nvidia_ctk()
+        
+        img = img or self.get_default_img()
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, 'run', '--net', 'host']
+        
+        # Basic options
+        cmd.extend(['--interactive'])
+        if sys.stdout.isatty():
+            cmd.append('--tty')
+            
+        # User permissions
+        if not as_root:
+            cmd.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
+        cmd.extend([
+            '-v', '/etc/group:/etc/group:ro',
+            '-v', '/etc/passwd:/etc/passwd:ro'
+        ])
+        
+        # Workspace mounting
+        cmd.extend([
+            '-v', f'{self.holohub_root}:/workspace/holohub',
+            '-w', '/workspace/holohub'
+        ])
+        
+        # GPU and device access
+        cmd.extend([
+            '--runtime=nvidia',
+            '--gpus', 'all',
+            '--cap-add', 'CAP_SYS_PTRACE',
+            '--ipc=host',
+            '-v', '/dev:/dev',
+            '--device-cgroup-rule', 'c 81:* rmw',
+            '--device-cgroup-rule', 'c 189:* rmw',
+        ])
+        
+        # Environment variables
+        cmd.extend([
+            '-e', 'NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display',
+            '-e', 'HOME=/workspace/holohub'
+        ])
+        
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(['-v', f'{volume}:/workspace/volumes/{base}'])
+            
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+        
+        # Add UCX options
+        cmd.extend(self.get_ucx_options())
+        
+        # Add display server options
+        self._add_display_options(cmd, ssh_x11)
+        
+        # Add local SDK options if provided
+        if local_sdk_root != "undefined":
+            self._add_local_sdk_options(cmd, local_sdk_root)
+            
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+            
+        # Add the image name
+        cmd.append(img)
+        
+        # Add any extra arguments
+        cmd.extend(extra_args)
+        
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+            
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def _add_display_options(self, cmd: List[str], ssh_x11: bool) -> None:
+        """Add display-related options to docker command"""
+        if 'XDG_SESSION_TYPE' in os.environ:
+            cmd.extend(['-e', 'XDG_SESSION_TYPE'])
+            if os.environ['XDG_SESSION_TYPE'] == 'wayland':
+                cmd.extend(['-e', 'WAYLAND_DISPLAY'])
+                
+        if 'XDG_RUNTIME_DIR' in os.environ:
+            cmd.extend(['-e', 'XDG_RUNTIME_DIR'])
+            if os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
+                cmd.extend(['-v', f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"])
+                
+        # Handle X11
+        session_type = os.environ.get('XDG_SESSION_TYPE', '')
+        if not session_type or session_type in ['x11', 'tty']:
+            if 'DISPLAY' in os.environ and shutil.which('xhost'):
+                run_command(['xhost', '+local:docker'])
+            cmd.extend([
+                '-v', '/tmp/.X11-unix:/tmp/.X11-unix',
+                '-e', 'DISPLAY'
+            ])
+
+    def _add_local_sdk_options(self, cmd: List[str], local_sdk_root: str) -> None:
+        """Add Holoscan SDK-related options to docker command"""
+        pythonpath = "/workspace/holohub/benchmarks/holoscan_flow_benchmarking"
+        
+        if os.path.isdir(local_sdk_root):
+            cmd.extend(['-v', f'{local_sdk_root}:/workspace/holoscan-sdk'])
+            cmd.extend([
+                '-e', 'HOLOSCAN_LIB_PATH=/workspace/holoscan-sdk/build/lib',
+                '-e', 'HOLOSCAN_SAMPLE_DATA_PATH=/workspace/holoscan-sdk/data',
+                '-e', 'HOLOSCAN_TESTS_DATA_PATH=/workspace/holoscan-sdk/tests/data'
+            ])
+            pythonpath = f"/workspace/holoscan-sdk/build/python/lib:{pythonpath}"
+        else:
+            pythonpath = f"/opt/nvidia/holoscan/python/lib:{pythonpath}"
+            
+        cmd.extend(['-e', f'PYTHONPATH={pythonpath}'])
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+    
+    def __init__(self):
+        self.container = HoloHubContainer()
+        self.parser = self._create_parser()
+        
+        # Add build configuration
+        self.build_dir = self.container.script_dir / "build"
+        self.data_dir = os.environ.get('HOLOHUB_DATA_PATH', 
+                                     self.container.script_dir / "data")
+        self.holoscan_sdk_install = os.environ.get('HOLOSCAN_LIB_PATH',
+                                                  '/opt/nvidia/holoscan/lib')
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(description='HoloHub CLI tool')
+        subparsers = parser.add_subparsers(dest='command', required=True)
+        
+        # build-container command
+        build_container = subparsers.add_parser('build-container', 
+            help='Build the development container')
+        build_container.add_argument('project', nargs='?',
+            help='Project to build container for')
+        build_container.add_argument('--base_img',
+            help='Fully qualified base image name')
+        build_container.add_argument('--docker_file',
+            help='Path to Dockerfile to use')
+        build_container.add_argument('--img',
+            help='Specify fully qualified container name')
+        build_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker build command')
+        build_container.add_argument('--no-cache', action='store_true',
+            help='Do not use cache when building the image')
+        build_container.add_argument('--build-args',
+            help='Provides extra arguments to docker build command')
+        build_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser('run-container',
+            help='Launch the development container')
+        run_container.add_argument('--img',
+            help='Fully qualified image name')
+        run_container.add_argument('--local_sdk_root',
+            help='Path to Holoscan SDK used for building local Holoscan SDK container')
+        run_container.add_argument('--ssh_x11', action='store_true',
+            help='Enable X11 forwarding of graphical HoloHub applications over SSH')
+        run_container.add_argument('--init', action='store_true',
+            help='Support tini entry point')
+        run_container.add_argument('--persistent', action='store_true',
+            help='Does not delete container after it is run')
+        run_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker run command')
+        run_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run_container.add_argument('--add-volume', action='append',
+            help='Mount additional volume')
+        run_container.add_argument('--as_root', action='store_true',
+            help='Run the container with root permissions')
+        run_container.add_argument('--docker_opts',
+            help='Additional options to pass to the Docker launch')
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser('build',
+            help='Build a project')
+        build.add_argument('project',
+            help='Project to build')
+        build.add_argument('--local', action='store_true',
+            help='Build locally instead of in container')
+        build.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        build.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        build.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser('run',
+            help='Run a project')
+        run.add_argument('project',
+            help='Project to run')
+        run.add_argument('--local', action='store_true',
+            help='Run locally instead of in container')
+        run.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        run.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run.add_argument('--nsys-profile', action='store_true',
+            help='Enable Nsight Systems profiling')
+        run.add_argument('--language', choices=['cpp', 'python'],
+            help='Specify language implementation')
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser('list',
+            help='List all available targets')
+        list_cmd.set_defaults(func=self.handle_list)
+        
+        # lint command
+        lint = subparsers.add_parser('lint',
+            help='Run linting tools')
+        lint.add_argument('path', nargs='?', default='.',
+            help='Path to lint')
+        lint.add_argument('--fix', action='store_true',
+            help='Fix linting issues')
+        lint.add_argument('--install-dependencies', action='store_true',
+            help='Install linting dependencies')
+        lint.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        lint.set_defaults(func=self.handle_lint)
+        
+        # setup command
+        setup = subparsers.add_parser('setup',
+            help='Install HoloHub main required packages')
+        setup.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser('install',
+            help='Install a project')
+        install.add_argument('project',
+            help='Project to install')
+        install.add_argument('--local', action='store_true',
+            help='Install locally instead of in container')
+        install.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        install.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser('clear-cache',
+            help='Clear cache folders')
+        clear_cache.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+            project=args.project,
+            verbose=args.verbose
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.launch(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root or "undefined",
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts or "",
+            add_volumes=args.add_volume,
+            verbose=args.verbose
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get('CMAKE_BUILD_TYPE', 'Release')
+            
+        build_type_map = {
+            'debug': 'Debug',
+            'Debug': 'Debug',
+            'release': 'Release',
+            'Release': 'Release',
+            'rel-debug': 'RelWithDebInfo',
+            'RelWithDebInfo': 'RelWithDebInfo'
+        }
+        return build_type_map.get(build_type, 'Release')
+
+    def get_app_default_language(self, app_name: str) -> str:
+        """Get default language if multiple are available"""
+        apps = list_metadata_json_dir(
+            self.container.script_dir / "applications",
+            self.container.script_dir / "benchmarks"
+        )
+        for name, lang in apps:
+            if name == app_name:
+                return lang.strip('()') if lang else ''
+        return ''
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        language = (hasattr(args, 'language') and args.language) or self.get_app_default_language(args.project)
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            build_type = self.get_buildtype_str(args.build_type)
+            
+            # Local build
+            build_dir = self.build_dir / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+            
+            if language == 'cpp':
+                # Build C++ project
+                run_command([
+                    'cmake', '-B', str(build_dir),
+                    '-S', str(app_source),
+                    f'-DCMAKE_BUILD_TYPE={build_type}',
+                    f'-DCMAKE_PREFIX_PATH={self.holoscan_sdk_install}'
+                ], dry_run=args.dryrun)
+                run_command(['cmake', '--build', str(build_dir), '--config', build_type],
+                          dry_run=args.dryrun)
+            else:
+                # For Python projects, just create the directory
+                pass
+        else:
+            # Build in container
+            self.container.do_dry_run = args.dryrun
+            self.container.build(project=args.project, verbose=args.verbose)
+            self.container.launch(
+                docker_opts="--entrypoint=bash",
+                extra_args=['-c', f'holohub build {args.project} --language {language} --local'],
+                verbose=args.verbose
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            language = args.language or self.get_app_default_language(args.project)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            metadata = get_metadata(app_source)
+            
+            # Get run configuration from metadata
+            project_type = "benchmark" if "benchmark" in metadata else "application"
+            run_config = metadata[project_type]["run"]
+            
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(self.data_dir))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source))
+            
+            if language == 'cpp':
+                build_dir = self.build_dir / args.project
+                cmd = cmd.replace("<holohub_app_bin>", str(build_dir))
+                
+                if not build_dir.is_dir() and not args.dryrun:
+                    fatal(f"The build directory for this application does not exist.\n"
+                          f"Did you forget to './holohub build {args.project}'?")
+            
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(app_source)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {app_source}\033[0m")
+            elif workdir == "holohub_bin":
+                if not args.dryrun:
+                    os.chdir(self.build_dir)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {self.build_dir}\033[0m")
+            else:  # default to app binary directory
+                target_dir = build_dir if language == 'cpp' else app_source
+                if not args.dryrun:
+                    os.chdir(target_dir)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {target_dir}\033[0m")
+            
+            # Set up environment
+            env = os.environ.copy()
+            env['PYTHONPATH'] = f"{self.holoscan_sdk_install}/../python/lib:{self.build_dir}/python/lib:{self.container.script_dir}"
+            env['HOLOHUB_DATA_PATH'] = str(self.data_dir)
+            env['HOLOSCAN_INPUT_PATH'] = os.environ.get('HOLOSCAN_INPUT_PATH', str(self.data_dir))
+            
+            # Print environment setup
+            if args.verbose or args.dryrun:
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport PYTHONPATH={env['PYTHONPATH']}\033[0m")
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport HOLOHUB_DATA_PATH={env['HOLOHUB_DATA_PATH']}\033[0m")
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport HOLOSCAN_INPUT_PATH={env['HOLOSCAN_INPUT_PATH']}\033[0m")
+            
+            # Handle Nsight Systems profiling
+            if args.nsys_profile:
+                if not shutil.which('nsys') and not os.path.isdir('/opt/nvidia/nsys-host') and not args.dryrun:
+                    fatal("Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted.")
+                nsys_cmd = '/opt/nvidia/nsys-host/bin/nsys' if not shutil.which('nsys') else 'nsys'
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a custom exception with a more descriptive error message.

```
                    raise FileNotFoundError("Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted.")
```
created_at: 2025-03-10T17:39:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
+        
+    return holohub_app_source
+
+def get_app_source_lang_dir(app_name: str, language: str, script_dir: Path) -> Path:
+    """Get source path for a given application language implementation"""
+    holohub_app_source = get_app_source_root_dir(app_name, script_dir)
+    lang_dir = holohub_app_source / language
+    
+    if lang_dir.is_dir():
+        return lang_dir
+    return holohub_app_source
+
+def get_pkg_dir(pkg_name: str, script_dir: Path) -> Path:
+    """Get source path for a given package according to HoloHub convention"""
+    if not pkg_name:
+        fatal("Missing package name argument for get_pkg_dir")
+        
+    holohub_pkg_source = script_dir / "pkg" / pkg_name
+    
+    # Check if the package is in a subdirectory
+    if not holohub_pkg_source.is_dir():
+        sub_pkg_paths = list(script_dir.glob(f"pkg/*/{pkg_name}"))
+        if sub_pkg_paths:
+            holohub_pkg_source = sub_pkg_paths[0]
+            
+    if not holohub_pkg_source.is_dir():
+        fatal(f"Could not find package {pkg_name}")
+        
+    return holohub_pkg_source
+
+def get_metadata(path: Path) -> Dict:
+    """Read and parse metadata.json file"""
+    try:
+        with open(path / "metadata.json") as f:
+            return json.load(f)
+    except FileNotFoundError:
+        fatal(f"The metadata file does not exist: {path}/metadata.json")
+    except json.JSONDecodeError:
+        fatal(f"Invalid JSON in metadata file: {path}/metadata.json")
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob('metadata.json'):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+            
+            if '{{' in dir_name and '}}' in dir_name:
+                continue  # Skip templates
+                
+            if dir_name in ['cpp', 'python']:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+                
+            results.append((name, language))
+    
+    return sorted(results)
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob('CMakeLists.txt'):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split('(')[1].split(')')[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+class HoloHubContainer:
+    """Manages container operations for HoloHub"""
+    
+    def __init__(self):
+        self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+        self.holohub_root = self.script_dir
+        
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get('HOLOSCAN_PY_EXE', 'python3')
+        self.holoscan_docker_exe = os.environ.get('HOLOSCAN_DOCKER_EXE', 'docker')
+        self.holoscan_sdk_version = 'sdk-latest'
+        self.holohub_container_base_name = 'holohub'
+        
+        self.do_dry_run = False
+
+    def get_default_base_img(self) -> str:
+        """Get default base image name"""
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v2.9.0-{get_host_gpu()}"
+
+    def get_default_img(self) -> str:
+        """Get default image name"""
+        return f"holohub:ngc-v2.9.0-{get_host_gpu()}"
+
+    def get_ucx_options(self) -> List[str]:
+        """Get UCX-related docker options"""
+        return [
+            '--ipc=host',
+            '--cap-add=CAP_SYS_PTRACE',
+            '--ulimit=memlock=-1',
+            '--ulimit=stack=67108864'
+        ]
+
+    def get_conditional_options(self, use_tini: bool = False, persistent: bool = False) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+        
+        # Add device mounts
+        device_paths = [
+            '/usr/lib/libvideomasterhd.so',
+            '/opt/deltacast/videomaster/Include',
+            '/opt/yuan/qcap/include',
+            '/opt/yuan/qcap/lib',
+            '/usr/lib/aarch64-linux-gnu/tegra'
+        ]
+        
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f'--volume={path}:{path}')
+                else:
+                    options.append(f'--volume={path}:{path}')
+
+        # Add group permissions
+        for group in ['video', 'render', 'docker']:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f'--group-add={gid}')
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append('--init')
+        if not persistent:
+            options.append('--rm')
+
+        return options
+
+    def get_dockerfile_path(self, app_name: str = None, language: Optional[str] = 'cpp') -> Path:
+        """Get Dockerfile path for an application or default"""
+        if not app_name:
+            return self.script_dir / "Dockerfile"
+            
+        app_source = get_app_source_lang_dir(app_name, language, self.script_dir)
+        
+        # Check metadata.json for dockerfile entry
+        try:
+            metadata = get_metadata(app_source)
+            if "dockerfile" in metadata:
+                dockerfile = metadata["dockerfile"].replace("<holohub_app_source>", str(app_source))
+                return Path(dockerfile)
+        except (FileNotFoundError, KeyError):
+            pass
+            
+        # Check for Dockerfile in application directory
+        app_dockerfile = app_source / "Dockerfile"
+        if app_dockerfile.exists():
+            return app_dockerfile
+        
+        # Check for Dockerfile in language directory
+        lang_dockerfile = app_source / "Dockerfile"
+        if lang_dockerfile.exists():
+            return lang_dockerfile
+            
+        # Use default Dockerfile
+        return self.script_dir / "Dockerfile"
+
+    def build(self, 
+              docker_file: Optional[str] = None,
+              base_img: Optional[str] = None,
+              img: Optional[str] = None,
+              no_cache: bool = False,
+              build_args: Optional[str] = None,
+              project: Optional[str] = None,
+              language: Optional[str] = 'cpp',
+              verbose: bool = False) -> None:
+        """Build the container image"""
+        
+        # Get Dockerfile path
+        if docker_file:
+            docker_file_path = Path(docker_file)
+        else:
+            docker_file_path = self.get_dockerfile_path(project, language)
+            
+        base_img = base_img or self.get_default_base_img()
+        img = img or self.get_default_img()
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        if verbose:
+            print(f"Build (HOLOHUB_ROOT: {self.holohub_root})")
+            print(f"Build (gpu_type: {gpu_type})")
+            print(f"Build (base_img: {base_img})")
+            print(f"Build (docker_file: {docker_file_path})")
+            print(f"Build (img: {img})")
+
+        # Check if buildx exists
+        try:
+            run_command(['docker', 'buildx', 'version'], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal("docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                  "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository")
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ['DOCKER_BUILDKIT'] = '1'
+        
+        cmd = [
+            'docker', 'build',
+            '--build-arg', f'BUILDKIT_INLINE_CACHE=1',
+            '--build-arg', f'BASE_IMAGE={base_img}',
+            '--build-arg', f'GPU_TYPE={gpu_type}',
+            '--build-arg', f'COMPUTE_CAPACITY={compute_capacity}',
+            '--network=host',
+        ]
+        
+        if no_cache:
+            cmd.append('--no-cache')
+            
+        if build_args:
+            cmd.extend(build_args.split())
+            
+        cmd.extend([
+            '-f', str(docker_file_path),
+            '-t', img,
+            str(self.holohub_root)
+        ])
+        
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def launch(self,
+               img: Optional[str] = None,
+               local_sdk_root: str = "undefined",
+               ssh_x11: bool = False,
+               use_tini: bool = False,
+               persistent: bool = False,
+               nsys_profile: bool = False,
+               nsys_location: str = "",
+               as_root: bool = False,
+               docker_opts: str = "",
+               add_volumes: List[str] = None,
+               verbose: bool = False,
+               dryrun: bool = False,
+               extra_args: List[str] = None) -> None:
+        """Launch the container"""
+        
+        check_nvidia_ctk()
+        
+        img = img or self.get_default_img()
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, 'run', '--net', 'host']
+        
+        # Basic options
+        cmd.extend(['--interactive'])
+        if sys.stdout.isatty():
+            cmd.append('--tty')
+            
+        # User permissions
+        if not as_root:
+            cmd.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
+        cmd.extend([
+            '-v', '/etc/group:/etc/group:ro',
+            '-v', '/etc/passwd:/etc/passwd:ro'
+        ])
+        
+        # Workspace mounting
+        cmd.extend([
+            '-v', f'{self.holohub_root}:/workspace/holohub',
+            '-w', '/workspace/holohub'
+        ])
+        
+        # GPU and device access
+        cmd.extend([
+            '--runtime=nvidia',
+            '--gpus', 'all',
+            '--cap-add', 'CAP_SYS_PTRACE',
+            '--ipc=host',
+            '-v', '/dev:/dev',
+            '--device-cgroup-rule', 'c 81:* rmw',
+            '--device-cgroup-rule', 'c 189:* rmw',
+        ])
+        
+        # Environment variables
+        cmd.extend([
+            '-e', 'NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display',
+            '-e', 'HOME=/workspace/holohub'
+        ])
+        
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(['-v', f'{volume}:/workspace/volumes/{base}'])
+            
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+        
+        # Add UCX options
+        cmd.extend(self.get_ucx_options())
+        
+        # Add display server options
+        self._add_display_options(cmd, ssh_x11)
+        
+        # Add local SDK options if provided
+        if local_sdk_root != "undefined":
+            self._add_local_sdk_options(cmd, local_sdk_root)
+            
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+            
+        # Add the image name
+        cmd.append(img)
+        
+        # Add any extra arguments
+        cmd.extend(extra_args)
+        
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+            
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def _add_display_options(self, cmd: List[str], ssh_x11: bool) -> None:
+        """Add display-related options to docker command"""
+        if 'XDG_SESSION_TYPE' in os.environ:
+            cmd.extend(['-e', 'XDG_SESSION_TYPE'])
+            if os.environ['XDG_SESSION_TYPE'] == 'wayland':
+                cmd.extend(['-e', 'WAYLAND_DISPLAY'])
+                
+        if 'XDG_RUNTIME_DIR' in os.environ:
+            cmd.extend(['-e', 'XDG_RUNTIME_DIR'])
+            if os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
+                cmd.extend(['-v', f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"])
+                
+        # Handle X11
+        session_type = os.environ.get('XDG_SESSION_TYPE', '')
+        if not session_type or session_type in ['x11', 'tty']:
+            if 'DISPLAY' in os.environ and shutil.which('xhost'):
+                run_command(['xhost', '+local:docker'])
+            cmd.extend([
+                '-v', '/tmp/.X11-unix:/tmp/.X11-unix',
+                '-e', 'DISPLAY'
+            ])
+
+    def _add_local_sdk_options(self, cmd: List[str], local_sdk_root: str) -> None:
+        """Add Holoscan SDK-related options to docker command"""
+        pythonpath = "/workspace/holohub/benchmarks/holoscan_flow_benchmarking"
+        
+        if os.path.isdir(local_sdk_root):
+            cmd.extend(['-v', f'{local_sdk_root}:/workspace/holoscan-sdk'])
+            cmd.extend([
+                '-e', 'HOLOSCAN_LIB_PATH=/workspace/holoscan-sdk/build/lib',
+                '-e', 'HOLOSCAN_SAMPLE_DATA_PATH=/workspace/holoscan-sdk/data',
+                '-e', 'HOLOSCAN_TESTS_DATA_PATH=/workspace/holoscan-sdk/tests/data'
+            ])
+            pythonpath = f"/workspace/holoscan-sdk/build/python/lib:{pythonpath}"
+        else:
+            pythonpath = f"/opt/nvidia/holoscan/python/lib:{pythonpath}"
+            
+        cmd.extend(['-e', f'PYTHONPATH={pythonpath}'])
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+    
+    def __init__(self):
+        self.container = HoloHubContainer()
+        self.parser = self._create_parser()
+        
+        # Add build configuration
+        self.build_dir = self.container.script_dir / "build"
+        self.data_dir = os.environ.get('HOLOHUB_DATA_PATH', 
+                                     self.container.script_dir / "data")
+        self.holoscan_sdk_install = os.environ.get('HOLOSCAN_LIB_PATH',
+                                                  '/opt/nvidia/holoscan/lib')
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(description='HoloHub CLI tool')
+        subparsers = parser.add_subparsers(dest='command', required=True)
+        
+        # build-container command
+        build_container = subparsers.add_parser('build-container', 
+            help='Build the development container')
+        build_container.add_argument('project', nargs='?',
+            help='Project to build container for')
+        build_container.add_argument('--base_img',
+            help='Fully qualified base image name')
+        build_container.add_argument('--docker_file',
+            help='Path to Dockerfile to use')
+        build_container.add_argument('--img',
+            help='Specify fully qualified container name')
+        build_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker build command')
+        build_container.add_argument('--no-cache', action='store_true',
+            help='Do not use cache when building the image')
+        build_container.add_argument('--build-args',
+            help='Provides extra arguments to docker build command')
+        build_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser('run-container',
+            help='Launch the development container')
+        run_container.add_argument('--img',
+            help='Fully qualified image name')
+        run_container.add_argument('--local_sdk_root',
+            help='Path to Holoscan SDK used for building local Holoscan SDK container')
+        run_container.add_argument('--ssh_x11', action='store_true',
+            help='Enable X11 forwarding of graphical HoloHub applications over SSH')
+        run_container.add_argument('--init', action='store_true',
+            help='Support tini entry point')
+        run_container.add_argument('--persistent', action='store_true',
+            help='Does not delete container after it is run')
+        run_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker run command')
+        run_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run_container.add_argument('--add-volume', action='append',
+            help='Mount additional volume')
+        run_container.add_argument('--as_root', action='store_true',
+            help='Run the container with root permissions')
+        run_container.add_argument('--docker_opts',
+            help='Additional options to pass to the Docker launch')
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser('build',
+            help='Build a project')
+        build.add_argument('project',
+            help='Project to build')
+        build.add_argument('--local', action='store_true',
+            help='Build locally instead of in container')
+        build.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        build.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        build.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser('run',
+            help='Run a project')
+        run.add_argument('project',
+            help='Project to run')
+        run.add_argument('--local', action='store_true',
+            help='Run locally instead of in container')
+        run.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        run.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run.add_argument('--nsys-profile', action='store_true',
+            help='Enable Nsight Systems profiling')
+        run.add_argument('--language', choices=['cpp', 'python'],
+            help='Specify language implementation')
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser('list',
+            help='List all available targets')
+        list_cmd.set_defaults(func=self.handle_list)
+        
+        # lint command
+        lint = subparsers.add_parser('lint',
+            help='Run linting tools')
+        lint.add_argument('path', nargs='?', default='.',
+            help='Path to lint')
+        lint.add_argument('--fix', action='store_true',
+            help='Fix linting issues')
+        lint.add_argument('--install-dependencies', action='store_true',
+            help='Install linting dependencies')
+        lint.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        lint.set_defaults(func=self.handle_lint)
+        
+        # setup command
+        setup = subparsers.add_parser('setup',
+            help='Install HoloHub main required packages')
+        setup.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser('install',
+            help='Install a project')
+        install.add_argument('project',
+            help='Project to install')
+        install.add_argument('--local', action='store_true',
+            help='Install locally instead of in container')
+        install.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        install.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser('clear-cache',
+            help='Clear cache folders')
+        clear_cache.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+            project=args.project,
+            verbose=args.verbose
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.launch(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root or "undefined",
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts or "",
+            add_volumes=args.add_volume,
+            verbose=args.verbose
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get('CMAKE_BUILD_TYPE', 'Release')
+            
+        build_type_map = {
+            'debug': 'Debug',
+            'Debug': 'Debug',
+            'release': 'Release',
+            'Release': 'Release',
+            'rel-debug': 'RelWithDebInfo',
+            'RelWithDebInfo': 'RelWithDebInfo'
+        }
+        return build_type_map.get(build_type, 'Release')
+
+    def get_app_default_language(self, app_name: str) -> str:
+        """Get default language if multiple are available"""
+        apps = list_metadata_json_dir(
+            self.container.script_dir / "applications",
+            self.container.script_dir / "benchmarks"
+        )
+        for name, lang in apps:
+            if name == app_name:
+                return lang.strip('()') if lang else ''
+        return ''
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        language = (hasattr(args, 'language') and args.language) or self.get_app_default_language(args.project)
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            build_type = self.get_buildtype_str(args.build_type)
+            
+            # Local build
+            build_dir = self.build_dir / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+            
+            if language == 'cpp':
+                # Build C++ project
+                run_command([
+                    'cmake', '-B', str(build_dir),
+                    '-S', str(app_source),
+                    f'-DCMAKE_BUILD_TYPE={build_type}',
+                    f'-DCMAKE_PREFIX_PATH={self.holoscan_sdk_install}'
+                ], dry_run=args.dryrun)
+                run_command(['cmake', '--build', str(build_dir), '--config', build_type],
+                          dry_run=args.dryrun)
+            else:
+                # For Python projects, just create the directory
+                pass
+        else:
+            # Build in container
+            self.container.do_dry_run = args.dryrun
+            self.container.build(project=args.project, verbose=args.verbose)
+            self.container.launch(
+                docker_opts="--entrypoint=bash",
+                extra_args=['-c', f'holohub build {args.project} --language {language} --local'],
+                verbose=args.verbose
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            language = args.language or self.get_app_default_language(args.project)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            metadata = get_metadata(app_source)
+            
+            # Get run configuration from metadata
+            project_type = "benchmark" if "benchmark" in metadata else "application"
+            run_config = metadata[project_type]["run"]
+            
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(self.data_dir))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source))
+            
+            if language == 'cpp':
+                build_dir = self.build_dir / args.project
+                cmd = cmd.replace("<holohub_app_bin>", str(build_dir))
+                
+                if not build_dir.is_dir() and not args.dryrun:
+                    fatal(f"The build directory for this application does not exist.\n"
+                          f"Did you forget to './holohub build {args.project}'?")
+            
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(app_source)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {app_source}\033[0m")
+            elif workdir == "holohub_bin":
+                if not args.dryrun:
+                    os.chdir(self.build_dir)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {self.build_dir}\033[0m")
+            else:  # default to app binary directory
+                target_dir = build_dir if language == 'cpp' else app_source
+                if not args.dryrun:
+                    os.chdir(target_dir)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {target_dir}\033[0m")
+            
+            # Set up environment
+            env = os.environ.copy()
+            env['PYTHONPATH'] = f"{self.holoscan_sdk_install}/../python/lib:{self.build_dir}/python/lib:{self.container.script_dir}"
+            env['HOLOHUB_DATA_PATH'] = str(self.data_dir)
+            env['HOLOSCAN_INPUT_PATH'] = os.environ.get('HOLOSCAN_INPUT_PATH', str(self.data_dir))
+            
+            # Print environment setup
+            if args.verbose or args.dryrun:
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport PYTHONPATH={env['PYTHONPATH']}\033[0m")
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport HOLOHUB_DATA_PATH={env['HOLOHUB_DATA_PATH']}\033[0m")
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport HOLOSCAN_INPUT_PATH={env['HOLOSCAN_INPUT_PATH']}\033[0m")
+            
+            # Handle Nsight Systems profiling
+            if args.nsys_profile:
+                if not shutil.which('nsys') and not os.path.isdir('/opt/nvidia/nsys-host') and not args.dryrun:
+                    fatal("Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted.")
+                nsys_cmd = '/opt/nvidia/nsys-host/bin/nsys' if not shutil.which('nsys') else 'nsys'
+                
+                # Check perf_event_paranoid level
+                if not args.dryrun:
+                    try:
+                        with open('/proc/sys/kernel/perf_event_paranoid') as f:
+                            if int(f.read()) > 2:
+                                fatal("For Nsight Systems profiling the Linux operating system's perf_event_paranoid level must be 2 or less.")
+                    except (IOError, ValueError):
+                        pass
+                
+                cmd = f"{nsys_cmd} profile --trace=cuda,vulkan,nvtx,osrt {cmd}"
+            
+            run_command(cmd.split(), env=env, dry_run=args.dryrun)
+        else:
+            # Pass dryrun to container launch if specified
+            self.container.do_dry_run = args.dryrun
+            
+            # Get language before launching container
+            language = args.language or self.get_app_default_language(args.project)
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a more specific exception type, such as `ValueError`, to provide more context about the error.

```
                raise ValueError("Could not determine project language. Please specify with --language")
```
created_at: 2025-03-10T17:39:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,1060 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional, Dict, Any, Tuple
+from pathlib import Path
+import shutil
+import pwd
+import grp
+from datetime import datetime
+import json
+import glob
+import traceback
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+def run_command(cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = ' '.join(str(x) for x in cmd)
+    if dry_run:
+        print(f"\033[34m{get_timestamp()} \033[36m[dryrun] \033[37m$ \033[32m{cmd_str}\033[0m")
+        return subprocess.CompletedProcess(cmd, 0)
+    
+    print(f"\033[34m{get_timestamp()} \033[37m$ \033[32m{cmd_str}\033[0m")
+    try:
+        return subprocess.run(cmd, check=check, **kwargs)
+    except subprocess.CalledProcessError as e:
+        print(f"Error running command: {cmd_str}")
+        print(f"Exit code: {e.returncode}")
+        sys.exit(e.returncode)
+
+def check_nvidia_ctk() -> None:
+    """Check NVIDIA Container Toolkit version"""
+    min_version = "1.12.0"
+    recommended_version = "1.14.1"
+    
+    if not shutil.which('nvidia-ctk'):
+        fatal("nvidia-ctk not found. Please install the NVIDIA Container Toolkit.")
+        
+    try:
+        output = subprocess.check_output(['nvidia-ctk', '--version'], text=True)
+        import re
+        match = re.search(r'(\d+\.\d+\.\d+)', output)
+        if match:
+            version = match.group(1)
+            from packaging import version as ver
+            if ver.parse(version) < ver.parse(min_version):
+                fatal(f"Found nvidia-ctk Version {version}. Version {min_version}+ is required ({recommended_version}+ recommended).")
+    except subprocess.CalledProcessError:
+        fatal(f"Could not determine nvidia-ctk version. Version {min_version}+ required.")
+
+def get_host_gpu() -> str:
+    """Determine if running on dGPU or iGPU"""
+    if not shutil.which('nvidia-smi'):
+        print("Could not find any GPU drivers on host. Defaulting build to target dGPU/CPU stack.", file=sys.stderr)
+        return "dgpu"
+        
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'])
+        if not output or 'Orin (nvgpu)' in output.decode():
+            return "igpu"
+    except subprocess.CalledProcessError:
+        return "dgpu"
+        
+    return "dgpu"
+
+def get_compute_capacity() -> str:
+    """Get GPU compute capacity"""
+    if not shutil.which('nvidia-smi'):
+        return "0.0"
+    try:
+        output = subprocess.check_output(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'])
+        return output.decode().strip().split('\n')[0]
+    except subprocess.CalledProcessError:
+        return "0.0"
+
+def get_group_id(group: str) -> Optional[int]:
+    """Get group ID for a given group name"""
+    try:
+        return grp.getgrnam(group).gr_gid
+    except KeyError:
+        return None
+
+def get_app_source_root_dir(app_name: str, script_dir: Path) -> Path:
+    """Get source path for a given application according to HoloHub convention"""
+    if not app_name:
+        fatal("Missing app name argument for get_app_source_root_dir")
+        
+    holohub_app_source = script_dir / "applications" / app_name
+    
+    # Check if the application is in a subdirectory
+    if not holohub_app_source.is_dir():
+        sub_app_paths = list(script_dir.glob(f"applications/*/{app_name}"))
+        if sub_app_paths:
+            holohub_app_source = sub_app_paths[0]
+            
+    # Check if the application is in the benchmarks folder
+    if not holohub_app_source.is_dir():
+        benchmark_path = script_dir / "benchmarks" / app_name
+        if benchmark_path.is_dir():
+            holohub_app_source = benchmark_path
+            
+    if not holohub_app_source.is_dir():
+        fatal(f"Could not find project {app_name}")
+        
+    return holohub_app_source
+
+def get_app_source_lang_dir(app_name: str, language: str, script_dir: Path) -> Path:
+    """Get source path for a given application language implementation"""
+    holohub_app_source = get_app_source_root_dir(app_name, script_dir)
+    lang_dir = holohub_app_source / language
+    
+    if lang_dir.is_dir():
+        return lang_dir
+    return holohub_app_source
+
+def get_pkg_dir(pkg_name: str, script_dir: Path) -> Path:
+    """Get source path for a given package according to HoloHub convention"""
+    if not pkg_name:
+        fatal("Missing package name argument for get_pkg_dir")
+        
+    holohub_pkg_source = script_dir / "pkg" / pkg_name
+    
+    # Check if the package is in a subdirectory
+    if not holohub_pkg_source.is_dir():
+        sub_pkg_paths = list(script_dir.glob(f"pkg/*/{pkg_name}"))
+        if sub_pkg_paths:
+            holohub_pkg_source = sub_pkg_paths[0]
+            
+    if not holohub_pkg_source.is_dir():
+        fatal(f"Could not find package {pkg_name}")
+        
+    return holohub_pkg_source
+
+def get_metadata(path: Path) -> Dict:
+    """Read and parse metadata.json file"""
+    try:
+        with open(path / "metadata.json") as f:
+            return json.load(f)
+    except FileNotFoundError:
+        fatal(f"The metadata file does not exist: {path}/metadata.json")
+    except json.JSONDecodeError:
+        fatal(f"Invalid JSON in metadata file: {path}/metadata.json")
+
+def list_metadata_json_dir(*paths: Path) -> List[Tuple[str, str]]:
+    """List all metadata.json files in given paths"""
+    results = []
+    for path in paths:
+        for json_path in path.rglob('metadata.json'):
+            json_dir = json_path.parent
+            dir_name = json_dir.name
+            
+            if '{{' in dir_name and '}}' in dir_name:
+                continue  # Skip templates
+                
+            if dir_name in ['cpp', 'python']:
+                language = f"({dir_name})"
+                name = json_dir.parent.name
+            else:
+                language = ""
+                name = dir_name
+                
+            results.append((name, language))
+    
+    return sorted(results)
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob('CMakeLists.txt'):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split('(')[1].split(')')[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+class HoloHubContainer:
+    """Manages container operations for HoloHub"""
+    
+    def __init__(self):
+        self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+        self.holohub_root = self.script_dir
+        
+        # Environment defaults
+        self.holoscan_py_exe = os.environ.get('HOLOSCAN_PY_EXE', 'python3')
+        self.holoscan_docker_exe = os.environ.get('HOLOSCAN_DOCKER_EXE', 'docker')
+        self.holoscan_sdk_version = 'sdk-latest'
+        self.holohub_container_base_name = 'holohub'
+        
+        self.do_dry_run = False
+
+    def get_default_base_img(self) -> str:
+        """Get default base image name"""
+        return f"nvcr.io/nvidia/clara-holoscan/holoscan:v2.9.0-{get_host_gpu()}"
+
+    def get_default_img(self) -> str:
+        """Get default image name"""
+        return f"holohub:ngc-v2.9.0-{get_host_gpu()}"
+
+    def get_ucx_options(self) -> List[str]:
+        """Get UCX-related docker options"""
+        return [
+            '--ipc=host',
+            '--cap-add=CAP_SYS_PTRACE',
+            '--ulimit=memlock=-1',
+            '--ulimit=stack=67108864'
+        ]
+
+    def get_conditional_options(self, use_tini: bool = False, persistent: bool = False) -> List[str]:
+        """Get conditional docker options"""
+        options = []
+        
+        # Add device mounts
+        device_paths = [
+            '/usr/lib/libvideomasterhd.so',
+            '/opt/deltacast/videomaster/Include',
+            '/opt/yuan/qcap/include',
+            '/opt/yuan/qcap/lib',
+            '/usr/lib/aarch64-linux-gnu/tegra'
+        ]
+        
+        for path in device_paths:
+            if os.path.exists(path):
+                if os.path.isfile(path):
+                    options.append(f'--volume={path}:{path}')
+                else:
+                    options.append(f'--volume={path}:{path}')
+
+        # Add group permissions
+        for group in ['video', 'render', 'docker']:
+            gid = get_group_id(group)
+            if gid is not None:
+                options.append(f'--group-add={gid}')
+
+        # Handle tini and persistence
+        if use_tini:
+            options.append('--init')
+        if not persistent:
+            options.append('--rm')
+
+        return options
+
+    def get_dockerfile_path(self, app_name: str = None, language: Optional[str] = 'cpp') -> Path:
+        """Get Dockerfile path for an application or default"""
+        if not app_name:
+            return self.script_dir / "Dockerfile"
+            
+        app_source = get_app_source_lang_dir(app_name, language, self.script_dir)
+        
+        # Check metadata.json for dockerfile entry
+        try:
+            metadata = get_metadata(app_source)
+            if "dockerfile" in metadata:
+                dockerfile = metadata["dockerfile"].replace("<holohub_app_source>", str(app_source))
+                return Path(dockerfile)
+        except (FileNotFoundError, KeyError):
+            pass
+            
+        # Check for Dockerfile in application directory
+        app_dockerfile = app_source / "Dockerfile"
+        if app_dockerfile.exists():
+            return app_dockerfile
+        
+        # Check for Dockerfile in language directory
+        lang_dockerfile = app_source / "Dockerfile"
+        if lang_dockerfile.exists():
+            return lang_dockerfile
+            
+        # Use default Dockerfile
+        return self.script_dir / "Dockerfile"
+
+    def build(self, 
+              docker_file: Optional[str] = None,
+              base_img: Optional[str] = None,
+              img: Optional[str] = None,
+              no_cache: bool = False,
+              build_args: Optional[str] = None,
+              project: Optional[str] = None,
+              language: Optional[str] = 'cpp',
+              verbose: bool = False) -> None:
+        """Build the container image"""
+        
+        # Get Dockerfile path
+        if docker_file:
+            docker_file_path = Path(docker_file)
+        else:
+            docker_file_path = self.get_dockerfile_path(project, language)
+            
+        base_img = base_img or self.get_default_base_img()
+        img = img or self.get_default_img()
+        gpu_type = get_host_gpu()
+        compute_capacity = get_compute_capacity()
+
+        if verbose:
+            print(f"Build (HOLOHUB_ROOT: {self.holohub_root})")
+            print(f"Build (gpu_type: {gpu_type})")
+            print(f"Build (base_img: {base_img})")
+            print(f"Build (docker_file: {docker_file_path})")
+            print(f"Build (img: {img})")
+
+        # Check if buildx exists
+        try:
+            run_command(['docker', 'buildx', 'version'], check=True, capture_output=True)
+        except subprocess.CalledProcessError:
+            fatal("docker buildx plugin is missing. Please install docker-buildx-plugin:\n"
+                  "https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository")
+
+        # Set DOCKER_BUILDKIT environment variable
+        os.environ['DOCKER_BUILDKIT'] = '1'
+        
+        cmd = [
+            'docker', 'build',
+            '--build-arg', f'BUILDKIT_INLINE_CACHE=1',
+            '--build-arg', f'BASE_IMAGE={base_img}',
+            '--build-arg', f'GPU_TYPE={gpu_type}',
+            '--build-arg', f'COMPUTE_CAPACITY={compute_capacity}',
+            '--network=host',
+        ]
+        
+        if no_cache:
+            cmd.append('--no-cache')
+            
+        if build_args:
+            cmd.extend(build_args.split())
+            
+        cmd.extend([
+            '-f', str(docker_file_path),
+            '-t', img,
+            str(self.holohub_root)
+        ])
+        
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def launch(self,
+               img: Optional[str] = None,
+               local_sdk_root: str = "undefined",
+               ssh_x11: bool = False,
+               use_tini: bool = False,
+               persistent: bool = False,
+               nsys_profile: bool = False,
+               nsys_location: str = "",
+               as_root: bool = False,
+               docker_opts: str = "",
+               add_volumes: List[str] = None,
+               verbose: bool = False,
+               dryrun: bool = False,
+               extra_args: List[str] = None) -> None:
+        """Launch the container"""
+        
+        check_nvidia_ctk()
+        
+        img = img or self.get_default_img()
+        add_volumes = add_volumes or []
+        extra_args = extra_args or []
+
+        # Build docker command
+        cmd = [self.holoscan_docker_exe, 'run', '--net', 'host']
+        
+        # Basic options
+        cmd.extend(['--interactive'])
+        if sys.stdout.isatty():
+            cmd.append('--tty')
+            
+        # User permissions
+        if not as_root:
+            cmd.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
+        cmd.extend([
+            '-v', '/etc/group:/etc/group:ro',
+            '-v', '/etc/passwd:/etc/passwd:ro'
+        ])
+        
+        # Workspace mounting
+        cmd.extend([
+            '-v', f'{self.holohub_root}:/workspace/holohub',
+            '-w', '/workspace/holohub'
+        ])
+        
+        # GPU and device access
+        cmd.extend([
+            '--runtime=nvidia',
+            '--gpus', 'all',
+            '--cap-add', 'CAP_SYS_PTRACE',
+            '--ipc=host',
+            '-v', '/dev:/dev',
+            '--device-cgroup-rule', 'c 81:* rmw',
+            '--device-cgroup-rule', 'c 189:* rmw',
+        ])
+        
+        # Environment variables
+        cmd.extend([
+            '-e', 'NVIDIA_DRIVER_CAPABILITIES=graphics,video,compute,utility,display',
+            '-e', 'HOME=/workspace/holohub'
+        ])
+        
+        # Additional volumes
+        for volume in add_volumes:
+            base = os.path.basename(volume)
+            cmd.extend(['-v', f'{volume}:/workspace/volumes/{base}'])
+            
+        # Add conditional options
+        cmd.extend(self.get_conditional_options(use_tini, persistent))
+        
+        # Add UCX options
+        cmd.extend(self.get_ucx_options())
+        
+        # Add display server options
+        self._add_display_options(cmd, ssh_x11)
+        
+        # Add local SDK options if provided
+        if local_sdk_root != "undefined":
+            self._add_local_sdk_options(cmd, local_sdk_root)
+            
+        # Add docker options if provided
+        if docker_opts:
+            cmd.extend(docker_opts.split())
+            
+        # Add the image name
+        cmd.append(img)
+        
+        # Add any extra arguments
+        cmd.extend(extra_args)
+        
+        if verbose:
+            print(f"Launch command: {' '.join(cmd)}")
+            
+        run_command(cmd, dry_run=self.do_dry_run)
+
+    def _add_display_options(self, cmd: List[str], ssh_x11: bool) -> None:
+        """Add display-related options to docker command"""
+        if 'XDG_SESSION_TYPE' in os.environ:
+            cmd.extend(['-e', 'XDG_SESSION_TYPE'])
+            if os.environ['XDG_SESSION_TYPE'] == 'wayland':
+                cmd.extend(['-e', 'WAYLAND_DISPLAY'])
+                
+        if 'XDG_RUNTIME_DIR' in os.environ:
+            cmd.extend(['-e', 'XDG_RUNTIME_DIR'])
+            if os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
+                cmd.extend(['-v', f"{os.environ['XDG_RUNTIME_DIR']}:{os.environ['XDG_RUNTIME_DIR']}"])
+                
+        # Handle X11
+        session_type = os.environ.get('XDG_SESSION_TYPE', '')
+        if not session_type or session_type in ['x11', 'tty']:
+            if 'DISPLAY' in os.environ and shutil.which('xhost'):
+                run_command(['xhost', '+local:docker'])
+            cmd.extend([
+                '-v', '/tmp/.X11-unix:/tmp/.X11-unix',
+                '-e', 'DISPLAY'
+            ])
+
+    def _add_local_sdk_options(self, cmd: List[str], local_sdk_root: str) -> None:
+        """Add Holoscan SDK-related options to docker command"""
+        pythonpath = "/workspace/holohub/benchmarks/holoscan_flow_benchmarking"
+        
+        if os.path.isdir(local_sdk_root):
+            cmd.extend(['-v', f'{local_sdk_root}:/workspace/holoscan-sdk'])
+            cmd.extend([
+                '-e', 'HOLOSCAN_LIB_PATH=/workspace/holoscan-sdk/build/lib',
+                '-e', 'HOLOSCAN_SAMPLE_DATA_PATH=/workspace/holoscan-sdk/data',
+                '-e', 'HOLOSCAN_TESTS_DATA_PATH=/workspace/holoscan-sdk/tests/data'
+            ])
+            pythonpath = f"/workspace/holoscan-sdk/build/python/lib:{pythonpath}"
+        else:
+            pythonpath = f"/opt/nvidia/holoscan/python/lib:{pythonpath}"
+            
+        cmd.extend(['-e', f'PYTHONPATH={pythonpath}'])
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+    
+    def __init__(self):
+        self.container = HoloHubContainer()
+        self.parser = self._create_parser()
+        
+        # Add build configuration
+        self.build_dir = self.container.script_dir / "build"
+        self.data_dir = os.environ.get('HOLOHUB_DATA_PATH', 
+                                     self.container.script_dir / "data")
+        self.holoscan_sdk_install = os.environ.get('HOLOSCAN_LIB_PATH',
+                                                  '/opt/nvidia/holoscan/lib')
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(description='HoloHub CLI tool')
+        subparsers = parser.add_subparsers(dest='command', required=True)
+        
+        # build-container command
+        build_container = subparsers.add_parser('build-container', 
+            help='Build the development container')
+        build_container.add_argument('project', nargs='?',
+            help='Project to build container for')
+        build_container.add_argument('--base_img',
+            help='Fully qualified base image name')
+        build_container.add_argument('--docker_file',
+            help='Path to Dockerfile to use')
+        build_container.add_argument('--img',
+            help='Specify fully qualified container name')
+        build_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker build command')
+        build_container.add_argument('--no-cache', action='store_true',
+            help='Do not use cache when building the image')
+        build_container.add_argument('--build-args',
+            help='Provides extra arguments to docker build command')
+        build_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser('run-container',
+            help='Launch the development container')
+        run_container.add_argument('--img',
+            help='Fully qualified image name')
+        run_container.add_argument('--local_sdk_root',
+            help='Path to Holoscan SDK used for building local Holoscan SDK container')
+        run_container.add_argument('--ssh_x11', action='store_true',
+            help='Enable X11 forwarding of graphical HoloHub applications over SSH')
+        run_container.add_argument('--init', action='store_true',
+            help='Support tini entry point')
+        run_container.add_argument('--persistent', action='store_true',
+            help='Does not delete container after it is run')
+        run_container.add_argument('--verbose', action='store_true',
+            help='Print variables passed to docker run command')
+        run_container.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run_container.add_argument('--add-volume', action='append',
+            help='Mount additional volume')
+        run_container.add_argument('--as_root', action='store_true',
+            help='Run the container with root permissions')
+        run_container.add_argument('--docker_opts',
+            help='Additional options to pass to the Docker launch')
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser('build',
+            help='Build a project')
+        build.add_argument('project',
+            help='Project to build')
+        build.add_argument('--local', action='store_true',
+            help='Build locally instead of in container')
+        build.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        build.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        build.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser('run',
+            help='Run a project')
+        run.add_argument('project',
+            help='Project to run')
+        run.add_argument('--local', action='store_true',
+            help='Run locally instead of in container')
+        run.add_argument('--verbose', action='store_true',
+            help='Print extra output')
+        run.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        run.add_argument('--nsys-profile', action='store_true',
+            help='Enable Nsight Systems profiling')
+        run.add_argument('--language', choices=['cpp', 'python'],
+            help='Specify language implementation')
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser('list',
+            help='List all available targets')
+        list_cmd.set_defaults(func=self.handle_list)
+        
+        # lint command
+        lint = subparsers.add_parser('lint',
+            help='Run linting tools')
+        lint.add_argument('path', nargs='?', default='.',
+            help='Path to lint')
+        lint.add_argument('--fix', action='store_true',
+            help='Fix linting issues')
+        lint.add_argument('--install-dependencies', action='store_true',
+            help='Install linting dependencies')
+        lint.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        lint.set_defaults(func=self.handle_lint)
+        
+        # setup command
+        setup = subparsers.add_parser('setup',
+            help='Install HoloHub main required packages')
+        setup.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser('install',
+            help='Install a project')
+        install.add_argument('project',
+            help='Project to install')
+        install.add_argument('--local', action='store_true',
+            help='Install locally instead of in container')
+        install.add_argument('--build-type', choices=['debug', 'release', 'rel-debug'],
+            help='Build type (debug, release, rel-debug)')
+        install.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser('clear-cache',
+            help='Clear cache folders')
+        clear_cache.add_argument('--dryrun', action='store_true',
+            help='Print commands without executing them')
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+            project=args.project,
+            verbose=args.verbose
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        self.container.do_dry_run = args.dryrun
+        self.container.launch(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root or "undefined",
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts or "",
+            add_volumes=args.add_volume,
+            verbose=args.verbose
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get('CMAKE_BUILD_TYPE', 'Release')
+            
+        build_type_map = {
+            'debug': 'Debug',
+            'Debug': 'Debug',
+            'release': 'Release',
+            'Release': 'Release',
+            'rel-debug': 'RelWithDebInfo',
+            'RelWithDebInfo': 'RelWithDebInfo'
+        }
+        return build_type_map.get(build_type, 'Release')
+
+    def get_app_default_language(self, app_name: str) -> str:
+        """Get default language if multiple are available"""
+        apps = list_metadata_json_dir(
+            self.container.script_dir / "applications",
+            self.container.script_dir / "benchmarks"
+        )
+        for name, lang in apps:
+            if name == app_name:
+                return lang.strip('()') if lang else ''
+        return ''
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        language = (hasattr(args, 'language') and args.language) or self.get_app_default_language(args.project)
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            build_type = self.get_buildtype_str(args.build_type)
+            
+            # Local build
+            build_dir = self.build_dir / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+            
+            if language == 'cpp':
+                # Build C++ project
+                run_command([
+                    'cmake', '-B', str(build_dir),
+                    '-S', str(app_source),
+                    f'-DCMAKE_BUILD_TYPE={build_type}',
+                    f'-DCMAKE_PREFIX_PATH={self.holoscan_sdk_install}'
+                ], dry_run=args.dryrun)
+                run_command(['cmake', '--build', str(build_dir), '--config', build_type],
+                          dry_run=args.dryrun)
+            else:
+                # For Python projects, just create the directory
+                pass
+        else:
+            # Build in container
+            self.container.do_dry_run = args.dryrun
+            self.container.build(project=args.project, verbose=args.verbose)
+            self.container.launch(
+                docker_opts="--entrypoint=bash",
+                extra_args=['-c', f'holohub build {args.project} --language {language} --local'],
+                verbose=args.verbose
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            language = args.language or self.get_app_default_language(args.project)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+                
+            app_source = get_app_source_lang_dir(args.project, language, self.container.script_dir)
+            metadata = get_metadata(app_source)
+            
+            # Get run configuration from metadata
+            project_type = "benchmark" if "benchmark" in metadata else "application"
+            run_config = metadata[project_type]["run"]
+            
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(self.data_dir))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source))
+            
+            if language == 'cpp':
+                build_dir = self.build_dir / args.project
+                cmd = cmd.replace("<holohub_app_bin>", str(build_dir))
+                
+                if not build_dir.is_dir() and not args.dryrun:
+                    fatal(f"The build directory for this application does not exist.\n"
+                          f"Did you forget to './holohub build {args.project}'?")
+            
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(app_source)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {app_source}\033[0m")
+            elif workdir == "holohub_bin":
+                if not args.dryrun:
+                    os.chdir(self.build_dir)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {self.build_dir}\033[0m")
+            else:  # default to app binary directory
+                target_dir = build_dir if language == 'cpp' else app_source
+                if not args.dryrun:
+                    os.chdir(target_dir)
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mcd {target_dir}\033[0m")
+            
+            # Set up environment
+            env = os.environ.copy()
+            env['PYTHONPATH'] = f"{self.holoscan_sdk_install}/../python/lib:{self.build_dir}/python/lib:{self.container.script_dir}"
+            env['HOLOHUB_DATA_PATH'] = str(self.data_dir)
+            env['HOLOSCAN_INPUT_PATH'] = os.environ.get('HOLOSCAN_INPUT_PATH', str(self.data_dir))
+            
+            # Print environment setup
+            if args.verbose or args.dryrun:
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport PYTHONPATH={env['PYTHONPATH']}\033[0m")
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport HOLOHUB_DATA_PATH={env['HOLOHUB_DATA_PATH']}\033[0m")
+                print(f"\033[34m{get_timestamp()} \033[37m$ \033[32mexport HOLOSCAN_INPUT_PATH={env['HOLOSCAN_INPUT_PATH']}\033[0m")
+            
+            # Handle Nsight Systems profiling
+            if args.nsys_profile:
+                if not shutil.which('nsys') and not os.path.isdir('/opt/nvidia/nsys-host') and not args.dryrun:
+                    fatal("Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted.")
+                nsys_cmd = '/opt/nvidia/nsys-host/bin/nsys' if not shutil.which('nsys') else 'nsys'
+                
+                # Check perf_event_paranoid level
+                if not args.dryrun:
+                    try:
+                        with open('/proc/sys/kernel/perf_event_paranoid') as f:
+                            if int(f.read()) > 2:
+                                fatal("For Nsight Systems profiling the Linux operating system's perf_event_paranoid level must be 2 or less.")
+                    except (IOError, ValueError):
+                        pass
+                
+                cmd = f"{nsys_cmd} profile --trace=cuda,vulkan,nvtx,osrt {cmd}"
+            
+            run_command(cmd.split(), env=env, dry_run=args.dryrun)
+        else:
+            # Pass dryrun to container launch if specified
+            self.container.do_dry_run = args.dryrun
+            
+            # Get language before launching container
+            language = args.language or self.get_app_default_language(args.project)
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
+            
+            # Build command with all necessary arguments
+            run_cmd = f'holohub run {args.project} --language {language} --local'
+            if args.dryrun:
+                run_cmd += ' --dryrun'
+            if args.verbose:
+                run_cmd += ' --verbose'
+            if args.nsys_profile:
+                run_cmd += ' --nsys-profile'
+                
+            self.container.launch(
+                docker_opts="--entrypoint=bash",
+                extra_args=['-c', run_cmd],
+                verbose=args.verbose
+            )
+
+    def handle_list(self, args: argparse.Namespace) -> None:
+        """Handle list command"""
+        print("\n\033[1;37m== APPLICATIONS =================\033[0m\n")
+        
+        apps = list_metadata_json_dir(
+            self.container.script_dir / "applications",
+            self.container.script_dir / "benchmarks"
+        )
+        for name, lang in apps:
+            print(f"{name} {lang}")
+            
+        print("\n\033[1;37m== OPERATORS ====================\033[0m\n")
+        
+        ops = list_metadata_json_dir(self.container.script_dir / "operators")
+        for name, lang in ops:
+            print(f"{name} {lang}")
+            
+        print("\n\033[1;37m== PACKAGES =====================\033[0m\n")
+        
+        pkgs = list_cmake_dir_options(self.container.script_dir, "add_holohub_package")
+        for pkg in pkgs:
+            print(pkg)
+            
+        print("\n\033[1;37m=================================\033[0m\n")
+
+    def handle_lint(self, args: argparse.Namespace) -> None:
+        """Handle lint command"""
+        if args.install_dependencies:
+            self._install_lint_deps()
+            return
+            
+        exit_code = 0
+        
+        # Change to script directory
+        os.chdir(self.container.script_dir)
+        
+        if args.fix:
+            # Fix Python
+            run_command(['ruff', 'check', '--fix', '--ignore', 'E712', args.path], check=False)
+            run_command(['isort', args.path], check=False)
+            run_command(['black', args.path], check=False)
+            run_command(['codespell', '-w', '-i', '3', args.path,
+                        '--ignore-words', 'codespell_ignore_words.txt',
+                        '--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data'],
+                       check=False)
+            
+            # Fix C++ with clang-format
+            try:
+                cpp_files = subprocess.check_output(
+                    [sys.executable, '-m', 'cpplint',
+                     '--exclude', 'build',
+                     '--exclude', 'install',
+                     '--exclude', 'build-*',
+                     '--exclude', 'install-*',
+                     '--exclude', 'applications/holoviz/template/cookiecutter*',
+                     '--recursive', args.path],
+                    stderr=subprocess.PIPE,
+                    text=True
+                )
+                for file in cpp_files.splitlines():
+                    if ':' in file:  # Only process files with issues
+                        file = file.split(':')[0]
+                        run_command(['clang-format', '--style=file', '--sort-includes=0',
+                                   '--lines=20:10000', '-i', file], check=False)
+            except subprocess.CalledProcessError:
+                pass
+                
+        else:
+            # Run linting checks
+            print("Linting Python")
+            if run_command(['ruff', 'check', '--ignore', 'E712', args.path], check=False).returncode != 0:
+                exit_code = 1
+            if run_command(['isort', '-c', args.path], check=False).returncode != 0:
+                exit_code = 1
+            if run_command(['black', '--check', args.path], check=False).returncode != 0:
+                exit_code = 1
+                
+            print("Linting C++")
+            if run_command(['python', '-m', 'cpplint',
+                          '--exclude', 'build',
+                          '--exclude', 'install',
+                          '--exclude', 'build-*',
+                          '--exclude', 'install-*',
+                          '--exclude', '.vscode-server',
+                          '--exclude', 'applications/holoviz/template/cookiecutter*',
+                          '--recursive', args.path], check=False).returncode != 0:
+                exit_code = 1
+                
+            print("Code spelling")
+            if run_command(['codespell', args.path,
+                          '--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data',
+                          '--ignore-words', 'codespell_ignore_words.txt',
+                          '--exclude-file', 'codespell.txt'], check=False).returncode != 0:
+                exit_code = 1
+                
+            print("Linting CMake")
+            cmake_files = list(Path(args.path).rglob('CMakeLists.txt'))
+            cmake_files.extend(Path(args.path).rglob('*.cmake'))
+            if cmake_files:
+                if run_command(['cmakelint',
+                              '--filter=-whitespace/indent,-linelength,-readability/wonkycase,-convention/filename,-package/stdargs',
+                              *[str(f) for f in cmake_files]], check=False).returncode != 0:
+                    exit_code = 1
+                    
+        if exit_code == 0:
+            print("Everything looks good!")
+        sys.exit(exit_code)
+
+    def _install_lint_deps(self) -> None:
+        """Install linting dependencies"""
+        os.chdir(self.container.script_dir)
+        
+        print("Install Lint Dependencies for Python")
+        run_command([sys.executable, '-m', 'pip', 'install', '-r',
+                    str(self.container.script_dir / 'utilities/requirements.lint.txt')])
+        run_command(['apt', 'install', '--no-install-recommends', '-y',
+                    'clang-format=1:14.0*'])
+
+    def handle_setup(self, args: argparse.Namespace) -> None:
+        """Handle setup command"""
+        # Install system dependencies
+        run_command(['sudo', 'apt-get', 'update'], dry_run=args.dryrun)
+        run_command(['sudo', 'apt-get', 'install', '-y',
+            'build-essential',
+            'cmake',
+            'git',
+            'python3-dev',
+            'python3-pip',
+            'ninja-build',
+            'pkg-config',
+            'libglib2.0-dev',
+            'clang-format',
+            'clang-tidy',
+            'cppcheck'
+        ], dry_run=args.dryrun)
+
+        # Install Python dependencies
+        run_command([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'], dry_run=args.dryrun)
+        run_command([sys.executable, '-m', 'pip', 'install',
+            'cmake-format',
+            'cpplint',
+            'pytest',
+            'pytest-cov',
+            'pytest-timeout',
+            'black',
+            'isort',
+            'ruff',
+            'codespell'
+        ], dry_run=args.dryrun)
+
+        # Create build directory
+        self.build_dir.mkdir(parents=True, exist_ok=True)
+
+    def handle_install(self, args: argparse.Namespace) -> None:
+        """Handle install command"""
+        if args.local:
+            app_source = get_app_source_root_dir(args.project, self.container.script_dir)
+            language = args.language or self.get_app_default_language(args.project)
+            
+            if not language:
+                fatal("Could not determine project language. Please specify with --language")
path: holohub
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider raising a more specific exception type, such as `ValueError`, to provide more context about the error.

```
                raise ValueError("Could not determine project language. Please specify with --language")
```
created_at: 2025-03-10T17:39:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import grp
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
+
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+
+def run_command(
+    cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs
+) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = " ".join(str(x) for x in cmd)
+    if dry_run:
path: utilities/cli/util.py
user: login: wyli
body: I use this dry_run option a lot, it helps me understand the underlying logic, it'd be nice to print dry run commands in a readable/editable format such as:
```
2025-04-11 08:47:40 [dryrun] $ docker build \
    --build-arg BUILDKIT_INLINE_CACHE=1 \
    --build-arg BASE_IMAGE=nvcr.io/nvidia/clara-holoscan/holoscan:v3.1.0-dgpu \
    --build-arg GPU_TYPE=dgpu \
    --build-arg COMPUTE_CAPACITY=8.9 \
    --network=host \
    -f /home/wenqil/Documents/holohub/applications/holochat/Dockerfile \
    -t holohub:holochat /home/wenqil/Documents/holohub
```

instead of everything in one line...
```
2025-04-11 08:45:32 [dryrun] $ docker build --build-arg BUILDKIT_INLINE_CACHE=1 --build-arg BASE_IMAGE=nvcr.io/nvidia/clara-holoscan/holoscan:v3.1.0-dgpu --build-arg GPU_TYPE=dgpu --build-arg COMPUTE_CAPACITY=8.9 --network=host -f /home/wenqil/Documents/holohub/applications/holochat/Dockerfile -t holohub:holochat /home/wenqil/Documents/holohub
```


(cursor's implementation of the formatting:)
```py
def format_long_command(cmd: List[str], max_line_length: int = 80) -> str:
    """Format a long command into multiple lines for better readability

    Args:
        cmd: Command to format as a list of strings
        max_line_length: Maximum line length before wrapping

    Returns:
        Formatted command string with line continuations
    """
    if not cmd:
        return ""

    # Start with the first command
    formatted = cmd[0]
    current_line = cmd[0]

    # Common patterns that suggest good break points
    break_patterns = {
        '--',  # Long options
        '-',   # Short options
        '&&',  # Command chaining
        '||',  # Command chaining
        '|',   # Pipes
        ';',   # Command separator
        '>',   # Output redirection
        '<',   # Input redirection
        '>>',  # Append redirection
        '2>',  # Error redirection
    }

    for i, arg in enumerate(cmd[1:]):
        # Check if this is a good place to break
        should_break = (
            # Break if we exceed max length
            len(current_line) + len(arg) + 1 > max_line_length or
            # Break before common command separators
            any(arg.startswith(pattern) for pattern in break_patterns) or
            # Break after common command separators
            any(cmd[i].endswith(pattern) for pattern in break_patterns)
        )

        if should_break:
            formatted += " \\\n    " + arg
            current_line = arg
        else:
            formatted += " " + arg
            current_line += " " + arg

    return formatted

cmd_str = format_long_command(cmd) if dry_run else " ".join(str(x) for x in cmd)
```
created_at: 2025-04-11T08:51:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1022 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+from collections import defaultdict
+from pathlib import Path
+from typing import List, Optional
+
+sys.path.append(Path(__file__).parent / "utilities")
+import utilities.cli.util as holohub_cli_util
+import utilities.metadata.gather_metadata as metadata_util
+from utilities.cli.container import HoloHubContainer
+
+host_root_dir = Path(__file__).parent
+
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob("CMakeLists.txt"):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split("(")[1].split(")")[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+
+    HOLOHUB_ROOT = Path(__file__).parent
+    DEFAULT_BUILD_PARENT_DIR = HOLOHUB_ROOT / "build"
+    DEFAULT_DATA_DIR = HOLOHUB_ROOT / "data"
+    DEFAULT_SDK_DIR = "/opt/nvidia/holoscan/lib"
+
+    def __init__(self):
+        self.parser = self._create_parser()
+        self._collect_metadata()
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(
+            description="HoloHub CLI tool for managing HoloHub applications and containers"
+        )
+        subparsers = parser.add_subparsers(dest="command", required=True)
+
+        # build-container command
+        build_container = subparsers.add_parser(
+            "build-container", help="Build the development container"
+        )
+        build_container.add_argument("project", nargs="?", help="Project to build container for")
+        build_container.add_argument("--base_img", help="Fully qualified base image name")
+        build_container.add_argument("--docker_file", help="Path to Dockerfile to use")
+        build_container.add_argument("--img", help="Specify fully qualified container name")
+        build_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker build command"
+        )
+        build_container.add_argument(
+            "--no-cache", action="store_true", help="Do not use cache when building the image"
+        )
+        build_container.add_argument(
+            "--build-args", help="Provides extra arguments to docker build command"
+        )
+        build_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser(
+            "run-container", help="Launch the development container"
+        )
+        run_container.add_argument("project", nargs="?", help="Project to run container for")
+        run_container.add_argument("--img", help="Fully qualified image name")
+        run_container.add_argument(
+            "--local_sdk_root",
+            help="Path to Holoscan SDK used for building local Holoscan SDK container",
+        )
+        run_container.add_argument(
+            "--ssh_x11",
+            action="store_true",
+            help="Enable X11 forwarding of graphical HoloHub applications over SSH",
+        )
+        run_container.add_argument("--init", action="store_true", help="Support tini entry point")
+        run_container.add_argument(
+            "--persistent", action="store_true", help="Does not delete container after it is run"
+        )
+        run_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker run command"
+        )
+        run_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run_container.add_argument("--add-volume", action="append", help="Mount additional volume")
+        run_container.add_argument(
+            "--as_root", action="store_true", help="Run the container with root permissions"
+        )
+        run_container.add_argument(
+            "--docker_opts", help="Additional options to pass to the Docker launch"
+        )
+        run_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser("build", help="Build a project")
+        build.add_argument("project", help="Project to build")
+        build.add_argument(
+            "--local", action="store_true", help="Build locally instead of in container"
+        )
+        build.add_argument("--verbose", action="store_true", help="Print extra output")
+        build.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        build.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser("run", help="Run a project")
+        run.add_argument("project", help="Project to run")
+        run.add_argument("--local", action="store_true", help="Run locally instead of in container")
+        run.add_argument("--verbose", action="store_true", help="Print extra output")
+        run.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run.add_argument(
+            "--nsys-profile", action="store_true", help="Enable Nsight Systems profiling"
+        )
+        run.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser("list", help="List all available targets")
+        list_cmd.set_defaults(func=self.handle_list)
+
+        # lint command
+        lint = subparsers.add_parser("lint", help="Run linting tools")
+        lint.add_argument("path", nargs="?", default=".", help="Path to lint")
+        lint.add_argument("--fix", action="store_true", help="Fix linting issues")
+        lint.add_argument(
+            "--install-dependencies", action="store_true", help="Install linting dependencies"
+        )
+        lint.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        lint.set_defaults(func=self.handle_lint)
+
+        # setup command
+        setup = subparsers.add_parser("setup", help="Install HoloHub main required packages")
+        setup.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser("install", help="Install a project")
+        install.add_argument("project", help="Project to install")
+        install.add_argument(
+            "--local", action="store_true", help="Install locally instead of in container"
+        )
+        install.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        install.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser("clear-cache", help="Clear cache folders")
+        clear_cache.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def _collect_metadata(self) -> None:
+        """Create an unstructured database of metadata for all projects"""
+
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            host_root_dir / "applications",
+            host_root_dir / "benchmarks",
+            host_root_dir / "gxf_extensions",
+            host_root_dir / "operators",
+            host_root_dir / "pkg",
+            host_root_dir / "workflows",
+        )
+        self.projects = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+
+    def _find_project(self, project_name: str, language: Optional[str] = None) -> dict:
+        """Find a project by name"""
+        normalized_language = holohub_cli_util.normalize_language(language) if language else None
+        for project in self.projects:
+            if project["project_name"] == project_name:
+                if (
+                    normalized_language
+                    and holohub_cli_util.normalize_language(project["metadata"]["language"])
+                    != normalized_language
+                ):
+                    continue
+                return project
+        return None
+
+    def _make_project_container(
+        self, project_name: Optional[str] = None, language: Optional[str] = None
+    ) -> HoloHubContainer:
+        """Define a project container"""
+        if not project_name:
+            return HoloHubContainer(project_metadata=None)
+
+        project_data = self._find_project(project_name=project_name, language=language)
+        if not project_data:
+            holohub_cli_util.fatal(
+                f"Project '{project_name}' {f'({language})' if language else ''} not found"
+            )
+        return HoloHubContainer(project_metadata=project_data)
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build()
+        container.run(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root,
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts,
+            add_volumes=args.add_volume,
+            verbose=args.verbose,
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get("CMAKE_BUILD_TYPE", "Release")
+
+        build_type_map = {
+            "debug": "Debug",
+            "release": "Release",
+            "rel-debug": "RelWithDebInfo",
+            "relwithdebinfo": "RelWithDebInfo",
+        }
+        return build_type_map.get(build_type.lower(), "Release")
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_type = self.get_buildtype_str(args.build_type)
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={build_type}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", build_type], dry_run=args.dryrun
+            )
+
+        else:
+            # Build in container
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            container.run(
+                docker_opts="--entrypoint=bash",
+                extra_args=["-c", f"./holohub build {args.project} --local"],
+                verbose=args.verbose,
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            BUILD_TYPE = "Release"
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={BUILD_TYPE}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", BUILD_TYPE], dry_run=args.dryrun
+            )
+
+            language = holohub_cli_util.normalize_language(
+                project_data.get("metadata", {}).get("language", None)
+            )
+
+            run_config = project_data.get("metadata", {}).get("run", {})
+            if not run_config:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' does not have a run configuration"
+                )
+
+            app_source_path = project_data.get("source_folder", "")
+
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(HoloHubCLI.DEFAULT_DATA_DIR))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source_path))
+
+            cmd = cmd.replace("<holohub_bin>", str(build_dir))
+
+            app_build_dir = build_dir / app_source_path.relative_to(HoloHubCLI.HOLOHUB_ROOT)
+            cmd = cmd.replace("<holohub_app_bin>", str(app_build_dir))
+
+            if language == "cpp":
+                if not build_dir.is_dir() and not args.dryrun:
+                    holohub_cli_util.fatal(
+                        f"The build directory for this application does not exist.\n"
+                        f"Did you forget to './holohub build {args.project}'?"
+                    )
+
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(project_data.get("source_folder", ""))
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {project_data.get('source_folder', '')}\033[0m"
+                )
+            elif workdir == "holohub_bin":
+                if not args.dryrun:
+                    os.chdir(build_dir)
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {build_dir}\033[0m"
+                )
+            else:  # default to app binary directory
+                target_dir = (
+                    build_dir if language == "cpp" else project_data.get("source_folder", "")
+                )
+                if not args.dryrun:
+                    os.chdir(target_dir)
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {target_dir}\033[0m"
+                )
+
+            # Set up environment
+            env = os.environ.copy()
+            env["PYTHONPATH"] = (
+                f"{HoloHubCLI.DEFAULT_SDK_DIR}/../python/lib:{build_dir}/python/lib:{host_root_dir}"
+            )
+            env["HOLOHUB_DATA_PATH"] = str(HoloHubCLI.DEFAULT_DATA_DIR)
+            env["HOLOSCAN_INPUT_PATH"] = os.environ.get(
+                "HOLOSCAN_INPUT_PATH", str(HoloHubCLI.DEFAULT_DATA_DIR)
+            )
+
+            # Print environment setup
+            if args.verbose or args.dryrun:
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport PYTHONPATH={env['PYTHONPATH']}\033[0m"
+                )
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport HOLOHUB_DATA_PATH={env['HOLOHUB_DATA_PATH']}\033[0m"
+                )
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport HOLOSCAN_INPUT_PATH={env['HOLOSCAN_INPUT_PATH']}\033[0m"
+                )
+
+            # Handle Nsight Systems profiling
+            if args.nsys_profile:
+                if (
+                    not shutil.which("nsys")
+                    and not os.path.isdir("/opt/nvidia/nsys-host")
+                    and not args.dryrun
+                ):
+                    holohub_cli_util.fatal(
+                        "Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted."
+                    )
+                nsys_cmd = "/opt/nvidia/nsys-host/bin/nsys" if not shutil.which("nsys") else "nsys"
+
+                # Check perf_event_paranoid level
+                if not args.dryrun:
+                    try:
+                        with open("/proc/sys/kernel/perf_event_paranoid") as f:
+                            if int(f.read()) > 2:
+                                holohub_cli_util.fatal(
+                                    "For Nsight Systems profiling the Linux operating system's perf_event_paranoid level must be 2 or less."
+                                )
+                    except (IOError, ValueError):
+                        pass
+
+                cmd = f"{nsys_cmd} profile --trace=cuda,vulkan,nvtx,osrt {cmd}"
+
+            holohub_cli_util.run_command(cmd.split(), env=env, dry_run=args.dryrun)
+        else:
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            # Get language before launching container
+            language = holohub_cli_util.normalize_language(
+                container.project_metadata.get("metadata", {}).get("language", None)
+            )
+
+            # Build command with all necessary arguments
+            run_cmd = f"./holohub run {args.project} --language {language} --local"
+            if args.dryrun:
+                run_cmd += " --dryrun"
+            if args.verbose:
+                run_cmd += " --verbose"
+            if args.nsys_profile:
+                run_cmd += " --nsys-profile"
+
+            container.run(
+                docker_opts="--entrypoint=bash", extra_args=["-c", run_cmd], verbose=args.verbose
+            )
+
+    def handle_list(self, args: argparse.Namespace) -> None:
+        """Handle list command"""
+        LIST_TYPES = [
+            "application",
+            "benchmark",
+            "gxf_extension",
+            "package",
+            "operator",
+            "tutorial",
+            "workflow",
+        ]
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            host_root_dir / "applications",
+            host_root_dir / "benchmarks",
+            host_root_dir / "gxf_extensions",
+            host_root_dir / "operators",
+            host_root_dir / "pkg",
+            host_root_dir / "workflows",
+        )
+        metadata = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+        grouped_metadata = defaultdict(list)
+        for project in metadata:
+            grouped_metadata[project.get("project_type", "")].append(project)
+
+        for project_type in LIST_TYPES:
+            if project_type not in grouped_metadata:
+                continue
+            print(f"\n\033[1;37m== {project_type.upper()}S =================\033[0m\n")
+            for project in sorted(grouped_metadata[project_type], key=lambda x: x["project_name"]):
+                language = project.get("metadata", {}).get("language", "")
+                language = f"({language})" if language else ""
+                print(f'{project["project_name"]} {language}')
+
+        print("\n\033[1;37m=================================\033[0m\n")
+
+    def handle_lint(self, args: argparse.Namespace) -> None:
+        """Handle lint command"""
+        if args.install_dependencies:
+            self._install_lint_deps(args.dryrun)
+            return
+
+        exit_code = 0
+
+        # Change to script directory
+        os.chdir(host_root_dir)
+
+        if args.fix:
+            # Fix Python
+            holohub_cli_util.run_command(
+                ["ruff", "check", "--fix", "--ignore", "E712", args.path],
+                check=False,
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(["isort", args.path], check=False, dry_run=args.dryrun)
+            holohub_cli_util.run_command(["black", args.path], check=False, dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                [
+                    "codespell",
+                    "-w",
+                    "-i",
+                    "3",
+                    args.path,
+                    "--ignore-words",
+                    "codespell_ignore_words.txt",
+                    "--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data",
+                ],
+                check=False,
+                dry_run=args.dryrun,
+            )
+
+            # Fix C++ with clang-format
+            try:
+                cmd = [
+                    sys.executable,
+                    "-m",
+                    "cpplint",
+                    "--exclude",
+                    "build",
+                    "--exclude",
+                    "install",
+                    "--exclude",
+                    "build-*",
+                    "--exclude",
+                    "install-*",
+                    "--exclude",
+                    "applications/holoviz/template/cookiecutter*",
+                    "--recursive",
+                    args.path,
+                ]
+                if args.dryrun:
+                    holohub_cli_util.run_command(cmd, dry_run=True)
+                    cpp_files = ""
+                else:
+                    cpp_files = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
+
+                for file in cpp_files.splitlines():
+                    if ":" in file:  # Only process files with issues
+                        file = file.split(":")[0]
+                        holohub_cli_util.run_command(
+                            [
+                                "clang-format",
+                                "--style=file",
+                                "--sort-includes=0",
+                                "--lines=20:10000",
+                                "-i",
+                                file,
+                            ],
+                            check=False,
+                            dry_run=args.dryrun,
+                        )
+            except subprocess.CalledProcessError:
+                pass
+
+        else:
+            # Run linting checks
+            print("Linting Python")
+            if (
+                holohub_cli_util.run_command(
+                    ["ruff", "check", "--ignore", "E712", args.path],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+            if (
+                holohub_cli_util.run_command(
+                    ["isort", "-c", args.path], check=False, dry_run=args.dryrun
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+            if (
+                holohub_cli_util.run_command(
+                    ["black", "--check", args.path], check=False, dry_run=args.dryrun
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Linting C++")
+            if (
+                holohub_cli_util.run_command(
+                    [
+                        "python",
+                        "-m",
+                        "cpplint",
+                        "--exclude",
+                        "build",
+                        "--exclude",
+                        "install",
+                        "--exclude",
+                        "build-*",
+                        "--exclude",
+                        "install-*",
+                        "--exclude",
+                        ".vscode-server",
+                        "--exclude",
+                        "applications/holoviz/template/cookiecutter*",
+                        "--recursive",
+                        args.path,
+                    ],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Code spelling")
+            if (
+                holohub_cli_util.run_command(
+                    [
+                        "codespell",
+                        args.path,
+                        "--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data",
+                        "--ignore-words",
+                        "codespell_ignore_words.txt",
+                        "--exclude-file",
+                        "codespell.txt",
+                    ],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Linting CMake")
+            cmake_files = list(Path(args.path).rglob("CMakeLists.txt"))
+            cmake_files.extend(Path(args.path).rglob("*.cmake"))
+            if cmake_files:
+                if (
+                    holohub_cli_util.run_command(
+                        [
+                            "cmakelint",
+                            "--filter=-whitespace/indent,-linelength,-readability/wonkycase,-convention/filename,-package/stdargs",
+                            *[str(f) for f in cmake_files],
+                        ],
+                        check=False,
+                        dry_run=args.dryrun,
+                    ).returncode
+                    != 0
+                ):
+                    exit_code = 1
+
+        if exit_code == 0:
+            print("Everything looks good!")
+        sys.exit(exit_code)
+
+    def _install_lint_deps(self, dry_run: bool = False) -> None:
+        """Install linting dependencies"""
+        os.chdir(host_root_dir)
+
+        print("Install Lint Dependencies for Python")
+        holohub_cli_util.run_command(
+            [
+                sys.executable,
+                "-m",
+                "pip",
+                "install",
+                "-r",
+                str(host_root_dir / "utilities/requirements.lint.txt"),
+            ],
+            dry_run=dry_run,
+        )
+        holohub_cli_util.run_command(
+            ["apt", "install", "--no-install-recommends", "-y", "clang-format=1:14.0*"],
+            dry_run=dry_run,
+        )
+
+    def handle_setup(self, args: argparse.Namespace) -> None:
+        """Handle setup command"""
+        # Install system dependencies
+        holohub_cli_util.run_command(["sudo", "apt-get", "update"], dry_run=args.dryrun)
+
+        # Install wget if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "-y", "wget"], dry_run=args.dryrun
+        )
+
+        # Install xvfb for running tests/examples headless
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "-y", "xvfb"], dry_run=args.dryrun
+        )
+
+        # Check and install CMake if needed
+        cmake_version = subprocess.check_output(
+            ["dpkg", "--status", "cmake"], text=True, stderr=subprocess.DEVNULL
+        )
+        ubuntu_codename = subprocess.check_output(["cat", "/etc/os-release"], text=True)
+        ubuntu_codename = re.search(r"UBUNTU_CODENAME=(\w+)", ubuntu_codename).group(1)
+
+        if not cmake_version or "3.24.0" > cmake_version:
+            holohub_cli_util.run_command(
+                ["sudo", "apt", "install", "--no-install-recommends", "-y", "gpg"],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                [
+                    "wget",
+                    "-O",
+                    "-",
+                    "https://apt.kitware.com/keys/kitware-archive-latest.asc",
+                    "2>/dev/null",
+                    "|",
+                    "gpg",
+                    "--dearmor",
+                    "-",
+                    "|",
+                    "sudo",
+                    "tee",
+                    "/usr/share/keyrings/kitware-archive-keyring.gpg",
+                    ">/dev/null",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                [
+                    "echo",
+                    f'"deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ {ubuntu_codename} main"',
+                    "|",
+                    "sudo",
+                    "tee",
+                    "/etc/apt/sources.list.d/kitware.list",
+                    ">/dev/null",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(["sudo", "apt-get", "update"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                [
+                    "sudo",
+                    "apt",
+                    "install",
+                    "--no-install-recommends",
+                    "-y",
+                    "cmake",
+                    "cmake-curses-gui",
+                ],
+                dry_run=args.dryrun,
+            )
+
+        # Install Ninja
+        holohub_cli_util.run_command(
+            ["sudo", "apt", "install", "--no-install-recommends", "-y", "ninja-build"],
+            dry_run=args.dryrun,
+        )
+
+        # Install Python dev
+        python3_dev_version = subprocess.check_output(
+            ["dpkg", "--status", "python3-dev"], text=True, stderr=subprocess.DEVNULL
+        )
+        if not python3_dev_version or "3.9.0" > python3_dev_version:
+            holohub_cli_util.run_command(
+                [
+                    "sudo",
+                    "apt",
+                    "install",
+                    "--no-install-recommends",
+                    "-y",
+                    "python3",
+                    "python3-dev",
+                ],
+                dry_run=args.dryrun,
+            )
+
+        # Install ffmpeg
+        holohub_cli_util.run_command(
+            ["sudo", "apt", "install", "--no-install-recommends", "-y", "ffmpeg"],
+            dry_run=args.dryrun,
+        )
+
+        # Install libv4l-dev
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "libv4l-dev"],
+            dry_run=args.dryrun,
+        )
+
+        # Install git if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "git"],
+            dry_run=args.dryrun,
+        )
+
+        # Install unzip if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "unzip"],
+            dry_run=args.dryrun,
+        )
+
+        # Install ngc-cli if not present
+        try:
+            subprocess.check_output(["ngc", "--version"], stderr=subprocess.DEVNULL)
+        except (subprocess.CalledProcessError, FileNotFoundError):
+            if platform.machine() == "aarch64":
+                holohub_cli_util.run_command(
+                    [
+                        "wget",
+                        "--content-disposition",
+                        "https://ngc.nvidia.com/downloads/ngccli_arm64.zip",
+                    ],
+                    dry_run=args.dryrun,
+                )
+                holohub_cli_util.run_command(["unzip", "ngccli_arm64.zip"], dry_run=args.dryrun)
+            else:
+                holohub_cli_util.run_command(
+                    [
+                        "wget",
+                        "--content-disposition",
+                        "https://ngc.nvidia.com/downloads/ngccli_linux.zip",
+                    ],
+                    dry_run=args.dryrun,
+                )
+                holohub_cli_util.run_command(["unzip", "ngccli_linux.zip"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(["chmod", "u+x", "ngc-cli/ngc"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                ["sudo", "ln", "-s", f"{os.getcwd()}/ngc-cli/ngc", "/usr/local/bin/"],
+                dry_run=args.dryrun,
+            )
+
+        # Install CUDA dependencies
+        for version in [
+            "12-6",
+            "12-5",
+            "12-4",
+            "12-3",
+            "12-2",
+            "12-1",
+            "12-0",
+            "11-8",
+            "11-7",
+            "11-6",
+            "11-4",
+        ]:
+            try:
+                cuda_version = subprocess.check_output(
+                    ["dpkg", "--status", f"cuda-cudart-{version}"],
+                    text=True,
+                    stderr=subprocess.DEVNULL,
+                )
+                if cuda_version:
+                    break
+            except subprocess.CalledProcessError:
+                continue
+
+        if cuda_version:
+            short_cuda_version = cuda_version.split(".")[0]
+
+            # Install cudnn9 first
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libcudnn9-cuda-12", "9.*", optional=True, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libcudnn9-dev-cuda-12", "9.*", optional=True, dry_run=args.dryrun
+            )
+
+            # Check if cudnn9 is installed, if not install cudnn8
+            installed_cudnn9_version = subprocess.check_output(
+                ["apt", "list", "--installed", "libcudnn9-cuda-12"],
+                text=True,
+                stderr=subprocess.DEVNULL,
+            )
+            if not installed_cudnn9_version:
+                holohub_cli_util.install_cuda_dependencies_package(
+                    "libcudnn8", f"cuda{short_cuda_version}", dry_run=args.dryrun
+                )
+                holohub_cli_util.install_cuda_dependencies_package(
+                    "libcudnn8-dev", f"cuda{short_cuda_version}", dry_run=args.dryrun
+                )
+
+            # Install TensorRT dependencies
+            installed_libnvinferbin = subprocess.check_output(
+                ["dpkg", "--status", "libnvinfer-bin"], text=True, stderr=subprocess.DEVNULL
+            )
+            # Extract version string using regex
+            version_match = re.search(
+                r"Version: (\d+\.\d+\.\d+)\.\d+-\d+\+cuda\d+\.\d+", installed_libnvinferbin
+            )
+            if version_match:
+                installed_libnvinferbin_version = version_match.group(1)
+            else:
+                holohub_cli_util.fatal("Could not determine libnvinfer-bin version")
+
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-headers-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-plugin-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvonnxparsers-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+
+        # Install the autocomplete
+        holohub_cli_util.run_command(
+            [
+                "sudo",
+                "cp",
+                f"{HoloHubCLI.HOLOHUB_ROOT}/utilities/holohub_autocomplete",
+                "/etc/bash_completion.d/",
+            ],
+            dry_run=args.dryrun,
+        )
+
+        print("Setup for HoloHub is ready. Happy Holocoding!")
+
+    def handle_install(self, args: argparse.Namespace) -> None:
+        """Handle install command"""
+        raise NotImplementedError("Install command not yet implemented")
+
+    def handle_clear_cache(self, args: argparse.Namespace) -> None:
+        """Handle clear-cache command"""
+        if args.dryrun:
+            print("Would clear cache folders:")
+            for pattern in ["build", "build-*", "install"]:
+                for path in host_root_dir.glob(pattern):
+                    if path.is_dir():
+                        print(f"  Would remove: {path}")
+        else:
+            print("Clearing cache...")
+            for pattern in ["build", "build-*", "install"]:
+                for path in host_root_dir.glob(pattern):
+                    if path.is_dir():
+                        shutil.rmtree(path)
+
+    def run(self) -> None:
+        """Main entry point for the CLI"""
+        args = self.parser.parse_args()
+        if hasattr(args, "func"):
+            args.func(args)
+        else:
+            self.parser.print_help()
+            sys.exit(1)
+
+
+def main():
path: holohub
user: login: wyli
body: I think class `HoloHubCLI` could also live in the `utilities/cli` folder, and `holohub` only keep the main entry point, mainly for the module flexibilities.
created_at: 2025-04-11T09:55:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1022 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+from collections import defaultdict
+from pathlib import Path
+from typing import List, Optional
+
+sys.path.append(Path(__file__).parent / "utilities")
+import utilities.cli.util as holohub_cli_util
+import utilities.metadata.gather_metadata as metadata_util
+from utilities.cli.container import HoloHubContainer
+
+host_root_dir = Path(__file__).parent
+
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob("CMakeLists.txt"):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split("(")[1].split(")")[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+
+    HOLOHUB_ROOT = Path(__file__).parent
+    DEFAULT_BUILD_PARENT_DIR = HOLOHUB_ROOT / "build"
+    DEFAULT_DATA_DIR = HOLOHUB_ROOT / "data"
+    DEFAULT_SDK_DIR = "/opt/nvidia/holoscan/lib"
+
+    def __init__(self):
+        self.parser = self._create_parser()
+        self._collect_metadata()
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(
+            description="HoloHub CLI tool for managing HoloHub applications and containers"
+        )
+        subparsers = parser.add_subparsers(dest="command", required=True)
+
+        # build-container command
+        build_container = subparsers.add_parser(
+            "build-container", help="Build the development container"
+        )
+        build_container.add_argument("project", nargs="?", help="Project to build container for")
+        build_container.add_argument("--base_img", help="Fully qualified base image name")
+        build_container.add_argument("--docker_file", help="Path to Dockerfile to use")
+        build_container.add_argument("--img", help="Specify fully qualified container name")
+        build_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker build command"
+        )
+        build_container.add_argument(
+            "--no-cache", action="store_true", help="Do not use cache when building the image"
+        )
+        build_container.add_argument(
+            "--build-args", help="Provides extra arguments to docker build command"
+        )
+        build_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser(
+            "run-container", help="Launch the development container"
+        )
+        run_container.add_argument("project", nargs="?", help="Project to run container for")
+        run_container.add_argument("--img", help="Fully qualified image name")
+        run_container.add_argument(
+            "--local_sdk_root",
+            help="Path to Holoscan SDK used for building local Holoscan SDK container",
+        )
+        run_container.add_argument(
+            "--ssh_x11",
+            action="store_true",
+            help="Enable X11 forwarding of graphical HoloHub applications over SSH",
+        )
+        run_container.add_argument("--init", action="store_true", help="Support tini entry point")
+        run_container.add_argument(
+            "--persistent", action="store_true", help="Does not delete container after it is run"
+        )
+        run_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker run command"
+        )
+        run_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run_container.add_argument("--add-volume", action="append", help="Mount additional volume")
+        run_container.add_argument(
+            "--as_root", action="store_true", help="Run the container with root permissions"
+        )
+        run_container.add_argument(
+            "--docker_opts", help="Additional options to pass to the Docker launch"
+        )
+        run_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser("build", help="Build a project")
+        build.add_argument("project", help="Project to build")
+        build.add_argument(
+            "--local", action="store_true", help="Build locally instead of in container"
+        )
+        build.add_argument("--verbose", action="store_true", help="Print extra output")
+        build.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        build.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser("run", help="Run a project")
+        run.add_argument("project", help="Project to run")
+        run.add_argument("--local", action="store_true", help="Run locally instead of in container")
+        run.add_argument("--verbose", action="store_true", help="Print extra output")
+        run.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run.add_argument(
+            "--nsys-profile", action="store_true", help="Enable Nsight Systems profiling"
+        )
+        run.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser("list", help="List all available targets")
+        list_cmd.set_defaults(func=self.handle_list)
+
+        # lint command
+        lint = subparsers.add_parser("lint", help="Run linting tools")
+        lint.add_argument("path", nargs="?", default=".", help="Path to lint")
+        lint.add_argument("--fix", action="store_true", help="Fix linting issues")
+        lint.add_argument(
+            "--install-dependencies", action="store_true", help="Install linting dependencies"
+        )
+        lint.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        lint.set_defaults(func=self.handle_lint)
+
+        # setup command
+        setup = subparsers.add_parser("setup", help="Install HoloHub main required packages")
+        setup.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser("install", help="Install a project")
+        install.add_argument("project", help="Project to install")
+        install.add_argument(
+            "--local", action="store_true", help="Install locally instead of in container"
+        )
+        install.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        install.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser("clear-cache", help="Clear cache folders")
+        clear_cache.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def _collect_metadata(self) -> None:
+        """Create an unstructured database of metadata for all projects"""
+
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            host_root_dir / "applications",
+            host_root_dir / "benchmarks",
+            host_root_dir / "gxf_extensions",
+            host_root_dir / "operators",
+            host_root_dir / "pkg",
+            host_root_dir / "workflows",
+        )
+        self.projects = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+
+    def _find_project(self, project_name: str, language: Optional[str] = None) -> dict:
+        """Find a project by name"""
+        normalized_language = holohub_cli_util.normalize_language(language) if language else None
+        for project in self.projects:
+            if project["project_name"] == project_name:
+                if (
+                    normalized_language
+                    and holohub_cli_util.normalize_language(project["metadata"]["language"])
+                    != normalized_language
+                ):
+                    continue
+                return project
+        return None
+
+    def _make_project_container(
+        self, project_name: Optional[str] = None, language: Optional[str] = None
+    ) -> HoloHubContainer:
+        """Define a project container"""
+        if not project_name:
+            return HoloHubContainer(project_metadata=None)
+
+        project_data = self._find_project(project_name=project_name, language=language)
+        if not project_data:
+            holohub_cli_util.fatal(
+                f"Project '{project_name}' {f'({language})' if language else ''} not found"
+            )
+        return HoloHubContainer(project_metadata=project_data)
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build()
+        container.run(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root,
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts,
+            add_volumes=args.add_volume,
+            verbose=args.verbose,
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get("CMAKE_BUILD_TYPE", "Release")
+
+        build_type_map = {
+            "debug": "Debug",
+            "release": "Release",
+            "rel-debug": "RelWithDebInfo",
+            "relwithdebinfo": "RelWithDebInfo",
+        }
+        return build_type_map.get(build_type.lower(), "Release")
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_type = self.get_buildtype_str(args.build_type)
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={build_type}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", build_type], dry_run=args.dryrun
+            )
+
+        else:
+            # Build in container
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            container.run(
+                docker_opts="--entrypoint=bash",
+                extra_args=["-c", f"./holohub build {args.project} --local"],
+                verbose=args.verbose,
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            BUILD_TYPE = "Release"
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={BUILD_TYPE}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", BUILD_TYPE], dry_run=args.dryrun
+            )
+
+            language = holohub_cli_util.normalize_language(
+                project_data.get("metadata", {}).get("language", None)
+            )
+
+            run_config = project_data.get("metadata", {}).get("run", {})
+            if not run_config:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' does not have a run configuration"
+                )
+
+            app_source_path = project_data.get("source_folder", "")
+
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(HoloHubCLI.DEFAULT_DATA_DIR))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source_path))
+
+            cmd = cmd.replace("<holohub_bin>", str(build_dir))
+
+            app_build_dir = build_dir / app_source_path.relative_to(HoloHubCLI.HOLOHUB_ROOT)
+            cmd = cmd.replace("<holohub_app_bin>", str(app_build_dir))
+
+            if language == "cpp":
+                if not build_dir.is_dir() and not args.dryrun:
+                    holohub_cli_util.fatal(
+                        f"The build directory for this application does not exist.\n"
+                        f"Did you forget to './holohub build {args.project}'?"
+                    )
+
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(project_data.get("source_folder", ""))
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {project_data.get('source_folder', '')}\033[0m"
path: holohub
user: login: wyli
body: the color rendering could be unified and decoupled to a separate class
created_at: 2025-04-11T10:02:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1022 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+from collections import defaultdict
+from pathlib import Path
+from typing import List, Optional
+
+sys.path.append(Path(__file__).parent / "utilities")
+import utilities.cli.util as holohub_cli_util
+import utilities.metadata.gather_metadata as metadata_util
+from utilities.cli.container import HoloHubContainer
+
+host_root_dir = Path(__file__).parent
+
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob("CMakeLists.txt"):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split("(")[1].split(")")[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+
+    HOLOHUB_ROOT = Path(__file__).parent
+    DEFAULT_BUILD_PARENT_DIR = HOLOHUB_ROOT / "build"
+    DEFAULT_DATA_DIR = HOLOHUB_ROOT / "data"
+    DEFAULT_SDK_DIR = "/opt/nvidia/holoscan/lib"
+
+    def __init__(self):
+        self.parser = self._create_parser()
+        self._collect_metadata()
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(
+            description="HoloHub CLI tool for managing HoloHub applications and containers"
+        )
+        subparsers = parser.add_subparsers(dest="command", required=True)
+
+        # build-container command
+        build_container = subparsers.add_parser(
+            "build-container", help="Build the development container"
+        )
+        build_container.add_argument("project", nargs="?", help="Project to build container for")
+        build_container.add_argument("--base_img", help="Fully qualified base image name")
+        build_container.add_argument("--docker_file", help="Path to Dockerfile to use")
+        build_container.add_argument("--img", help="Specify fully qualified container name")
+        build_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker build command"
+        )
+        build_container.add_argument(
+            "--no-cache", action="store_true", help="Do not use cache when building the image"
+        )
+        build_container.add_argument(
+            "--build-args", help="Provides extra arguments to docker build command"
+        )
+        build_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser(
+            "run-container", help="Launch the development container"
+        )
+        run_container.add_argument("project", nargs="?", help="Project to run container for")
+        run_container.add_argument("--img", help="Fully qualified image name")
+        run_container.add_argument(
+            "--local_sdk_root",
+            help="Path to Holoscan SDK used for building local Holoscan SDK container",
+        )
+        run_container.add_argument(
+            "--ssh_x11",
+            action="store_true",
+            help="Enable X11 forwarding of graphical HoloHub applications over SSH",
+        )
+        run_container.add_argument("--init", action="store_true", help="Support tini entry point")
+        run_container.add_argument(
+            "--persistent", action="store_true", help="Does not delete container after it is run"
+        )
+        run_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker run command"
+        )
+        run_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run_container.add_argument("--add-volume", action="append", help="Mount additional volume")
+        run_container.add_argument(
+            "--as_root", action="store_true", help="Run the container with root permissions"
+        )
+        run_container.add_argument(
+            "--docker_opts", help="Additional options to pass to the Docker launch"
+        )
+        run_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser("build", help="Build a project")
+        build.add_argument("project", help="Project to build")
+        build.add_argument(
+            "--local", action="store_true", help="Build locally instead of in container"
+        )
+        build.add_argument("--verbose", action="store_true", help="Print extra output")
+        build.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        build.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser("run", help="Run a project")
+        run.add_argument("project", help="Project to run")
+        run.add_argument("--local", action="store_true", help="Run locally instead of in container")
+        run.add_argument("--verbose", action="store_true", help="Print extra output")
+        run.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run.add_argument(
+            "--nsys-profile", action="store_true", help="Enable Nsight Systems profiling"
+        )
+        run.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser("list", help="List all available targets")
+        list_cmd.set_defaults(func=self.handle_list)
+
+        # lint command
+        lint = subparsers.add_parser("lint", help="Run linting tools")
+        lint.add_argument("path", nargs="?", default=".", help="Path to lint")
+        lint.add_argument("--fix", action="store_true", help="Fix linting issues")
+        lint.add_argument(
+            "--install-dependencies", action="store_true", help="Install linting dependencies"
+        )
+        lint.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        lint.set_defaults(func=self.handle_lint)
+
+        # setup command
+        setup = subparsers.add_parser("setup", help="Install HoloHub main required packages")
+        setup.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser("install", help="Install a project")
+        install.add_argument("project", help="Project to install")
+        install.add_argument(
+            "--local", action="store_true", help="Install locally instead of in container"
+        )
+        install.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        install.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser("clear-cache", help="Clear cache folders")
+        clear_cache.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def _collect_metadata(self) -> None:
+        """Create an unstructured database of metadata for all projects"""
+
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            host_root_dir / "applications",
+            host_root_dir / "benchmarks",
+            host_root_dir / "gxf_extensions",
+            host_root_dir / "operators",
+            host_root_dir / "pkg",
+            host_root_dir / "workflows",
+        )
+        self.projects = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+
+    def _find_project(self, project_name: str, language: Optional[str] = None) -> dict:
+        """Find a project by name"""
+        normalized_language = holohub_cli_util.normalize_language(language) if language else None
+        for project in self.projects:
+            if project["project_name"] == project_name:
+                if (
+                    normalized_language
+                    and holohub_cli_util.normalize_language(project["metadata"]["language"])
+                    != normalized_language
+                ):
+                    continue
+                return project
+        return None
+
+    def _make_project_container(
+        self, project_name: Optional[str] = None, language: Optional[str] = None
+    ) -> HoloHubContainer:
+        """Define a project container"""
+        if not project_name:
+            return HoloHubContainer(project_metadata=None)
+
+        project_data = self._find_project(project_name=project_name, language=language)
+        if not project_data:
+            holohub_cli_util.fatal(
+                f"Project '{project_name}' {f'({language})' if language else ''} not found"
+            )
+        return HoloHubContainer(project_metadata=project_data)
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build()
+        container.run(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root,
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts,
+            add_volumes=args.add_volume,
+            verbose=args.verbose,
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get("CMAKE_BUILD_TYPE", "Release")
+
+        build_type_map = {
+            "debug": "Debug",
+            "release": "Release",
+            "rel-debug": "RelWithDebInfo",
+            "relwithdebinfo": "RelWithDebInfo",
+        }
+        return build_type_map.get(build_type.lower(), "Release")
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_type = self.get_buildtype_str(args.build_type)
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={build_type}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", build_type], dry_run=args.dryrun
+            )
+
+        else:
+            # Build in container
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            container.run(
+                docker_opts="--entrypoint=bash",
+                extra_args=["-c", f"./holohub build {args.project} --local"],
+                verbose=args.verbose,
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            BUILD_TYPE = "Release"
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={BUILD_TYPE}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", BUILD_TYPE], dry_run=args.dryrun
+            )
+
+            language = holohub_cli_util.normalize_language(
+                project_data.get("metadata", {}).get("language", None)
+            )
+
+            run_config = project_data.get("metadata", {}).get("run", {})
+            if not run_config:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' does not have a run configuration"
+                )
+
+            app_source_path = project_data.get("source_folder", "")
+
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(HoloHubCLI.DEFAULT_DATA_DIR))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source_path))
+
+            cmd = cmd.replace("<holohub_bin>", str(build_dir))
+
+            app_build_dir = build_dir / app_source_path.relative_to(HoloHubCLI.HOLOHUB_ROOT)
+            cmd = cmd.replace("<holohub_app_bin>", str(app_build_dir))
+
+            if language == "cpp":
+                if not build_dir.is_dir() and not args.dryrun:
+                    holohub_cli_util.fatal(
+                        f"The build directory for this application does not exist.\n"
+                        f"Did you forget to './holohub build {args.project}'?"
+                    )
+
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(project_data.get("source_folder", ""))
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {project_data.get('source_folder', '')}\033[0m"
+                )
+            elif workdir == "holohub_bin":
+                if not args.dryrun:
+                    os.chdir(build_dir)
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {build_dir}\033[0m"
+                )
+            else:  # default to app binary directory
+                target_dir = (
+                    build_dir if language == "cpp" else project_data.get("source_folder", "")
+                )
+                if not args.dryrun:
+                    os.chdir(target_dir)
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {target_dir}\033[0m"
+                )
+
+            # Set up environment
+            env = os.environ.copy()
+            env["PYTHONPATH"] = (
+                f"{HoloHubCLI.DEFAULT_SDK_DIR}/../python/lib:{build_dir}/python/lib:{host_root_dir}"
+            )
+            env["HOLOHUB_DATA_PATH"] = str(HoloHubCLI.DEFAULT_DATA_DIR)
+            env["HOLOSCAN_INPUT_PATH"] = os.environ.get(
+                "HOLOSCAN_INPUT_PATH", str(HoloHubCLI.DEFAULT_DATA_DIR)
+            )
+
+            # Print environment setup
+            if args.verbose or args.dryrun:
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport PYTHONPATH={env['PYTHONPATH']}\033[0m"
+                )
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport HOLOHUB_DATA_PATH={env['HOLOHUB_DATA_PATH']}\033[0m"
+                )
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mexport HOLOSCAN_INPUT_PATH={env['HOLOSCAN_INPUT_PATH']}\033[0m"
+                )
+
+            # Handle Nsight Systems profiling
+            if args.nsys_profile:
+                if (
+                    not shutil.which("nsys")
+                    and not os.path.isdir("/opt/nvidia/nsys-host")
+                    and not args.dryrun
+                ):
+                    holohub_cli_util.fatal(
+                        "Nsight Systems CLI command 'nsys' not found. No Nsight installation from the host is also mounted."
+                    )
+                nsys_cmd = "/opt/nvidia/nsys-host/bin/nsys" if not shutil.which("nsys") else "nsys"
+
+                # Check perf_event_paranoid level
+                if not args.dryrun:
+                    try:
+                        with open("/proc/sys/kernel/perf_event_paranoid") as f:
+                            if int(f.read()) > 2:
+                                holohub_cli_util.fatal(
+                                    "For Nsight Systems profiling the Linux operating system's perf_event_paranoid level must be 2 or less."
+                                )
+                    except (IOError, ValueError):
+                        pass
+
+                cmd = f"{nsys_cmd} profile --trace=cuda,vulkan,nvtx,osrt {cmd}"
+
+            holohub_cli_util.run_command(cmd.split(), env=env, dry_run=args.dryrun)
+        else:
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            # Get language before launching container
+            language = holohub_cli_util.normalize_language(
+                container.project_metadata.get("metadata", {}).get("language", None)
+            )
+
+            # Build command with all necessary arguments
+            run_cmd = f"./holohub run {args.project} --language {language} --local"
+            if args.dryrun:
+                run_cmd += " --dryrun"
+            if args.verbose:
+                run_cmd += " --verbose"
+            if args.nsys_profile:
+                run_cmd += " --nsys-profile"
+
+            container.run(
+                docker_opts="--entrypoint=bash", extra_args=["-c", run_cmd], verbose=args.verbose
+            )
+
+    def handle_list(self, args: argparse.Namespace) -> None:
+        """Handle list command"""
+        LIST_TYPES = [
+            "application",
+            "benchmark",
+            "gxf_extension",
+            "package",
+            "operator",
+            "tutorial",
+            "workflow",
+        ]
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            host_root_dir / "applications",
+            host_root_dir / "benchmarks",
+            host_root_dir / "gxf_extensions",
+            host_root_dir / "operators",
+            host_root_dir / "pkg",
+            host_root_dir / "workflows",
+        )
+        metadata = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+        grouped_metadata = defaultdict(list)
+        for project in metadata:
+            grouped_metadata[project.get("project_type", "")].append(project)
+
+        for project_type in LIST_TYPES:
+            if project_type not in grouped_metadata:
+                continue
+            print(f"\n\033[1;37m== {project_type.upper()}S =================\033[0m\n")
+            for project in sorted(grouped_metadata[project_type], key=lambda x: x["project_name"]):
+                language = project.get("metadata", {}).get("language", "")
+                language = f"({language})" if language else ""
+                print(f'{project["project_name"]} {language}')
+
+        print("\n\033[1;37m=================================\033[0m\n")
+
+    def handle_lint(self, args: argparse.Namespace) -> None:
+        """Handle lint command"""
+        if args.install_dependencies:
+            self._install_lint_deps(args.dryrun)
+            return
+
+        exit_code = 0
+
+        # Change to script directory
+        os.chdir(host_root_dir)
+
+        if args.fix:
+            # Fix Python
+            holohub_cli_util.run_command(
+                ["ruff", "check", "--fix", "--ignore", "E712", args.path],
+                check=False,
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(["isort", args.path], check=False, dry_run=args.dryrun)
+            holohub_cli_util.run_command(["black", args.path], check=False, dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                [
+                    "codespell",
+                    "-w",
+                    "-i",
+                    "3",
+                    args.path,
+                    "--ignore-words",
+                    "codespell_ignore_words.txt",
+                    "--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data",
+                ],
+                check=False,
+                dry_run=args.dryrun,
+            )
+
+            # Fix C++ with clang-format
+            try:
+                cmd = [
+                    sys.executable,
+                    "-m",
+                    "cpplint",
+                    "--exclude",
+                    "build",
+                    "--exclude",
+                    "install",
+                    "--exclude",
+                    "build-*",
+                    "--exclude",
+                    "install-*",
+                    "--exclude",
+                    "applications/holoviz/template/cookiecutter*",
+                    "--recursive",
+                    args.path,
+                ]
+                if args.dryrun:
+                    holohub_cli_util.run_command(cmd, dry_run=True)
+                    cpp_files = ""
+                else:
+                    cpp_files = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
+
+                for file in cpp_files.splitlines():
+                    if ":" in file:  # Only process files with issues
+                        file = file.split(":")[0]
+                        holohub_cli_util.run_command(
+                            [
+                                "clang-format",
+                                "--style=file",
+                                "--sort-includes=0",
+                                "--lines=20:10000",
+                                "-i",
+                                file,
+                            ],
+                            check=False,
+                            dry_run=args.dryrun,
+                        )
+            except subprocess.CalledProcessError:
+                pass
+
+        else:
+            # Run linting checks
+            print("Linting Python")
+            if (
+                holohub_cli_util.run_command(
+                    ["ruff", "check", "--ignore", "E712", args.path],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+            if (
+                holohub_cli_util.run_command(
+                    ["isort", "-c", args.path], check=False, dry_run=args.dryrun
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+            if (
+                holohub_cli_util.run_command(
+                    ["black", "--check", args.path], check=False, dry_run=args.dryrun
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Linting C++")
+            if (
+                holohub_cli_util.run_command(
+                    [
+                        "python",
+                        "-m",
+                        "cpplint",
+                        "--exclude",
+                        "build",
+                        "--exclude",
+                        "install",
+                        "--exclude",
+                        "build-*",
+                        "--exclude",
+                        "install-*",
+                        "--exclude",
+                        ".vscode-server",
+                        "--exclude",
+                        "applications/holoviz/template/cookiecutter*",
+                        "--recursive",
+                        args.path,
+                    ],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Code spelling")
+            if (
+                holohub_cli_util.run_command(
+                    [
+                        "codespell",
+                        args.path,
+                        "--skip=*.onnx,*.min.js,*.min.js.map,Contrastive_learning_Notebook.ipynb,./data",
+                        "--ignore-words",
+                        "codespell_ignore_words.txt",
+                        "--exclude-file",
+                        "codespell.txt",
+                    ],
+                    check=False,
+                    dry_run=args.dryrun,
+                ).returncode
+                != 0
+            ):
+                exit_code = 1
+
+            print("Linting CMake")
+            cmake_files = list(Path(args.path).rglob("CMakeLists.txt"))
+            cmake_files.extend(Path(args.path).rglob("*.cmake"))
+            if cmake_files:
+                if (
+                    holohub_cli_util.run_command(
+                        [
+                            "cmakelint",
+                            "--filter=-whitespace/indent,-linelength,-readability/wonkycase,-convention/filename,-package/stdargs",
+                            *[str(f) for f in cmake_files],
+                        ],
+                        check=False,
+                        dry_run=args.dryrun,
+                    ).returncode
+                    != 0
+                ):
+                    exit_code = 1
+
+        if exit_code == 0:
+            print("Everything looks good!")
+        sys.exit(exit_code)
+
+    def _install_lint_deps(self, dry_run: bool = False) -> None:
+        """Install linting dependencies"""
+        os.chdir(host_root_dir)
+
+        print("Install Lint Dependencies for Python")
+        holohub_cli_util.run_command(
+            [
+                sys.executable,
+                "-m",
+                "pip",
+                "install",
+                "-r",
+                str(host_root_dir / "utilities/requirements.lint.txt"),
+            ],
+            dry_run=dry_run,
+        )
+        holohub_cli_util.run_command(
+            ["apt", "install", "--no-install-recommends", "-y", "clang-format=1:14.0*"],
+            dry_run=dry_run,
+        )
+
+    def handle_setup(self, args: argparse.Namespace) -> None:
+        """Handle setup command"""
+        # Install system dependencies
+        holohub_cli_util.run_command(["sudo", "apt-get", "update"], dry_run=args.dryrun)
+
+        # Install wget if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "-y", "wget"], dry_run=args.dryrun
+        )
+
+        # Install xvfb for running tests/examples headless
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "-y", "xvfb"], dry_run=args.dryrun
+        )
+
+        # Check and install CMake if needed
+        cmake_version = subprocess.check_output(
+            ["dpkg", "--status", "cmake"], text=True, stderr=subprocess.DEVNULL
+        )
+        ubuntu_codename = subprocess.check_output(["cat", "/etc/os-release"], text=True)
+        ubuntu_codename = re.search(r"UBUNTU_CODENAME=(\w+)", ubuntu_codename).group(1)
+
+        if not cmake_version or "3.24.0" > cmake_version:
+            holohub_cli_util.run_command(
+                ["sudo", "apt", "install", "--no-install-recommends", "-y", "gpg"],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                [
+                    "wget",
+                    "-O",
+                    "-",
+                    "https://apt.kitware.com/keys/kitware-archive-latest.asc",
+                    "2>/dev/null",
+                    "|",
+                    "gpg",
+                    "--dearmor",
+                    "-",
+                    "|",
+                    "sudo",
+                    "tee",
+                    "/usr/share/keyrings/kitware-archive-keyring.gpg",
+                    ">/dev/null",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                [
+                    "echo",
+                    f'"deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ {ubuntu_codename} main"',
+                    "|",
+                    "sudo",
+                    "tee",
+                    "/etc/apt/sources.list.d/kitware.list",
+                    ">/dev/null",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(["sudo", "apt-get", "update"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                [
+                    "sudo",
+                    "apt",
+                    "install",
+                    "--no-install-recommends",
+                    "-y",
+                    "cmake",
+                    "cmake-curses-gui",
+                ],
+                dry_run=args.dryrun,
+            )
+
+        # Install Ninja
+        holohub_cli_util.run_command(
+            ["sudo", "apt", "install", "--no-install-recommends", "-y", "ninja-build"],
+            dry_run=args.dryrun,
+        )
+
+        # Install Python dev
+        python3_dev_version = subprocess.check_output(
+            ["dpkg", "--status", "python3-dev"], text=True, stderr=subprocess.DEVNULL
+        )
+        if not python3_dev_version or "3.9.0" > python3_dev_version:
+            holohub_cli_util.run_command(
+                [
+                    "sudo",
+                    "apt",
+                    "install",
+                    "--no-install-recommends",
+                    "-y",
+                    "python3",
+                    "python3-dev",
+                ],
+                dry_run=args.dryrun,
+            )
+
+        # Install ffmpeg
+        holohub_cli_util.run_command(
+            ["sudo", "apt", "install", "--no-install-recommends", "-y", "ffmpeg"],
+            dry_run=args.dryrun,
+        )
+
+        # Install libv4l-dev
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "libv4l-dev"],
+            dry_run=args.dryrun,
+        )
+
+        # Install git if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "git"],
+            dry_run=args.dryrun,
+        )
+
+        # Install unzip if not present
+        holohub_cli_util.run_command(
+            ["sudo", "apt-get", "install", "--no-install-recommends", "-y", "unzip"],
+            dry_run=args.dryrun,
+        )
+
+        # Install ngc-cli if not present
+        try:
+            subprocess.check_output(["ngc", "--version"], stderr=subprocess.DEVNULL)
+        except (subprocess.CalledProcessError, FileNotFoundError):
+            if platform.machine() == "aarch64":
+                holohub_cli_util.run_command(
+                    [
+                        "wget",
+                        "--content-disposition",
+                        "https://ngc.nvidia.com/downloads/ngccli_arm64.zip",
+                    ],
+                    dry_run=args.dryrun,
+                )
+                holohub_cli_util.run_command(["unzip", "ngccli_arm64.zip"], dry_run=args.dryrun)
+            else:
+                holohub_cli_util.run_command(
+                    [
+                        "wget",
+                        "--content-disposition",
+                        "https://ngc.nvidia.com/downloads/ngccli_linux.zip",
+                    ],
+                    dry_run=args.dryrun,
+                )
+                holohub_cli_util.run_command(["unzip", "ngccli_linux.zip"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(["chmod", "u+x", "ngc-cli/ngc"], dry_run=args.dryrun)
+            holohub_cli_util.run_command(
+                ["sudo", "ln", "-s", f"{os.getcwd()}/ngc-cli/ngc", "/usr/local/bin/"],
+                dry_run=args.dryrun,
+            )
+
+        # Install CUDA dependencies
+        for version in [
+            "12-6",
+            "12-5",
+            "12-4",
+            "12-3",
+            "12-2",
+            "12-1",
+            "12-0",
+            "11-8",
+            "11-7",
+            "11-6",
+            "11-4",
+        ]:
+            try:
+                cuda_version = subprocess.check_output(
+                    ["dpkg", "--status", f"cuda-cudart-{version}"],
+                    text=True,
+                    stderr=subprocess.DEVNULL,
+                )
+                if cuda_version:
+                    break
+            except subprocess.CalledProcessError:
+                continue
+
+        if cuda_version:
+            short_cuda_version = cuda_version.split(".")[0]
+
+            # Install cudnn9 first
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libcudnn9-cuda-12", "9.*", optional=True, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libcudnn9-dev-cuda-12", "9.*", optional=True, dry_run=args.dryrun
+            )
+
+            # Check if cudnn9 is installed, if not install cudnn8
+            installed_cudnn9_version = subprocess.check_output(
+                ["apt", "list", "--installed", "libcudnn9-cuda-12"],
+                text=True,
+                stderr=subprocess.DEVNULL,
+            )
+            if not installed_cudnn9_version:
+                holohub_cli_util.install_cuda_dependencies_package(
+                    "libcudnn8", f"cuda{short_cuda_version}", dry_run=args.dryrun
+                )
+                holohub_cli_util.install_cuda_dependencies_package(
+                    "libcudnn8-dev", f"cuda{short_cuda_version}", dry_run=args.dryrun
+                )
+
+            # Install TensorRT dependencies
+            installed_libnvinferbin = subprocess.check_output(
+                ["dpkg", "--status", "libnvinfer-bin"], text=True, stderr=subprocess.DEVNULL
+            )
+            # Extract version string using regex
+            version_match = re.search(
+                r"Version: (\d+\.\d+\.\d+)\.\d+-\d+\+cuda\d+\.\d+", installed_libnvinferbin
+            )
+            if version_match:
+                installed_libnvinferbin_version = version_match.group(1)
+            else:
+                holohub_cli_util.fatal("Could not determine libnvinfer-bin version")
+
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-headers-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvinfer-plugin-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+            holohub_cli_util.install_cuda_dependencies_package(
+                "libnvonnxparsers-dev", installed_libnvinferbin_version, dry_run=args.dryrun
+            )
+
+        # Install the autocomplete
+        holohub_cli_util.run_command(
+            [
+                "sudo",
+                "cp",
+                f"{HoloHubCLI.HOLOHUB_ROOT}/utilities/holohub_autocomplete",
+                "/etc/bash_completion.d/",
+            ],
+            dry_run=args.dryrun,
+        )
+
+        print("Setup for HoloHub is ready. Happy Holocoding!")
+
+    def handle_install(self, args: argparse.Namespace) -> None:
+        """Handle install command"""
+        raise NotImplementedError("Install command not yet implemented")
+
+    def handle_clear_cache(self, args: argparse.Namespace) -> None:
+        """Handle clear-cache command"""
+        if args.dryrun:
+            print("Would clear cache folders:")
+            for pattern in ["build", "build-*", "install"]:
+                for path in host_root_dir.glob(pattern):
+                    if path.is_dir():
+                        print(f"  Would remove: {path}")
+        else:
+            print("Clearing cache...")
+            for pattern in ["build", "build-*", "install"]:
+                for path in host_root_dir.glob(pattern):
+                    if path.is_dir():
+                        shutil.rmtree(path)
+
+    def run(self) -> None:
+        """Main entry point for the CLI"""
+        args = self.parser.parse_args()
+        if hasattr(args, "func"):
+            args.func(args)
+        else:
+            self.parser.print_help()
+            sys.exit(1)
+
+
+def main():
path: holohub
user: login: tbirdso
body: Agreed! Updated
created_at: 2025-04-15T20:35:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import grp
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+
+# Utility Functions
+def get_timestamp() -> str:
+    """Get current timestamp in the format used by the bash script"""
+    return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
+
+
+def fatal(message: str) -> None:
+    """Print fatal error and exit with backtrace"""
+    print(f"\033[31m{get_timestamp()} [FATAL] \033[0m{message}", file=sys.stderr)
+    print("\nBacktrace: ...", file=sys.stderr)
+    traceback.print_list(traceback.extract_stack()[-3:], file=sys.stderr)
+    sys.exit(1)
+
+
+def run_command(
+    cmd: List[str], dry_run: bool = False, check: bool = True, **kwargs
+) -> subprocess.CompletedProcess:
+    """Run a shell command and handle errors"""
+    cmd_str = " ".join(str(x) for x in cmd)
+    if dry_run:
path: utilities/cli/util.py
user: login: tbirdso
body: Good idea! Added this to `util.py`
created_at: 2025-04-15T20:36:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,1022 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+from collections import defaultdict
+from pathlib import Path
+from typing import List, Optional
+
+sys.path.append(Path(__file__).parent / "utilities")
+import utilities.cli.util as holohub_cli_util
+import utilities.metadata.gather_metadata as metadata_util
+from utilities.cli.container import HoloHubContainer
+
+host_root_dir = Path(__file__).parent
+
+
+def list_cmake_dir_options(script_dir: Path, cmake_function: str) -> List[str]:
+    """Get list of directories from CMakeLists.txt files"""
+    results = []
+    for cmakelists in script_dir.rglob("CMakeLists.txt"):
+        with open(cmakelists) as f:
+            content = f.read()
+            for line in content.splitlines():
+                if cmake_function in line:
+                    try:
+                        name = line.split("(")[1].split(")")[0].strip()
+                        results.append(name)
+                    except IndexError:
+                        continue
+    return sorted(results)
+
+
+class HoloHubCLI:
+    """Command-line interface for HoloHub"""
+
+    HOLOHUB_ROOT = Path(__file__).parent
+    DEFAULT_BUILD_PARENT_DIR = HOLOHUB_ROOT / "build"
+    DEFAULT_DATA_DIR = HOLOHUB_ROOT / "data"
+    DEFAULT_SDK_DIR = "/opt/nvidia/holoscan/lib"
+
+    def __init__(self):
+        self.parser = self._create_parser()
+        self._collect_metadata()
+
+    def _create_parser(self) -> argparse.ArgumentParser:
+        """Create the argument parser with all supported commands"""
+        parser = argparse.ArgumentParser(
+            description="HoloHub CLI tool for managing HoloHub applications and containers"
+        )
+        subparsers = parser.add_subparsers(dest="command", required=True)
+
+        # build-container command
+        build_container = subparsers.add_parser(
+            "build-container", help="Build the development container"
+        )
+        build_container.add_argument("project", nargs="?", help="Project to build container for")
+        build_container.add_argument("--base_img", help="Fully qualified base image name")
+        build_container.add_argument("--docker_file", help="Path to Dockerfile to use")
+        build_container.add_argument("--img", help="Specify fully qualified container name")
+        build_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker build command"
+        )
+        build_container.add_argument(
+            "--no-cache", action="store_true", help="Do not use cache when building the image"
+        )
+        build_container.add_argument(
+            "--build-args", help="Provides extra arguments to docker build command"
+        )
+        build_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        build_container.set_defaults(func=self.handle_build_container)
+
+        # run-container command
+        run_container = subparsers.add_parser(
+            "run-container", help="Launch the development container"
+        )
+        run_container.add_argument("project", nargs="?", help="Project to run container for")
+        run_container.add_argument("--img", help="Fully qualified image name")
+        run_container.add_argument(
+            "--local_sdk_root",
+            help="Path to Holoscan SDK used for building local Holoscan SDK container",
+        )
+        run_container.add_argument(
+            "--ssh_x11",
+            action="store_true",
+            help="Enable X11 forwarding of graphical HoloHub applications over SSH",
+        )
+        run_container.add_argument("--init", action="store_true", help="Support tini entry point")
+        run_container.add_argument(
+            "--persistent", action="store_true", help="Does not delete container after it is run"
+        )
+        run_container.add_argument(
+            "--verbose", action="store_true", help="Print variables passed to docker run command"
+        )
+        run_container.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run_container.add_argument("--add-volume", action="append", help="Mount additional volume")
+        run_container.add_argument(
+            "--as_root", action="store_true", help="Run the container with root permissions"
+        )
+        run_container.add_argument(
+            "--docker_opts", help="Additional options to pass to the Docker launch"
+        )
+        run_container.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run_container.set_defaults(func=self.handle_run_container)
+
+        # build command
+        build = subparsers.add_parser("build", help="Build a project")
+        build.add_argument("project", help="Project to build")
+        build.add_argument(
+            "--local", action="store_true", help="Build locally instead of in container"
+        )
+        build.add_argument("--verbose", action="store_true", help="Print extra output")
+        build.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        build.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        build.set_defaults(func=self.handle_build)
+
+        # run command
+        run = subparsers.add_parser("run", help="Run a project")
+        run.add_argument("project", help="Project to run")
+        run.add_argument("--local", action="store_true", help="Run locally instead of in container")
+        run.add_argument("--verbose", action="store_true", help="Print extra output")
+        run.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        run.add_argument(
+            "--nsys-profile", action="store_true", help="Enable Nsight Systems profiling"
+        )
+        run.add_argument(
+            "--language", choices=["cpp", "python"], help="Specify language implementation"
+        )
+        run.set_defaults(func=self.handle_run)
+
+        # list command
+        list_cmd = subparsers.add_parser("list", help="List all available targets")
+        list_cmd.set_defaults(func=self.handle_list)
+
+        # lint command
+        lint = subparsers.add_parser("lint", help="Run linting tools")
+        lint.add_argument("path", nargs="?", default=".", help="Path to lint")
+        lint.add_argument("--fix", action="store_true", help="Fix linting issues")
+        lint.add_argument(
+            "--install-dependencies", action="store_true", help="Install linting dependencies"
+        )
+        lint.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        lint.set_defaults(func=self.handle_lint)
+
+        # setup command
+        setup = subparsers.add_parser("setup", help="Install HoloHub main required packages")
+        setup.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        setup.set_defaults(func=self.handle_setup)
+
+        # Add install command
+        install = subparsers.add_parser("install", help="Install a project")
+        install.add_argument("project", help="Project to install")
+        install.add_argument(
+            "--local", action="store_true", help="Install locally instead of in container"
+        )
+        install.add_argument(
+            "--build-type",
+            choices=["debug", "release", "rel-debug"],
+            help="Build type (debug, release, rel-debug)",
+        )
+        install.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        install.set_defaults(func=self.handle_install)
+
+        # Add clear-cache command
+        clear_cache = subparsers.add_parser("clear-cache", help="Clear cache folders")
+        clear_cache.add_argument(
+            "--dryrun", action="store_true", help="Print commands without executing them"
+        )
+        clear_cache.set_defaults(func=self.handle_clear_cache)
+
+        return parser
+
+    def _collect_metadata(self) -> None:
+        """Create an unstructured database of metadata for all projects"""
+
+        EXCLUDE_PATHS = ["applications/holoviz/template"]
+        # Known exceptions, such as template files that do not represent a standalone project
+
+        app_paths = (
+            host_root_dir / "applications",
+            host_root_dir / "benchmarks",
+            host_root_dir / "gxf_extensions",
+            host_root_dir / "operators",
+            host_root_dir / "pkg",
+            host_root_dir / "workflows",
+        )
+        self.projects = metadata_util.gather_metadata(app_paths, exclude_paths=EXCLUDE_PATHS)
+
+    def _find_project(self, project_name: str, language: Optional[str] = None) -> dict:
+        """Find a project by name"""
+        normalized_language = holohub_cli_util.normalize_language(language) if language else None
+        for project in self.projects:
+            if project["project_name"] == project_name:
+                if (
+                    normalized_language
+                    and holohub_cli_util.normalize_language(project["metadata"]["language"])
+                    != normalized_language
+                ):
+                    continue
+                return project
+        return None
+
+    def _make_project_container(
+        self, project_name: Optional[str] = None, language: Optional[str] = None
+    ) -> HoloHubContainer:
+        """Define a project container"""
+        if not project_name:
+            return HoloHubContainer(project_metadata=None)
+
+        project_data = self._find_project(project_name=project_name, language=language)
+        if not project_data:
+            holohub_cli_util.fatal(
+                f"Project '{project_name}' {f'({language})' if language else ''} not found"
+            )
+        return HoloHubContainer(project_metadata=project_data)
+
+    def handle_build_container(self, args: argparse.Namespace) -> None:
+        """Handle build-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build(
+            docker_file=args.docker_file,
+            base_img=args.base_img,
+            img=args.img,
+            no_cache=args.no_cache,
+            build_args=args.build_args,
+        )
+
+    def handle_run_container(self, args: argparse.Namespace) -> None:
+        """Handle run-container command"""
+        container = self._make_project_container(
+            project_name=args.project, language=args.language if hasattr(args, "language") else None
+        )
+        container.dryrun = args.dryrun
+        container.build()
+        container.run(
+            img=args.img,
+            local_sdk_root=args.local_sdk_root,
+            ssh_x11=args.ssh_x11,
+            use_tini=args.init,
+            persistent=args.persistent,
+            as_root=args.as_root,
+            docker_opts=args.docker_opts,
+            add_volumes=args.add_volume,
+            verbose=args.verbose,
+        )
+
+    def get_buildtype_str(self, build_type: Optional[str]) -> str:
+        """Get CMake build type string"""
+        if not build_type:
+            return os.environ.get("CMAKE_BUILD_TYPE", "Release")
+
+        build_type_map = {
+            "debug": "Debug",
+            "release": "Release",
+            "rel-debug": "RelWithDebInfo",
+            "relwithdebinfo": "RelWithDebInfo",
+        }
+        return build_type_map.get(build_type.lower(), "Release")
+
+    def handle_build(self, args: argparse.Namespace) -> None:
+        """Handle build command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_type = self.get_buildtype_str(args.build_type)
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            build_dir.mkdir(parents=True, exist_ok=True)
+
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={build_type}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", build_type], dry_run=args.dryrun
+            )
+
+        else:
+            # Build in container
+            container = self._make_project_container(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            container.dryrun = args.dryrun
+            container.build()
+
+            container.run(
+                docker_opts="--entrypoint=bash",
+                extra_args=["-c", f"./holohub build {args.project} --local"],
+                verbose=args.verbose,
+            )
+
+    def handle_run(self, args: argparse.Namespace) -> None:
+        """Handle run command"""
+        if args.local or os.environ.get("HOLOHUB_BUILD_LOCAL"):
+            project_data = self._find_project(
+                project_name=args.project,
+                language=args.language if hasattr(args, "language") else None,
+            )
+            if not project_data:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' {f'({args.language})' if hasattr(args, 'language') else ''} not found"
+                )
+
+            build_dir = HoloHubCLI.DEFAULT_BUILD_PARENT_DIR / args.project
+            BUILD_TYPE = "Release"
+            holohub_cli_util.run_command(
+                [
+                    "cmake",
+                    "-B",
+                    str(build_dir),
+                    "-S",
+                    str(HoloHubCLI.HOLOHUB_ROOT),
+                    "-G",
+                    "Ninja",
+                    f"-DCMAKE_BUILD_TYPE={BUILD_TYPE}",
+                    f"-DCMAKE_PREFIX_PATH={HoloHubCLI.DEFAULT_SDK_DIR}",
+                    f"-DAPP_{args.project}=ON",
+                ],
+                dry_run=args.dryrun,
+            )
+            holohub_cli_util.run_command(
+                ["cmake", "--build", str(build_dir), "--config", BUILD_TYPE], dry_run=args.dryrun
+            )
+
+            language = holohub_cli_util.normalize_language(
+                project_data.get("metadata", {}).get("language", None)
+            )
+
+            run_config = project_data.get("metadata", {}).get("run", {})
+            if not run_config:
+                holohub_cli_util.fatal(
+                    f"Project '{args.project}' does not have a run configuration"
+                )
+
+            app_source_path = project_data.get("source_folder", "")
+
+            # Process command template
+            cmd = run_config["command"]
+            cmd = cmd.replace("<holohub_data_dir>", str(HoloHubCLI.DEFAULT_DATA_DIR))
+            cmd = cmd.replace("<holohub_app_source>", str(app_source_path))
+
+            cmd = cmd.replace("<holohub_bin>", str(build_dir))
+
+            app_build_dir = build_dir / app_source_path.relative_to(HoloHubCLI.HOLOHUB_ROOT)
+            cmd = cmd.replace("<holohub_app_bin>", str(app_build_dir))
+
+            if language == "cpp":
+                if not build_dir.is_dir() and not args.dryrun:
+                    holohub_cli_util.fatal(
+                        f"The build directory for this application does not exist.\n"
+                        f"Did you forget to './holohub build {args.project}'?"
+                    )
+
+            # Handle workdir
+            workdir = run_config.get("workdir", "holohub_app_bin")
+            if workdir == "holohub_app_source":
+                if not args.dryrun:
+                    os.chdir(project_data.get("source_folder", ""))
+                print(
+                    f"\033[34m{holohub_cli_util.get_timestamp()} \033[37m$ \033[32mcd {project_data.get('source_folder', '')}\033[0m"
path: holohub
user: login: tbirdso
body: Given the limited scope I'd like to defer on this for now, but open to revisiting in the future.
created_at: 2025-04-15T20:37:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:34Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:36Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:38Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:42Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:50Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:53Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:55:55Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-09T20:56:13Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE