Fix macOS desktop packaging when Electron binary is missing - #38673
Fix macOS desktop packaging when Electron binary is missing#38673ChasLui wants to merge 1 commit into
Conversation
Use the installed Electron distribution during desktop packaging and run a macOS prebuilder patch before electron-builder renames the app binary. This keeps npm run pack and the installer desktop stage working even after node_modules is recreated.
e877429 to
a9e9bd1
Compare
austinpickett
left a comment
There was a problem hiding this comment.
Approve.
Verified on main: apps/desktop/scripts/patch-electron-builder-mac-binary.cjs is absent and apps/desktop/package.json has neither a prebuilder script nor build.electronDist — the packaging gap is present.
Diff scope (2 files):
- Adds
prebuilderscript +build.electronDistpointing at the workspace-hoisted../../node_modules/electron/dist(correct for a monorepo where electron-builder otherwise can't locate the dist). - New darwin-only patch script that idempotently shims
app-builder-lib'selectronMac.js: it is a guarded no-op when the file is missing or the expected code shape isn't found, is marker-guarded against double-application, and restores the Electron MacOS binary from candidate runtime locations before the rename (fixing the ENOENT during macOS arm64 packaging).
Hygiene: no package-lock.json churn, no version bumps. Patch is defensive (warn-and-skip on shape mismatch) so it degrades gracefully if app-builder-lib updates.
Cluster: independent of #39093 — both edit apps/desktop/package.json but in different, non-overlapping regions (this PR: scripts.prebuilder + build.electronDist; #39093: build.asarUnpack). No textual conflict expected. Independent of #37762 entirely.
austinpickett
left a comment
There was a problem hiding this comment.
Code Review — #38673 Fix macOS desktop packaging when Electron binary is missing
Verdict: ✅ Approve
Summary
Pragmatic workaround for a real electron-builder 26.8.x bug where the Electron binary is absent from the staged bundle before the rename step. The two-pronged approach (set electronDist + patch the internal rename code) is defensive and correct given the upstream bug.
electronDist in package.json
Pointing build.electronDist at the monorepo's node_modules/electron/dist is the recommended electron-builder escape hatch for this exact scenario and should resolve the issue on its own on most machines.
Patch script
The patch script is well-structured:
- Guard on
process.platform !== 'darwin'is correct — the bug is macOS-only. - The idempotency check via the
markerstring is sound and won't double-apply. - The two-candidate fallback (packager's own dist → monorepo dist) provides good resilience.
- The explicit
chmod 0o755is required and present. - Failing silently (via
console.warn+process.exit(0)) when the shape doesn't match is the right approach — better than blocking a build for a patching issue.
Concerns
- Patching
node_modules— This is a last-resort technique and the script is aware of it (notes idempotency after reinstall). Theprebuilderhook ensures the patch is re-applied, but this will be fragile if upstreamapp-builder-libchanges the exact surrounding string. The needle is brittle — a minor formatting change in a futureelectron-builderrelease could silently skip the patch (current behaviour: warn + no-op, which is acceptable). Consider adding a CI assertion that validates the patch was applied on macOS builds. - The
promises_1.copyFileandpromises_1.chmodreferences assume the compiled output ofapp-builder-libuses that specific binding name. This is an internal implementation detail — it works today but has no semver protection.
Overall
Despite the brittleness of node_modules patching, this is the right approach while waiting for upstream to fix the bug, and the defensive guards make failures observable rather than silent.
Reviewed by Hermes Agent
Code Review SummaryPR #38673 — Fix macOS desktop packaging when Electron binary is missing What this doesAddresses a macOS arm64 packaging failure in Two-pronged approach
Concerns to track
OverallThis is an appropriate short-term workaround with correct defensive guards. The patch is well-commented and idempotent. Track for removal once upstream Reviewed by Hermes Agent |
|
Salvaged into #46836 — rebased onto current |
…-pack fix(desktop): restore Electron binary before macOS pack rename (salvage #38673)
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…47266) (#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (#47266) Since #38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help.
* feat: add z-ai/glm-5.2 to OpenRouter and Nous model lists Z.ai released GLM 5.2 on 2026-06-15, available on OpenRouter: - https://openrouter.ai/z-ai/glm-5.2 GLM-5.2 is Z.ai's flagship for long-horizon tasks, shipping a 1M-token context window (up from 200K on GLM 5.1) and tool calling. Per the OpenRouter API: text-only, context_length 1048576, tools supported. No separate -fast variant exists. The 1M context length, native zai picker entry, setup wizard, and Z.ai coding-plan auth entries for glm-5.2 already landed on main. This fills the remaining gap: the two aggregator surfaces where glm-5.1 appears but glm-5.2 did not. Changes: hermes_cli/models.py - Add z-ai/glm-5.2 to the OpenRouter fallback snapshot (OPENROUTER_MODELS) and the Nous Portal curated list (_PROVIDER_MODELS["nous"]), newest flagship first. Live catalogs surface it automatically when reachable; the fallback lists matter when the manifest fetch fails. website/static/api/model-catalog.json - Regenerated via scripts/build_model_catalog.py (not hand-edited) so the manifest stays in sync with the source lists; guarded by tests/hermes_cli/test_model_catalog.py. * feat(prompt): make context-file truncation limit configurable PROBLEM: Automatic context files such as SOUL.md and AGENTS.md were capped by a hardcoded CONTEXT_FILE_MAX_CHARS value. Amy's local fork had raised that constant from 20K to 25K so a larger SOUL.md would not be silently truncated, but the hardcoded 25K value changed upstream default behavior and made the patch less generally useful. SOLUTION: Restore the upstream-compatible 20K default, add a context_file_max_chars config setting for users who intentionally keep larger identity/project-context files, keep chat-visible truncation warnings, and document the new setting. Tests cover the default, config override, explicit max_chars precedence, and the warning text. * fix(prompt): isolate truncation warnings per context Follow-up to salvaged PR NousResearch#41619: replace the module-global _truncation_warnings list with a contextvars.ContextVar so concurrent gateway-session prompt builds can't drain or clear each other's pending warnings (cross-session leak). Adds a context-isolation test. * feat(desktop): stream subagent activity into watch windows (NousResearch#47060) * feat(desktop): stream subagent replies into watch windows A desktop watch window resumes a child session lazily (no full agent) and mirrors the parent-relayed `subagent.*` events into native child-session stream events. The child's streamed reply text was never relayed, so the window sat blank while the subagent "talked". - delegate_tool: forward the child's `run_conversation` stream tokens up the progress relay as `subagent.text` (inert under CLI/TUI — their progress handlers ignore non-tool event types; only a gateway watch window mirrors it). - server: mirror `subagent.text` -> `message.delta` on the child sid only, and skip the parent emit (per-token frames are meaningless on the parent session, which shows the child via the spawn tree). Demote `subagent.start` to a one-time goal header and drop the noisy `subagent.progress` mirror — tools already mirror natively. - server: guard `_start_agent_build` so a lazy watch session spectating an in-flight child stays lazy; incidental RPCs were upgrading it to a full agent mid-stream and silently killing the mirror. * fix(desktop): keep watch-window chat clear of titlebar chrome Secondary windows (new-session scratch, subagent watch, cmd-click pop-out) hide the titlebar tool cluster + session header, so the transcript ran to the window's top edge and streamed text slid up under the OS traffic lights. - Gate the hidden chrome on `isSecondaryWindow()` everywhere (app-shell, chat header, thread list) instead of the narrower new-session flag. - Add a fixed opaque drag-strip at the top of the secondary-window transcript: content padding alone scrolls away with the text, so the strip masks anything behind it and keeps the window draggable like the main header. * fix: WSL subagent window * fix: subagent window top padding --------- Co-authored-by: Austin Pickett <pickett.austin@gmail.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> * feat(hooks): session:compress event_callback for MemPalace sync * test(hooks): cover session:compress event; drop dead import Follow-up to salvaged PR NousResearch#41624: - Remove stray urllib.parse import in run_agent.py (cherry-pick cruft, unused) - Add tests: session:compress emits with correct context, no-callback is safe, and a callback exception does not break compression * feat(xai): default to grok-build-0.1 Switch the default model for the xAI/Grok provider and the xAI web search backend from grok-4.3 to grok-build-0.1. grok-build-0.1 is already recognized by the model metadata, so no new model definition is required; grok-4.3 remains selectable. * docs(xai): update default model references to grok-build-0.1 Reflect the default-model change in the xAI Grok OAuth guide, the web search docs (EN + zh-Hans), and the web provider docstring. grok-4.3 is kept in the model tables as the previous default; the Nous/OpenRouter aggregator catalog still lists grok-4.3 and is left unchanged. * fix(inventory): keep user-defined custom providers in model dedup The NousResearch#45954 model-dedup builds `user_models` from every is_user_defined row, then strips those model IDs from every row where is_aggregator(slug) is True. But is_aggregator() returns True for *every* `custom:*` slug, and list_authenticated_providers emits named custom providers with slug `custom:<name>` and is_user_defined=True. So a user's own custom provider is treated as an aggregator and filtered against user_models — which holds exactly its own models (the row helped build that set). Every model is removed, the row drops to zero, and the provider disappears from the model picker. Guard the dedup loop to skip is_user_defined rows: a user's configured provider is never an aggregator duplicate of itself. Built-in aggregators (openrouter, etc.) are still deduped as before. Adds a regression test. * chore(release): map cyb0rgk1tty noreply email for AUTHOR_MAP Salvage follow-up for PR NousResearch#46921 — CI matches contributor authorship on the commit email, which is the GitHub noreply form. * fix(telegram): resolve replies to rich (sendRichMessage) messages Telegram does not echo a sendRichMessage's content back in reply_to_message (.text/.caption empty, .api_kwargs None), so replies to rich sends (briefings, the gateway's own rich finals) arrived with no quotable text and the [Replying to: ...] injection was skipped. Remember message_id -> text at send time in a best-effort JSON index (gateway/rich_sent_store.py), and recover it on inbound when text and caption are both empty. Best-effort and no-throw throughout: any failure degrades to prior behavior and never breaks a send or message. Salvaged from NousResearch#47375 by @x1erra. Dropped the cross-platform run.py reply-prefix rewrite (out of scope; bloated every reply on every platform) and scrubbed a docstring reference to an out-of-repo script. Kept the inbound reply_to logging enrichment used to verify the fix. * chore(release): AUTHOR_MAP entry for x1erra (Sierra) * test(telegram): rich-reply recovery via send-time index Cover NousResearch#47375 fix: record-on-rich-send + lookup-on-reply round trip, lookup miss leaving reply_to_text None, and precedence (native quote and echoed caption both win over the index fallback). * fix(model-switch): probe /v1/models for providers without api_key Section 3 of list_authenticated_providers (user-defined endpoints from the providers: config section) required an api_key before probing the endpoint's /v1/models for live model discovery. This broke local self-hosted backends (llama.cpp, Ollama, vLLM, etc.) that don't require authentication — they would only ever show the single default_model from config instead of the full model catalog. Section 4 (custom_providers list) already handled this correctly with the policy: probe when api_key is set OR when no explicit models are configured. Apply the same logic to Section 3 so local backends get full model discovery without requiring a placeholder api_key workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(model-switch): cover section-3 no-auth probe; map chimpera author Salvage follow-up for PR NousResearch#29575: add regression tests for the section-3 no-api_key /v1/models probe (probes bare endpoints, skips when explicit models set) and add the contributor AUTHOR_MAP entry. * fix(skills): ignore support docs in skill discovery Support files under references/, templates/, assets/, and scripts/ are progressive-disclosure data loaded through skill_view(..., file_path=...). They should not be treated as standalone skills during discovery or collision checks. This prevents archived skill packages or support markdown files inside a real skill from shadowing active skills with the same name while still allowing top-level categories named scripts/templates/assets/references. Tests cover: - pruning nested SKILL.md files inside skill support directories - preserving support-named top-level categories - avoiding skill_view collisions from support markdown - keeping archived package SKILL.md files accessible only through file_path * fix(models): pass model.base_url to fetch_models in /model picker The /model interactive picker resolved a base_url from user credentials but never passed it to ProviderProfile.fetch_models(), causing the picker to always query the provider's hardcoded default endpoint instead of the user's custom URL (e.g. a company litellm proxy). - providers/base.py: add optional base_url parameter to fetch_models() - hermes_cli/models.py: pass resolved base_url to fetch_models() - Update all subclass overrides for signature compatibility - Add 6 regression tests covering override, fallback, and integration * test(model-picker): cover two overlapping user-defined custom providers Guards that two user-defined custom endpoints exposing an overlapping model each keep their full catalog — the dedup must never cross-filter two user-defined rows against each other. * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) (NousResearch#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help. * fix(desktop): honor pre-session model pick + restore global reasoning/speed defaults (NousResearch#47447) * fix(desktop): keep the pre-session model pick selected in the picker The composer picker derived its "current" row from `model.options ?? store`, so model.options always won. Pre-session that query returns the PROFILE DEFAULT, not the sticky composer pick — so selecting a model before a session exists left the checkmark (and the picker's "current" line) on the default, making the pick look ignored even though the pill updated. Add `currentPickerSelection()`: with a live session the gateway's model.options is authoritative; pre-session the sticky `$currentModel`/`$currentProvider` wins, falling back to options. Wire it into ModelMenuPanel and ModelPickerDialog. * feat(desktop): global reasoning/speed defaults in Settings → Model The composer picker is now sticky-UI/per-session only and never writes the profile default (NousResearch#46959), but Settings → Model had no reasoning/speed control and `agent.reasoning_effort` wasn't in the curated config surface at all (`service_tier` was buried in Advanced) — so there was nowhere to set the profile default that crons/subagents/messaging resolve from. Add capability-gated Reasoning (effort) + Fast controls beside the main model, gated by the applied model's reported capabilities (reasoning defaults on, fast off when unreported — same as the composer). They read/write `agent.reasoning_effort` and `agent.service_tier` by round-tripping the config record, matching the gateway's value semantics (service_tier "fast"/"priority"/"on" ⇒ fast). * refactor(desktop): don't open the reasoning select from its row label A <label> wrapping the Select forwarded text clicks to the trigger, opening the dropdown unexpectedly. Plain row for reasoning; Fast stays a <label> so clicking its text toggles the switch (expected for a checkbox-like control). * feat(gateway): inject stable human-readable message timestamps Consolidates these related Amy fork patches: - 429830f39 feat(gateway): inject message timestamps into user messages for LLM context - 3c3d6fac0 fix: handle both ISO string and epoch float timestamps in history replay - 2874f7725 feat: human-friendly timestamp format with weekday and timezone name - 3735f4c8b fix: render gateway message timestamps once * feat(gateway): gate message timestamps behind opt-in (default off) Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was unconditional. Gate the in-context render behind gateway.message_timestamps.enabled (default false) at both the live-message and history-replay sites; timestamp metadata is still captured + persisted regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry, docs, and gate tests. * Sync homelab/main to upstream/main (17251e8) with minimal carried patches Rebased onto current upstream/main and reapplied the minimal homelab patch set: - Dockerfile: add iproute2 + GitHub CLI (gh) from official apt repo - pyproject.toml: add langfuse optional extra - plugins/observability/langfuse/__init__.py: Responses API serialization - plugins/platforms/discord/adapter.py: role-mention invocation support - tests/gateway/test_discord_role_mentions.py: role-mention test coverage - tests/plugins/test_langfuse_plugin.py: Responses API test coverage - .github/workflows/build.yml: GHCR image publish workflow Upstream changes since last sync (9d2ec8d -> 17251e8): - 35 commits from NousResearch/hermes-agent:main - Desktop: tighten composer model picker, open new sessions in compact windows - fix(skills): guard recursive skill delete against tree-escape - fix(delegation): forward background flag in delegate_task dispatch - fix(desktop): route global remote profile REST calls, sync sessions across windows - fix(desktop): open remote-gateway artifacts via authenticated download - fix(models): keep curated-first ordering in live+curated merge - fix(memory): strip skill scaffolding for all providers - feat(skills): replace shop-app with CLI-based shop skill - And more (see upstream log) Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com> --------- Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Co-authored-by: Wolfram Ravenwolf <github.com@wolfram.ravenwolf.de> Co-authored-by: teknium <127238744+teknium1@users.noreply.github.com> Co-authored-by: brooklyn! <brooklyn.bb.nicholson@gmail.com> Co-authored-by: Austin Pickett <pickett.austin@gmail.com> Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com> Co-authored-by: cyb0rgk1tty <214562553+cyb0rgk1tty@users.noreply.github.com> Co-authored-by: Sierra (Hermes Agent) <stevenn.damatoo@gmail.com> Co-authored-by: chimpera <11052595+chimpera@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: liuhao1024 <sunsky.lau@gmail.com> Co-authored-by: xxxigm <54813621+xxxigm@users.noreply.github.com> Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…ousResearch#47266) (NousResearch#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help.
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…ousResearch#47266) (NousResearch#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help.
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…cos-electron-pack fix(desktop): restore Electron binary before macOS pack rename (salvage NousResearch#38673)
…ousResearch#47266) (NousResearch#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help.
…stalls (supersedes NousResearch#48081/NousResearch#48082) (NousResearch#48091) * fix(desktop): resolve electronDist dynamically + self-heal blocked installs Supersedes the static-path approach (NousResearch#48081) and the install-step self-heal (NousResearch#48082) with a fix that removes the whole failure class instead of chasing each symptom. Three distinct faults converged into the June desktop-build outage; this closes all three. Root cause (the part NousResearch#48081 left open — "Gap B"): build.electronDist was a static relative path in apps/desktop/package.json, but npm workspace hoisting is NOT deterministic — depending on the npm version and what else is installed, npm nests the workspace-only electron devDep under apps/desktop/node_modules/electron OR hoists it to the repo root. A static path matches only one layout, so a clean install intermittently fails with "The specified electronDist does not exist". NousResearch#48081 re-pointed the path at the nested layout (correct today) but electron-builder reads electronDist STATICALLY, so any future hoist change silently breaks it again — only caught by a CI invariant, never self-corrected. Fix: - scripts/run-electron-builder.cjs: resolve electron the way Node's runtime does — require.resolve("electron/package.json") walks node_modules from the desktop project upward and finds electron wherever npm actually put it. The path can never drift out of sync with the install layout again, on any OS/npm version. * dist present -> pass -c.electronDist=<abs>/dist so electron-builder reuses the unpacked runtime (keeps the NousResearch#38673 fast path that dodges the 26.8.x missing-binary re-unpack bug). * dist absent -> omit electronDist; electron-builder fetches Electron itself via @electron/get honoring electronVersion + ELECTRON_MIRROR. package.json: builder script now runs the wrapper; the static build.electronDist is removed (the resolver owns it). - main.py / install.sh / install.ps1: on a dependency-install failure where the electron package staged but its dist is missing (electron's install.js process.exit(1) on a blocked/throttled binary download — NousResearch#47266/NousResearch#47917/NousResearch#48021), repopulate the dist via electron's downloader (canonical, then npmmirror.com) and CONTINUE to the build instead of aborting. npm runs postinstall LAST, so the only casualty is electron/dist; bailing here is what made the pack-time mirror self-heal unreachable on a blocked network. Hard-fail only when electron never staged at all (a genuine dependency error). - The pack-time mirror fallback now retries the build even when the pre-fetch can't populate the dist: the wrapper lets electron-builder download Electron itself via the mirror, so the retry is no longer a no-op (it was, when electronDist was a static path). The exact 40.10.2 pin (already on main) keeps the third mode — the native @electron-internal/extract-zip win32 binding that 40.10.3/40.10.4 ship without a published prebuild — from recurring. Tests: - test_desktop_electron_pin.py: replace the static-path-matches-lockfile invariant with contracts that there is no hardcoded electronDist to drift, the builder script routes through the resolver, and the resolver uses Node module resolution + injects -c.electronDist. - test_gui_command.py: install-failure self-heal continues to build; genuine (electron-never-staged) install failure still hard-fails; pack retries under the mirror even when the pre-fetch is blocked. Salvages/supersedes the overlapping community work in NousResearch#48003 (sitkarev), NousResearch#48012 (omegazheng), NousResearch#48033 (james47kjv), and NousResearch#48082. Co-authored-by: sitkarev <59806492+sitkarev@users.noreply.github.com> Co-authored-by: omegazheng <zheng@omegasys.eu> Co-authored-by: james47kjv <220877172+james47kjv@users.noreply.github.com> * fix(desktop): narrow Electron self-heal to real missing-dist failures Follow-up on NousResearch#48091 to remove the remaining misdiagnosis risk from the installer/build fallback path (NousResearch#46785 concern): only take the Electron repair/retry path when Electron's package files are staged and dist is actually missing/corrupt. - main.py: add _electron_pkg_staged_missing_dist() and use it to gate install failure recovery; fail fast for unrelated npm install errors. - main.py/install.sh/install.ps1: run cache purge + retry only when dist is missing; do not retry unrelated tsc/vite/build failures under an Electron-specific narrative. - install.sh/install.ps1: tighten install-stage self-heal guard to require both package.json + install.js and missing dist. - tests: add coverage that install failure hard-fails when Electron dist already exists, and update retry test to reflect the tightened recovery condition. Validation: - Python tests: 64 passed - install.sh-related tests included in the run - Real mac build on this machine: - npm ci at repo root: success - cd apps/desktop && npm run pack: success - electron-builder packaged darwin arm64 and used custom unpacked Electron dist * refactor(desktop): trim electron self-heal helpers and comments Deduplicate mirror-retry into _try_redownload_electron_dist / shell counterparts; shorten wrapper and install-script commentary without changing recovery semantics. --------- Co-authored-by: sitkarev <59806492+sitkarev@users.noreply.github.com> Co-authored-by: omegazheng <zheng@omegasys.eu> Co-authored-by: james47kjv <220877172+james47kjv@users.noreply.github.com>
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…cos-electron-pack fix(desktop): restore Electron binary before macOS pack rename (salvage NousResearch#38673)
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…cos-electron-pack fix(desktop): restore Electron binary before macOS pack rename (salvage NousResearch#38673)
…ousResearch#47266) (NousResearch#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help.
…stalls (supersedes NousResearch#48081/NousResearch#48082) (NousResearch#48091) * fix(desktop): resolve electronDist dynamically + self-heal blocked installs Supersedes the static-path approach (NousResearch#48081) and the install-step self-heal (NousResearch#48082) with a fix that removes the whole failure class instead of chasing each symptom. Three distinct faults converged into the June desktop-build outage; this closes all three. Root cause (the part NousResearch#48081 left open — "Gap B"): build.electronDist was a static relative path in apps/desktop/package.json, but npm workspace hoisting is NOT deterministic — depending on the npm version and what else is installed, npm nests the workspace-only electron devDep under apps/desktop/node_modules/electron OR hoists it to the repo root. A static path matches only one layout, so a clean install intermittently fails with "The specified electronDist does not exist". NousResearch#48081 re-pointed the path at the nested layout (correct today) but electron-builder reads electronDist STATICALLY, so any future hoist change silently breaks it again — only caught by a CI invariant, never self-corrected. Fix: - scripts/run-electron-builder.cjs: resolve electron the way Node's runtime does — require.resolve("electron/package.json") walks node_modules from the desktop project upward and finds electron wherever npm actually put it. The path can never drift out of sync with the install layout again, on any OS/npm version. * dist present -> pass -c.electronDist=<abs>/dist so electron-builder reuses the unpacked runtime (keeps the NousResearch#38673 fast path that dodges the 26.8.x missing-binary re-unpack bug). * dist absent -> omit electronDist; electron-builder fetches Electron itself via @electron/get honoring electronVersion + ELECTRON_MIRROR. package.json: builder script now runs the wrapper; the static build.electronDist is removed (the resolver owns it). - main.py / install.sh / install.ps1: on a dependency-install failure where the electron package staged but its dist is missing (electron's install.js process.exit(1) on a blocked/throttled binary download — NousResearch#47266/NousResearch#47917/NousResearch#48021), repopulate the dist via electron's downloader (canonical, then npmmirror.com) and CONTINUE to the build instead of aborting. npm runs postinstall LAST, so the only casualty is electron/dist; bailing here is what made the pack-time mirror self-heal unreachable on a blocked network. Hard-fail only when electron never staged at all (a genuine dependency error). - The pack-time mirror fallback now retries the build even when the pre-fetch can't populate the dist: the wrapper lets electron-builder download Electron itself via the mirror, so the retry is no longer a no-op (it was, when electronDist was a static path). The exact 40.10.2 pin (already on main) keeps the third mode — the native @electron-internal/extract-zip win32 binding that 40.10.3/40.10.4 ship without a published prebuild — from recurring. Tests: - test_desktop_electron_pin.py: replace the static-path-matches-lockfile invariant with contracts that there is no hardcoded electronDist to drift, the builder script routes through the resolver, and the resolver uses Node module resolution + injects -c.electronDist. - test_gui_command.py: install-failure self-heal continues to build; genuine (electron-never-staged) install failure still hard-fails; pack retries under the mirror even when the pre-fetch is blocked. Salvages/supersedes the overlapping community work in NousResearch#48003 (sitkarev), NousResearch#48012 (omegazheng), NousResearch#48033 (james47kjv), and NousResearch#48082. Co-authored-by: sitkarev <59806492+sitkarev@users.noreply.github.com> Co-authored-by: omegazheng <zheng@omegasys.eu> Co-authored-by: james47kjv <220877172+james47kjv@users.noreply.github.com> * fix(desktop): narrow Electron self-heal to real missing-dist failures Follow-up on NousResearch#48091 to remove the remaining misdiagnosis risk from the installer/build fallback path (NousResearch#46785 concern): only take the Electron repair/retry path when Electron's package files are staged and dist is actually missing/corrupt. - main.py: add _electron_pkg_staged_missing_dist() and use it to gate install failure recovery; fail fast for unrelated npm install errors. - main.py/install.sh/install.ps1: run cache purge + retry only when dist is missing; do not retry unrelated tsc/vite/build failures under an Electron-specific narrative. - install.sh/install.ps1: tighten install-stage self-heal guard to require both package.json + install.js and missing dist. - tests: add coverage that install failure hard-fails when Electron dist already exists, and update retry test to reflect the tightened recovery condition. Validation: - Python tests: 64 passed - install.sh-related tests included in the run - Real mac build on this machine: - npm ci at repo root: success - cd apps/desktop && npm run pack: success - electron-builder packaged darwin arm64 and used custom unpacked Electron dist * refactor(desktop): trim electron self-heal helpers and comments Deduplicate mirror-retry into _try_redownload_electron_dist / shell counterparts; shorten wrapper and install-script commentary without changing recovery semantics. --------- Co-authored-by: sitkarev <59806492+sitkarev@users.noreply.github.com> Co-authored-by: omegazheng <zheng@omegasys.eu> Co-authored-by: james47kjv <220877172+james47kjv@users.noreply.github.com>
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
…cos-electron-pack fix(desktop): restore Electron binary before macOS pack rename (salvage NousResearch#38673)
…ousResearch#47266) (NousResearch#47276) * fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist, electron-builder reads the Electron binary straight from there and never downloads it during `npm run pack`. That dist tree is only produced by the electron package's postinstall (install.js) during `npm ci`. When that download is blocked or throttled (GitHub's release host is unreachable in some regions), the dist is missing and the build dies with: The specified electronDist does not exist: .../node_modules/electron/dist The existing ELECTRON_MIRROR fallback in all three desktop-build paths (scripts/install.ps1, scripts/install.sh, and `hermes desktop` in hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but pack never downloads Electron anymore, so the mirror was never used and the retry re-read the same missing dist. The fallback was effectively dead. Drive the mirror through electron's own downloader instead: - Add a dist-presence check + a downloader helper (Test-ElectronDist / Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist, _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist + the path.txt version marker (electron's install.js short-circuits on it) and re-runs `node install.js`, optionally via a mirror. - On the first retry, repopulate a missing dist from the canonical source; on the mirror retry, re-fetch through npmmirror.com, then pack. - Gate the re-download on the dist check so an unrelated build failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final pack when the binary still can't be fetched instead of failing the same way. * test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266) Add behavior coverage for the electronDist re-download fix: - _electron_dist_ok across linux/win32/darwin, including the partial-dist case (dir present but binary missing) that makes the pinned electronDist fail. - _redownload_electron_dist: no-op when the binary is present, bail when install.js is absent, wipe a stale dist + path.txt marker and run electron's downloader with ELECTRON_MIRROR injected, and report failure when the download still produces no binary. - `hermes desktop`: the mirror fallback now drives electron's own downloader before re-running pack, and skips the final pack entirely when the binary can't be fetched. Replaces the old mirror test that asserted the (now-fixed) dead behavior of re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads Electron under the pinned electronDist, so that retry could never help.
…stalls (supersedes NousResearch#48081/NousResearch#48082) (NousResearch#48091) * fix(desktop): resolve electronDist dynamically + self-heal blocked installs Supersedes the static-path approach (NousResearch#48081) and the install-step self-heal (NousResearch#48082) with a fix that removes the whole failure class instead of chasing each symptom. Three distinct faults converged into the June desktop-build outage; this closes all three. Root cause (the part NousResearch#48081 left open — "Gap B"): build.electronDist was a static relative path in apps/desktop/package.json, but npm workspace hoisting is NOT deterministic — depending on the npm version and what else is installed, npm nests the workspace-only electron devDep under apps/desktop/node_modules/electron OR hoists it to the repo root. A static path matches only one layout, so a clean install intermittently fails with "The specified electronDist does not exist". NousResearch#48081 re-pointed the path at the nested layout (correct today) but electron-builder reads electronDist STATICALLY, so any future hoist change silently breaks it again — only caught by a CI invariant, never self-corrected. Fix: - scripts/run-electron-builder.cjs: resolve electron the way Node's runtime does — require.resolve("electron/package.json") walks node_modules from the desktop project upward and finds electron wherever npm actually put it. The path can never drift out of sync with the install layout again, on any OS/npm version. * dist present -> pass -c.electronDist=<abs>/dist so electron-builder reuses the unpacked runtime (keeps the NousResearch#38673 fast path that dodges the 26.8.x missing-binary re-unpack bug). * dist absent -> omit electronDist; electron-builder fetches Electron itself via @electron/get honoring electronVersion + ELECTRON_MIRROR. package.json: builder script now runs the wrapper; the static build.electronDist is removed (the resolver owns it). - main.py / install.sh / install.ps1: on a dependency-install failure where the electron package staged but its dist is missing (electron's install.js process.exit(1) on a blocked/throttled binary download — NousResearch#47266/NousResearch#47917/NousResearch#48021), repopulate the dist via electron's downloader (canonical, then npmmirror.com) and CONTINUE to the build instead of aborting. npm runs postinstall LAST, so the only casualty is electron/dist; bailing here is what made the pack-time mirror self-heal unreachable on a blocked network. Hard-fail only when electron never staged at all (a genuine dependency error). - The pack-time mirror fallback now retries the build even when the pre-fetch can't populate the dist: the wrapper lets electron-builder download Electron itself via the mirror, so the retry is no longer a no-op (it was, when electronDist was a static path). The exact 40.10.2 pin (already on main) keeps the third mode — the native @electron-internal/extract-zip win32 binding that 40.10.3/40.10.4 ship without a published prebuild — from recurring. Tests: - test_desktop_electron_pin.py: replace the static-path-matches-lockfile invariant with contracts that there is no hardcoded electronDist to drift, the builder script routes through the resolver, and the resolver uses Node module resolution + injects -c.electronDist. - test_gui_command.py: install-failure self-heal continues to build; genuine (electron-never-staged) install failure still hard-fails; pack retries under the mirror even when the pre-fetch is blocked. Salvages/supersedes the overlapping community work in NousResearch#48003 (sitkarev), NousResearch#48012 (omegazheng), NousResearch#48033 (james47kjv), and NousResearch#48082. Co-authored-by: sitkarev <59806492+sitkarev@users.noreply.github.com> Co-authored-by: omegazheng <zheng@omegasys.eu> Co-authored-by: james47kjv <220877172+james47kjv@users.noreply.github.com> * fix(desktop): narrow Electron self-heal to real missing-dist failures Follow-up on NousResearch#48091 to remove the remaining misdiagnosis risk from the installer/build fallback path (NousResearch#46785 concern): only take the Electron repair/retry path when Electron's package files are staged and dist is actually missing/corrupt. - main.py: add _electron_pkg_staged_missing_dist() and use it to gate install failure recovery; fail fast for unrelated npm install errors. - main.py/install.sh/install.ps1: run cache purge + retry only when dist is missing; do not retry unrelated tsc/vite/build failures under an Electron-specific narrative. - install.sh/install.ps1: tighten install-stage self-heal guard to require both package.json + install.js and missing dist. - tests: add coverage that install failure hard-fails when Electron dist already exists, and update retry test to reflect the tightened recovery condition. Validation: - Python tests: 64 passed - install.sh-related tests included in the run - Real mac build on this machine: - npm ci at repo root: success - cd apps/desktop && npm run pack: success - electron-builder packaged darwin arm64 and used custom unpacked Electron dist * refactor(desktop): trim electron self-heal helpers and comments Deduplicate mirror-retry into _try_redownload_electron_dist / shell counterparts; shorten wrapper and install-script commentary without changing recovery semantics. --------- Co-authored-by: sitkarev <59806492+sitkarev@users.noreply.github.com> Co-authored-by: omegazheng <zheng@omegasys.eu> Co-authored-by: james47kjv <220877172+james47kjv@users.noreply.github.com>
NousResearch#38673) electron-builder 26.8.x can stage an Electron.app without its Contents/MacOS/Electron binary, then fail renaming it to Hermes: ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes This breaks `npm run pack` and the installer desktop stage before a launchable Hermes.app exists. - Point build.electronDist at the already-installed Electron dist so electron-builder reuses it instead of re-unpacking from cache. - Add a darwin-only prebuilder patch that restores the missing main binary from the runtime dist before the rename. Idempotent (marker guard), soft-fails on shape mismatch, survives node_modules reinstall. Co-authored-by: ChasLui <chaslui@outlook.com>
Summary
This fixes a macOS desktop packaging failure where
electron-buildercan produce anElectron.appbundle without the mainContents/MacOS/Electronbinary, then fail while renaming it toHermes:When this happens,
npm run packand the installer desktop stage fail before producing a launchableHermes.app.What changed
build.electronDist, instead of askingelectron-builderto unpack Electron from the cache again.prebuilderstep that runs before everyelectron-builderinvocation.electron-builderperforms theElectron->Hermesrename.The patch script is idempotent, so it also works after
node_modulesis recreated bynpm installornpm ci.Verification
Verified on macOS arm64 in a local Hermes install tree:
node -c apps/desktop/scripts/patch-electron-builder-mac-binary.cjsnpm run builder -- --dirnpm run packhermes desktop --build-only --force-buildThe final command produced a launchable app at: