diff --git a/README.md b/README.md index 979295c..7e15fb3 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Suitup can bootstrap Zsh and Homebrew for you, but the most reliable path is to - Recommended: install Zsh first, switch into a Zsh session, then run suitup - Recommended: install Homebrew first so later package/tool steps run in a known-good environment - Optional: if you skip either one, keep the `Bootstrap` step selected and let suitup set them up for you -- If your setup stopped halfway, run `node src/cli.js append` to add missing blocks or switch the prompt preset without replacing your whole `.zshrc` +- If your setup stopped halfway, run `node src/cli.js append` to add missing blocks, re-install missing tools tied to those blocks, or switch the prompt preset without replacing your whole `.zshrc` - When suitup detects existing suitup-managed config or already-installed frontend prerequisites, setup now deselects those completed steps by default so reruns stay focused ### Install and run @@ -121,7 +121,7 @@ For users who already have a `.zshrc` and want to cherry-pick suitup configs: node src/cli.js append ``` -Uses idempotent marker blocks (`# >>> suitup/... >>>`) to safely append selected configs: +Uses idempotent marker blocks (`# >>> suitup/... >>>`) to safely append selected configs and re-run related installers when required tools are missing: - Suitup aliases - Zinit plugins diff --git a/README.zh-CN.md b/README.zh-CN.md index 6768a1c..044845e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -35,7 +35,7 @@ Suitup 可以帮你初始化 Zsh 和 Homebrew,但更稳妥的路径仍然是 - 推荐:先安装 Zsh,并切到 Zsh 会话里再运行 suitup - 推荐:先安装 Homebrew,这样后续包管理和工具安装会更稳定 - 可选:如果你不想手动准备,也可以保留 `Bootstrap` 步骤,让 suitup 代为安装 -- 如果初始化做到一半中断了,可以运行 `node src/cli.js append` 继续补齐缺失配置,或者切换 prompt 预设,而不必整体重写 `.zshrc` +- 如果初始化做到一半中断了,可以运行 `node src/cli.js append` 继续补齐缺失配置、重试安装这些配置依赖的工具,或者切换 prompt 预设,而不必整体重写 `.zshrc` - 如果 suitup 检测到本地已经存在 suitup 管理的配置,或者前端工具链已经装好,setup 现在会默认把这些已完成步骤反选掉,方便你只补剩余内容 ### 安装并运行 @@ -121,7 +121,7 @@ Bootstrap 细节: node src/cli.js append ``` -通过幂等标记块(`# >>> suitup/... >>>`)安全追加: +通过幂等标记块(`# >>> suitup/... >>>`)安全追加;如果相关工具缺失,也会一起重试安装: - aliases - zinit 插件 diff --git a/src/append.js b/src/append.js index adaedfa..c367478 100644 --- a/src/append.js +++ b/src/append.js @@ -7,9 +7,13 @@ import { appendIfMissing, ensureDir, readFileSafe, copyFile, writeFile } from ". import { CONFIGS_DIR } from "./constants.js"; import { backupShellRcFiles } from "./steps/zsh-config.js"; import { installZinit } from "./steps/plugin-manager.js"; +import { installCliTools } from "./steps/cli-tools.js"; +import { installFrontendTools } from "./steps/frontend.js"; +import { commandExists } from "./utils/shell.js"; const ZSHRC = join(homedir(), ".zshrc"); const SUITUP_DIR = join(homedir(), ".config", "suitup"); +const TOOLS_INIT_COMMANDS = ["atuin", "fzf", "zoxide", "fnm"]; function sourcePromptTemplate(preset) { return preset === "basic" @@ -47,6 +51,36 @@ export function ensurePromptSource({ home } = {}) { ); } +export function getMissingToolsInitCommands(commandExistsFn = commandExists) { + return TOOLS_INIT_COMMANDS.filter((tool) => !commandExistsFn(tool)); +} + +export function needsToolsInitRepair(existing = "", commandExistsFn = commandExists) { + return !existing.includes("suitup/tools-init") || getMissingToolsInitCommands(commandExistsFn).length > 0; +} + +export async function ensureToolsInitDependencies({ + commandExistsFn = commandExists, + installCliToolsFn = installCliTools, + installFrontendToolsFn = installFrontendTools, +} = {}) { + const missing = getMissingToolsInitCommands(commandExistsFn); + const missingCliTools = missing.filter((tool) => tool !== "fnm"); + let changed = false; + + if (missingCliTools.length > 0) { + await installCliToolsFn(missingCliTools); + changed = true; + } + + if (missing.includes("fnm")) { + await installFrontendToolsFn(); + changed = true; + } + + return changed; +} + /** Appendable config blocks. */ const BLOCKS = [ { @@ -115,7 +149,11 @@ const BLOCKS = [ hint: "atuin, fzf, zoxide, fnm", group: "Shell Enhancements", marker: "suitup/tools-init", - apply() { + isAvailable({ existing, commandExistsFn = commandExists } = {}) { + return needsToolsInitRepair(existing, commandExistsFn); + }, + async apply() { + const installed = await ensureToolsInitDependencies(); const block = [ "", "# >>> suitup/tools-init >>>", @@ -127,7 +165,8 @@ const BLOCKS = [ "# <<< suitup/tools-init <<<", "", ].join("\n"); - return appendIfMissing(ZSHRC, block, "suitup/tools-init"); + const appended = appendIfMissing(ZSHRC, block, "suitup/tools-init"); + return installed || appended; }, }, { @@ -250,13 +289,13 @@ export async function runAppend() { const block = BLOCKS.find((b) => b.value === value); if (block && await block.apply()) { appended++; - p.log.success(`Appended: ${block.label}`); + p.log.success(`Applied: ${block.label}`); } } p.outro( appended > 0 - ? `Appended ${appended} config(s). Run ${pc.cyan("exec zsh")} to reload.` - : "No changes made (configs already present)." + ? `Applied ${appended} selection(s). Run ${pc.cyan("exec zsh")} to reload.` + : "No changes made (CLI tools and configs are already present)." ); } diff --git a/tests/append.test.js b/tests/append.test.js index 6a8ba06..52c5079 100644 --- a/tests/append.test.js +++ b/tests/append.test.js @@ -5,12 +5,45 @@ import { tmpdir } from "node:os"; import { appendIfMissing, ensureDir } from "../src/utils/fs.js"; import { CONFIGS_DIR } from "../src/constants.js"; +vi.mock("@clack/prompts", () => ({ + log: { success: vi.fn(), step: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + intro: vi.fn(), + outro: vi.fn(), + cancel: vi.fn(), + isCancel: vi.fn(() => false), + groupMultiselect: vi.fn(), +})); + vi.mock("../src/steps/plugin-manager.js", () => ({ installZinit: vi.fn(() => Promise.resolve()), })); -import { ensurePromptSource, writePromptPreset } from "../src/append.js"; +vi.mock("../src/steps/cli-tools.js", () => ({ + installCliTools: vi.fn(() => Promise.resolve()), +})); + +vi.mock("../src/steps/frontend.js", () => ({ + installFrontendTools: vi.fn(() => Promise.resolve()), +})); + +vi.mock("../src/utils/shell.js", () => ({ + commandExists: vi.fn(), + brewInstalled: vi.fn(), + brewInstall: vi.fn(() => true), + run: vi.fn(() => ""), + runStream: vi.fn(() => Promise.resolve(0)), +})); + +import { + ensurePromptSource, + ensureToolsInitDependencies, + getMissingToolsInitCommands, + needsToolsInitRepair, + writePromptPreset, +} from "../src/append.js"; import { installZinit } from "../src/steps/plugin-manager.js"; +import { installCliTools } from "../src/steps/cli-tools.js"; +import { installFrontendTools } from "../src/steps/frontend.js"; describe("Append mode utilities", () => { let sandbox; @@ -129,6 +162,43 @@ describe("Append mode utilities", () => { expect(changed).toBe(true); expect(readFileSync(zshrcPath, "utf-8")).toContain('source_if_exists "$HOME/.config/zsh/shared/prompt.zsh"'); }); + + test("tools-init repair is needed when config exists but fnm is missing", () => { + const existing = [ + "# >>> suitup/tools-init >>>", + 'command -v atuin &>/dev/null && eval "$(atuin init zsh)"', + 'command -v fzf &>/dev/null && eval "$(fzf --zsh)"', + 'command -v zoxide &>/dev/null && eval "$(zoxide init zsh)"', + 'command -v fnm &>/dev/null && eval "$(fnm env --use-on-cd --version-file-strategy=recursive --shell zsh)"', + "# <<< suitup/tools-init <<<", + "", + ].join("\n"); + + const needsRepair = needsToolsInitRepair(existing, (name) => name !== "fnm"); + + expect(needsRepair).toBe(true); + expect(getMissingToolsInitCommands((name) => name !== "fnm")).toEqual(["fnm"]); + }); + + test("ensureToolsInitDependencies installs missing shell tools and frontend tools together", async () => { + const commandExistsFn = vi.fn((name) => !["atuin", "fzf", "fnm"].includes(name)); + + const changed = await ensureToolsInitDependencies({ commandExistsFn }); + + expect(changed).toBe(true); + expect(installCliTools).toHaveBeenCalledWith(["atuin", "fzf"]); + expect(installFrontendTools).toHaveBeenCalledTimes(1); + }); + + test("ensureToolsInitDependencies skips installers when everything is already present", async () => { + const commandExistsFn = vi.fn(() => true); + + const changed = await ensureToolsInitDependencies({ commandExistsFn }); + + expect(changed).toBe(false); + expect(installCliTools).not.toHaveBeenCalled(); + expect(installFrontendTools).not.toHaveBeenCalled(); + }); }); describe("fzf-config block", () => {