diff --git a/apps/bootstrap-installer/src/store.ts b/apps/bootstrap-installer/src/store.ts index 63333e23dd5..2ae4a3c0c97 100644 --- a/apps/bootstrap-installer/src/store.ts +++ b/apps/bootstrap-installer/src/store.ts @@ -206,7 +206,13 @@ export async function initialize(): Promise { installRoot: payload.installRoot, currentStage: null }) - $route.set('success') + // Install: show the "launch Hermes" success screen. Update: this is a + // hand-off — the installer relaunches the desktop and exits within a + // few hundred ms, so routing to success just flashes that screen + // before the window closes. Stay on progress until we exit. + if ($mode.get() !== 'update') { + $route.set('success') + } break case 'failed': $bootstrap.set({ diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8497b147383..d442b8b613e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -116,6 +116,7 @@ "globals": "^16.5.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", + "rcedit": "^5.0.2", "typescript": "^6.0.3", "vite": "^8.0.10", "vitest": "^4.1.5", diff --git a/apps/desktop/scripts/set-exe-identity.cjs b/apps/desktop/scripts/set-exe-identity.cjs new file mode 100644 index 00000000000..9afc11255dc --- /dev/null +++ b/apps/desktop/scripts/set-exe-identity.cjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +// set-exe-identity.cjs — stamp the Hermes icon + version metadata onto the +// built Hermes.exe using rcedit, completely decoupled from electron-builder's +// signing path. +// +// WHY THIS EXISTS +// --------------- +// apps/desktop/package.json sets build.win.signAndEditExecutable=false. That +// flag is load-bearing: turning electron-builder's own exe-editing ON also +// re-enables its signtool step, which fetches winCodeSign-2.6.0.7z, whose +// macOS symlinks crash 7-Zip on non-admin Windows (no Developer Mode = no +// SeCreateSymbolicLinkPrivilege). That is an unfixable dead end — we do NOT +// try to extract winCodeSign. +// +// The cost of disabling signAndEditExecutable is that electron-builder also +// skips rcedit, so the unpacked Hermes.exe keeps the stock Electron icon and +// "Electron" taskbar name. This script restores the icon + identity by calling +// rcedit DIRECTLY. rcedit is a pure PE resource editor: no signing, no certs, +// no winCodeSign, no symlinks. Invoked from install.ps1's Install-Desktop +// after `npm run pack`. +// +// USAGE +// node scripts/set-exe-identity.cjs +// +// Exits 0 on success, non-zero on failure. install.ps1 treats failure as +// non-fatal (worst case: stock icon, not a broken app). + +const path = require('node:path') +const fs = require('node:fs') + +async function main() { + const exe = process.argv[2] + if (!exe) { + console.error('[set-exe-identity] usage: set-exe-identity.cjs ') + process.exit(2) + } + if (!fs.existsSync(exe)) { + console.error(`[set-exe-identity] target exe not found: ${exe}`) + process.exit(2) + } + + // Icon lives beside this script's package root: apps/desktop/assets/icon.ico + const desktopRoot = path.resolve(__dirname, '..') + const icon = path.join(desktopRoot, 'assets', 'icon.ico') + if (!fs.existsSync(icon)) { + console.error(`[set-exe-identity] icon not found: ${icon}`) + process.exit(2) + } + + // rcedit is a direct devDependency of apps/desktop, so it resolves whether + // we're run from the desktop dir or the repo root (workspace hoist). + // rcedit@5 exports a NAMED `rcedit` function (CommonJS: { rcedit }), not a + // default export. + let rcedit + try { + const mod = require('rcedit') + rcedit = typeof mod === 'function' ? mod : mod.rcedit + if (typeof rcedit !== 'function') { + throw new Error(`unexpected rcedit export shape: ${typeof mod} keys=${Object.keys(mod)}`) + } + } catch (err) { + console.error(`[set-exe-identity] could not load rcedit module: ${err.message}`) + process.exit(3) + } + + console.log(`[set-exe-identity] stamping ${exe}`) + console.log(`[set-exe-identity] icon: ${icon}`) + + try { + await rcedit(exe, { + icon, + 'version-string': { + ProductName: 'Hermes', + FileDescription: 'Hermes', + CompanyName: 'Nous Research', + LegalCopyright: 'Copyright (c) 2026 Nous Research' + } + }) + } catch (err) { + console.error(`[set-exe-identity] rcedit failed: ${err.message}`) + process.exit(1) + } + + console.log('[set-exe-identity] done — Hermes icon + identity stamped') +} + +main() diff --git a/package-lock.json b/package-lock.json index 55f5c154bb0..fe19020fb2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -176,6 +176,7 @@ "globals": "^16.5.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", + "rcedit": "^5.0.2", "typescript": "^6.0.3", "vite": "^8.0.10", "vitest": "^4.1.5", @@ -10888,6 +10889,54 @@ "node": ">= 8" } }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn-windows-exe/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -14928,6 +14977,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -15264,6 +15329,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -18930,6 +19008,20 @@ "rc": "cli.js" } }, + "node_modules/rcedit": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz", + "integrity": "sha512-dgysxaeXZ4snLpPjn8aVtHvZDCx+aRcvZbaWBgl1poU6OPustMvOkj9a9ZqASQ6i5Y5szJ13LSvglEOwrmgUxA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 22.12.0" + } + }, "node_modules/react": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", diff --git a/scripts/install.ps1 b/scripts/install.ps1 index db1aa454a4b..6cebfd10b7e 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -2014,13 +2014,13 @@ function Install-Desktop { # launchable binary the Tauri installer can spawn. # # CSC_IDENTITY_AUTO_DISCOVERY=false tells electron-builder we are - # NOT signing the output. This short-circuits the winCodeSign fetch + - # extraction entirely (which fails on non-admin Windows due to a - # macOS-symlink extraction crash electron-builder hasn't fixed in - # years). We never had a signing cert to use, so the apparatus was - # dead weight that broke fresh installs. The produced Hermes.exe - # is functionally identical — just unsigned, same as it would be - # if signing had been attempted but no cert was configured. + # NOT signing the output. Combined with signAndEditExecutable=false in + # apps/desktop/package.json's build.win block, electron-builder never + # invokes signtool and therefore never fetches/extracts winCodeSign + # (whose macOS symlinks crash 7-Zip on non-admin Windows — a dead end we + # are NOT trying to work around). The Hermes icon + product name are + # stamped onto Hermes.exe by our own rcedit step (Set-DesktopExeIdentity) + # AFTER this build, completely decoupled from electron-builder signing. # # WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD explicitly cleared as # belt-and-suspenders: if the user's environment has them set @@ -2086,6 +2086,13 @@ function Install-Desktop { throw "Desktop build completed but no Hermes.exe was found under $desktopDir\release\*-unpacked\" } + # 3b. Stamp the Hermes icon + identity onto Hermes.exe ourselves. + # electron-builder's own rcedit step is disabled (signAndEditExecutable + # =false) because enabling it drags in signtool -> winCodeSign -> the + # unfixable symlink crash. So we run rcedit directly on the produced + # exe — no signing, no winCodeSign, just PE resource editing. + Set-DesktopExeIdentity -TargetExe $desktopExe -DesktopDir $desktopDir + # 4. Create Start Menu + Desktop shortcuts pointing DIRECTLY at the packed # Hermes.exe. We deliberately do NOT point them at `hermes desktop`: that # command rebuilds (npm install + electron-builder) on every launch, @@ -2095,6 +2102,57 @@ function Install-Desktop { New-DesktopShortcuts -TargetExe $desktopExe } +function Set-DesktopExeIdentity { + # Stamp the Hermes icon + version metadata into Hermes.exe via rcedit, + # delegated to apps/desktop/scripts/set-exe-identity.cjs (which uses the + # `rcedit` npm package — a direct devDependency, so always present). + # + # Why a Node script instead of calling rcedit from PowerShell: passing + # arguments through PowerShell -> exe -> JSON parsers double-escapes + # Windows backslashes and breaks app-builder's --args JSON. Letting Node + # build the rcedit argv natively sidesteps all shell-quoting hazards. + # + # This replaces electron-builder's built-in signAndEditExecutable step + # (kept false, because enabling it re-triggers signtool -> winCodeSign -> + # the macOS-symlink 7-Zip crash on non-admin Windows). rcedit is a pure PE + # resource editor — no signing, no certs, no winCodeSign, no symlinks. + # + # Best-effort: a stamping failure must not fail an otherwise-good install + # (worst case is the stock Electron icon, not a broken app). + param( + [Parameter(Mandatory = $true)][string]$TargetExe, + [Parameter(Mandatory = $true)][string]$DesktopDir + ) + + $nodeExe = Get-Command node -ErrorAction SilentlyContinue + if (-not $nodeExe) { + Write-Warn "node not on PATH; cannot stamp Hermes.exe identity (it will keep the stock Electron icon)" + return + } + + $script = Join-Path $DesktopDir "scripts\set-exe-identity.cjs" + if (-not (Test-Path $script)) { + Write-Warn "set-exe-identity.cjs not found at $script; skipping exe identity stamp" + return + } + + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" + # Run from $DesktopDir so the script's `require('rcedit')` resolves against + # the desktop workspace's node_modules. + Push-Location $DesktopDir + & $nodeExe.Source $script $TargetExe 2>&1 | ForEach-Object { "$_" } + $code = $LASTEXITCODE + Pop-Location + $ErrorActionPreference = $prevEAP + + if ($code -eq 0) { + Write-Success "Stamped Hermes icon + identity onto $TargetExe" + } else { + Write-Warn "Exe identity stamp failed (exit $code); Hermes.exe keeps the stock Electron icon" + } +} + function New-DesktopShortcuts { param([Parameter(Mandatory = $true)][string]$TargetExe)