From 3bc81a8ec329978fc0b03a1cec5d4292f6bcf63f Mon Sep 17 00:00:00 2001 From: ChangeHow Date: Sun, 12 Jul 2026 21:40:47 +0800 Subject: [PATCH 1/3] feat(frontend): add Bun setup support --- README.md | 3 ++- README.zh-CN.md | 3 ++- configs/core/paths.zsh | 8 ++++++++ src/setup.js | 3 ++- src/steps/frontend.js | 16 ++++++++++++++++ src/verify.js | 1 + tests/configs.test.js | 6 ++++++ tests/frontend.test.js | 10 ++++++++++ tests/setup.test.js | 3 ++- 9 files changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c5935d2..6c776e1 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Interactive step-by-step setup with selectable steps: 4. **Prompt Preset** — Powerlevel10k (recommended) or a basic zsh prompt 5. **CLI Tools** — bat, eza, fzf, fd, zoxide, atuin, ripgrep... 6. **GUI Apps** — optional Ghostty, Raycast, VS Code, fonts... -7. **Frontend Tools** — choose fnm, Node.js via fnm, pnpm, git-cz +7. **Frontend Tools** — choose fnm, Node.js via fnm, pnpm, Bun, git-cz 8. **Shell Aliases** — git, eza, fzf shortcuts 9. **SSH Key** — generate GitHub SSH key 10. **Vim Config** — basic vim setup @@ -141,6 +141,7 @@ Bootstrap details: - Suitup now also writes a minimal `~/.zshenv` so non-interactive shells can still load shared env vars and PATH setup - When fnm installs Node.js, suitup keeps both the `fnm` binary and the installed default Node version on PATH so `fnm`, `node`, `npm`, and globally installed CLIs resolve correctly in both interactive and non-interactive shells - Frontend tool installs use a user-local npm prefix under `~/.local/` so `pnpm` and `git-cz` do not require sudo or writes to system npm directories +- Suitup installs Bun with its official installer and exposes `~/.bun/bin` through the managed PATH configuration ### Append diff --git a/README.zh-CN.md b/README.zh-CN.md index 29d5720..ae72455 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -114,7 +114,7 @@ node src/cli.js 4. **Prompt Preset** — Powerlevel10k(推荐)或基础 zsh prompt 5. **CLI Tools** — bat、eza、fzf、fd、zoxide、atuin、ripgrep 等 6. **GUI Apps** — 可选的 Ghostty、Raycast、VS Code、字体等 -7. **Frontend Tools** — 可选 fnm、通过 fnm 安装的 Node.js、pnpm、git-cz +7. **Frontend Tools** — 可选 fnm、通过 fnm 安装的 Node.js、pnpm、Bun、git-cz 8. **Shell Aliases** — git、eza、fzf 等快捷命令 9. **SSH Key** — 生成 GitHub SSH 密钥 10. **Vim Config** — 基础 Vim 配置 @@ -141,6 +141,7 @@ Bootstrap 细节: - suitup 也会生成一个精简的 `~/.zshenv`,保证非交互式 shell 也能加载共享环境变量和 PATH - 当 fnm 安装 Node.js 后,suitup 会把 `fnm` 自身和该默认 Node 版本一起放进 PATH,确保交互式/非交互式 shell 下的 `fnm`、`node`、`npm` 和全局 CLI 都能正确解析 - Frontend 工具安装会使用 `~/.local/` 下的用户级 npm prefix,这样安装 `pnpm` 和 `git-cz` 不需要 sudo,也不会写入系统级 npm 目录 +- suitup 会通过 Bun 官方安装器完成安装,并由托管的 PATH 配置加载 `~/.bun/bin` ### Append(追加) diff --git a/configs/core/paths.zsh b/configs/core/paths.zsh index 499429b..b4e358f 100644 --- a/configs/core/paths.zsh +++ b/configs/core/paths.zsh @@ -37,4 +37,12 @@ if [[ -d "$_suitup_fnm_default_bin" && ":${PATH}:" != *":${_suitup_fnm_default_b fi unset _suitup_fnm_dir _suitup_fnm_default_bin +# Bun's official installer uses ~/.bun by default. Keep its runtime available +# to interactive and non-interactive shells without relying on installer-added +# startup snippets. +export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}" +if [[ -d "$BUN_INSTALL/bin" && ":${PATH}:" != *":${BUN_INSTALL}/bin:"* ]]; then + export PATH="${BUN_INSTALL}/bin:${PATH}" +fi + # Keep this file for user PATH overrides if needed. diff --git a/src/setup.js b/src/setup.js index fbe86b7..7701c9b 100644 --- a/src/setup.js +++ b/src/setup.js @@ -157,6 +157,7 @@ export function detectCompletedSteps({ commandExistsFn("fnm") && commandExistsFn("node") && commandExistsFn("pnpm") && + commandExistsFn("bun") && commandExistsFn("git-cz") ) { completed.add("frontend"); @@ -261,7 +262,7 @@ export async function runSetup({ defaults = false } = {}) { { value: "plugins", label: "Plugin Manager", hint: "recommended zinit or skip" }, { value: "cli-tools", label: "CLI Tools", hint: "bat, eza, fzf, fd, zoxide, atuin..." }, { value: "apps", label: "GUI Apps", hint: "Ghostty, Raycast, VS Code..." }, - { value: "frontend", label: "Frontend Tools", hint: "fnm, Node.js, pnpm, git-cz" }, + { value: "frontend", label: "Frontend Tools", hint: "fnm, Node.js, pnpm, Bun, git-cz" }, { value: "aliases", label: "Shell Aliases", hint: "git, eza, fzf shortcuts" }, { value: "ssh", label: "SSH Key", hint: "generate GitHub SSH key" }, { value: "vim", label: "Vim Config", hint: "basic vim setup" }, diff --git a/src/steps/frontend.js b/src/steps/frontend.js index b77e905..d0f7170 100644 --- a/src/steps/frontend.js +++ b/src/steps/frontend.js @@ -13,6 +13,7 @@ export const FRONTEND_TOOLS = { ], packageManagers: [ { value: "pnpm", label: "pnpm", hint: "fast, disk-efficient package manager" }, + { value: "bun", label: "Bun", hint: "JavaScript runtime and package manager" }, ], git: [ { value: "git-cz", label: "git-cz", hint: "Conventional Commits CLI" }, @@ -162,6 +163,21 @@ export async function installFrontendTools(selectedTools = getAllFrontendToolVal } } + // Bun + if (!wanted.has("bun")) { + // skip + } else if (commandExists("bun")) { + p.log.success("Bun is already installed"); + } else { + p.log.step("Installing Bun..."); + try { + await runStream("curl -fsSL https://bun.sh/install | bash"); + p.log.success("Bun installed"); + } catch { + p.log.warn("Could not install Bun automatically — try rerunning after checking your network connection"); + } + } + // git-cz if (!wanted.has("git-cz")) { // skip diff --git a/src/verify.js b/src/verify.js index 47b6e6a..fdcc003 100644 --- a/src/verify.js +++ b/src/verify.js @@ -44,6 +44,7 @@ const CHECKS = { { cmd: "fnm", label: "fnm" }, { cmd: "node", label: "Node.js" }, { cmd: "pnpm", label: "pnpm" }, + { cmd: "bun", label: "Bun" }, ], }; diff --git a/tests/configs.test.js b/tests/configs.test.js index 8bd6fa4..24e7287 100644 --- a/tests/configs.test.js +++ b/tests/configs.test.js @@ -193,6 +193,12 @@ describe("Static config templates", () => { expect(content).toContain(":${PATH}:"); }); + test("core/paths.zsh keeps Bun on PATH for non-interactive shells", () => { + const content = readFileSync(join(CONFIGS_DIR, "core", "paths.zsh"), "utf-8"); + expect(content).toContain('export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"'); + expect(content).toContain('${BUN_INSTALL}/bin:${PATH}'); + }); + test("core/env.zsh does not contain API keys", () => { const content = readFileSync(join(CONFIGS_DIR, "core", "env.zsh"), "utf-8"); for (const pattern of FORBIDDEN_PATTERNS) { diff --git a/tests/frontend.test.js b/tests/frontend.test.js index f436428..4340266 100644 --- a/tests/frontend.test.js +++ b/tests/frontend.test.js @@ -54,6 +54,7 @@ describe("frontend step", () => { // Should NOT contain curl fnm install or npm install -g expect(calls.some((c) => c.includes("fnm.vercel.app"))).toBe(false); expect(calls.some((c) => c.includes("npm install -g pnpm"))).toBe(false); + expect(calls.some((c) => c.includes("bun.sh/install"))).toBe(false); expect(calls.some((c) => c.includes("npm install -g git-cz"))).toBe(false); }); @@ -129,6 +130,14 @@ describe("frontend step", () => { ); }); + test("installs Bun when not present", async () => { + commandExists.mockImplementation((name) => name !== "bun"); + + await installFrontendTools(["bun"], { home: sandbox }); + + expect(runStream).toHaveBeenCalledWith("curl -fsSL https://bun.sh/install | bash"); + }); + test("installs git-cz when not present", async () => { commandExists.mockImplementation((name) => { if (name === "git-cz") return false; @@ -160,6 +169,7 @@ describe("frontend step", () => { expect(calls.some((cmd) => cmd.includes("fnm install"))).toBe(false); expect(calls).toContain("npm install -g git-cz"); expect(calls.some((cmd) => cmd.includes("npm install -g pnpm"))).toBe(false); + expect(calls.some((cmd) => cmd.includes("bun.sh/install"))).toBe(false); }); test("removes legacy bootstrap node shims when fnm default node exists", async () => { diff --git a/tests/setup.test.js b/tests/setup.test.js index 43e1b70..bd4f971 100644 --- a/tests/setup.test.js +++ b/tests/setup.test.js @@ -186,6 +186,7 @@ describe("Setup simulation in sandbox", () => { "fnm", "node", "pnpm", + "bun", "git-cz", ]); }); @@ -270,7 +271,7 @@ describe("Setup simulation in sandbox", () => { home: sandbox, platform: "darwin", commandExistsFn(name) { - return ["zsh", "brew", "fnm", "node", "pnpm", "git-cz"].includes(name); + return ["zsh", "brew", "fnm", "node", "pnpm", "bun", "git-cz"].includes(name); }, }); From 1b233573738b54f57b7e38ca68036dd163e21983 Mon Sep 17 00:00:00 2001 From: ChangeHow Date: Sun, 12 Jul 2026 21:40:52 +0800 Subject: [PATCH 2/3] fix(fzf): defer preview path expansion --- configs/shared/tools/fzf.zsh | 12 ++++++------ tests/configs.test.js | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/configs/shared/tools/fzf.zsh b/configs/shared/tools/fzf.zsh index a924fe7..205ab33 100644 --- a/configs/shared/tools/fzf.zsh +++ b/configs/shared/tools/fzf.zsh @@ -34,13 +34,13 @@ export FZF_CTRL_T_OPTS=" --height 100% --header '[C-/] toggle preview | [Alt-j/k] scroll preview' --preview 'target={}; - if [[ \"$target\" != /* ]]; then - target=\"${FZF_CTRL_T_PREVIEW_ROOT:-$PWD}/$target\"; + if [[ \"\$target\" != /* ]]; then + target=\"\${FZF_CTRL_T_PREVIEW_ROOT:-\$PWD}/\$target\"; fi; - if [ -f \"$target\" ]; then - bat --color=always --style=plain --line-range :300 \"$target\"; - elif [ -d \"$target\" ]; then - eza -L 2 -T --git-ignore \"$target\" 2>/dev/null | head -20; + if [ -f \"\$target\" ]; then + bat --color=always --style=plain --line-range :300 \"\$target\"; + elif [ -d \"\$target\" ]; then + eza -L 2 -T --git-ignore \"\$target\" 2>/dev/null | head -20; fi' --preview-window=right:50%:wrap --bind 'ctrl-/:toggle-preview' diff --git a/tests/configs.test.js b/tests/configs.test.js index 24e7287..2fed679 100644 --- a/tests/configs.test.js +++ b/tests/configs.test.js @@ -219,6 +219,24 @@ describe("Static config templates", () => { expect(fzfContent).not.toContain("fzf --zsh"); }); + test("shared/tools/fzf.zsh defers Ctrl-T preview paths until fzf runs", () => { + const fzfOpts = execFileSync( + "zsh", + ["-f", "-c", 'source "$FZF_CONFIG"; print -r -- "$FZF_CTRL_T_OPTS"'], + { + encoding: "utf-8", + env: { + ...process.env, + FZF_CONFIG: join(CONFIGS_DIR, "shared", "tools", "fzf.zsh"), + }, + } + ); + + expect(fzfOpts).toContain('if [[ "$target" != /* ]]'); + expect(fzfOpts).toContain('target="${FZF_CTRL_T_PREVIEW_ROOT:-$PWD}/$target"'); + expect(fzfOpts).toContain('bat --color=always --style=plain --line-range :300 "$target"'); + }); + test("shared/tools.zsh is a thin orchestrator that loads tool configs", () => { const content = readFileSync(join(CONFIGS_DIR, "shared", "tools.zsh"), "utf-8"); expect(content).toContain("_load_tool_config"); From a4894e71c60967da4b6e34851982d7af692bec03 Mon Sep 17 00:00:00 2001 From: ChangeHow Date: Sun, 12 Jul 2026 21:49:09 +0800 Subject: [PATCH 3/3] fix(setup): keep Bun optional in completion detection --- src/setup.js | 1 - tests/setup.test.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/setup.js b/src/setup.js index 7701c9b..4243ebf 100644 --- a/src/setup.js +++ b/src/setup.js @@ -157,7 +157,6 @@ export function detectCompletedSteps({ commandExistsFn("fnm") && commandExistsFn("node") && commandExistsFn("pnpm") && - commandExistsFn("bun") && commandExistsFn("git-cz") ) { completed.add("frontend"); diff --git a/tests/setup.test.js b/tests/setup.test.js index bd4f971..a29865c 100644 --- a/tests/setup.test.js +++ b/tests/setup.test.js @@ -244,7 +244,7 @@ describe("Setup simulation in sandbox", () => { expect(message).toContain("Suit up!"); }); - test("detects completed suitup-managed setup steps", () => { + test("detects completed suitup-managed setup steps without optional Bun", () => { mkdirSync(join(sandbox, ".config", "zsh", "core"), { recursive: true }); mkdirSync(join(sandbox, ".config", "zsh", "shared"), { recursive: true }); mkdirSync(join(sandbox, ".config", "zsh", "local"), { recursive: true }); @@ -271,7 +271,7 @@ describe("Setup simulation in sandbox", () => { home: sandbox, platform: "darwin", commandExistsFn(name) { - return ["zsh", "brew", "fnm", "node", "pnpm", "bun", "git-cz"].includes(name); + return ["zsh", "brew", "fnm", "node", "pnpm", "git-cz"].includes(name); }, });