diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 73a87ffd7c..55cd6fa733 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -873,6 +873,70 @@ from hermes_cli.model_setup_flows import ( _model_flow_ai_gateway, ) logger = logging.getLogger(__name__) +from hermes_cli.main_desktop import ( # noqa: E402,F401 (re-exported; tests patch hermes_cli.main.) + _DESKTOP_PREVIOUS_SUFFIX, + _DESKTOP_STAGING_PREFIX, + _ELECTRON_FALLBACK_MIRROR, + _HTML_TAG_WITH_URL, + _LINUX_PASSWORD_STORES, + _MACHINE_ATTRIBUTE_USER_ENABLED, + _MODULE_TAG, + _PE_MACHINE_AMD64, + _PE_MACHINE_ARM64, + _PE_MACHINE_I386, + _PE_MACHINE_NAMES, + _PE_MACHINE_TO_NAME, + _build_desktop_app, + _compute_desktop_content_hash, + _desktop_backup_unpacked_dir, + _desktop_build_needed, + _desktop_dist_exists, + _desktop_exe_integrity_error, + _desktop_launch_options, + _desktop_linux_needs_disable_setuid_sandbox, + _desktop_linux_needs_no_sandbox, + _desktop_linux_sandbox_fixup, + _desktop_linux_sandbox_helper_is_regular_file, + _desktop_linux_userns_sandbox_available, + _desktop_macos_bundle_id, + _desktop_macos_has_valid_real_signature, + _desktop_macos_local_codesign, + _desktop_macos_local_signing_identity, + _desktop_macos_relaunchable_fixup, + _desktop_macos_setup_tcc_identity, + _desktop_packaged_executable, + _desktop_packaged_executable_in, + _desktop_staging_dir, + _desktop_stamp_path, + _desktop_unpacked_root, + _detect_linux_password_store, + _discard_desktop_staging, + _electron_dir, + _electron_dist_binary, + _electron_dist_ok, + _electron_download_cache_dirs, + _electron_pkg_staged_missing_dist, + _ensure_desktop_exe_launchable, + _expected_windows_pe_machines, + _force_adhoc_macos_signing, + _macos_codesigning_identity_valid, + _parse_pe_machine, + _pe_machine_or_none, + _purge_electron_build_cache, + _redownload_electron_dist, + _register_linux_desktop_entry, + _renderer_bundle_dir, + _renderer_bundle_torn, + _rollback_desktop_from_backup, + _stop_desktop_processes_locking_build, + _swap_staged_desktop_app, + _try_redownload_electron_dist, + _windows_native_machine, + _windows_native_machine_from_iswow64, + _windows_user_runnable_pe_machines, + _write_desktop_build_stamp, + cmd_gui, +) from hermes_cli.main_web_build import ( # noqa: E402,F401 (re-exported; tests patch hermes_cli.main.) _BYTECODE_FINGERPRINT_FILE, _build_web_ui, @@ -4455,11 +4519,6 @@ def _clear_bytecode_cache(root: Path) -> int: # paying the update_cmd import cost on every CLI invocation. -def _desktop_dist_exists(desktop_dir: Path) -> bool: - """Return True when a local desktop renderer build is present.""" - return (desktop_dir / "dist" / "index.html").exists() - - # --------------------------------------------------------------------------- # Desktop build stamp — content-hash based skip logic # --------------------------------------------------------------------------- @@ -4481,241 +4540,6 @@ def _desktop_dist_exists(desktop_dir: Path) -> bool: # "builtAt": "" # } -def _compute_desktop_content_hash(project_root: Path) -> str: - """Return a SHA-256 hex digest of all source files that feed the desktop build. - - Covers ``apps/desktop/`` (excluding anything matched by .gitignore) - plus the root ``package.json`` / ``package-lock.json`` (workspace config - that determines dependency resolution for the desktop workspace). - - Parses the repo-root ``.gitignore`` via *pathspec* so we automatically - skip ``node_modules/``, ``dist/``, ``*.pyc``, etc. without maintaining - a hardcoded skip-list. - """ - h = hashlib.sha256() - - def _hash_file(path: Path) -> None: - rel = str(path.relative_to(project_root)) - h.update(rel.encode()) - h.update(b"\0") - try: - with open(path, "rb") as f: - for chunk in iter(lambda: f.read(65536), b""): - h.update(chunk) - except (OSError, IOError): - pass - h.update(b"\0") - - - from pathspec import PathSpec - - gitignore = project_root / ".gitignore" - lines: list[str] = [] - if gitignore.is_file(): - lines = gitignore.read_text(encoding="utf-8").splitlines() - spec = PathSpec.from_lines("gitignore", lines) - - # Root workspace config - for name in ("package.json", "package-lock.json"): - p = project_root / name - if p.is_file(): - rel = str(p.relative_to(project_root)) - if not spec.match_file(rel): - _hash_file(p) - - # Walk apps/desktop/ — prune ignored directories in-place - desktop_dir = project_root / "apps" / "desktop" - for dirpath, dirnames, filenames in os.walk(desktop_dir, topdown=True): - # Prune ignored directories so we never descend into them - dirnames[:] = [ - d for d in dirnames - if not spec.match_file(str((Path(dirpath) / d).relative_to(project_root))) - ] - - for fn in sorted(filenames): - fp = Path(dirpath) / fn - rel = str(fp.relative_to(project_root)) - if not spec.match_file(rel): - _hash_file(fp) - - return h.hexdigest() - - -def _desktop_stamp_path() -> Path: - """Return the path to the desktop build stamp file under $HERMES_HOME.""" - from hermes_constants import get_hermes_home - return get_hermes_home() / "desktop-build-stamp.json" - - -def _renderer_bundle_dir(desktop_dir: Path, *, source_mode: bool) -> Optional[Path]: - """The renderer ``dist`` directory a launch loads, when it is inspectable. - - Source mode builds to ``apps/desktop/dist``. A packaged app ships the same - bundle twice — inside ``app.asar`` and, because ``asarUnpack`` lists - ``dist/**``, beside it in ``app.asar.unpacked``. Only the unpacked copy is - a real directory; that is also the one an interrupted replace tears, so - checking it catches the failure we care about. - """ - if source_mode: - return desktop_dir / "dist" - - executable = _desktop_packaged_executable(desktop_dir) - if executable is None: - return None - - # macOS: …/Hermes.app/Contents/MacOS/Hermes → …/Contents/Resources - resources = ( - executable.parent.parent / "Resources" - if sys.platform == "darwin" - else executable.parent / "resources" - ) - return resources / "app.asar.unpacked" / "dist" - - -# The module files the renderer fetches before any app code runs: Vite emits -# them as `