sima-cli 2.1.13__tar.gz → 2.1.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sima_cli-2.1.13/sima_cli.egg-info → sima_cli-2.1.14}/PKG-INFO +1 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14}/pyproject.toml +1 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/__version__.py +1 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/package_builder.py +11 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/install.py +25 -5
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/neat.py +272 -42
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/network_doctor.py +1 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/utils.py +209 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14/sima_cli.egg-info}/PKG-INFO +1 -1
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_package_builder.py +91 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_sdk_image_detection.py +403 -11
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_sdk_network_doctor.py +4 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/LICENSE +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/MANIFEST.in +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/README.md +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/requirements.txt +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/setup.cfg +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/setup.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/__main__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/app_zoo/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/app_zoo/app.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/app_zoo/commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/auth/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/auth/auth0.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/auth/devportal.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/auth/login.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/cli.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/data/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/data/resources_internal.yaml +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/data/resources_public.yaml +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/deploy_only/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/deploy_only/device/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/deploy_only/device/commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/deploy_only/mpk/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/deploy_only/mpk/commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/discover/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/discover/discover.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/discover/linuxll.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/download/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/download/downloader.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/compatibility.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/github_assets.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/hostdriver.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/metadata_info.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/metadata_installer.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/metadata_validator.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/optiview.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/palette.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/install/registry.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/mla/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/mla/meminfo.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/model_zoo/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/model_zoo/model.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/network/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/network/network.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/playbooks/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/playbooks/commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/playbooks/manager.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/cmdexec.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/config.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/linux_devkit_network.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/linux_shared_network.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/preinstall.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/requirements.json +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/script.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/stop.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/sdk/uninstall.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/serial/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/serial/serial.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/storage/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/storage/nvme.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/storage/sdcard.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/bmaptool.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/bootimg.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/cleanlog.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/elxr.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/local.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/netboot.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/query.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/remote.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/update/updater.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/upgrade/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/upgrade/selfupdate.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/api_common.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/artifactory.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/common.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/config.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/config_loader.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/container_registries.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/deprecation.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/device_api.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/disk.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/docker.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/env.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/errors.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/mpk_api.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/net.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/network.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/pcie.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/pkg_update_check.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/serializers.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/services.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/utils/tag.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/vulcan/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/vulcan/artifacts.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli/vulcan/commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli.egg-info/SOURCES.txt +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli.egg-info/dependency_links.txt +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli.egg-info/entry_points.txt +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli.egg-info/requires.txt +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/sima_cli.egg-info/top_level.txt +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/e2e/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/__init__.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_app_zoo.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_appzoo_commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_auth.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_auth0.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_cli.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_cli_stdio.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_cli_update.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_cli_update_rerun.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_deprecation.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_docker_utils.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_download.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_elxr_update.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_firmware.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_generate_cli_markdown_docs.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_install_stub.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_metadata_installer.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_metadata_validator.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_model_zoo.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_netboot.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_pkg_update_check.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_sdk_linux_shared_network.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_sdk_preinstall.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_sdk_uninstall.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_selfupdate.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_skills_commands.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_skills_manager.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_utils.py +0 -0
- {sima_cli-2.1.13 → sima_cli-2.1.14}/tests/unit/test_vulcan.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# sima_cli/__version__.py
|
|
2
|
-
__version__ = "2.1.
|
|
2
|
+
__version__ = "2.1.14"
|
|
@@ -136,7 +136,17 @@ def _format_size(total_bytes: int) -> str:
|
|
|
136
136
|
|
|
137
137
|
def _matches_exclude_pattern(resource: str, patterns: Sequence[str]) -> bool:
|
|
138
138
|
filename = Path(resource).name
|
|
139
|
-
|
|
139
|
+
path_parts = resource.split("/")
|
|
140
|
+
for pattern in patterns:
|
|
141
|
+
if not pattern:
|
|
142
|
+
continue
|
|
143
|
+
if pattern.startswith(".") and "/" not in pattern and "\\" not in pattern:
|
|
144
|
+
if filename.endswith(pattern) or pattern in path_parts:
|
|
145
|
+
return True
|
|
146
|
+
continue
|
|
147
|
+
if pattern in resource or pattern in filename:
|
|
148
|
+
return True
|
|
149
|
+
return False
|
|
140
150
|
|
|
141
151
|
|
|
142
152
|
def collect_artifact_resources(
|
|
@@ -765,6 +765,22 @@ def _format_gb(byte_count: int) -> str:
|
|
|
765
765
|
return f"{byte_count / (1024 ** 3):.1f} GB"
|
|
766
766
|
|
|
767
767
|
|
|
768
|
+
def _ensure_writable_sdk_extensions_dir(path: Path) -> Path:
|
|
769
|
+
resolved = Path(os.path.realpath(str(path)))
|
|
770
|
+
try:
|
|
771
|
+
resolved.mkdir(parents=True, exist_ok=True)
|
|
772
|
+
probe = resolved / ".sima-cli-write-test"
|
|
773
|
+
with open(probe, "w", encoding="utf-8") as handle:
|
|
774
|
+
handle.write("ok\n")
|
|
775
|
+
probe.unlink()
|
|
776
|
+
except OSError as e:
|
|
777
|
+
raise RuntimeError(
|
|
778
|
+
f"SDK extensions directory is not writable: {resolved}. "
|
|
779
|
+
"Choose another directory where your user can create files."
|
|
780
|
+
) from e
|
|
781
|
+
return resolved
|
|
782
|
+
|
|
783
|
+
|
|
768
784
|
def _setup_sdk_extensions(
|
|
769
785
|
selected_images: List[str],
|
|
770
786
|
noninteractive: bool = False,
|
|
@@ -791,12 +807,16 @@ def _setup_sdk_extensions(
|
|
|
791
807
|
if noninteractive or yes_to_all:
|
|
792
808
|
extensions_dir = default_extensions_dir
|
|
793
809
|
click.echo(f"ℹ️ Using default SDK extensions directory: {extensions_dir}")
|
|
810
|
+
extensions_dir = _ensure_writable_sdk_extensions_dir(extensions_dir)
|
|
794
811
|
else:
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
812
|
+
while True:
|
|
813
|
+
response = input(f"Enter SDK extensions directory [{default_extensions_dir}]: ").strip()
|
|
814
|
+
extensions_dir = Path(response).expanduser() if response else default_extensions_dir
|
|
815
|
+
try:
|
|
816
|
+
extensions_dir = _ensure_writable_sdk_extensions_dir(extensions_dir)
|
|
817
|
+
break
|
|
818
|
+
except RuntimeError as e:
|
|
819
|
+
click.secho(f"⚠️ {e}", fg="yellow")
|
|
800
820
|
print(f"✅ SDK extensions directory configured: {extensions_dir}")
|
|
801
821
|
return str(extensions_dir)
|
|
802
822
|
|
|
@@ -3,14 +3,19 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
5
|
import random
|
|
6
|
+
import secrets
|
|
6
7
|
import shutil
|
|
7
8
|
import socket
|
|
8
9
|
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
import webbrowser
|
|
9
12
|
from dataclasses import dataclass
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from typing import Dict, List, Optional, Tuple
|
|
12
15
|
|
|
13
16
|
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
14
19
|
|
|
15
20
|
from sima_cli.utils.net import get_local_ip_candidates
|
|
16
21
|
|
|
@@ -23,6 +28,9 @@ NEAT_MEDIAMTX_RTSP_TRANSPORTS = "tcp"
|
|
|
23
28
|
NEAT_WEBRTC_UDP_SEARCH_START = 40000
|
|
24
29
|
NEAT_WEBRTC_UDP_SEARCH_END = 65535
|
|
25
30
|
NEAT_WEBRTC_UDP_PORT_COUNT = 200
|
|
31
|
+
OPENVSCODE_TOKEN_BYTES = 32
|
|
32
|
+
OPENVSCODE_TOKEN_CACHE_FILE = "sdk-code-ui-tokens.json"
|
|
33
|
+
console = Console()
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
@dataclass
|
|
@@ -35,6 +43,8 @@ class NeatRunConfig:
|
|
|
35
43
|
cert_file_host_path: str
|
|
36
44
|
key_file_host_path: str
|
|
37
45
|
webrtc_host_ip: str = ""
|
|
46
|
+
code_ui_token: str = ""
|
|
47
|
+
code_ui_supported: bool = True
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
def _can_bind_tcp(port: int) -> bool:
|
|
@@ -173,6 +183,8 @@ def allocate_neat_ports(
|
|
|
173
183
|
web_ssh = _allocate_single_port(8022, "tcp", reserved)
|
|
174
184
|
rtsp_tcp = _allocate_single_port(8554, "tcp", reserved)
|
|
175
185
|
main_ui = _allocate_single_port(9900, "tcp", reserved)
|
|
186
|
+
code_ui = _allocate_single_port(9999, "tcp", reserved)
|
|
187
|
+
code_ui_https = _allocate_single_port(10000, "tcp", reserved)
|
|
176
188
|
video_ui = _allocate_single_port(8081, "tcp", reserved)
|
|
177
189
|
video_udp_start, video_udp_end = _allocate_port_range(9000, 9079, "udp", reserved)
|
|
178
190
|
metadata_udp_start, metadata_udp_end = _allocate_port_range(9100, 9179, "udp", reserved)
|
|
@@ -188,6 +200,8 @@ def allocate_neat_ports(
|
|
|
188
200
|
port_map.update(
|
|
189
201
|
{
|
|
190
202
|
"mainUI": {"protocol": "tcp", "host": main_ui, "container": 9900},
|
|
203
|
+
"codeUI": {"protocol": "tcp", "host": code_ui, "container": 9999},
|
|
204
|
+
"codeUIHttps": {"protocol": "tcp", "host": code_ui_https, "container": 10000, "scheme": "https"},
|
|
191
205
|
"videoUI": {"protocol": "tcp", "host": video_ui, "container": 8081},
|
|
192
206
|
"webSSH": {"protocol": "tcp", "host": web_ssh, "container": 8022},
|
|
193
207
|
"rtsp": {
|
|
@@ -220,6 +234,8 @@ def allocate_neat_ports(
|
|
|
220
234
|
[
|
|
221
235
|
f"{web_ssh}:8022/tcp",
|
|
222
236
|
f"{main_ui}:9900/tcp",
|
|
237
|
+
f"{code_ui}:9999/tcp",
|
|
238
|
+
f"{code_ui_https}:10000/tcp",
|
|
223
239
|
f"{video_ui}:8081/tcp",
|
|
224
240
|
f"{rtsp_tcp}:8554/tcp",
|
|
225
241
|
f"{video_udp_start}-{video_udp_end}:9000-9079/udp",
|
|
@@ -312,6 +328,30 @@ def _detect_webrtc_host_ip(devkit_env: Optional[dict]) -> str:
|
|
|
312
328
|
return physical_ips[0] if physical_ips else ""
|
|
313
329
|
|
|
314
330
|
|
|
331
|
+
def _detect_browser_host_ip() -> str:
|
|
332
|
+
"""Return the host IP to advertise/open for local browser access.
|
|
333
|
+
|
|
334
|
+
DevKit sync may legitimately use a VPN/tunnel IP as the WebRTC/ICE host.
|
|
335
|
+
Browser access to Docker-published SDK ports should prefer a physical host
|
|
336
|
+
address so local setup output is not broken by VPN routing.
|
|
337
|
+
"""
|
|
338
|
+
physical_ips = _collect_physical_ipv4s_for_certs()
|
|
339
|
+
if physical_ips:
|
|
340
|
+
return physical_ips[0]
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
for iface, ip in get_local_ip_candidates():
|
|
344
|
+
name = (iface or "").lower()
|
|
345
|
+
if name.startswith(("lo", "docker", "br-", "veth", "virbr", "tun", "tap", "utun", "wg", "tailscale", "zt", "vmnet", "vboxnet")):
|
|
346
|
+
continue
|
|
347
|
+
if _is_usable_ipv4(ip):
|
|
348
|
+
return ip
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
return "localhost"
|
|
353
|
+
|
|
354
|
+
|
|
315
355
|
def _is_wsl() -> bool:
|
|
316
356
|
if os.getenv("WSL_DISTRO_NAME"):
|
|
317
357
|
return True
|
|
@@ -486,8 +526,17 @@ def _ensure_certificates(cert_dir: Path, devkit_env: Optional[dict], yes_to_all:
|
|
|
486
526
|
cert_dir.mkdir(parents=True, exist_ok=True)
|
|
487
527
|
cert_file = cert_dir / "neat-sdk.pem"
|
|
488
528
|
key_file = cert_dir / "neat-sdk-key.pem"
|
|
489
|
-
mkcert = _ensure_mkcert(yes_to_all=yes_to_all, noninteractive=noninteractive)
|
|
490
529
|
hosts = _collect_cert_hosts(devkit_env)
|
|
530
|
+
try:
|
|
531
|
+
mkcert = _ensure_mkcert(yes_to_all=yes_to_all, noninteractive=noninteractive)
|
|
532
|
+
except (RuntimeError, subprocess.CalledProcessError) as e:
|
|
533
|
+
click.secho(
|
|
534
|
+
f"⚠️ mkcert could not be installed: {e}. Falling back to a self-signed certificate.",
|
|
535
|
+
fg="yellow",
|
|
536
|
+
)
|
|
537
|
+
_generate_self_signed_cert(cert_file, key_file, hosts)
|
|
538
|
+
return cert_file, key_file
|
|
539
|
+
|
|
491
540
|
if _run_mkcert_with_fallback([mkcert, "-install"], "install the local CA into the trust store"):
|
|
492
541
|
subprocess.run(
|
|
493
542
|
[mkcert, "-cert-file", str(cert_file), "-key-file", str(key_file), *hosts],
|
|
@@ -498,40 +547,66 @@ def _ensure_certificates(cert_dir: Path, devkit_env: Optional[dict], yes_to_all:
|
|
|
498
547
|
return cert_file, key_file
|
|
499
548
|
|
|
500
549
|
|
|
501
|
-
def
|
|
502
|
-
"""Return
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
re-point when the host IP hasn't changed. On any failure to verify (no
|
|
506
|
-
openssl, parse error) returns False so the caller re-issues to be safe.
|
|
550
|
+
def _cert_san_hosts(cert_file: Path) -> Optional[List[str]]:
|
|
551
|
+
"""Return the SAN entries of ``cert_file`` (both ``DNS:`` names and
|
|
552
|
+
``IP Address:`` addresses), or ``None`` when the cert can't be read (no
|
|
553
|
+
openssl / parse error) so callers treat it as "unknown -> re-issue".
|
|
507
554
|
"""
|
|
508
|
-
if not host_ip:
|
|
509
|
-
return False
|
|
510
555
|
try:
|
|
511
556
|
result = subprocess.run(
|
|
512
557
|
["openssl", "x509", "-noout", "-ext", "subjectAltName", "-in", str(cert_file)],
|
|
513
558
|
capture_output=True, text=True, check=False,
|
|
514
559
|
)
|
|
515
560
|
text = result.stdout or ""
|
|
516
|
-
if result.returncode != 0 or "IP Address" not in text:
|
|
561
|
+
if result.returncode != 0 or ("IP Address" not in text and "DNS" not in text):
|
|
517
562
|
result = subprocess.run(
|
|
518
563
|
["openssl", "x509", "-noout", "-text", "-in", str(cert_file)],
|
|
519
564
|
capture_output=True, text=True, check=False,
|
|
520
565
|
)
|
|
521
566
|
text = result.stdout or ""
|
|
522
|
-
# Parse the comma-separated "IP Address:<ip>" SAN entries and match
|
|
523
|
-
# exactly. A bare substring check would treat a routed IP that is a
|
|
524
|
-
# *prefix* of an existing entry as covered (e.g. 192.168.1.5 vs
|
|
525
|
-
# IP Address:192.168.1.50), wrongly skipping re-issue and leaving the
|
|
526
|
-
# browser with a SAN mismatch after that re-point.
|
|
527
|
-
san_ips = []
|
|
528
|
-
for token in text.replace("\n", ",").split(","):
|
|
529
|
-
token = token.strip()
|
|
530
|
-
if token.startswith("IP Address:"):
|
|
531
|
-
san_ips.append(token[len("IP Address:"):].strip())
|
|
532
|
-
return host_ip in san_ips
|
|
533
567
|
except OSError:
|
|
568
|
+
return None
|
|
569
|
+
# Parse the comma/newline-separated SAN entries as exact tokens (not
|
|
570
|
+
# substrings) so 192.168.1.5 is not treated as covered by
|
|
571
|
+
# IP Address:192.168.1.50.
|
|
572
|
+
hosts = []
|
|
573
|
+
for token in text.replace("\n", ",").split(","):
|
|
574
|
+
token = token.strip()
|
|
575
|
+
if token.startswith("IP Address:"):
|
|
576
|
+
hosts.append(token[len("IP Address:"):].strip())
|
|
577
|
+
elif token.startswith("DNS:"):
|
|
578
|
+
hosts.append(token[len("DNS:"):].strip())
|
|
579
|
+
return hosts
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _cert_covers_host(cert_file: Path, host_ip: str) -> bool:
|
|
583
|
+
"""Return True if the cert's SAN lists ``host_ip``. False on any failure
|
|
584
|
+
(no openssl / parse error) so the caller re-issues to be safe."""
|
|
585
|
+
if not host_ip:
|
|
586
|
+
return False
|
|
587
|
+
san = _cert_san_hosts(cert_file)
|
|
588
|
+
return san is not None and host_ip in san
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _cert_covers_hosts(cert_file: Path, hosts: List[str]) -> bool:
|
|
592
|
+
"""Return True only if the cert's SAN covers **every** host in ``hosts``.
|
|
593
|
+
|
|
594
|
+
On a re-point the set of host addresses can grow (e.g. a VPN/tunnel IP
|
|
595
|
+
appears) while the primary host IP is unchanged. Checking the full expected
|
|
596
|
+
list -- not just the primary IP -- ensures an older cert missing a newly
|
|
597
|
+
discovered address is re-issued rather than skipped. Returns False on any
|
|
598
|
+
failure so the caller re-issues to be safe; an empty ``hosts`` is treated as
|
|
599
|
+
"nothing to verify -> covered" (the caller's no-host-IP guard already handles
|
|
600
|
+
the do-nothing case).
|
|
601
|
+
"""
|
|
602
|
+
wanted = [host for host in hosts if host]
|
|
603
|
+
if not wanted:
|
|
604
|
+
return True
|
|
605
|
+
san = _cert_san_hosts(cert_file)
|
|
606
|
+
if san is None:
|
|
534
607
|
return False
|
|
608
|
+
san_set = set(san)
|
|
609
|
+
return all(host in san_set for host in wanted)
|
|
535
610
|
|
|
536
611
|
|
|
537
612
|
def _container_sdk_cert_dir(container_name: str) -> Optional[Path]:
|
|
@@ -578,9 +653,11 @@ def refresh_neat_certificates(
|
|
|
578
653
|
to pick up the rewritten file from the bind-mounted cert dir.
|
|
579
654
|
|
|
580
655
|
(Re)generates when the cert is **missing** (e.g. the user deleted it) or its
|
|
581
|
-
SAN doesn't cover
|
|
582
|
-
|
|
583
|
-
|
|
656
|
+
SAN doesn't cover **every** currently expected host -- the primary host IP
|
|
657
|
+
*and* all alternate addresses (e.g. VPN/tunnel IPs) -- so an older cert that
|
|
658
|
+
covers only the primary IP is still refreshed. No-op (returns False) only when
|
|
659
|
+
there is no usable host IP, or a valid cert already covers the full host list.
|
|
660
|
+
Returns True when a cert was generated.
|
|
584
661
|
"""
|
|
585
662
|
host_ip = (devkit_env or {}).get("host_ip", "")
|
|
586
663
|
if not host_ip:
|
|
@@ -592,7 +669,12 @@ def refresh_neat_certificates(
|
|
|
592
669
|
cert_dir = _container_sdk_cert_dir(container_name) \
|
|
593
670
|
or (Path(workspace) / f".{container_name}" / "sdk-cert")
|
|
594
671
|
cert_file = cert_dir / "neat-sdk.pem"
|
|
595
|
-
|
|
672
|
+
# Re-issue unless the existing cert already covers the full set of hosts we
|
|
673
|
+
# would issue for now (primary host IP + all alternate IPv4s incl. VPN/tunnel),
|
|
674
|
+
# not just the primary host_ip -- otherwise a newly discovered address is
|
|
675
|
+
# silently left out on a re-run.
|
|
676
|
+
expected_hosts = _collect_cert_hosts(devkit_env)
|
|
677
|
+
if cert_file.exists() and _cert_covers_hosts(cert_file, expected_hosts):
|
|
596
678
|
return False
|
|
597
679
|
action = "Re-issuing" if cert_file.exists() else "Generating"
|
|
598
680
|
print(f"🔐 {action} Neat SDK certificate for host IP {host_ip} ...")
|
|
@@ -609,6 +691,61 @@ def _write_port_map(config_dir: Path, port_map: Dict) -> Path:
|
|
|
609
691
|
return path
|
|
610
692
|
|
|
611
693
|
|
|
694
|
+
def _generate_code_ui_token() -> str:
|
|
695
|
+
return secrets.token_urlsafe(OPENVSCODE_TOKEN_BYTES)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _sima_cli_home() -> Path:
|
|
699
|
+
return Path(os.environ.get("SIMA_CLI_HOME", str(Path.home() / ".sima-cli"))).expanduser()
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _code_ui_token_cache_path() -> Path:
|
|
703
|
+
return _sima_cli_home() / OPENVSCODE_TOKEN_CACHE_FILE
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _read_code_ui_token_cache() -> Dict:
|
|
707
|
+
path = _code_ui_token_cache_path()
|
|
708
|
+
if not path.exists():
|
|
709
|
+
return {}
|
|
710
|
+
try:
|
|
711
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
712
|
+
except (OSError, json.JSONDecodeError):
|
|
713
|
+
return {}
|
|
714
|
+
return data if isinstance(data, dict) else {}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _write_code_ui_token_cache(container_name: str, base_url: str, token: str) -> Path:
|
|
718
|
+
cache_dir = _sima_cli_home()
|
|
719
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
720
|
+
try:
|
|
721
|
+
cache_dir.chmod(0o700)
|
|
722
|
+
except OSError:
|
|
723
|
+
pass
|
|
724
|
+
|
|
725
|
+
cache = _read_code_ui_token_cache()
|
|
726
|
+
entries = cache.setdefault("containers", {})
|
|
727
|
+
if not isinstance(entries, dict):
|
|
728
|
+
entries = {}
|
|
729
|
+
cache["containers"] = entries
|
|
730
|
+
host_port = int(base_url.rsplit(":", 1)[1])
|
|
731
|
+
entries[container_name] = {
|
|
732
|
+
"codeUI": {
|
|
733
|
+
"host": host_port,
|
|
734
|
+
"token": token,
|
|
735
|
+
"url": f"{base_url}/?tkn={token}&folder=/workspace",
|
|
736
|
+
},
|
|
737
|
+
"updated_at": int(time.time()),
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
path = _code_ui_token_cache_path()
|
|
741
|
+
path.write_text(json.dumps(cache, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
742
|
+
try:
|
|
743
|
+
path.chmod(0o600)
|
|
744
|
+
except OSError:
|
|
745
|
+
pass
|
|
746
|
+
return path
|
|
747
|
+
|
|
748
|
+
|
|
612
749
|
def reserved_ports_from_neat_port_map(port_map: Dict) -> set:
|
|
613
750
|
reserved = set()
|
|
614
751
|
for entry in port_map.values():
|
|
@@ -669,6 +806,15 @@ def prepare_neat_container_run(
|
|
|
669
806
|
yes_to_all=yes_to_all,
|
|
670
807
|
noninteractive=noninteractive,
|
|
671
808
|
)
|
|
809
|
+
code_ui_token = ""
|
|
810
|
+
code_ui = port_map.get("codeUI")
|
|
811
|
+
if isinstance(code_ui, dict):
|
|
812
|
+
code_ui_token = _generate_code_ui_token()
|
|
813
|
+
code_ui_https = port_map.get("codeUIHttps")
|
|
814
|
+
code_ui_url = f"http://localhost:{int(code_ui['host'])}"
|
|
815
|
+
if isinstance(code_ui_https, dict):
|
|
816
|
+
code_ui_url = f"https://localhost:{int(code_ui_https['host'])}"
|
|
817
|
+
_write_code_ui_token_cache(container_name, code_ui_url, code_ui_token)
|
|
672
818
|
port_map_path = _write_port_map(config_dir, port_map)
|
|
673
819
|
return NeatRunConfig(
|
|
674
820
|
port_map=port_map,
|
|
@@ -679,6 +825,7 @@ def prepare_neat_container_run(
|
|
|
679
825
|
cert_file_host_path=str(cert_file),
|
|
680
826
|
key_file_host_path=str(key_file),
|
|
681
827
|
webrtc_host_ip=webrtc_host_ip,
|
|
828
|
+
code_ui_token=code_ui_token,
|
|
682
829
|
)
|
|
683
830
|
|
|
684
831
|
|
|
@@ -687,6 +834,15 @@ def append_neat_docker_args(docker_cmd: List[str], config: NeatRunConfig) -> Non
|
|
|
687
834
|
docker_cmd.extend(["-e", f"MTX_RTSPTRANSPORTS={NEAT_MEDIAMTX_RTSP_TRANSPORTS}"])
|
|
688
835
|
if config.webrtc_host_ip:
|
|
689
836
|
docker_cmd.extend(["-e", f"CONTAINER_HOST_IP={config.webrtc_host_ip}"])
|
|
837
|
+
if config.code_ui_token:
|
|
838
|
+
docker_cmd.extend(["-e", f"OPENVSCODE_SERVER_TOKEN={config.code_ui_token}"])
|
|
839
|
+
cert_config = config.port_map.get("cert")
|
|
840
|
+
if config.cert_host_dir and isinstance(cert_config, dict):
|
|
841
|
+
docker_cmd.extend(["-e", f"OPENVSCODE_SERVER_CERT={cert_config['certFile']}"])
|
|
842
|
+
docker_cmd.extend(["-e", f"OPENVSCODE_SERVER_CERT_KEY={cert_config['keyFile']}"])
|
|
843
|
+
code_ui_https = config.port_map.get("codeUIHttps")
|
|
844
|
+
if isinstance(code_ui_https, dict):
|
|
845
|
+
docker_cmd.extend(["-e", f"OPENVSCODE_SERVER_HTTPS_PORT={code_ui_https['container']}"])
|
|
690
846
|
for mapping in config.port_args:
|
|
691
847
|
docker_cmd.extend(["-p", mapping])
|
|
692
848
|
if config.config_host_dir:
|
|
@@ -695,50 +851,124 @@ def append_neat_docker_args(docker_cmd: List[str], config: NeatRunConfig) -> Non
|
|
|
695
851
|
docker_cmd.extend(["-v", f"{config.cert_host_dir}:/sdk-cert"])
|
|
696
852
|
|
|
697
853
|
|
|
854
|
+
def _code_ui_url(config: NeatRunConfig, display_host: str) -> str:
|
|
855
|
+
port_map = config.port_map
|
|
856
|
+
if not config.code_ui_supported or "codeUI" not in port_map:
|
|
857
|
+
return ""
|
|
858
|
+
|
|
859
|
+
display_entry = port_map.get("codeUIHttps") if config.cert_host_dir else None
|
|
860
|
+
if not isinstance(display_entry, dict):
|
|
861
|
+
display_entry = port_map["codeUI"]
|
|
862
|
+
scheme = display_entry.get("scheme", "http")
|
|
863
|
+
code_url = f"{scheme}://{display_host}:{display_entry['host']}"
|
|
864
|
+
token = config.code_ui_token or port_map["codeUI"].get("token")
|
|
865
|
+
if token:
|
|
866
|
+
code_url = f"{code_url}/?tkn={token}&folder=/workspace"
|
|
867
|
+
return code_url
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _open_code_ui_if_browser_available(code_url: str) -> bool:
|
|
871
|
+
if not code_url:
|
|
872
|
+
return False
|
|
873
|
+
try:
|
|
874
|
+
webbrowser.get()
|
|
875
|
+
except webbrowser.Error:
|
|
876
|
+
return False
|
|
877
|
+
try:
|
|
878
|
+
return bool(webbrowser.open(code_url))
|
|
879
|
+
except Exception:
|
|
880
|
+
return False
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _print_code_ui_panel(code_url: str, opened: bool) -> None:
|
|
884
|
+
if not code_url:
|
|
885
|
+
return
|
|
886
|
+
status = "Opened in your browser." if opened else "Open this URL in your browser."
|
|
887
|
+
console.print(
|
|
888
|
+
Panel(
|
|
889
|
+
"\n".join(
|
|
890
|
+
[
|
|
891
|
+
f"[bold]{status}[/bold]",
|
|
892
|
+
f"[cyan]{code_url}[/cyan]",
|
|
893
|
+
"",
|
|
894
|
+
"[yellow]Security:[/yellow] Do not share this URL. It contains a Code UI access token.",
|
|
895
|
+
]
|
|
896
|
+
),
|
|
897
|
+
title="VS Code Browser Access",
|
|
898
|
+
border_style="cyan",
|
|
899
|
+
expand=False,
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
|
|
698
904
|
def print_neat_setup_summary(config: NeatRunConfig) -> None:
|
|
699
905
|
port_map = config.port_map
|
|
700
|
-
|
|
906
|
+
rows = []
|
|
907
|
+
display_host = _detect_browser_host_ip()
|
|
908
|
+
web_scheme = "https" if config.cert_host_dir else "http"
|
|
909
|
+
code_url = _code_ui_url(config, display_host)
|
|
701
910
|
if "mainUI" in port_map:
|
|
702
|
-
|
|
911
|
+
rows.append(("mainUI", f"{web_scheme}://{display_host}:{port_map['mainUI']['host']}"))
|
|
912
|
+
if code_url:
|
|
913
|
+
rows.append(("codeUI", code_url))
|
|
914
|
+
if "codeUIHttps" in port_map:
|
|
915
|
+
rows.append(("codeUIHttp", f"http://{display_host}:{port_map['codeUI']['host']}"))
|
|
703
916
|
if "videoUI" in port_map:
|
|
704
|
-
|
|
917
|
+
rows.append(("videoUI", f"{web_scheme}://{display_host}:{port_map['videoUI']['host']}"))
|
|
705
918
|
if "webSSH" in port_map:
|
|
706
|
-
|
|
919
|
+
rows.append(("webSSH", f"http://{display_host}:{port_map['webSSH']['host']}"))
|
|
707
920
|
if "rtsp" in port_map:
|
|
708
|
-
|
|
921
|
+
rows.append(("rtsp", f"rtsp://{display_host}:{port_map['rtsp']['tcp']['host']}"))
|
|
709
922
|
if "videoUDP" in port_map:
|
|
710
|
-
|
|
711
|
-
"
|
|
923
|
+
rows.append(
|
|
924
|
+
("videoUDP", "{}-{}/udp -> {}-{}/udp".format(
|
|
712
925
|
port_map["videoUDP"]["hostStart"],
|
|
713
926
|
port_map["videoUDP"]["hostEnd"],
|
|
714
927
|
port_map["videoUDP"]["containerStart"],
|
|
715
928
|
port_map["videoUDP"]["containerEnd"],
|
|
716
|
-
)
|
|
929
|
+
))
|
|
717
930
|
)
|
|
718
931
|
if "metadataUDP" in port_map:
|
|
719
|
-
|
|
720
|
-
"
|
|
932
|
+
rows.append(
|
|
933
|
+
("metadataUDP", "{}-{}/udp -> {}-{}/udp".format(
|
|
721
934
|
port_map["metadataUDP"]["hostStart"],
|
|
722
935
|
port_map["metadataUDP"]["hostEnd"],
|
|
723
936
|
port_map["metadataUDP"]["containerStart"],
|
|
724
937
|
port_map["metadataUDP"]["containerEnd"],
|
|
725
|
-
)
|
|
938
|
+
))
|
|
726
939
|
)
|
|
727
940
|
if "webRTC" in port_map:
|
|
728
|
-
|
|
729
|
-
"
|
|
941
|
+
rows.append(
|
|
942
|
+
("webRTC", "{}-{}/udp -> {}-{}/udp".format(
|
|
730
943
|
port_map["webRTC"]["hostStart"],
|
|
731
944
|
port_map["webRTC"]["hostEnd"],
|
|
732
945
|
port_map["webRTC"]["containerStart"],
|
|
733
946
|
port_map["webRTC"]["containerEnd"],
|
|
734
|
-
)
|
|
947
|
+
))
|
|
735
948
|
)
|
|
736
949
|
if config.webrtc_host_ip:
|
|
737
|
-
|
|
950
|
+
rows.append(("iceHost", config.webrtc_host_ip))
|
|
738
951
|
if config.port_map_host_path:
|
|
739
|
-
|
|
952
|
+
rows.append(("config", config.port_map_host_path))
|
|
740
953
|
if config.cert_host_dir:
|
|
741
|
-
|
|
954
|
+
rows.append(("certs", config.cert_host_dir))
|
|
955
|
+
|
|
956
|
+
print("🔌 Neat SDK port map:")
|
|
957
|
+
if not rows:
|
|
958
|
+
print(" No Neat SDK ports are mapped.")
|
|
959
|
+
return
|
|
960
|
+
|
|
961
|
+
name_width = max(len("Name"), *(len(name) for name, _ in rows))
|
|
962
|
+
print(f" {'Name'.ljust(name_width)} | Endpoint / Value")
|
|
963
|
+
print(f" {'-' * name_width}-+-{'-' * len('Endpoint / Value')}")
|
|
964
|
+
for name, value in rows:
|
|
965
|
+
print(f" {name.ljust(name_width)} | {value}")
|
|
966
|
+
if display_host == "localhost":
|
|
967
|
+
print(" Note: Use localhost for local access, or replace it with this machine's external IP/DNS name for remote access.")
|
|
968
|
+
else:
|
|
969
|
+
print(" Note: Use the shown host IP for remote access, or replace it with localhost/127.0.0.1 for local access.")
|
|
970
|
+
opened = _open_code_ui_if_browser_available(code_url)
|
|
971
|
+
_print_code_ui_panel(code_url, opened)
|
|
742
972
|
|
|
743
973
|
|
|
744
974
|
def is_docker_port_collision_error(error_text: str) -> bool:
|
|
@@ -324,7 +324,7 @@ def port_specs_from_port_map(port_map: Optional[dict]) -> List[PortSpec]:
|
|
|
324
324
|
return []
|
|
325
325
|
|
|
326
326
|
specs: List[PortSpec] = []
|
|
327
|
-
for name in ("mainUI", "videoUI", "webSSH"):
|
|
327
|
+
for name in ("mainUI", "codeUI", "codeUIHttps", "videoUI", "webSSH"):
|
|
328
328
|
entry = port_map.get(name)
|
|
329
329
|
if not isinstance(entry, dict):
|
|
330
330
|
continue
|