Skip to content

feat: add optional Ghostty setup and package updates#41

Merged
ChangeHow merged 2 commits into
mainfrom
feat/ghostty-app-setup
Jul 11, 2026
Merged

feat: add optional Ghostty setup and package updates#41
ChangeHow merged 2 commits into
mainfrom
feat/ghostty-app-setup

Conversation

@ChangeHow

Copy link
Copy Markdown
Owner

改动内容

  • 将可选终端从 iTerm2 替换为 Ghostty,并允许 GUI Apps 空选跳过
  • 提供脱敏的模块化 Ghostty preset;交互式 setup 会询问是否初始化,并在覆盖前备份已有配置
  • 让用户选中的 Homebrew formula/cask 始终执行安装或升级检查
  • 为所有 Homebrew 安装入口启用 --no-ask 非交互模式
  • 保留 ss 对新版 fzf walker 的目录排除能力

验证

  • npm test(228 tests passed)
  • git diff --check origin/main...HEAD

@github-actions

Copy link
Copy Markdown

Zsh startup benchmark

Ubuntu 24.04 with Linuxbrew, atuin, fnm, fzf, zoxide, zinit, and the shipped plugins. Lower is better.

Warm startup (daily use)

Command Mean [ms] Min [ms] Max [ms] Relative
current (119878b) 71.6 ± 3.2 69.7 85.2 1.02 ± 0.05
main (d343526) 70.8 ± 0.7 69.5 72.8 1.00 ± 0.01
previous commit (4fa88e9) 70.5 ± 0.6 69.5 71.7 1.00

Cache-miss startup

Completion and tool-init caches are removed before every run; installed tools and plugins remain.

Command Mean [ms] Min [ms] Max [ms] Relative
current (119878b) 478.0 ± 1.5 475.8 480.3 1.00
main (d343526) 480.9 ± 2.5 476.4 484.6 1.01 ± 0.01
previous commit (4fa88e9) 479.0 ± 2.1 476.4 483.1 1.00 ± 0.01

@ChangeHow
ChangeHow merged commit 8165d07 into main Jul 11, 2026
2 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/utils/shell.js
Comment on lines 59 to 63
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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;
  }
}

Comment thread install.sh
Comment on lines 84 to 87
case "${manager}" in
brew)
brew install "$@"
brew install --no-ask "$@"
;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

请移除 brew install 中的 --no-ask 选项,因为 Homebrew 并不支持该参数,会导致安装失败。如果需要非交互模式,Homebrew 官方推荐使用环境变量 NONINTERACTIVE=1(例如 NONINTERACTIVE=1 brew install ...)。

Suggested change
case "${manager}" in
brew)
brew install "$@"
brew install --no-ask "$@"
;;
brew)
brew install "$@"
;;

Comment thread src/steps/bootstrap.js
Comment on lines 137 to 139
if (manager === "brew") {
await runStream("brew install zsh");
await runStream("brew install --no-ask zsh");
} else if (manager === "apt-get") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

请移除 brew install 中的 --no-ask 选项,因为 Homebrew 并不支持该参数,在真实环境中会导致安装 Zsh 失败。

  if (manager === "brew") {
    await runStream("brew install zsh");
  }

Comment thread src/steps/apps.js
Comment on lines +54 to 64
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}`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

直接无条件调用 brewInstall 会导致在应用已安装时报错(Homebrew 会因为已安装而返回非零状态码),从而错误地显示 Failed to install <app>

建议先通过 brewInstalled 检查是否已安装:如果已安装则调用 brewUpgrade 进行升级,未安装则调用 brewInstall 进行安装。这样既能保证“始终执行安装或升级检查”,又不会误报失败。

Suggested change
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}`);
}
}

Comment thread src/steps/cli-tools.js
Comment on lines 28 to 39
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}`);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

apps.js 类似,直接无条件调用 brewInstall 会导致在工具已安装时报错。建议先通过 brewInstalled 检查是否已安装,如果已安装则调用 brewUpgrade 进行升级,未安装则调用 brewInstall 进行安装。

Suggested change
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}`);
}
}
}

Comment thread src/steps/apps.js
import { cpSync, existsSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { brewInstall } from "../utils/shell.js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了支持在已安装时进行升级检查,我们需要导入 brewInstalledbrewUpgrade

Suggested change
import { brewInstall } from "../utils/shell.js";
import { brewInstall, brewInstalled, brewUpgrade } from "../utils/shell.js";

Comment thread src/steps/apps.js
Comment on lines +36 to +45
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/");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果用户的 ~/.config 目录在运行此脚本之前不存在(例如在全新的系统或新用户账户中),cpSync(..., destination, { recursive: true }) 会因为找不到父目录而抛出 ENOENT 错误。

建议在复制之前,先使用 mkdirSync 确保 ~/.config 目录存在。同时,请记得在文件顶部从 node:fs 导入 mkdirSync

Suggested change
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 });

Comment thread src/steps/cli-tools.js
@@ -1,5 +1,5 @@
import * as p from "@clack/prompts";
import { brewInstalled, brewInstall } from "../utils/shell.js";
import { brewInstall } from "../utils/shell.js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

导入 brewInstalledbrewUpgrade 以支持在工具已安装时进行升级检查。

Suggested change
import { brewInstall } from "../utils/shell.js";
import { brewInstall, brewInstalled, brewUpgrade } from "../utils/shell.js";

Comment thread tests/shell.test.js
Comment on lines +48 to +54
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" });
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

由于 brew install 并不支持 --no-ask 选项,在修复了 src/utils/shell.js 中的该问题后,此处的单元测试断言也需要同步更新,移除对 --no-ask 的匹配,否则测试将会失败。

Suggested change
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" });
});

Comment thread tests/bootstrap.test.js
Comment on lines 85 to 88
await bootstrap({ platform: "darwin" });

expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install zsh"));
expect(runStream).toHaveBeenCalledWith(expect.stringContaining("brew install --no-ask zsh"));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在移除 src/steps/bootstrap.js 中的 --no-ask 选项后,此处的测试断言也需要同步更新,移除对 --no-ask 的匹配。

Suggested change
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"));
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant