Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/zpm-switch/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub async fn run_default() -> ExitCode {
cwd,
args,
version,
} = extract_bin_meta();
} = extract_bin_meta().expect("Failed to extract binary metadata");

if let Some(cwd) = cwd {
set_fake_cwd(cwd);
Expand Down
6 changes: 2 additions & 4 deletions packages/zpm-switch/src/commands/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::process::ExitStatus;
use clipanion::cli;
use zpm_utils::ToFileString;

use crate::{cwd::{get_fake_cwd, get_final_cwd}, errors::Error, manifest::{find_closest_package_manager, validate_package_manager}, yarn::get_default_yarn_version, yarn_enums::ReleaseLine};
use crate::{cwd::{get_final_cwd, restore_args}, errors::Error, manifest::{find_closest_package_manager, validate_package_manager}, yarn::get_default_yarn_version, yarn_enums::ReleaseLine};

use super::switch::explicit::ExplicitCommand;

Expand Down Expand Up @@ -39,9 +39,7 @@ impl ProxyCommand {
= self.args.clone();

// Don't forget to add back the cwd parameter that was removed earlier on!
if let Some(cwd) = get_fake_cwd() {
args.insert(0, cwd.to_file_string());
}
restore_args(&mut args);

ExplicitCommand::run(&reference, &args).await
}
Expand Down
6 changes: 2 additions & 4 deletions packages/zpm-switch/src/commands/switch/explicit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::process::{Command, ExitStatus, Stdio};
use clipanion::cli;
use zpm_utils::ToFileString;

use crate::{cwd::{get_fake_cwd, get_final_cwd}, errors::Error, install::install_package_manager, manifest::{find_closest_package_manager, PackageManagerReference, VersionPackageManagerReference}, yarn::resolve_selector, yarn_enums::Selector};
use crate::{cwd::{get_final_cwd, restore_args}, errors::Error, install::install_package_manager, manifest::{find_closest_package_manager, PackageManagerReference, VersionPackageManagerReference}, yarn::resolve_selector, yarn_enums::Selector};

#[cli::command(proxy)]
#[cli::path("switch")]
Expand Down Expand Up @@ -44,9 +44,7 @@ impl ExplicitCommand {
= self.args.clone();

// Don't forget to add back the cwd parameter that was removed earlier on!
if let Some(cwd) = get_fake_cwd() {
args.insert(0, cwd.to_file_string());
}
restore_args(&mut args);

let version
= resolve_selector(&self.selector).await?;
Expand Down
9 changes: 8 additions & 1 deletion packages/zpm-switch/src/cwd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use std::sync::Mutex;

use zpm_utils::{Path, PathError};
use zpm_utils::{Path, PathError, ToFileString};

static FAKE_CWD: Mutex<Option<Path>> = Mutex::new(None);

Expand All @@ -25,3 +25,10 @@ pub fn get_final_cwd() -> Result<Path, PathError> {
Path::current_dir()
}
}

pub fn restore_args(args: &mut Vec<String>) {
if let Some(cwd) = get_fake_cwd() {
// We add an explicit `--cwd` so that both implicit and explicit cwd arguments are correctly forwarded.
args.insert(0, format!("--cwd={}", cwd.to_file_string()));
}
}
37 changes: 30 additions & 7 deletions packages/zpm-switch/src/yarn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use serde::Deserialize;
use serde_with::serde_as;
use std::{collections::BTreeMap, str::FromStr};
use zpm_semver::{Range, Version};
use zpm_utils::{ExplicitPath, FromFileString, Path, ToFileString};
use zpm_utils::{ExplicitPath, FromFileString, Path, PathError, RawPath, ToFileString};

use crate::{errors::Error, http::fetch, manifest::{PackageManagerReference, VersionPackageManagerReference}, yarn_enums::{ChannelSelector, Selector}};

Expand Down Expand Up @@ -105,29 +105,52 @@ pub fn get_bin_version() -> String {
.to_string()
}

pub fn extract_bin_meta() -> BinMeta {
const CWD_FLAG: &str = "--cwd";
const CWD_FLAG_EQUALS: &str = "--cwd=";

pub fn extract_bin_meta() -> Result<BinMeta, PathError> {
let mut cwd = None;

let mut args = std::env::args()
.skip(1)
.collect::<Vec<_>>();

if args.len() >= 2 && args[0] == CWD_FLAG {
let raw_path
= RawPath::from_str(&args[1])?;

cwd = Some(raw_path.path);
args.drain(..2);
} else if args.len() >= 1 && args[0].starts_with(CWD_FLAG_EQUALS) {
let raw_path
= RawPath::from_str(&args[0][CWD_FLAG_EQUALS.len()..])?;

cwd = Some(raw_path.path);
args.remove(0);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Implicit Path Overrides Explicit --cwd

The implicit path detection logic runs after explicit --cwd flag parsing, allowing a positional path argument to override an explicitly provided --cwd value. For example, yarn --cwd /path1 /path2 command would incorrectly use /path2 instead of /path1. Explicit flags should take precedence over implicit arguments.

Locations (1)

Fix in CursorFix in Web

Copy link
Member Author

@paul-soporan paul-soporan Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually correct (at least according to Yarn's behavior) that yarn --cwd /path1 /path2 command executes the command in /path2.

What does not currently work correctly is yarn --cwd ./a ./b which should execute in ./a/b.

Yarn gets that right because implicit path arguments are simply handled by clipanion, which also enables things like yarn workspace foo ./subfolder exec pwd to work.

I don't think I'll touch this until we look into making the implicit path argument handler a regular clipanion command instead (at least in some cases so that zpm-switch still retains the correct behavior).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we define the --cwd path as a command similar to this?

#[cli::command(proxy)]
struct MyCommand {
  #[cli::option("--cwd")]
  cwd: String,

  rest: Vec<String>,
}

Copy link
Member Author

@paul-soporan paul-soporan Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so - proxies on commands without any path segments or required positional arguments consume all arguments provided, including all options.

i.e. rest will also contain --cwd while cwd will be None.

Oh wait, I misread your comment. So you mean having a special command with a required --cwd option? That might work 🤔

Copy link
Member Author

@paul-soporan paul-soporan Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made both --cwd and implicit cwd (yarn ./path ...) go through clipanion in the case of the zpm binary.

Edit: As a result, both are now supported in forwarded commands (e.g. yarn workspace foo ./subpath exec pwd).

However, for zpm-switch, I'm not sure the complexity is worth it.
Since we need to support both zpm-switch --cwd foo ... / zpm-switch ./foo ... and zpm-switch switch <selector> --cwd foo ... / zpm-switch switch ./foo ..., we'd need to have 4 different clipanion entries for this. 🤔

What do you think?

if let Some(first_arg) = args.first() {
let explicit_path
= ExplicitPath::from_str(first_arg);

if let Ok(explicit_path) = explicit_path {
cwd = Some(explicit_path.raw_path.path);
args.remove(0);
match explicit_path {
Ok(explicit_path) => {
cwd = Some(explicit_path.raw_path.path);
args.remove(0);
},
Err(PathError::InvalidExplicitPathParameter(_)) => {
// This is not a valid explicit path, so we don't modify `cwd`.
},
Err(err) => return Err(err),
}
}

let version
= get_bin_version();

BinMeta {
Ok(BinMeta {
cwd,
args,
version,
}
})
}
23 changes: 23 additions & 0 deletions packages/zpm/src/commands/entries/cwd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::{path::PathBuf, process::ExitCode};

use clipanion::{cli, prelude::Cli};

use crate::{commands::YarnCli, error::Error};

// TODO: Use clipanion to error on incorrect placement of `--cwd` argument.
#[cli::command(default, proxy)]
#[derive(Debug)]
pub struct Cwd {
#[cli::option("--cwd")]
cwd: String,

args: Vec<String>,
}

impl Cwd {
pub fn execute(&self) -> Result<ExitCode, Error> {
std::env::set_current_dir(PathBuf::from(&self.cwd))?;

Ok(YarnCli::run(self.cli_environment.clone().with_argv(self.args.clone())))
}
}
2 changes: 2 additions & 0 deletions packages/zpm/src/commands/entries/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod cwd;
pub mod run;
37 changes: 37 additions & 0 deletions packages/zpm/src/commands/entries/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::{process::ExitCode, str::FromStr};

use clipanion::{cli, prelude::Cli};
use zpm_utils::{ExplicitPath, PathError};

use crate::{commands::YarnCli, error::Error};

#[cli::command(default, proxy)]
#[derive(Debug)]
pub struct Run {
leading_argument: String,

args: Vec<String>,
}

impl Run {
pub fn execute(&self) -> Result<ExitCode, Error> {
match ExplicitPath::from_str(&self.leading_argument) {
Ok(explicit_path) => {
std::env::set_current_dir(explicit_path.raw_path.path.to_path_buf())?;

Ok(YarnCli::run(self.cli_environment.clone().with_argv(self.args.clone())))
},

Err(PathError::InvalidExplicitPathParameter(_)) => {
Ok(YarnCli::run(self.cli_environment.clone().with_argv(
["run".to_owned(), self.leading_argument.clone()]
.into_iter()
.chain(self.args.clone())
.collect()
)))
},

Err(err) => Err(err.into()),
}
}
}
21 changes: 7 additions & 14 deletions packages/zpm/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use std::process::ExitCode;

use clipanion::{prelude::*, program, Environment};
use zpm_macros::track_time;
use zpm_switch::{extract_bin_meta, BinMeta};
use zpm_switch::{extract_bin_meta, get_bin_version, BinMeta};

mod debug;
mod entries;

mod add;
mod bin;
Expand Down Expand Up @@ -33,6 +34,9 @@ program!(YarnCli, [
debug::check_semver_version::CheckSemverVersion,
debug::print_platform::PrintPlatform,

entries::cwd::Cwd,
entries::run::Run,

add::Add,
bin::BinList,
bin::Bin,
Expand All @@ -58,23 +62,12 @@ program!(YarnCli, [

#[track_time]
pub fn run_default() -> ExitCode {
let BinMeta {
cwd,
args,
version,
} = extract_bin_meta();

if let Some(cwd) = cwd {
cwd.sys_set_current_dir()
.expect("Failed to set current directory");
}

let env
= Environment::default()
.with_program_name("Yarn Package Manager".to_string())
.with_binary_name("yarn".to_string())
.with_version(version)
.with_argv(args);
.with_version(get_bin_version())
.with_argv(std::env::args().skip(1).collect());

YarnCli::run(env)
}
2 changes: 1 addition & 1 deletion packages/zpm/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clipanion::cli;

use crate::{error::Error, project, script::ScriptEnvironment};

#[cli::command(default, proxy)]
#[cli::command(proxy)]
#[cli::path("run")]
#[cli::category("Scripting commands")]
#[cli::description("Run a dependency binary or local script")]
Expand Down