Skip to content

Commit f637878

Browse files
fix(cli): clear each plugin-cache root independently in doctor
Follow-up to #199 (cubic P2): the bulk cache clear ran all roots inside one try/catch, so a mid-loop rmSync failure aborted the rest and the error result reported clearTargets[0] - a root that may have already been deleted - while hiding that one root was cleared. Now each root is removed independently: the error result points at the root that ACTUALLY failed, successfully-cleared roots still get removed, and partial outcomes are reported accurately. Added an injected-remover seam so the per-root failure path is deterministically tested. Gate: CLI 213/0, tsc + biome clean. Co-authored-by: Alfonso [Magic Context] <288211368+alfonso-magic-context@users.noreply.github.com>
1 parent d4b15b7 commit f637878

2 files changed

Lines changed: 72 additions & 15 deletions

File tree

packages/cli/src/commands/doctor-opencode-cache.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ function readCachedPluginVersion(pluginCacheDir: string): string | undefined {
2626

2727
export async function clearPluginCache(
2828
options: { force?: boolean; latestVersion?: string | null } = {},
29+
deps: { remove?: (path: string) => void } = {},
2930
): Promise<PluginCacheResult> {
31+
// Injected remover keeps the per-root deletion failure path deterministically
32+
// testable; defaults to a real recursive remove.
33+
const remove =
34+
deps.remove ?? ((path: string) => rmSync(path, { recursive: true, force: true }));
3035
const pluginCacheRoots = getOpenCodePluginCacheRoots();
3136
const existingRoots = pluginCacheRoots.filter((root) => existsSync(root));
3237

@@ -66,24 +71,39 @@ export async function clearPluginCache(
6671
};
6772
}
6873

69-
try {
70-
for (const entry of clearTargets) {
71-
rmSync(entry.path, { recursive: true, force: true });
74+
// Clear each root independently so one root's failure neither aborts the
75+
// others nor mislabels an already-deleted path as the one needing manual
76+
// cleanup. The error result points at the root that actually failed.
77+
const cleared: typeof clearTargets = [];
78+
const failed: Array<{ path: string; error: string }> = [];
79+
for (const entry of clearTargets) {
80+
try {
81+
remove(entry.path);
82+
cleared.push(entry);
83+
} catch (err: unknown) {
84+
failed.push({
85+
path: entry.path,
86+
error: err instanceof Error ? err.message : String(err),
87+
});
7288
}
73-
const firstTarget = clearTargets[0];
74-
return {
75-
action: "cleared",
76-
path: firstTarget?.path ?? pluginCacheRoots[0] ?? "",
77-
paths: clearTargets.map((entry) => entry.path),
78-
cached: firstTarget?.cached,
79-
latest: latestVersion,
80-
};
81-
} catch (err: unknown) {
82-
const message = err instanceof Error ? err.message : String(err);
89+
}
90+
91+
if (failed.length > 0) {
92+
const firstFailure = failed[0];
8393
return {
8494
action: "error",
85-
path: clearTargets[0]?.path ?? existingRoots[0] ?? "",
86-
error: message,
95+
path: firstFailure?.path ?? clearTargets[0]?.path ?? existingRoots[0] ?? "",
96+
paths: failed.map((entry) => entry.path),
97+
error: firstFailure?.error,
8798
};
8899
}
100+
101+
const firstTarget = cleared[0];
102+
return {
103+
action: "cleared",
104+
path: firstTarget?.path ?? pluginCacheRoots[0] ?? "",
105+
paths: cleared.map((entry) => entry.path),
106+
cached: firstTarget?.cached,
107+
latest: latestVersion,
108+
};
89109
}

packages/cli/src/commands/doctor-opencode.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,43 @@ describe("doctor OpenCode plugin cache", () => {
253253
expect(result.latest).toBeUndefined();
254254
expect(existsSync(pluginCachePath)).toBe(false);
255255
});
256+
257+
it("reports the actually-failed root and clears the rest when one root fails", async () => {
258+
const cacheRoot = makeTempDir("mc-opencode-cache-");
259+
originalXdgCacheHome = process.env.XDG_CACHE_HOME;
260+
process.env.XDG_CACHE_HOME = cacheRoot;
261+
const latestCachePath = createCachedOpenCodePlugin(cacheRoot, "0.26.0");
262+
const versionlessCachePath = createCachedOpenCodePlugin(
263+
cacheRoot,
264+
"0.26.0",
265+
OPENCODE_PLUGIN_NAME,
266+
);
267+
268+
// Fail only the second root: the first must still be removed, and the
269+
// error must point at the failed root, not the already-removed one.
270+
const removed: string[] = [];
271+
const result = await clearPluginCache(
272+
{ latestVersion: "0.29.1" },
273+
{
274+
remove: (path) => {
275+
if (path === versionlessCachePath) {
276+
throw new Error("EACCES: permission denied");
277+
}
278+
rmSync(path, { recursive: true, force: true });
279+
removed.push(path);
280+
},
281+
},
282+
);
283+
284+
expect(result).toMatchObject({
285+
action: "error",
286+
path: versionlessCachePath,
287+
paths: [versionlessCachePath],
288+
error: "EACCES: permission denied",
289+
});
290+
expect(removed).toEqual([latestCachePath]);
291+
expect(existsSync(latestCachePath)).toBe(false);
292+
});
256293
});
257294

258295
describe("doctor v22 backfill commands", () => {

0 commit comments

Comments
 (0)