Skip to content

Commit df7d18a

Browse files
committed
feat(install): DMG → Applications install guidance end-to-end
Three changes fix the "user runs app directly from DMG → keychain broken" funnel at three points: 1. Boot-time check (install-check.ts) — if `process.execPath` starts with `/Volumes/`, show a blocking dialog before anything else loads. Primary button opens the Applications folder and quits so the user can drag-install cleanly. Escape hatch left for power users who really want to run from the mount. 2. Custom DMG background + layout (build/dmg-background.{svg,png,@2x.png}, electron-builder.yml dmg: block) — replaces the default empty DMG window with a branded 640×480 backdrop: headline "把 Open CoDesign 拖到「应用程序」", an arrow pointing right, and a footer warning about the keychain consequence of double-clicking from the mount. Icon positions pinned so drag-to-install is the obvious path. 3. Smart download on the website (SmartDownload.vue + theme/index.ts registration, embedded in index.md and zh/index.md) — detects UA + platform, picks the right .dmg / .exe / .AppImage, and renders a prominent primary button with exact file name and size. All platforms still reachable via a collapsible "Other platforms" section. Includes an explicit install hint so users know to drag to /Applications before launching. Plus two UX polishes reported while reviewing the page: - Hero image swapped from og.svg (social card shape) to logo-hero.png (the transparent no-text logo variant) in both EN and zh index.md. - Hide VitePress's default "#" header-anchor on marketing-home section headings — the anchor is useful in docs, noise on landing. The `apps/desktop/build/` directory is now un-gitignored so the DMG background ships with the repo instead of being regenerated at every build. - apps/desktop/src/main/install-check.ts: boot-time DMG detection - apps/desktop/src/main/index.ts: wire install-check into whenReady - apps/desktop/electron-builder.yml: dmg window + icon layout - apps/desktop/build/dmg-background.{svg,png,@2x.png}: branded backdrop - website/.vitepress/theme/SmartDownload.vue: new component - website/.vitepress/theme/index.ts: register SmartDownload globally - website/.vitepress/theme/style.css: hide header-anchor in sections - website/index.md + zh/index.md: hero image, SmartDownload embed - website/public/logo-hero.png: transparent no-text logo for hero
1 parent da29e23 commit df7d18a

13 files changed

Lines changed: 447 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ node_modules/
66
dist/
77
dist-electron/
88
build/
9+
!apps/desktop/build/
910
out/
1011
release/
1112
*.tsbuildinfo
32.8 KB
Loading
Lines changed: 66 additions & 0 deletions
Loading
71.6 KB
Loading

apps/desktop/electron-builder.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ mac:
2525
target:
2626
- target: dmg
2727
arch: [arm64, x64]
28+
dmg:
29+
# Custom window layout guides users to drag the app into /Applications.
30+
# Running from the DMG mount leaves safeStorage unavailable (the volume
31+
# is read-only so macOS can't establish a keychain trust entry), which
32+
# breaks API key save / import. The background, arrow, and in-app boot
33+
# check (install-check.ts) work together to steer users to the right
34+
# flow.
35+
background: build/dmg-background.png
36+
iconSize: 92
37+
window:
38+
width: 640
39+
height: 480
40+
contents:
41+
- x: 140
42+
y: 240
43+
type: file
44+
- x: 500
45+
y: 240
46+
type: link
47+
path: /Applications
2848
win:
2949
icon: resources/icon.ico
3050
target:

apps/desktop/src/main/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { makeRuntimeVerifier } from './done-verify';
3131
import { BrowserWindow, app, dialog, ipcMain, shell } from './electron-runtime';
3232
import { registerExporterIpc } from './exporter-ipc';
3333
import { armGenerationTimeout, cancelGenerationRequest } from './generation-ipc';
34+
import { maybeAbortIfRunningFromDmg } from './install-check';
3435
import { registerLocaleIpc } from './locale-ipc';
3536
import { getLogPath, getLogger, initLogger } from './logger';
3637
import {
@@ -786,6 +787,11 @@ function setupAutoUpdater(): void {
786787

787788
void app.whenReady().then(async () => {
788789
initLogger();
790+
// Show a blocking dialog if the user launched from the DMG mount. If
791+
// they accept the remedy, we quit here before touching safeStorage / the
792+
// snapshots DB so nothing half-initialises against a bad install.
793+
const aborted = await maybeAbortIfRunningFromDmg();
794+
if (aborted) return;
789795
await loadConfigOnBoot();
790796
// Snapshot persistence is best-effort at boot — a failure here (corrupt DB,
791797
// permission denied, missing native binding) must NOT block the BrowserWindow
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { app, dialog, shell } from './electron-runtime';
2+
import { getLogger } from './logger';
3+
4+
/**
5+
* Installation sanity checks that run before anything else at boot.
6+
*
7+
* The main case we care about: on macOS, users frequently double-click the
8+
* app icon inside the mounted DMG instead of dragging it to /Applications
9+
* first. That launches the binary from `/Volumes/Open CoDesign/...` — a
10+
* read-only mount whose identity Electron can't use to establish a stable
11+
* keychain entry, so `safeStorage.isEncryptionAvailable()` returns false
12+
* and every "save API key" / "decrypt API key" path fails hard.
13+
*
14+
* Rather than let the user hit that wall minutes later with a cryptic
15+
* error, we detect the condition at startup and offer a one-click fix:
16+
* open the Applications folder, quit, and let them drag-install properly.
17+
*/
18+
19+
const log = getLogger('install-check');
20+
21+
function isRunningFromDmgMount(): boolean {
22+
if (process.platform !== 'darwin') return false;
23+
// Apps launched from a mounted DMG always have an exec path somewhere
24+
// under /Volumes/. Apps dragged to /Applications live under
25+
// /Applications/... and never match. Dev builds from pnpm run from
26+
// node_modules/electron/dist and also don't match.
27+
return process.execPath.startsWith('/Volumes/');
28+
}
29+
30+
/**
31+
* If the app is running from a DMG mount, show an explanatory dialog.
32+
* When the user clicks the primary action we open Finder at the
33+
* Applications folder and quit the app — they can then drag the app
34+
* across and relaunch from the correct location.
35+
*
36+
* Returns true when the app should stop booting (user chose to quit),
37+
* false to continue normally.
38+
*/
39+
export async function maybeAbortIfRunningFromDmg(): Promise<boolean> {
40+
if (!isRunningFromDmgMount()) return false;
41+
log.warn('boot.blocked', { reason: 'running_from_dmg', execPath: process.execPath });
42+
43+
const { response } = await dialog.showMessageBox({
44+
type: 'warning',
45+
title: 'Open CoDesign 还没安装完成',
46+
message: '请先把 Open CoDesign 拖到「应用程序」文件夹',
47+
detail: [
48+
'当前是从 DMG 直接运行的。这样 macOS 无法为 Open CoDesign 建立钥匙串条目,',
49+
'你的 API key 会无法加密保存,导入配置也会失败。',
50+
'',
51+
'正确步骤:',
52+
'1. 点下面的「打开「应用程序」文件夹」',
53+
'2. 把 Finder 里的 Open CoDesign.app 拖进去',
54+
'3. 从「应用程序」文件夹双击启动',
55+
'',
56+
'首次从「应用程序」启动时 macOS 会弹「来自网络的 app」确认框,点「打开」即可。',
57+
].join('\n'),
58+
buttons: ['打开「应用程序」文件夹并退出', '仍要从 DMG 运行(不推荐)'],
59+
defaultId: 0,
60+
cancelId: 1,
61+
});
62+
63+
if (response === 0) {
64+
try {
65+
await shell.openPath('/Applications');
66+
} catch (err) {
67+
log.warn('openPath.applications.fail', err);
68+
}
69+
app.quit();
70+
return true;
71+
}
72+
73+
log.warn('boot.continued_from_dmg_despite_warning');
74+
return false;
75+
}

0 commit comments

Comments
 (0)