forked from farhoodlabs/paperclip
fix(cli): stop worktree init --force from wiping repo worktrees/ (#6240)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Each working tree gets an isolated Paperclip instance; the CLI's
`paperclip worktree init` is what bootstraps that instance and writes
`<repo>/.paperclip/config.json` + `.env`
> - When `--force` was passed, the init path tried to "start clean" by
recursively removing the entire `<repo>/.paperclip/` directory before
rewriting those two files
> - But `<repo>/.paperclip/` also holds `worktrees/`, which contains
every repo-managed worktree checkout (70+ on this machine). The
recursive rm silently nuked all of them.
> - This PR narrows the `--force` reset so it only deletes the two files
it's about to rewrite (`config.json`, `.env`), instead of wiping the
whole `repoConfigDir`
> - It also adds a regression test that drops a sentinel file into
`<repoConfigDir>/worktrees/` and asserts it survives a `--force` init
> - The benefit is that `worktree init --force` becomes safe to run from
inside the main repo without destroying every sibling worktree checkout
## What Changed
- `cli/src/commands/worktree.ts`: in the `--force` branch of
`runWorktreeInit`, replace `rmSync(paths.repoConfigDir, { recursive:
true, force: true })` with targeted removals of `paths.configPath` and
`paths.envPath`. `paths.instanceRoot` removal is unchanged — that path
is per-instance and safe to wipe.
- `cli/src/__tests__/worktree.test.ts`: new regression test that seeds a
fake `worktrees/<name>/` checkout inside the repo's `.paperclip/` and
verifies `runWorktreeInit({ force: true, ... })` does not delete it.
## Verification
- `pnpm --filter @paperclip/cli test -- worktree` — the new regression
test fails on the old code and passes on the fix
- Manual: from a repo checkout, `npx paperclipai worktree init --force
…` no longer removes `<repo>/.paperclip/worktrees/`; only `config.json`
and `.env` are rewritten
## Risks
- Low. The change strictly narrows what `--force` removes. Any caller
that depended on `--force` also wiping unrelated files under
`<repo>/.paperclip/` (there shouldn't be any — it's documented as just
config + env) would see those files persist. `instanceRoot` cleanup is
unchanged.
## Model Used
- Claude (Anthropic), model `claude-opus-4-7`, ~200K context,
extended-thinking + tool-use enabled, run via the Paperclip
`claude_local` adapter.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — CLI-only fix)
- [x] I have updated relevant documentation to reflect my changes (no
doc surface affected)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -512,6 +512,45 @@ describe("worktree helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves repo-managed worktree checkouts when --force re-runs from the source repo", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-force-preserve-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
const repoConfigDir = path.join(repoRoot, ".paperclip");
|
||||
fs.mkdirSync(repoConfigDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(repoConfigDir, "config.json"), "stale", "utf8");
|
||||
fs.writeFileSync(path.join(repoConfigDir, ".env"), "STALE=1", "utf8");
|
||||
|
||||
// Simulate the repo-managed worktrees subfolder that holds every
|
||||
// worktree checkout (the directory PAPA-358 reported as nuked).
|
||||
const worktreesDir = path.join(repoConfigDir, "worktrees");
|
||||
const checkoutDir = path.join(worktreesDir, "PAP-100-feature");
|
||||
fs.mkdirSync(checkoutDir, { recursive: true });
|
||||
const sentinelPath = path.join(checkoutDir, "sentinel.txt");
|
||||
fs.writeFileSync(sentinelPath, "do-not-delete", "utf8");
|
||||
|
||||
process.chdir(repoRoot);
|
||||
|
||||
await worktreeInitCommand({
|
||||
seed: false,
|
||||
force: true,
|
||||
fromConfig: path.join(tempRoot, "missing", "config.json"),
|
||||
home: path.join(tempRoot, ".paperclip-worktrees"),
|
||||
});
|
||||
|
||||
expect(fs.existsSync(sentinelPath)).toBe(true);
|
||||
expect(fs.readFileSync(sentinelPath, "utf8")).toBe("do-not-delete");
|
||||
expect(fs.existsSync(path.join(repoConfigDir, "config.json"))).toBe(true);
|
||||
expect(fs.readFileSync(path.join(repoConfigDir, "config.json"), "utf8")).not.toBe("stale");
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
itEmbeddedPostgres(
|
||||
"seeds authenticated users into minimally cloned worktree instances",
|
||||
async () => {
|
||||
|
||||
@@ -1385,7 +1385,12 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
|
||||
}
|
||||
|
||||
if (opts.force) {
|
||||
rmSync(paths.repoConfigDir, { recursive: true, force: true });
|
||||
// Only remove the specific files we're about to rewrite, not the whole
|
||||
// repoConfigDir — that directory can contain sibling state such as
|
||||
// <repo>/.paperclip/worktrees/ holding every repo-managed worktree
|
||||
// checkout, and a recursive rmSync here would nuke them all.
|
||||
rmSync(paths.configPath, { force: true });
|
||||
rmSync(paths.envPath, { force: true });
|
||||
rmSync(paths.instanceRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user