fix(pm): allow bun pm cache and bun pm cache rm to work without package.json

The cache subcommand now works without requiring a package.json, since the
cache directory can be determined independently from the project. This is a
better fix than just documenting the -g flag, as the operation is global by
nature.

Changes:
- Extract cache subcommand handling to run before PackageManager.init
- Add getCachePathWithoutPackageManager() to determine cache path using env vars
- Remove the -g flag documentation for cache rm since it's no longer needed

Fixes #26427

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-25 00:17:45 +00:00
parent 827c7091d9
commit 159d135f29
2 changed files with 177 additions and 58 deletions

View File

@@ -143,6 +143,15 @@ pub const PackageManagerCommand = struct {
const is_direct_whoami = if (bun.argv.len > 1) strings.eqlComptime(bun.argv[1], "whoami") else false;
const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .pm);
// Handle "cache" subcommand before PackageManager.init since it doesn't require a package.json
var cli_positionals = cli.positionals;
const early_subcommand = getSubcommand(&cli_positionals);
if (strings.eqlComptime(early_subcommand, "cache")) {
execCacheSubcommand(ctx, cli.positionals);
return;
}
var pm, const cwd = PackageManager.init(ctx, cli, PackageManager.Subcommand.pm) catch |err| {
if (err == error.MissingPackageJSON) {
var cwd_buf: bun.PathBuffer = undefined;
@@ -248,64 +257,6 @@ pub const PackageManagerCommand = struct {
_ = try pm.lockfile.hasMetaHashChanged(true, pm.lockfile.packages.len);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "cache")) {
var dir: bun.PathBuffer = undefined;
var fd = pm.getCacheDirectory();
const outpath = bun.getFdPath(.fromStdDir(fd), &dir) catch |err| {
Output.prettyErrorln("{s} getting cache directory", .{@errorName(err)});
Global.crash();
};
if (pm.options.positionals.len > 1 and strings.eqlComptime(pm.options.positionals[1], "rm")) {
fd.close();
var had_err = false;
std.fs.deleteTreeAbsolute(outpath) catch |err| {
Output.err(err, "Could not delete {s}", .{outpath});
had_err = true;
};
Output.prettyln("Cleared 'bun install' cache", .{});
bunx: {
const tmp = bun.fs.FileSystem.RealFS.platformTempDir();
const tmp_dir = std.fs.openDirAbsolute(tmp, .{ .iterate = true }) catch |err| {
Output.err(err, "Could not open {s}", .{tmp});
had_err = true;
break :bunx;
};
var iter = tmp_dir.iterate();
// This is to match 'bunx_command.BunxCommand.exec's logic
const prefix = try std.fmt.allocPrint(ctx.allocator, "bunx-{d}-", .{
if (bun.Environment.isPosix) bun.c.getuid() else bun.windows.userUniqueId(),
});
var deleted: usize = 0;
while (iter.next() catch |err| {
Output.err(err, "Could not read {s}", .{tmp});
had_err = true;
break :bunx;
}) |entry| {
if (std.mem.startsWith(u8, entry.name, prefix)) {
tmp_dir.deleteTree(entry.name) catch |err| {
Output.err(err, "Could not delete {s}", .{entry.name});
had_err = true;
continue;
};
deleted += 1;
}
}
Output.prettyln("Cleared {d} cached 'bunx' packages", .{deleted});
}
Global.exit(if (had_err) 1 else 0);
}
Output.writer().writeAll(outpath) catch {};
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "default-trusted")) {
try DefaultTrustedCommand.exec();
Global.exit(0);
@@ -457,6 +408,129 @@ pub const PackageManagerCommand = struct {
}
};
/// Get the cache directory path without requiring a PackageManager instance.
/// This uses the same logic as PackageManager.fetchCacheDirectoryPath but with direct env access.
fn getCachePathWithoutPackageManager() []const u8 {
// Check BUN_INSTALL_CACHE_DIR first (via system env since we don't have DotEnv loaded)
if (bun.getenvZ("BUN_INSTALL_CACHE_DIR")) |dir| {
return Fs.FileSystem.instance.abs(&[_]string{dir});
}
// Check BUN_INSTALL
if (bun.getenvZ("BUN_INSTALL")) |dir| {
var parts = [_]string{ dir, "install/", "cache/" };
return Fs.FileSystem.instance.abs(&parts);
}
// Check XDG_CACHE_HOME
if (bun.env_var.XDG_CACHE_HOME.get()) |dir| {
var parts = [_]string{ dir, ".bun/", "install/", "cache/" };
return Fs.FileSystem.instance.abs(&parts);
}
// Fall back to HOME
if (bun.env_var.HOME.get()) |dir| {
var parts = [_]string{ dir, ".bun/", "install/", "cache/" };
return Fs.FileSystem.instance.abs(&parts);
}
// Ultimate fallback to node_modules/.bun-cache
var fallback_parts = [_]string{"node_modules/.bun-cache"};
return Fs.FileSystem.instance.abs(&fallback_parts);
}
const ClearBunxCacheResult = struct {
deleted: usize,
had_err: bool,
};
/// Clear cached bunx packages from the temp directory.
/// Returns the number of deleted packages and whether any errors occurred.
fn clearBunxCache(allocator: std.mem.Allocator) ClearBunxCacheResult {
var result = ClearBunxCacheResult{ .deleted = 0, .had_err = false };
const tmp = bun.fs.FileSystem.RealFS.platformTempDir();
var tmp_dir = std.fs.openDirAbsolute(tmp, .{ .iterate = true }) catch |err| {
Output.err(err, "Could not open {s}", .{tmp});
result.had_err = true;
return result;
};
defer tmp_dir.close();
var iter = tmp_dir.iterate();
// This is to match 'bunx_command.BunxCommand.exec's logic
const prefix = std.fmt.allocPrint(allocator, "bunx-{d}-", .{
if (bun.Environment.isPosix) bun.c.getuid() else bun.windows.userUniqueId(),
}) catch |err| {
Output.err(err, "Could not allocate prefix", .{});
result.had_err = true;
return result;
};
defer allocator.free(prefix);
while (iter.next() catch |err| {
Output.err(err, "Could not read {s}", .{tmp});
result.had_err = true;
return result;
}) |entry| {
if (std.mem.startsWith(u8, entry.name, prefix)) {
tmp_dir.deleteTree(entry.name) catch |err| {
Output.err(err, "Could not delete {s}", .{entry.name});
result.had_err = true;
continue;
};
result.deleted += 1;
}
}
return result;
}
/// Handle "bun pm cache" and "bun pm cache rm" without requiring a package.json.
/// This is a standalone function because the cache directory can be determined
/// independently from the project's package.json.
fn execCacheSubcommand(ctx: Command.Context, positionals: []const string) void {
// Get cache directory path without requiring a PackageManager instance.
const cache_path = getCachePathWithoutPackageManager();
// Check if this is "cache rm" (positionals would be ["pm", "cache", "rm"] or ["cache", "rm"])
// We need to find "rm" after "cache" in the positionals
const has_rm = for (positionals, 0..) |pos, i| {
if (strings.eqlComptime(pos, "cache")) {
if (i + 1 < positionals.len and strings.eqlComptime(positionals[i + 1], "rm")) {
break true;
}
}
} else false;
if (has_rm) {
var had_err = false;
std.fs.deleteTreeAbsolute(cache_path) catch |err| {
// FileNotFound is not an error - the cache may not exist yet
if (err != error.FileNotFound) {
Output.err(err, "Could not delete {s}", .{cache_path});
had_err = true;
}
};
Output.prettyln("Cleared 'bun install' cache", .{});
const bunx_result = clearBunxCache(ctx.allocator);
if (bunx_result.had_err) {
had_err = true;
}
Output.prettyln("Cleared {d} cached 'bunx' packages", .{bunx_result.deleted});
Global.exit(if (had_err) 1 else 0);
}
// Just print the cache path
Output.writer().writeAll(cache_path) catch {};
Global.exit(0);
}
fn printNodeModulesFolderStructure(
directory: *const NodeModulesFolder,
directory_package_id: ?PackageID,

View File

@@ -0,0 +1,45 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// Test that "bun pm cache rm" works without a package.json
// https://github.com/oven-sh/bun/issues/26427
test("bun pm cache rm works without package.json", async () => {
// Use a temp directory without a package.json
using dir = tempDir("bun-test-26427", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache", "rm"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
// Should succeed and clear the cache without requiring -g flag
expect(stdout).toContain("Cleared");
expect(exitCode).toBe(0);
});
// Test that "bun pm cache" (print path) works without a package.json
test("bun pm cache works without package.json", async () => {
// Use a temp directory without a package.json
using dir = tempDir("bun-test-26427-cache", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
// Should succeed and print an absolute path to the cache directory
const trimmedOutput = stdout.trim();
// Check that it's an absolute path (starts with / on Unix or drive letter on Windows)
expect(trimmedOutput.startsWith("/") || /^[A-Za-z]:/.test(trimmedOutput)).toBe(true);
expect(exitCode).toBe(0);
});