feat: add optional Ghostty setup and package updates#41
Conversation
Zsh startup benchmarkUbuntu 24.04 with Linuxbrew, atuin, fnm, fzf, zoxide, zinit, and the shipped plugins. Lower is better. Warm startup (daily use)
Cache-miss startupCompletion and tool-init caches are removed before every run; installed tools and plugins remain.
|
There was a problem hiding this comment.
Code Review
This pull request replaces iTerm2 with Ghostty as the recommended terminal emulator, introduces modular Ghostty configuration presets, and adds backup and initialization logic for Ghostty configs. It also attempts to force non-interactive Homebrew installations using a --no-ask flag and changes the installation flow to always run the install command. Feedback highlights several critical issues: 1) Homebrew does not support the --no-ask flag, which will cause all brew install commands to fail. 2) Unconditionally running brew install on already installed packages/casks will result in errors; instead, the code should check if they are installed and use brew upgrade if necessary. 3) The Ghostty configuration setup may fail with ENOENT if the ~/.config directory does not exist, so it should be explicitly created using mkdirSync. 4) Test suites need to be updated to reflect the removal of the invalid --no-ask flag.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function brewInstall(name, { cask = false } = {}) { | ||
| const args = cask ? ["install", "--cask", name] : ["install", name]; | ||
| const args = cask ? ["install", "--no-ask", "--cask", name] : ["install", "--no-ask", name]; | ||
| try { | ||
| execSync(`brew ${args.join(" ")}`, { stdio: "inherit" }); | ||
| return true; |
There was a problem hiding this comment.
Homebrew 的 brew install 命令并没有 --no-ask 选项。传入未知的选项会导致 Homebrew 报错 Error: invalid option: --no-ask 并终止执行,从而导致所有工具和应用的安装失败。
另外,为了支持“始终执行安装或升级检查”的需求,建议在此处新增一个 brewUpgrade 函数,以便在软件已安装时调用 brew upgrade 进行更新。
export function brewInstall(name, { cask = false } = {}) {
const args = cask ? ["install", "--cask", name] : ["install", name];
try {
execSync("brew " + args.join(" "), { stdio: "inherit" });
return true;
} catch {
return false;
}
}
/**
* Upgrade a Homebrew formula or cask. Returns true on success.
*/
export function brewUpgrade(name, { cask = false } = {}) {
const args = cask ? ["upgrade", "--cask", name] : ["upgrade", name];
try {
execSync("brew " + args.join(" "), { stdio: "inherit" });
return true;
} catch {
return false;
}
}| case "${manager}" in | ||
| brew) | ||
| brew install "$@" | ||
| brew install --no-ask "$@" | ||
| ;; |
There was a problem hiding this comment.
| if (manager === "brew") { | ||
| await runStream("brew install zsh"); | ||
| await runStream("brew install --no-ask zsh"); | ||
| } else if (manager === "apt-get") { |
| export async function installApps(apps, { configureGhostty = true } = {}) { | ||
| for (const app of apps) { | ||
| if (brewInstalled(app)) { | ||
| p.log.success(`${app} is already installed`); | ||
| const s = p.spinner(); | ||
| s.start(`Installing or updating ${app}...`); | ||
| const ok = brewInstall(app, { cask: true }); | ||
| if (ok) { | ||
| s.stop(`${app} is ready`); | ||
| } else { | ||
| const s = p.spinner(); | ||
| s.start(`Installing ${app}...`); | ||
| const ok = brewInstall(app, { cask: true }); | ||
| if (ok) { | ||
| s.stop(`${app} installed`); | ||
| } else { | ||
| s.stop(`Failed to install ${app}`); | ||
| } | ||
| s.stop(`Failed to install ${app}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
直接无条件调用 brewInstall 会导致在应用已安装时报错(Homebrew 会因为已安装而返回非零状态码),从而错误地显示 Failed to install <app>。
建议先通过 brewInstalled 检查是否已安装:如果已安装则调用 brewUpgrade 进行升级,未安装则调用 brewInstall 进行安装。这样既能保证“始终执行安装或升级检查”,又不会误报失败。
| export async function installApps(apps, { configureGhostty = true } = {}) { | |
| for (const app of apps) { | |
| if (brewInstalled(app)) { | |
| p.log.success(`${app} is already installed`); | |
| const s = p.spinner(); | |
| s.start(`Installing or updating ${app}...`); | |
| const ok = brewInstall(app, { cask: true }); | |
| if (ok) { | |
| s.stop(`${app} is ready`); | |
| } else { | |
| const s = p.spinner(); | |
| s.start(`Installing ${app}...`); | |
| const ok = brewInstall(app, { cask: true }); | |
| if (ok) { | |
| s.stop(`${app} installed`); | |
| } else { | |
| s.stop(`Failed to install ${app}`); | |
| } | |
| s.stop(`Failed to install ${app}`); | |
| } | |
| } | |
| export async function installApps(apps, { configureGhostty = true } = {}) { | |
| for (const app of apps) { | |
| const s = p.spinner(); | |
| s.start(`Installing or updating ${app}...`); | |
| const installed = brewInstalled(app); | |
| const ok = installed ? brewUpgrade(app, { cask: true }) : brewInstall(app, { cask: true }); | |
| if (ok) { | |
| s.stop(`${app} is ready`); | |
| } else { | |
| s.stop(`Failed to install ${app}`); | |
| } | |
| } |
| export async function installCliTools(tools) { | ||
| for (const tool of tools) { | ||
| if (brewInstalled(tool)) { | ||
| p.log.success(`${tool} is already installed`); | ||
| const s = p.spinner(); | ||
| s.start(`Installing or updating ${tool}...`); | ||
| const ok = brewInstall(tool); | ||
| if (ok) { | ||
| s.stop(`${tool} is ready`); | ||
| } else { | ||
| const s = p.spinner(); | ||
| s.start(`Installing ${tool}...`); | ||
| const ok = brewInstall(tool); | ||
| if (ok) { | ||
| s.stop(`${tool} installed`); | ||
| } else { | ||
| s.stop(`Failed to install ${tool}`); | ||
| } | ||
| s.stop(`Failed to install ${tool}`); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
与 apps.js 类似,直接无条件调用 brewInstall 会导致在工具已安装时报错。建议先通过 brewInstalled 检查是否已安装,如果已安装则调用 brewUpgrade 进行升级,未安装则调用 brewInstall 进行安装。
| export async function installCliTools(tools) { | |
| for (const tool of tools) { | |
| if (brewInstalled(tool)) { | |
| p.log.success(`${tool} is already installed`); | |
| const s = p.spinner(); | |
| s.start(`Installing or updating ${tool}...`); | |
| const ok = brewInstall(tool); | |
| if (ok) { | |
| s.stop(`${tool} is ready`); | |
| } else { | |
| const s = p.spinner(); | |
| s.start(`Installing ${tool}...`); | |
| const ok = brewInstall(tool); | |
| if (ok) { | |
| s.stop(`${tool} installed`); | |
| } else { | |
| s.stop(`Failed to install ${tool}`); | |
| } | |
| s.stop(`Failed to install ${tool}`); | |
| } | |
| } | |
| } | |
| export async function installCliTools(tools) { | |
| for (const tool of tools) { | |
| const s = p.spinner(); | |
| s.start(`Installing or updating ${tool}...`); | |
| const installed = brewInstalled(tool); | |
| const ok = installed ? brewUpgrade(tool) : brewInstall(tool); | |
| if (ok) { | |
| s.stop(`${tool} is ready`); | |
| } else { | |
| s.stop(`Failed to install ${tool}`); | |
| } | |
| } | |
| } |
| import { cpSync, existsSync, rmSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { brewInstall } from "../utils/shell.js"; |
| const destination = join(home, ".config", "ghostty"); | ||
| if (existsSync(destination)) { | ||
| const backup = `${destination}.backup-${new Date().toISOString().replace(/[:.]/g, "-")}`; | ||
| cpSync(destination, backup, { recursive: true }); | ||
| p.log.info(`Existing Ghostty config backed up to ${backup.replace(home, "~")}`); | ||
| rmSync(destination, { recursive: true, force: true }); | ||
| } | ||
|
|
||
| cpSync(join(CONFIGS_DIR, "ghostty"), destination, { recursive: true }); | ||
| p.log.success("Ghostty config initialized at ~/.config/ghostty/"); |
There was a problem hiding this comment.
如果用户的 ~/.config 目录在运行此脚本之前不存在(例如在全新的系统或新用户账户中),cpSync(..., destination, { recursive: true }) 会因为找不到父目录而抛出 ENOENT 错误。
建议在复制之前,先使用 mkdirSync 确保 ~/.config 目录存在。同时,请记得在文件顶部从 node:fs 导入 mkdirSync。
| const destination = join(home, ".config", "ghostty"); | |
| if (existsSync(destination)) { | |
| const backup = `${destination}.backup-${new Date().toISOString().replace(/[:.]/g, "-")}`; | |
| cpSync(destination, backup, { recursive: true }); | |
| p.log.info(`Existing Ghostty config backed up to ${backup.replace(home, "~")}`); | |
| rmSync(destination, { recursive: true, force: true }); | |
| } | |
| cpSync(join(CONFIGS_DIR, "ghostty"), destination, { recursive: true }); | |
| p.log.success("Ghostty config initialized at ~/.config/ghostty/"); | |
| const destination = join(home, ".config", "ghostty"); | |
| if (existsSync(destination)) { | |
| const backup = `${destination}.backup-${new Date().toISOString().replace(/[:.]/g, "-")}`; | |
| cpSync(destination, backup, { recursive: true }); | |
| p.log.info(`Existing Ghostty config backed up to ${backup.replace(home, "~")}`); | |
| rmSync(destination, { recursive: true, force: true }); | |
| } | |
| mkdirSync(join(home, ".config"), { recursive: true }); | |
| cpSync(join(CONFIGS_DIR, "ghostty"), destination, { recursive: true }); |
| @@ -1,5 +1,5 @@ | |||
| import * as p from "@clack/prompts"; | |||
| import { brewInstalled, brewInstall } from "../utils/shell.js"; | |||
| import { brewInstall } from "../utils/shell.js"; | |||
| test("installs Homebrew packages without prompting", () => { | ||
| expect(brewInstall("jq")).toBe(true); | ||
| expect(execSync).toHaveBeenCalledWith("brew install --no-ask jq", { stdio: "inherit" }); | ||
|
|
||
| expect(brewInstall("ghostty", { cask: true })).toBe(true); | ||
| expect(execSync).toHaveBeenCalledWith("brew install --no-ask --cask ghostty", { stdio: "inherit" }); | ||
| }); |
There was a problem hiding this comment.
由于 brew install 并不支持 --no-ask 选项,在修复了 src/utils/shell.js 中的该问题后,此处的单元测试断言也需要同步更新,移除对 --no-ask 的匹配,否则测试将会失败。
| test("installs Homebrew packages without prompting", () => { | |
| expect(brewInstall("jq")).toBe(true); | |
| expect(execSync).toHaveBeenCalledWith("brew install --no-ask jq", { stdio: "inherit" }); | |
| expect(brewInstall("ghostty", { cask: true })).toBe(true); | |
| expect(execSync).toHaveBeenCalledWith("brew install --no-ask --cask ghostty", { stdio: "inherit" }); | |
| }); | |
| test("installs Homebrew packages without prompting", () => { | |
| expect(brewInstall("jq")).toBe(true); | |
| expect(execSync).toHaveBeenCalledWith("brew install jq", { stdio: "inherit" }); | |
| expect(brewInstall("ghostty", { cask: true })).toBe(true); | |
| expect(execSync).toHaveBeenCalledWith("brew install --cask ghostty", { stdio: "inherit" }); | |
| }); |
| await bootstrap({ platform: "darwin" }); | ||
|
|
||
| expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install zsh")); | ||
| expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install --no-ask zsh")); | ||
| }); |
There was a problem hiding this comment.
在移除 src/steps/bootstrap.js 中的 --no-ask 选项后,此处的测试断言也需要同步更新,移除对 --no-ask 的匹配。
| await bootstrap({ platform: "darwin" }); | |
| expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install zsh")); | |
| expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install --no-ask zsh")); | |
| }); | |
| await bootstrap({ platform: "darwin" }); | |
| expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install zsh")); | |
| }); |
改动内容
--no-ask非交互模式ss对新版 fzf walker 的目录排除能力验证
npm test(228 tests passed)git diff --check origin/main...HEAD