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:
1 parent
7fa10cd954
commit
d13a0396f9
4 files changed
+85
-16
No files matched your search
+13
-12
@@ -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,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 "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
</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')}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '全部',
|
||||
|
||||
Reference in new issue
Block a user