use super::install::{FrozenMode, InstallOptions};
use clap::{Args, CommandFactory};
use miette::{Context, IntoDiagnostic, miette};
#[derive(Debug, Args)]
#[command(disable_help_flag = true)]
pub struct DlxArgs {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub params: Vec<String>,
#[arg(short = 'c', long)]
pub shell_mode: bool,
#[arg(short = 'p', long = "package")]
pub package: Vec<String>,
}
pub async fn run(args: DlxArgs) -> miette::Result<()> {
let DlxArgs {
params,
package,
shell_mode,
} = args;
let first = params.first().map(String::as_str);
if matches!(first, None | Some("--help" | "-h")) && package.is_empty() {
crate::Cli::command()
.find_subcommand_mut("dlx")
.expect("dlx is a registered subcommand")
.print_help()
.map_err(|e| miette!("failed to render help: {e}"))?;
println!();
return Ok(());
}
let command = params
.first()
.cloned()
.ok_or_else(|| miette!("dlx: missing command to run"))?;
let bin_args: Vec<String> = params.iter().skip(1).cloned().collect();
let explicit_package = !package.is_empty();
let install_specs: Vec<String> = if package.is_empty() {
if shell_mode {
let first_word = command
.split_whitespace()
.next()
.ok_or_else(|| miette!("dlx --shell-mode: missing command line to run"))?;
vec![first_word.to_string()]
} else {
vec![command.clone()]
}
} else {
package
};
let bin_name = bin_name_for(&command);
let tmp = tempfile::Builder::new()
.prefix("aube-dlx-")
.tempdir()
.into_diagnostic()
.wrap_err("failed to create dlx scratch dir")?;
let project_dir = tmp.path().to_path_buf();
let mut deps: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
for spec in &install_specs {
let (mut name, value) = synthesize_dlx_dep(spec);
if deps.contains_key(&name) {
let mut suffix = 2usize;
while deps.contains_key(&format!("{name}-{suffix}")) {
suffix += 1;
}
name = format!("{name}-{suffix}");
}
deps.insert(name, serde_json::Value::String(value));
}
let manifest = serde_json::json!({
"name": "aube-dlx",
"version": "0.0.0",
"private": true,
"dependencies": deps,
});
let manifest_bytes = serde_json::to_vec_pretty(&manifest).into_diagnostic()?;
aube_util::fs_atomic::atomic_write(&project_dir.join("package.json"), &manifest_bytes)
.into_diagnostic()
.wrap_err("failed to write dlx package.json")?;
let prev_cwd = {
let _cwd_guard = CwdGuard::switch_to(&project_dir)?;
let mut opts = dlx_install_options();
opts.project_dir = Some(project_dir.clone());
let install_result = super::install::run(opts).await;
let prev = _cwd_guard.original.clone();
install_result.wrap_err("dlx install failed")?;
prev
};
let status = if shell_mode {
let line = std::iter::once(command.as_str())
.chain(bin_args.iter().map(String::as_str))
.collect::<Vec<_>>()
.join(" ");
let bin_dir = super::project_modules_dir(&project_dir).join(".bin");
let new_path = aube_scripts::prepend_path(&bin_dir);
let mut cmd = aube_scripts::spawn_shell(&line);
cmd.env("PATH", &new_path)
.current_dir(&prev_cwd)
.stderr(aube_scripts::child_stderr())
.status()
.await
.into_diagnostic()
.wrap_err("failed to execute dlx shell command")?
} else {
let modules_dir = super::project_modules_dir(&project_dir);
let bin_dir = modules_dir.join(".bin");
let resolved_bin_name = if !explicit_package && !bin_dir.join(&bin_name).exists() {
let (pkg_name, _) = synthesize_dlx_dep(&install_specs[0]);
resolve_bin_from_package(&modules_dir, &pkg_name).unwrap_or_else(|| bin_name.clone())
} else {
bin_name.clone()
};
let bin_path = bin_dir.join(&resolved_bin_name);
if !bin_path.exists() {
return Err(miette!(
"dlx: binary not found after install: {resolved_bin_name}\n\
help: the package may ship the binary under a different name — try `aube dlx -p <package> <bin>`"
));
}
let exec_path = resolve_exec_shim(&bin_path);
tokio::process::Command::new(&exec_path)
.args(&bin_args)
.current_dir(&prev_cwd)
.stderr(aube_scripts::child_stderr())
.status()
.await
.into_diagnostic()
.wrap_err("failed to execute dlx binary")?
};
drop(tmp);
if !status.success() {
std::process::exit(aube_scripts::exit_code_from_status(status));
}
Ok(())
}
fn dlx_install_options() -> InstallOptions {
let mut opts = InstallOptions::with_mode(FrozenMode::No);
let gvs = super::global_virtual_store_flags();
if gvs.is_set() {
opts.cli_flags.extend(gvs.to_cli_flag_bag());
} else {
opts.cli_flags.push((
"disable-global-virtual-store".to_string(),
"false".to_string(),
));
}
opts.cli_flags
.push(("ignore-scripts".to_string(), "true".to_string()));
opts
}
struct CwdGuard {
original: std::path::PathBuf,
}
impl CwdGuard {
fn switch_to(new_dir: &std::path::Path) -> miette::Result<Self> {
let original = std::env::current_dir().into_diagnostic()?;
std::env::set_current_dir(new_dir)
.into_diagnostic()
.wrap_err("failed to switch into dlx scratch dir")?;
Ok(Self { original })
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
fn split_spec(spec: &str) -> (&str, &str) {
let (name, version) = super::split_name_spec(spec);
(name, version.unwrap_or("latest"))
}
fn is_non_registry_spec(s: &str) -> bool {
if s.starts_with("github:")
|| s.starts_with("gitlab:")
|| s.starts_with("bitbucket:")
|| s.starts_with("gist:")
|| s.starts_with("git+")
|| s.starts_with("git://")
|| s.starts_with("https://")
|| s.starts_with("http://")
|| s.starts_with("ssh://")
|| s.starts_with("file:")
|| s.starts_with("link:")
{
return true;
}
is_scp_form(s) || is_owner_repo_shorthand(s)
}
fn is_scp_form(s: &str) -> bool {
if s.contains("://") {
return false;
}
let Some(colon) = s.find(':') else {
return false;
};
let before = &s[..colon];
let Some(at) = before.find('@') else {
return false;
};
let user = &before[..at];
let host = &before[at + 1..];
if user.is_empty() || host.is_empty() {
return false;
}
matches!(host, "github.com" | "gitlab.com" | "bitbucket.org")
}
fn is_owner_repo_shorthand(s: &str) -> bool {
let body = s.split('#').next().unwrap_or(s);
if body.starts_with('@') || body.contains('@') || body.contains(':') {
return false;
}
let mut parts = body.splitn(2, '/');
let Some(owner) = parts.next() else {
return false;
};
let Some(repo) = parts.next() else {
return false;
};
!owner.is_empty() && !repo.is_empty() && !repo.contains('/')
}
fn derive_dlx_pkg_name(spec: &str) -> Option<String> {
let body = spec.split('#').next().unwrap_or(spec);
let after_colon = body.rsplit(':').next().unwrap_or(body);
let last = after_colon.rsplit('/').next().unwrap_or(after_colon);
let trimmed = last.strip_suffix(".git").unwrap_or(last);
if trimmed.is_empty() {
return None;
}
Some(trimmed.to_string())
}
fn synthesize_dlx_dep(spec: &str) -> (String, String) {
if is_owner_repo_shorthand(spec) {
let name = derive_dlx_pkg_name(spec).unwrap_or_else(|| "aube-dlx-pkg".to_string());
return (name, format!("github:{spec}"));
}
if is_non_registry_spec(spec) {
let name = derive_dlx_pkg_name(spec).unwrap_or_else(|| "aube-dlx-pkg".to_string());
return (name, spec.to_string());
}
let (name, version) = split_spec(spec);
(name.to_string(), version.to_string())
}
fn bin_name_for(command: &str) -> String {
if is_non_registry_spec(command) {
return derive_dlx_pkg_name(command).unwrap_or_else(|| "aube-dlx-pkg".to_string());
}
let (name, _) = split_spec(command);
name.rsplit('/').next().unwrap_or(name).to_string()
}
fn resolve_exec_shim(bin_path: &std::path::Path) -> std::path::PathBuf {
#[cfg(windows)]
{
if bin_path.extension().is_none() {
let cmd_path = bin_path.with_extension("cmd");
if cmd_path.exists() {
return cmd_path;
}
}
}
bin_path.to_path_buf()
}
fn resolve_bin_from_package(modules_dir: &std::path::Path, pkg_name: &str) -> Option<String> {
let pkg_json_path = modules_dir.join(pkg_name).join("package.json");
let content = std::fs::read_to_string(&pkg_json_path).ok()?;
let pkg_json: serde_json::Value = serde_json::from_str(&content).ok()?;
let bin = pkg_json.get("bin")?;
let inferred = pkg_name.rsplit('/').next().unwrap_or(pkg_name);
match bin {
serde_json::Value::String(_) => Some(inferred.to_string()),
serde_json::Value::Object(bins) => {
if bins.contains_key(inferred) {
Some(inferred.to_string())
} else if bins.len() == 1 {
bins.keys().next().cloned()
} else {
None
}
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_spec_plain() {
assert_eq!(split_spec("cowsay"), ("cowsay", "latest"));
}
#[test]
fn split_spec_versioned() {
assert_eq!(split_spec("cowsay@1.5.0"), ("cowsay", "1.5.0"));
}
#[test]
fn split_spec_scoped() {
assert_eq!(split_spec("@scope/foo"), ("@scope/foo", "latest"));
}
#[test]
fn split_spec_scoped_versioned() {
assert_eq!(split_spec("@scope/foo@2.0.0"), ("@scope/foo", "2.0.0"));
}
#[test]
fn bin_name_strips_scope_and_version() {
assert_eq!(bin_name_for("cowsay@1.5.0"), "cowsay");
assert_eq!(bin_name_for("@scope/foo@2"), "foo");
assert_eq!(bin_name_for("@scope/foo"), "foo");
}
#[test]
fn resolve_exec_shim_returns_bare_path_when_no_sibling() {
let tmp = tempfile::tempdir().unwrap();
let bare = tmp.path().join("loner");
std::fs::write(&bare, b"#!/bin/sh\n").unwrap();
assert_eq!(resolve_exec_shim(&bare), bare);
}
#[cfg(windows)]
#[test]
fn resolve_exec_shim_prefers_cmd_sibling_on_windows() {
let tmp = tempfile::tempdir().unwrap();
let bare = tmp.path().join("cowsay");
let cmd_shim = tmp.path().join("cowsay.cmd");
std::fs::write(&bare, b"#!/bin/sh\n").unwrap();
std::fs::write(&cmd_shim, b"@echo off\n").unwrap();
assert_eq!(resolve_exec_shim(&bare), cmd_shim);
}
#[cfg(unix)]
#[test]
fn resolve_exec_shim_keeps_bare_path_on_unix() {
let tmp = tempfile::tempdir().unwrap();
let bare = tmp.path().join("cowsay");
let cmd_shim = tmp.path().join("cowsay.cmd");
std::fs::write(&bare, b"#!/bin/sh\n").unwrap();
std::fs::write(&cmd_shim, b"@echo off\n").unwrap();
assert_eq!(resolve_exec_shim(&bare), bare);
}
#[test]
fn dlx_install_disables_global_virtual_store() {
let opts = dlx_install_options();
let empty_npmrc = Vec::new();
let empty_workspace = std::collections::BTreeMap::new();
let empty_env = Vec::new();
let ctx = aube_settings::ResolveCtx {
npmrc: &empty_npmrc,
workspace_yaml: &empty_workspace,
env: &empty_env,
cli: &opts.cli_flags,
};
assert_eq!(
aube_settings::resolved::enable_global_virtual_store(&ctx),
Some(false)
);
}
fn write_pkg_json(modules_dir: &std::path::Path, pkg_name: &str, pkg_json: serde_json::Value) {
let dir = modules_dir.join(pkg_name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("package.json"),
serde_json::to_string_pretty(&pkg_json).unwrap(),
)
.unwrap();
}
#[test]
fn resolve_bin_single_object_bin_picks_it() {
let tmp = tempfile::tempdir().unwrap();
write_pkg_json(
tmp.path(),
"@tanstack/cli",
serde_json::json!({
"name": "@tanstack/cli",
"bin": {"tanstack": "dist/bin.js"},
}),
);
assert_eq!(
resolve_bin_from_package(tmp.path(), "@tanstack/cli"),
Some("tanstack".to_string())
);
}
#[test]
fn resolve_bin_object_with_matching_key_prefers_it() {
let tmp = tempfile::tempdir().unwrap();
write_pkg_json(
tmp.path(),
"foo",
serde_json::json!({
"name": "foo",
"bin": {"foo": "x.js", "foo-helper": "y.js"},
}),
);
assert_eq!(
resolve_bin_from_package(tmp.path(), "foo"),
Some("foo".to_string())
);
}
#[test]
fn resolve_bin_object_multiple_no_match_returns_none() {
let tmp = tempfile::tempdir().unwrap();
write_pkg_json(
tmp.path(),
"foo",
serde_json::json!({
"name": "foo",
"bin": {"a": "a.js", "b": "b.js"},
}),
);
assert_eq!(resolve_bin_from_package(tmp.path(), "foo"), None);
}
#[test]
fn resolve_bin_string_bin_returns_package_tail() {
let tmp = tempfile::tempdir().unwrap();
write_pkg_json(
tmp.path(),
"@scope/foo",
serde_json::json!({
"name": "@scope/foo",
"bin": "./x.js",
}),
);
assert_eq!(
resolve_bin_from_package(tmp.path(), "@scope/foo"),
Some("foo".to_string())
);
}
#[test]
fn resolve_bin_no_bin_field_returns_none() {
let tmp = tempfile::tempdir().unwrap();
write_pkg_json(tmp.path(), "foo", serde_json::json!({"name": "foo"}));
assert_eq!(resolve_bin_from_package(tmp.path(), "foo"), None);
}
#[test]
fn synthesize_dlx_dep_handles_github_shorthand() {
let (name, value) = synthesize_dlx_dep("github:user/repo");
assert_eq!(name, "repo");
assert_eq!(value, "github:user/repo");
}
#[test]
fn synthesize_dlx_dep_handles_github_shorthand_with_ref() {
let (name, value) = synthesize_dlx_dep("github:user/repo#v1.2.3");
assert_eq!(name, "repo");
assert_eq!(value, "github:user/repo#v1.2.3");
}
#[test]
fn synthesize_dlx_dep_handles_scp_url() {
let (name, value) = synthesize_dlx_dep("git@github.com:user/repo.git");
assert_eq!(name, "repo");
assert_eq!(value, "git@github.com:user/repo.git");
}
#[test]
fn synthesize_dlx_dep_handles_scp_url_bitbucket() {
let (name, value) = synthesize_dlx_dep("git@bitbucket.org:pnpmjs/git-resolver.git");
assert_eq!(name, "git-resolver");
assert_eq!(value, "git@bitbucket.org:pnpmjs/git-resolver.git");
}
#[test]
fn synthesize_dlx_dep_rejects_unknown_host_scp() {
let (name, _) = synthesize_dlx_dep("alice@host.example.com:org/repo.git");
assert_ne!(name, "repo");
}
#[test]
fn synthesize_dlx_dep_handles_owner_repo_shorthand() {
let (name, value) = synthesize_dlx_dep("zkochan/is-negative");
assert_eq!(name, "is-negative");
assert_eq!(value, "github:zkochan/is-negative");
}
#[test]
fn synthesize_dlx_dep_handles_owner_repo_shorthand_with_ref() {
let (name, value) = synthesize_dlx_dep("zkochan/is-negative#2.0.1");
assert_eq!(name, "is-negative");
assert_eq!(value, "github:zkochan/is-negative#2.0.1");
}
#[test]
fn synthesize_dlx_dep_handles_git_plus_url() {
let (name, value) = synthesize_dlx_dep("git+https://host/u/r.git#v1");
assert_eq!(name, "r");
assert_eq!(value, "git+https://host/u/r.git#v1");
}
#[test]
fn synthesize_dlx_dep_registry_spec_unchanged() {
let (name, value) = synthesize_dlx_dep("lodash@4.17.0");
assert_eq!(name, "lodash");
assert_eq!(value, "4.17.0");
}
#[test]
fn synthesize_dlx_dep_scoped_registry_spec_unchanged() {
let (name, value) = synthesize_dlx_dep("@babel/core@7.0.0");
assert_eq!(name, "@babel/core");
assert_eq!(value, "7.0.0");
}
#[test]
fn bin_name_for_non_registry_spec_uses_repo_name() {
assert_eq!(bin_name_for("github:user/repo"), "repo");
assert_eq!(bin_name_for("git@github.com:user/repo.git"), "repo");
}
}