Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 现在会默认把这些已完成步骤反选掉,方便你只补剩余内容

### 安装并运行
Expand Down Expand Up @@ -121,7 +121,7 @@ Bootstrap 细节:
node src/cli.js append
```

通过幂等标记块(`# >>> suitup/... >>>`)安全追加:
通过幂等标记块(`# >>> suitup/... >>>`)安全追加;如果相关工具缺失,也会一起重试安装

- aliases
- zinit 插件
Expand Down
49 changes: 44 additions & 5 deletions src/append.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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 >>>",
Expand All @@ -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;
},
},
{
Expand Down Expand Up @@ -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)."
);
}
72 changes: 71 additions & 1 deletion tests/append.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading