Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
5acb27fa57 fix: address review comments - use tempDir and add exitCode assertion
- Replace tmpdirSync with tempDir per coding guidelines
- Add missing exitCode assertion in scoped package test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 01:29:06 +00:00
autofix-ci[bot]
48c8d81c39 [autofix.ci] apply automated fixes 2026-01-15 01:17:14 +00:00
Claude Bot
43d600964b fix(bunx): support HTTPS tarball URLs
When running `bunx https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz`,
the package name was not being inferred from the tarball URL, resulting
in an empty `initial_bin_name`. This caused the install parameter to be
formatted as `@https://...` (with an empty name) instead of
`cowsay@https://...`, which failed to parse.

This fix adds a `inferBinNameFromTarball` function that:
1. Extracts package name from npm registry URLs (between domain and "/-/")
2. Falls back to parsing the filename, stripping extension and version

Fixes #3675

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 01:15:18 +00:00
2 changed files with 107 additions and 0 deletions

View File

@@ -368,6 +368,8 @@ pub const BunxCommand = struct {
"tsc"
else if (update_request.version.tag == .github)
update_request.version.value.github.repo.slice(update_request.version_buf)
else if (update_request.version.tag == .tarball)
inferBinNameFromTarball(update_request.version.literal.slice(update_request.version_buf))
else if (strings.lastIndexOfChar(update_request.name, '/')) |index|
update_request.name[index + 1 ..]
else
@@ -865,6 +867,59 @@ pub const BunxCommand = struct {
const string = []const u8;
/// Infers the binary name from a tarball URL string.
/// For URLs like https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz
/// extracts "cowsay" as the package name.
/// For local tarballs like ./my-package-1.0.0.tgz, extracts "my-package".
fn inferBinNameFromTarball(url: string) string {
// For npm registry URLs: https://registry.npmjs.org/package/-/package-version.tgz
// Extract package name between the registry domain and "/-/"
if (strings.indexOf(url, "/-/")) |separator_idx| {
// Find the start of the package name after the domain
var package_start: usize = 0;
if (strings.indexOf(url, "://")) |protocol_end| {
const after_protocol = protocol_end + 3;
// Find the next "/" after the protocol (end of domain)
if (strings.indexOfCharPos(url, '/', after_protocol)) |domain_end| {
package_start = domain_end + 1;
}
}
if (package_start > 0 and package_start < separator_idx) {
return url[package_start..separator_idx];
}
}
// Fallback: extract name from tarball filename
// Get the filename part (after last /)
const filename = if (strings.lastIndexOfChar(url, '/')) |idx|
url[idx + 1 ..]
else
url;
// Strip .tgz or .tar.gz extension
const basename = if (strings.endsWithComptime(filename, ".tar.gz"))
filename[0 .. filename.len - ".tar.gz".len]
else if (strings.endsWithComptime(filename, ".tgz"))
filename[0 .. filename.len - ".tgz".len]
else
filename;
// Try to strip version suffix (e.g., "cowsay-1.5.0" -> "cowsay")
// Look for the last dash followed by a digit (version number)
var i: usize = basename.len;
while (i > 0) {
i -= 1;
if (basename[i] == '-') {
// Check if what follows looks like a version (starts with digit)
if (i + 1 < basename.len and basename[i + 1] >= '0' and basename[i + 1] <= '9') {
return basename[0..i];
}
}
}
return basename;
}
const std = @import("std");
const Run = @import("./run_command.zig").RunCommand;
const Allocator = std.mem.Allocator;

View File

@@ -0,0 +1,52 @@
// https://github.com/oven-sh/bun/issues/3675
// bunx should support HTTPS tarball URLs like npx does
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
describe("issue/03675", () => {
test("bunx can run package from HTTPS tarball URL", async () => {
using tmp = tempDir("bunx-tarball-test", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "x", "https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz", "Hello"],
cwd: String(tmp),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("unrecognised dependency format");
expect(stderr).not.toContain("error:");
expect(stdout).toContain("Hello");
expect(stdout).toContain("< Hello >"); // cowsay output format
expect(exitCode).toBe(0);
});
test("bunx can run scoped package from HTTPS tarball URL", async () => {
using tmp = tempDir("bunx-tarball-scoped-test", {});
// Use a scoped package to test the scoped package name extraction
await using proc = Bun.spawn({
cmd: [
bunExe(),
"x",
"https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-0.2.59.tgz",
"--version",
],
cwd: String(tmp),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("unrecognised dependency format");
// The package should be installed and run successfully
// Note: we don't check the version output as it may vary
// The main test is that it doesn't error on the tarball URL format
expect(exitCode).toBe(0);
});
});