fix: replace tarball fallback with Homebrew install guide for macOS without brew

- electron/main.ts: check-prerequisites now returns hasHomebrew + platform
  so the frontend can distinguish macOS-without-Homebrew from other scenarios
- electron/main.ts: remove untested tarball fallback (hardcoded Node version,
  custom ~/.codepilot/node/bin PATH), replace with clean Homebrew-only path
  that returns a clear error message when Homebrew is absent
- electron/main.ts: remove ~/.codepilot/node/bin from getExpandedShellPath()
  since the tarball install target no longer exists
- InstallWizard.tsx: when Node.js is missing AND Homebrew is absent on macOS,
  show a guided UI with the Homebrew install command, copy button, and
  step-by-step instructions instead of a broken "Install" button
- InstallWizard.tsx: footer shows "Recheck" button (re-runs checkPrereqs)
  instead of "Install" in the no-Homebrew case
- en.ts/zh.ts: add 8 new i18n keys for the Homebrew guidance UI

Root cause: the previous tarball fallback was untested, pinned to a specific
Node version (v22.12.0), and required maintaining a custom PATH entry. Guiding
users to install Homebrew first is more reliable and maintainable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-27 15:30:32 +08:00
1 parent 7fa10cd954
commit d13a0396f9
4 files changed
+85 -16

No files matched your search

+13 -12
View File
@@ -199,7 +199,7 @@ function getExpandedShellPath(): string {
return [...new Set(allParts)].join(sep);
} else {
const basePath = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin`;
const raw = `${basePath}:${home}/.npm-global/bin:${home}/.local/bin:${home}/.claude/bin:${home}/.codepilot/node/bin:${shellPath}`;
const raw = `${basePath}:${home}/.npm-global/bin:${home}/.local/bin:${home}/.claude/bin:${shellPath}`;
const allParts = raw.split(':').filter(Boolean);
return [...new Set(allParts)].join(':');
}
@@ -459,7 +459,14 @@ app.whenReady().then(async () => {
// claude not found
}
return { hasNode, nodeVersion, hasClaude, claudeVersion };
// Check Homebrew on macOS
let hasHomebrew = false;
if (process.platform === 'darwin') {
const brewPaths = ['/opt/homebrew/bin/brew', '/usr/local/bin/brew'];
hasHomebrew = brewPaths.some(p => fs.existsSync(p));
}
return { hasNode, nodeVersion, hasClaude, claudeVersion, hasHomebrew, platform: process.platform };
});
ipcMain.handle('install:start', (_event: Electron.IpcMainInvokeEvent, options?: { includeNode?: boolean }) => {
@@ -528,7 +535,7 @@ app.whenReady().then(async () => {
let args: string[];
if (isMac) {
// Try Homebrew first, fallback to official tarball download
// macOS: Homebrew only — UI should guide the user to install Homebrew first
const brewPaths = ['/opt/homebrew/bin/brew', '/usr/local/bin/brew'];
const brewPath = brewPaths.find(p => fs.existsSync(p));
if (brewPath) {
@@ -536,15 +543,9 @@ app.whenReady().then(async () => {
args = ['install', 'node'];
addLog(`Running: ${brewPath} install node`);
} else {
// Fallback: download official Node.js binary tarball (no Homebrew/sudo needed)
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
const nodeVersion = 'v22.12.0';
const nodeDir = path.join(home, '.codepilot', 'node');
const tarballUrl = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-darwin-${arch}.tar.gz`;
addLog(`Homebrew not found. Downloading Node.js ${nodeVersion} from nodejs.org...`);
addLog(`Architecture: ${arch}, Target: ${nodeDir}`);
cmd = 'sh';
args = ['-c', `mkdir -p "${nodeDir}" && curl -fsSL "${tarballUrl}" | tar xz --strip-components=1 -C "${nodeDir}"`];
addLog('Homebrew is required. Please install Homebrew first and retry.');
resolve(false);
return;
}
} else if (isWin) {
cmd = 'winget';
+54 -4
View File
@@ -54,6 +54,8 @@ interface PrereqResult {
nodeVersion?: string;
hasClaude: boolean;
claudeVersion?: string;
hasHomebrew?: boolean;
platform?: string;
}
function getInstallAPI() {
@@ -88,6 +90,7 @@ export function InstallWizard({
const [progress, setProgress] = useState<InstallProgress | null>(null);
const [logs, setLogs] = useState<string[]>([]);
const [copied, setCopied] = useState(false);
const [copiedBrew, setCopiedBrew] = useState(false);
const [prereqs, setPrereqs] = useState<PrereqResult | null>(null);
const logEndRef = useRef<HTMLDivElement>(null);
const cleanupRef = useRef<(() => void) | null>(null);
@@ -223,6 +226,7 @@ export function InstallWizard({
setLogs([]); // eslint-disable-line react-hooks/set-state-in-effect
setProgress(null); // eslint-disable-line react-hooks/set-state-in-effect
setCopied(false); // eslint-disable-line react-hooks/set-state-in-effect
setCopiedBrew(false); // eslint-disable-line react-hooks/set-state-in-effect
setPrereqs(null); // eslint-disable-line react-hooks/set-state-in-effect
checkPrereqs();
}
@@ -288,12 +292,53 @@ export function InstallWizard({
)}
{/* Phase: confirm — ask user before installing */}
{phase === "confirm" && (
{phase === "confirm" && prereqs && !prereqs.hasNode && !prereqs.hasHomebrew && prereqs.platform === "darwin" && (
<div className="space-y-3">
<div className="rounded-lg bg-amber-500/10 px-4 py-3 text-sm space-y-1.5">
<p className="text-amber-700 dark:text-amber-400 font-medium">
{t('install.homebrewRequired')}
</p>
<p className="text-muted-foreground text-xs">
{t('install.homebrewDescription')}
</p>
</div>
<div className="rounded-md bg-zinc-950 dark:bg-zinc-900 border border-zinc-800 px-3 py-2.5 flex items-center gap-2">
<code className="flex-1 text-xs text-zinc-300 break-all select-all">
/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;
</code>
<Button
variant="ghost"
size="sm"
className="shrink-0 h-7 px-2"
onClick={async () => {
try {
await navigator.clipboard.writeText('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
setCopiedBrew(true);
setTimeout(() => setCopiedBrew(false), 2000);
} catch { /* clipboard not available */ }
}}
>
<HugeiconsIcon icon={Copy01Icon} className="size-3.5" />
<span className="text-xs">{copiedBrew ? t('install.copied') : t('install.copy')}</span>
</Button>
</div>
<div className="text-sm text-muted-foreground space-y-1">
<p>{t('install.homebrewSteps')}</p>
<ol className="list-decimal list-inside space-y-0.5 text-xs">
<li>{t('install.homebrewStep1')}</li>
<li>{t('install.homebrewStep2')}</li>
<li>{t('install.homebrewStep3')}</li>
<li>{t('install.homebrewStep4')}</li>
</ol>
</div>
</div>
)}
{phase === "confirm" && !(prereqs && !prereqs.hasNode && !prereqs.hasHomebrew && prereqs.platform === "darwin") && (
<div className="space-y-3">
<div className="rounded-lg bg-amber-500/10 px-4 py-3 text-sm space-y-1.5">
{prereqs && !prereqs.hasNode && (
<p className="text-amber-700 dark:text-amber-400">
Node.js — not found (will be installed via {process.platform === "win32" ? "winget" : "Homebrew"})
Node.js — not found (will be installed via {prereqs.platform === "win32" ? "winget" : "Homebrew"})
</p>
)}
{prereqs?.hasNode && (
@@ -369,8 +414,13 @@ export function InstallWizard({
</Button>
)}
{/* Confirm phase: single "Install" button */}
{phase === "confirm" && (
{/* Confirm phase: "Recheck" when Homebrew missing on macOS, otherwise "Install" */}
{phase === "confirm" && prereqs && !prereqs.hasNode && !prereqs.hasHomebrew && prereqs.platform === "darwin" && (
<Button size="sm" onClick={checkPrereqs}>
{t('install.recheck')}
</Button>
)}
{phase === "confirm" && !(prereqs && !prereqs.hasNode && !prereqs.hasHomebrew && prereqs.platform === "darwin") && (
<Button size="sm" onClick={handleConfirmInstall}>
<HugeiconsIcon icon={Download04Icon} />
{t('install.install')}
+9
View File
@@ -267,6 +267,15 @@ const en = {
'install.cancel': 'Cancel',
'install.retry': 'Retry',
'install.done': 'Done',
'install.recheck': 'Recheck',
'install.copy': 'Copy',
'install.homebrewRequired': 'Homebrew is required',
'install.homebrewDescription': 'Homebrew is the package manager for macOS. Node.js installation requires it.',
'install.homebrewSteps': 'Follow these steps:',
'install.homebrewStep1': 'Open Terminal',
'install.homebrewStep2': 'Paste the command above and press Enter',
'install.homebrewStep3': 'Follow the on-screen instructions to complete the installation',
'install.homebrewStep4': 'Come back here and click "Recheck"',
// ── Task list ───────────────────────────────────────────────
'tasks.all': 'All',
+9
View File
@@ -264,6 +264,15 @@ const zh: Record<TranslationKey, string> = {
'install.cancel': '取消',
'install.retry': '重试',
'install.done': '完成',
'install.recheck': '重新检测',
'install.copy': '复制',
'install.homebrewRequired': '需要先安装 Homebrew',
'install.homebrewDescription': 'Homebrew 是 macOS 的包管理器,安装 Node.js 需要它。',
'install.homebrewSteps': '请按以下步骤操作:',
'install.homebrewStep1': '打开终端(Terminal)',
'install.homebrewStep2': '粘贴上面的命令并按回车',
'install.homebrewStep3': '按提示完成安装',
'install.homebrewStep4': '回到这里点击"重新检测"',
// ── Task list ───────────────────────────────────────────────
'tasks.all': '全部',