From 2ee2169883ef7f73ff5c2dd9b05763e50b4444ef Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Wed, 9 Sep 2026 20:56:50 +0300 Subject: [PATCH] fix: identify Frost startup launcher --- docs/docs/platforms.html | 8 ++++--- forge.config.js | 49 +++++++++++++++++++++++++++++++++++++++- src/squirrel.ts | 38 +++++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/docs/docs/platforms.html b/docs/docs/platforms.html index c39e1d1..d8de19d 100644 --- a/docs/docs/platforms.html +++ b/docs/docs/platforms.html @@ -168,9 +168,11 @@

Start at login

Frost yourself.

- On Windows the login item deliberately points at Squirrel's - Update.exe rather than the executable directly, because the - executable lives in a versioned directory that moves on every update. + On Windows the login item points at Squirrel's stable + Frost.exe launcher rather than the versioned application + executable, which moves on every update. This keeps the entry valid + while Windows identifies it as Frost instead of Squirrel's generic + Update.exe.

One instance at a time

diff --git a/forge.config.js b/forge.config.js index da46e90..e03473a 100644 --- a/forge.config.js +++ b/forge.config.js @@ -9,9 +9,43 @@ const BUNDLE_ID = "frost"; // --no-git-tag-version`) before invoking Forge, so this is the version being // built. Used to name the Windows installer, which Squirrel does not version // for us. -const { version } = JSON.parse( +const packageMetadata = JSON.parse( readFileSync(new URL("./package.json", import.meta.url), "utf8") ); +const { version, repository } = packageMetadata; + +function githubOwner(repositoryMetadata) { + const repositoryUrl = + typeof repositoryMetadata === "string" + ? repositoryMetadata + : repositoryMetadata?.url; + if (typeof repositoryUrl !== "string") { + throw new Error( + "Windows publisher metadata requires a GitHub repository URL in package.json" + ); + } + + const scpMatch = /^git@github\.com:([^/\s]+)\//i.exec(repositoryUrl); + if (scpMatch) { + return scpMatch[1]; + } + + try { + const url = new URL(repositoryUrl.replace(/^git\+/, "")); + const owner = url.pathname.split("/").filter(Boolean)[0]; + if (url.hostname.toLowerCase() === "github.com" && owner) { + return owner; + } + } catch { + // The error below identifies the invalid repository metadata. + } + + throw new Error( + "Windows publisher metadata requires a GitHub repository URL in package.json" + ); +} + +const GITHUB_OWNER = githubOwner(repository); // Signing is opt-in through the environment, the same way windowsSign is below. // Pull request validation builds deliberately run without the Developer ID in @@ -107,6 +141,13 @@ export default { appBundleId: BUNDLE_ID, extraResource, out: "./out", + // Squirrel's stable launcher copies these resource values from + // Frost.exe. Task Manager uses them for the startup item's identity. + win32metadata: { + CompanyName: GITHUB_OWNER, + FileDescription: "Frost", + ProductName: "Frost", + }, ...macSigning, extendInfo: { LSUIElement: true, @@ -133,6 +174,12 @@ export default { // step. name: squirrelPackageName(arch), exe: "Frost.exe", + // Squirrel uses this for the stable Frost.exe launcher it + // creates next to Update.exe. That is the launcher Windows + // Startup Apps should invoke, so keep its publisher aligned + // with the GitHub owner rather than the upstream updater. + authors: GITHUB_OWNER, + owners: GITHUB_OWNER, setupExe: `Frost-win32-${arch}-${version}.exe`, setupIcon: "./src/icons/AppIcon.ico", // Shown next to the entry in Add/Remove Programs. Squirrel diff --git a/src/squirrel.ts b/src/squirrel.ts index 8bd4b4a..6df081a 100644 --- a/src/squirrel.ts +++ b/src/squirrel.ts @@ -29,6 +29,16 @@ const APP_USER_MODEL_ID = */ const UPDATE_EXE = resolve(dirname(process.execPath), "..", "Update.exe"); const EXE_NAME = basename(process.execPath); +// Squirrel also creates a stable launcher next to Update.exe. Unlike the +// generic updater, it carries Frost's own version metadata, so Windows shows +// a recognizable name and publisher in its Startup Apps list. +const SQUIRREL_LAUNCHER = resolve(dirname(process.execPath), "..", EXE_NAME); +const LOGIN_ITEM_NAME = "Frost-Update"; +const LEGACY_LOGIN_ITEM_NAME = APP_USER_MODEL_ID; +const LEGACY_LOGIN_ITEM_OPTIONS = { + path: UPDATE_EXE, + args: ["--processStart", `"${EXE_NAME}"`], +}; const isWindows = process.platform === "win32"; @@ -104,15 +114,35 @@ export function setAppUserModelId() { * * On Windows a Squirrel install lives in a versioned directory, so pointing * the registry entry at the current `Frost.exe` would break on the next - * update. Launching through `Update.exe --processStart` keeps the entry valid - * because the updater always resolves the newest version for us. + * update. Squirrel's stable root launcher keeps the entry valid and carries + * Frost's metadata; using its generic `Update.exe` directly instead makes + * Task Manager display the updater as "Update", published by GitHub. */ export function setOpenAtLogin(openAtLogin: boolean) { if (isWindows && app.isPackaged) { + const legacyLaunchItem = app + .getLoginItemSettings(LEGACY_LOGIN_ITEM_OPTIONS) + .launchItems?.find( + (item) => item.name === LEGACY_LOGIN_ITEM_NAME + ); + + // Versions before Frost-Update used Electron's default registry value + // name. Remove that entry once so an upgrade does not leave two startup + // items behind. Preserve its enabled state: Task Manager records a + // user's choice to disable startup separately from the Run key. + if (legacyLaunchItem) { + app.setLoginItemSettings({ + openAtLogin: false, + name: LEGACY_LOGIN_ITEM_NAME, + }); + } app.setLoginItemSettings({ openAtLogin, - path: UPDATE_EXE, - args: ["--processStart", `"${EXE_NAME}"`], + name: LOGIN_ITEM_NAME, + path: SQUIRREL_LAUNCHER, + ...(legacyLaunchItem + ? { enabled: legacyLaunchItem.enabled } + : {}), }); return; }