Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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()));
}
}
38 changes: 31 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,53 @@ 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<_>>();

// TODO: Use clipanion to error on incorrect placement of `--cwd` argument.
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,
}
})
}
2 changes: 1 addition & 1 deletion packages/zpm/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub fn run_default() -> ExitCode {
cwd,
args,
version,
} = extract_bin_meta();
} = extract_bin_meta().expect("Failed to extract binary metadata");

if let Some(cwd) = cwd {
cwd.sys_set_current_dir()
Expand Down