Skip to content

Commit 81d5351

Browse files
committed
fix(updates): preserve legacy mac app path
1 parent 5f2c62d commit 81d5351

3 files changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { lstatSync, mkdirSync, mkdtempSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+
import { repairLegacyMacAppPath } from "./macAppPathMigration";
6+
7+
describe("repairLegacyMacAppPath", () => {
8+
let root: string;
9+
10+
beforeEach(() => {
11+
root = mkdtempSync(join(tmpdir(), "poracode-mac-app-path-"));
12+
});
13+
14+
afterEach(() => {
15+
rmSync(root, { recursive: true, force: true });
16+
});
17+
18+
function packagedExecutable(appName: string): string {
19+
const executablePath = join(root, appName, "Contents", "MacOS", appName.replace(/\.app$/u, ""));
20+
mkdirSync(join(root, appName, "Contents", "MacOS"), { recursive: true });
21+
writeFileSync(executablePath, "executable");
22+
return executablePath;
23+
}
24+
25+
it("restores the legacy Nightly path as a relative symlink", () => {
26+
const executablePath = packagedExecutable("Poracode Nightly.app");
27+
28+
expect(
29+
repairLegacyMacAppPath("nightly", {
30+
platform: "darwin",
31+
isPackaged: true,
32+
executablePath,
33+
}),
34+
).toBe("created");
35+
36+
const legacyPath = join(root, "Lightcode Nightly.app");
37+
expect(lstatSync(legacyPath).isSymbolicLink()).toBe(true);
38+
expect(readlinkSync(legacyPath)).toBe("Poracode Nightly.app");
39+
});
40+
41+
it("restores the legacy Stable path", () => {
42+
const executablePath = packagedExecutable("Poracode.app");
43+
44+
expect(
45+
repairLegacyMacAppPath("stable", {
46+
platform: "darwin",
47+
isPackaged: true,
48+
executablePath,
49+
}),
50+
).toBe("created");
51+
expect(readlinkSync(join(root, "Lightcode.app"))).toBe("Poracode.app");
52+
});
53+
54+
it("never replaces an existing legacy app", () => {
55+
const executablePath = packagedExecutable("Poracode Nightly.app");
56+
const legacyPath = join(root, "Lightcode Nightly.app");
57+
mkdirSync(legacyPath);
58+
59+
expect(
60+
repairLegacyMacAppPath("nightly", {
61+
platform: "darwin",
62+
isPackaged: true,
63+
executablePath,
64+
}),
65+
).toBe("skipped");
66+
expect(lstatSync(legacyPath).isDirectory()).toBe(true);
67+
});
68+
69+
it("skips unpackaged, non-macOS, and unexpectedly named bundles", () => {
70+
const executablePath = packagedExecutable("Poracode Nightly.app");
71+
const otherExecutablePath = packagedExecutable("Renamed.app");
72+
73+
expect(
74+
repairLegacyMacAppPath("nightly", {
75+
platform: "linux",
76+
isPackaged: true,
77+
executablePath,
78+
}),
79+
).toBe("skipped");
80+
expect(
81+
repairLegacyMacAppPath("nightly", {
82+
platform: "darwin",
83+
isPackaged: false,
84+
executablePath,
85+
}),
86+
).toBe("skipped");
87+
expect(
88+
repairLegacyMacAppPath("nightly", {
89+
platform: "darwin",
90+
isPackaged: true,
91+
executablePath: otherExecutablePath,
92+
}),
93+
).toBe("skipped");
94+
});
95+
});

src/main/macAppPathMigration.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { lstatSync, realpathSync, symlinkSync } from "node:fs";
2+
import { basename, dirname, join } from "node:path";
3+
import type { PoracodeChannel } from "@/shared/channel";
4+
5+
interface MacAppPathMigrationOptions {
6+
platform?: NodeJS.Platform;
7+
isPackaged?: boolean;
8+
executablePath?: string;
9+
}
10+
11+
type MacAppPathMigrationResult = "created" | "skipped" | "failed";
12+
13+
function appNamesFor(channel: PoracodeChannel): { current: string; legacy: string } {
14+
return channel === "nightly"
15+
? { current: "Poracode Nightly.app", legacy: "Lightcode Nightly.app" }
16+
: { current: "Poracode.app", legacy: "Lightcode.app" };
17+
}
18+
19+
function bundlePathFromExecutable(executablePath: string): string {
20+
return dirname(dirname(dirname(executablePath)));
21+
}
22+
23+
/**
24+
* Keep the pre-rebrand application path usable after Squirrel renames the
25+
* installed bundle. macOS Dock items retain that path, so removing it leaves a
26+
* dead tile even though the renamed Poracode bundle launches normally.
27+
*
28+
* The relative symlink is deliberately best-effort and never replaces an
29+
* existing file. Squirrel resolves the running application's canonical path
30+
* before preparing later updates, so installs continue targeting Poracode.
31+
*/
32+
export function repairLegacyMacAppPath(
33+
channel: PoracodeChannel,
34+
options: MacAppPathMigrationOptions = {},
35+
): MacAppPathMigrationResult {
36+
const platform = options.platform ?? process.platform;
37+
const isPackaged = options.isPackaged ?? false;
38+
if (platform !== "darwin" || !isPackaged) return "skipped";
39+
40+
try {
41+
const executablePath = realpathSync(options.executablePath ?? process.execPath);
42+
const currentBundlePath = bundlePathFromExecutable(executablePath);
43+
const names = appNamesFor(channel);
44+
if (basename(currentBundlePath) !== names.current) return "skipped";
45+
46+
const legacyBundlePath = join(dirname(currentBundlePath), names.legacy);
47+
try {
48+
lstatSync(legacyBundlePath);
49+
return "skipped";
50+
} catch (error) {
51+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
52+
}
53+
54+
symlinkSync(names.current, legacyBundlePath, "dir");
55+
console.info(`[poracode] restored legacy macOS app path at ${legacyBundlePath}`);
56+
return "created";
57+
} catch (error) {
58+
// The app remains launchable from its canonical Poracode path if the
59+
// install directory is read-only or a filesystem policy rejects symlinks.
60+
console.warn("[poracode] failed to restore legacy macOS app path", error);
61+
return "failed";
62+
}
63+
}

src/main/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import {
8585
import { AppControlsMcpIngress } from "./app-controls";
8686
import { legacyProductNameFor, resolveLegacyElectronUserDataDir } from "./legacyDataMigration";
8787
import { refreshMacDockIcon } from "./macDockIcon";
88+
import { repairLegacyMacAppPath } from "./macAppPathMigration";
8889

8990
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
9091
const channel = resolvePoracodeChannel();
@@ -561,6 +562,7 @@ if (!hasSingleInstanceLock) {
561562
.whenReady()
562563
.then(async () => {
563564
if (preserveLegacySafeStorageIdentity) app.setName(productNameFor(channel));
565+
repairLegacyMacAppPath(channel, { isPackaged: app.isPackaged });
564566
refreshMacDockIcon();
565567
Menu.setApplicationMenu(null);
566568

0 commit comments

Comments
 (0)