Skip to content

Commit 02623e4

Browse files
CopilotChangeHow
andauthored
Reconcile append mode tool installation with config repair (#24)
`append` only updated shell config blocks, so partially failed setups could still leave required tooling missing. In particular, selecting `tools-init` did not retry installation when commands like `fnm` were absent, leaving CLI state and config state out of sync. - **Append now repairs capability, not just config** - `tools-init` is treated as a combined reconciliation step for both: - the `suitup/tools-init` block in `.zshrc` - the required commands behind that block: `atuin`, `fzf`, `zoxide`, `fnm` - If the config block already exists but any of those commands are missing, append still surfaces the item and applies repair. - **Reuse existing installers instead of duplicating logic** - Missing shell tools are routed through the existing `installCliTools()` flow. - Missing `fnm` triggers the existing frontend installer path via `installFrontendTools()`, so append reuses the same install/retry behavior as setup. - **Idempotent config behavior is preserved** - The init block is only appended when missing. - If config is already present, append can still perform the install side of the repair without duplicating markers or rewriting unrelated content. - **Focused append coverage** - Added coverage for: - config present + `fnm` missing - mixed missing dependencies across CLI tools and frontend tools - no-op behavior when both tools and config are already present - **Docs updated** - `README.md` and `README.zh-CN.md` now describe append as repairing missing tool dependencies in addition to appending config blocks. Example of the repaired behavior: ```js // Existing ~/.zshrc already contains: # >>> 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 <<< // Before: // append would skip this block entirely because the marker already existed. // After: // append detects missing commands (for example `fnm`), // reruns the relevant installer(s), // and only appends the block if the config itself is missing. ``` <!-- START COPILOT CODING AGENT TIPS --> --- 🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. [Learn more about Advanced Security.](https://gh.io/cca-advanced-security) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ChangeHow <23733347+ChangeHow@users.noreply.github.com>
1 parent 473992a commit 02623e4

4 files changed

Lines changed: 119 additions & 10 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Suitup can bootstrap Zsh and Homebrew for you, but the most reliable path is to
3535
- Recommended: install Zsh first, switch into a Zsh session, then run suitup
3636
- Recommended: install Homebrew first so later package/tool steps run in a known-good environment
3737
- Optional: if you skip either one, keep the `Bootstrap` step selected and let suitup set them up for you
38-
- 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`
38+
- 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`
3939
- When suitup detects existing suitup-managed config or already-installed frontend prerequisites, setup now deselects those completed steps by default so reruns stay focused
4040

4141
### Install and run
@@ -121,7 +121,7 @@ For users who already have a `.zshrc` and want to cherry-pick suitup configs:
121121
node src/cli.js append
122122
```
123123

124-
Uses idempotent marker blocks (`# >>> suitup/... >>>`) to safely append selected configs:
124+
Uses idempotent marker blocks (`# >>> suitup/... >>>`) to safely append selected configs and re-run related installers when required tools are missing:
125125

126126
- Suitup aliases
127127
- Zinit plugins

README.zh-CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Suitup 可以帮你初始化 Zsh 和 Homebrew,但更稳妥的路径仍然是
3535
- 推荐:先安装 Zsh,并切到 Zsh 会话里再运行 suitup
3636
- 推荐:先安装 Homebrew,这样后续包管理和工具安装会更稳定
3737
- 可选:如果你不想手动准备,也可以保留 `Bootstrap` 步骤,让 suitup 代为安装
38-
- 如果初始化做到一半中断了,可以运行 `node src/cli.js append` 继续补齐缺失配置,或者切换 prompt 预设,而不必整体重写 `.zshrc`
38+
- 如果初始化做到一半中断了,可以运行 `node src/cli.js append` 继续补齐缺失配置、重试安装这些配置依赖的工具,或者切换 prompt 预设,而不必整体重写 `.zshrc`
3939
- 如果 suitup 检测到本地已经存在 suitup 管理的配置,或者前端工具链已经装好,setup 现在会默认把这些已完成步骤反选掉,方便你只补剩余内容
4040

4141
### 安装并运行
@@ -121,7 +121,7 @@ Bootstrap 细节:
121121
node src/cli.js append
122122
```
123123

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

126126
- aliases
127127
- zinit 插件

src/append.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import { appendIfMissing, ensureDir, readFileSafe, copyFile, writeFile } from ".
77
import { CONFIGS_DIR } from "./constants.js";
88
import { backupShellRcFiles } from "./steps/zsh-config.js";
99
import { installZinit } from "./steps/plugin-manager.js";
10+
import { installCliTools } from "./steps/cli-tools.js";
11+
import { installFrontendTools } from "./steps/frontend.js";
12+
import { commandExists } from "./utils/shell.js";
1013

1114
const ZSHRC = join(homedir(), ".zshrc");
1215
const SUITUP_DIR = join(homedir(), ".config", "suitup");
16+
const TOOLS_INIT_COMMANDS = ["atuin", "fzf", "zoxide", "fnm"];
1317

1418
function sourcePromptTemplate(preset) {
1519
return preset === "basic"
@@ -47,6 +51,36 @@ export function ensurePromptSource({ home } = {}) {
4751
);
4852
}
4953

54+
export function getMissingToolsInitCommands(commandExistsFn = commandExists) {
55+
return TOOLS_INIT_COMMANDS.filter((tool) => !commandExistsFn(tool));
56+
}
57+
58+
export function needsToolsInitRepair(existing = "", commandExistsFn = commandExists) {
59+
return !existing.includes("suitup/tools-init") || getMissingToolsInitCommands(commandExistsFn).length > 0;
60+
}
61+
62+
export async function ensureToolsInitDependencies({
63+
commandExistsFn = commandExists,
64+
installCliToolsFn = installCliTools,
65+
installFrontendToolsFn = installFrontendTools,
66+
} = {}) {
67+
const missing = getMissingToolsInitCommands(commandExistsFn);
68+
const missingCliTools = missing.filter((tool) => tool !== "fnm");
69+
let changed = false;
70+
71+
if (missingCliTools.length > 0) {
72+
await installCliToolsFn(missingCliTools);
73+
changed = true;
74+
}
75+
76+
if (missing.includes("fnm")) {
77+
await installFrontendToolsFn();
78+
changed = true;
79+
}
80+
81+
return changed;
82+
}
83+
5084
/** Appendable config blocks. */
5185
const BLOCKS = [
5286
{
@@ -115,7 +149,11 @@ const BLOCKS = [
115149
hint: "atuin, fzf, zoxide, fnm",
116150
group: "Shell Enhancements",
117151
marker: "suitup/tools-init",
118-
apply() {
152+
isAvailable({ existing, commandExistsFn = commandExists } = {}) {
153+
return needsToolsInitRepair(existing, commandExistsFn);
154+
},
155+
async apply() {
156+
const installed = await ensureToolsInitDependencies();
119157
const block = [
120158
"",
121159
"# >>> suitup/tools-init >>>",
@@ -127,7 +165,8 @@ const BLOCKS = [
127165
"# <<< suitup/tools-init <<<",
128166
"",
129167
].join("\n");
130-
return appendIfMissing(ZSHRC, block, "suitup/tools-init");
168+
const appended = appendIfMissing(ZSHRC, block, "suitup/tools-init");
169+
return installed || appended;
131170
},
132171
},
133172
{
@@ -250,13 +289,13 @@ export async function runAppend() {
250289
const block = BLOCKS.find((b) => b.value === value);
251290
if (block && await block.apply()) {
252291
appended++;
253-
p.log.success(`Appended: ${block.label}`);
292+
p.log.success(`Applied: ${block.label}`);
254293
}
255294
}
256295

257296
p.outro(
258297
appended > 0
259-
? `Appended ${appended} config(s). Run ${pc.cyan("exec zsh")} to reload.`
260-
: "No changes made (configs already present)."
298+
? `Applied ${appended} selection(s). Run ${pc.cyan("exec zsh")} to reload.`
299+
: "No changes made (CLI tools and configs are already present)."
261300
);
262301
}

tests/append.test.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,45 @@ import { tmpdir } from "node:os";
55
import { appendIfMissing, ensureDir } from "../src/utils/fs.js";
66
import { CONFIGS_DIR } from "../src/constants.js";
77

8+
vi.mock("@clack/prompts", () => ({
9+
log: { success: vi.fn(), step: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
10+
intro: vi.fn(),
11+
outro: vi.fn(),
12+
cancel: vi.fn(),
13+
isCancel: vi.fn(() => false),
14+
groupMultiselect: vi.fn(),
15+
}));
16+
817
vi.mock("../src/steps/plugin-manager.js", () => ({
918
installZinit: vi.fn(() => Promise.resolve()),
1019
}));
1120

12-
import { ensurePromptSource, writePromptPreset } from "../src/append.js";
21+
vi.mock("../src/steps/cli-tools.js", () => ({
22+
installCliTools: vi.fn(() => Promise.resolve()),
23+
}));
24+
25+
vi.mock("../src/steps/frontend.js", () => ({
26+
installFrontendTools: vi.fn(() => Promise.resolve()),
27+
}));
28+
29+
vi.mock("../src/utils/shell.js", () => ({
30+
commandExists: vi.fn(),
31+
brewInstalled: vi.fn(),
32+
brewInstall: vi.fn(() => true),
33+
run: vi.fn(() => ""),
34+
runStream: vi.fn(() => Promise.resolve(0)),
35+
}));
36+
37+
import {
38+
ensurePromptSource,
39+
ensureToolsInitDependencies,
40+
getMissingToolsInitCommands,
41+
needsToolsInitRepair,
42+
writePromptPreset,
43+
} from "../src/append.js";
1344
import { installZinit } from "../src/steps/plugin-manager.js";
45+
import { installCliTools } from "../src/steps/cli-tools.js";
46+
import { installFrontendTools } from "../src/steps/frontend.js";
1447

1548
describe("Append mode utilities", () => {
1649
let sandbox;
@@ -129,6 +162,43 @@ describe("Append mode utilities", () => {
129162
expect(changed).toBe(true);
130163
expect(readFileSync(zshrcPath, "utf-8")).toContain('source_if_exists "$HOME/.config/zsh/shared/prompt.zsh"');
131164
});
165+
166+
test("tools-init repair is needed when config exists but fnm is missing", () => {
167+
const existing = [
168+
"# >>> suitup/tools-init >>>",
169+
'command -v atuin &>/dev/null && eval "$(atuin init zsh)"',
170+
'command -v fzf &>/dev/null && eval "$(fzf --zsh)"',
171+
'command -v zoxide &>/dev/null && eval "$(zoxide init zsh)"',
172+
'command -v fnm &>/dev/null && eval "$(fnm env --use-on-cd --version-file-strategy=recursive --shell zsh)"',
173+
"# <<< suitup/tools-init <<<",
174+
"",
175+
].join("\n");
176+
177+
const needsRepair = needsToolsInitRepair(existing, (name) => name !== "fnm");
178+
179+
expect(needsRepair).toBe(true);
180+
expect(getMissingToolsInitCommands((name) => name !== "fnm")).toEqual(["fnm"]);
181+
});
182+
183+
test("ensureToolsInitDependencies installs missing shell tools and frontend tools together", async () => {
184+
const commandExistsFn = vi.fn((name) => !["atuin", "fzf", "fnm"].includes(name));
185+
186+
const changed = await ensureToolsInitDependencies({ commandExistsFn });
187+
188+
expect(changed).toBe(true);
189+
expect(installCliTools).toHaveBeenCalledWith(["atuin", "fzf"]);
190+
expect(installFrontendTools).toHaveBeenCalledTimes(1);
191+
});
192+
193+
test("ensureToolsInitDependencies skips installers when everything is already present", async () => {
194+
const commandExistsFn = vi.fn(() => true);
195+
196+
const changed = await ensureToolsInitDependencies({ commandExistsFn });
197+
198+
expect(changed).toBe(false);
199+
expect(installCliTools).not.toHaveBeenCalled();
200+
expect(installFrontendTools).not.toHaveBeenCalled();
201+
});
132202
});
133203

134204
describe("fzf-config block", () => {

0 commit comments

Comments
 (0)