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;
}