fix: prevent uninstall from truncating RC files with missing end marker (#2927)

If the end marker (# <<< spawn <<<) is missing from .bashrc/.zshrc,
cleanRcFile dropped all content after the start marker. Now detects
unclosed blocks and skips the file with a warning instead of writing
a truncated version.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
Ahmed Abushagur 2026-03-23 16:54:10 -07:00 committed by GitHub
parent 9651e029df
commit 42df6f753a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 0 deletions

View file

@ -391,6 +391,58 @@ describe("cmdUninstall", () => {
expect(clack.logSuccess).toHaveBeenCalledWith("Removed:");
});
it("preserves RC file when end marker is missing (unclosed block)", async () => {
const binaryPath = join(home, ".local", "bin", "spawn");
fs.mkdirSync(join(home, ".local", "bin"), {
recursive: true,
});
fs.writeFileSync(binaryPath, "#!/bin/bash\necho spawn");
// Remove optional dirs
const spawnDir = join(home, ".spawn");
const configDir = join(home, ".config", "spawn");
if (fs.existsSync(spawnDir)) {
fs.rmSync(spawnDir, {
recursive: true,
force: true,
});
}
if (fs.existsSync(configDir)) {
fs.rmSync(configDir, {
recursive: true,
force: true,
});
}
// Write an RC file with start marker but NO end marker
const rcPath = join(home, ".bashrc");
const rcContent = [
"# existing config",
"alias ll='ls -la'",
"",
RC_MARKER_START,
'export PATH="$HOME/.local/bin:$PATH"',
"",
"# user aliases that would be lost",
"alias gs='git status'",
].join("\n");
fs.writeFileSync(rcPath, rcContent);
clack.confirm.mockResolvedValue(true);
await cmdUninstall();
// File should be unchanged — unclosed block means no write
const after = fs.readFileSync(rcPath, "utf-8");
expect(after).toBe(rcContent);
expect(after).toContain("# user aliases that would be lost");
expect(after).toContain("alias gs='git status'");
// Should have warned the user
const warnCalls = clack.logWarn.mock.calls.map((c: unknown[]) => String(c[0]));
expect(warnCalls.some((msg: string) => msg.includes("missing end marker"))).toBe(true);
});
it("shows shell RC hint when RC files were cleaned", async () => {
const binaryPath = join(home, ".local", "bin", "spawn");
fs.mkdirSync(join(home, ".local", "bin"), {

View file

@ -71,6 +71,14 @@ function cleanRcFile(rcPath: string): boolean {
cleaned.push(line);
}
// Safety: if insideBlock is still true, the end marker is missing.
// Abort to avoid truncating the user's shell config.
if (insideBlock) {
p.log.warn(`Spawn block in ${rcPath} is missing end marker — skipping to avoid data loss.`);
p.log.warn(`Manually remove the line "${RC_MARKER_START}" and the spawn PATH export from ${rcPath}.`);
return false;
}
if (changed) {
fs.writeFileSync(rcPath, cleaned.join("\n"));
}