Skip to content
Draft
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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ vite-plus/
└── crates/vp_trampoline/ # Windows shim trampoline
```

On-disk paths (bin, config, data, state, cache) are resolved centrally via `vp_shared::Dirs` (`crates/vp_shared/src/dirs.rs`) — legacy monolithic `~/.vite-plus` root or split XDG/platform layout; no call site constructs `~/.vite-plus/...` or reads `XDG_*` itself.

`packages/test` is no longer tracked. The public test API is `vite-plus/test*`, generated by `packages/cli/build.ts` as shims over upstream `vitest` and `@vitest/browser*` exports.

## Where to Start
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pnpm bootstrap-cli
vp --version
```

This builds all packages, compiles the Rust `vp` binary, and installs the CLI to `~/.vite-plus`.
This builds all packages, compiles the Rust `vp` binary, and installs the CLI to `~/.vite-plus` (the legacy monolithic layout; on-disk paths are resolved by `vp_shared::Dirs` in `crates/vp_shared/src/dirs.rs`).

To switch back to a release version, use `vp upgrade --force` (`current` points to `local-dev-*` but the binary version may still match the release, so `--force` is needed)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ d="$(dirname "$(dirname "$(dirname "$0")")")"
__vp_shell=/bin/sh
[ -x "$__vp_shell" ] || __vp_shell=$(command -v sh)

if [ -n "${VP_HOME-}" ]; then
if [ -n "${VP_BIN_DIR-}" ]; then
__vp_bin="$VP_BIN_DIR"
elif [ -n "${VP_HOME-}" ]; then
__vp_bin="$VP_HOME/bin"
elif [ -n "${HOME-}" ]; then
elif [ -n "${HOME-}" ] && [ -d "$HOME/.vite-plus/bin" ]; then
__vp_bin="$HOME/.vite-plus/bin"
elif [ -n "${HOME-}" ]; then
__vp_bin="$HOME/.local/bin"
else
__vp_bin=""
fi
Expand Down
20 changes: 11 additions & 9 deletions crates/vp_command/src/ps1_shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ use vt_powershell::{POWERSHELL_PREFIX, find_ps1_sibling, is_stdin_terminal, powe
/// - no `PowerShell` host (`pwsh.exe` or `powershell.exe`) is on PATH,
/// - stdin is not a terminal (the `.ps1` wrappers hang on piped/null
/// stdin and the Ctrl+C concern doesn't apply without a TTY),
/// - the resolved path is outside `$VP_HOME` (or `$VP_HOME` is
/// unresolvable) AND not under any `node_modules/.bin/`,
/// - the resolved path is outside the vite-plus install root
/// AND not under any `node_modules/.bin/`,
/// - the resolved path is not a `.cmd` (case-insensitive),
/// - the `.cmd` has no sibling `.ps1`.
#[must_use]
Expand All @@ -61,16 +61,18 @@ pub fn rewrite_cmd_to_powershell(
rewrite_in_scope(resolved, vp_home().map(AsRef::as_ref), host, is_stdin_terminal())
}

/// Cached `$VP_HOME` (`~/.vite-plus` by default; overridable via env var).
/// Returns `None` if `vp_shared::get_vp_home()` failed; the rewrite still
/// applies to `node_modules/.bin/*.cmd` paths in that case (the two scopes
/// are independent).
/// Cached vite-plus install root (`~/.vite-plus` under the legacy layout; the
/// data directory under the split layout).
///
/// The returned value is always `Some`; the `Option` only exists because the
/// rewrite scope check also applies to `node_modules/.bin/*.cmd` paths, which
/// are independent of the install root.
fn vp_home() -> Option<&'static AbsolutePathBuf> {
use std::sync::LazyLock;

static VP_HOME: LazyLock<Option<AbsolutePathBuf>> =
LazyLock::new(|| vp_shared::get_vp_home().ok());
VP_HOME.as_ref()
static INSTALL_ROOT: LazyLock<AbsolutePathBuf> =
LazyLock::new(|| vp_shared::Dirs::get().data_dir());
Some(&INSTALL_ROOT)
}

/// Pure rewrite logic. Factored out so tests can drive it on any platform
Expand Down
6 changes: 3 additions & 3 deletions crates/vp_global_cli/src/commands/env/bin_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use serde::{Deserialize, Serialize};
use vt_path::AbsolutePathBuf;

use super::config::get_vp_home;
use crate::error::Error;

/// Source that installed a binary.
Expand Down Expand Up @@ -52,9 +51,10 @@ impl BinConfig {
Self { name, package, version: String::new(), node_version, source: BinSource::Npm }
}

/// Get the bins directory path (~/.vite-plus/bins/).
/// Get the bins directory path (`<data>/bins/`; `~/.vite-plus/bins/` under
/// the legacy layout — identical on disk).
pub fn bins_dir() -> Result<AbsolutePathBuf, Error> {
Ok(get_vp_home()?.join("bins"))
Ok(vp_shared::Dirs::get().bins_dir())
}

/// Get the path to a binary's config file.
Expand Down
8 changes: 4 additions & 4 deletions crates/vp_global_cli/src/commands/env/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use crate::error::Error;

/// Execute the clean command.
pub async fn execute(cwd: AbsolutePathBuf) -> Result<ExitStatus, Error> {
let home_dir = vp_shared::get_vp_home()?;
let node_dir = home_dir.join("js_runtime").join("node");
let package_manager_dir = home_dir.join("package_manager");
let dirs = vp_shared::Dirs::get();
let node_dir = dirs.js_runtime_dir().join("node");
let package_manager_dir = dirs.package_manager_dir();
let protected_versions = protected_node_versions(&cwd).await?;

let corepack_cleaned = run_corepack_cache_clean(&cwd).await?;
Expand Down Expand Up @@ -138,7 +138,7 @@ async fn corepack_cache_clean_would_auto_install(
cwd: &AbsolutePathBuf,
corepack_path: &AbsolutePath,
) -> Result<bool, Error> {
let bin_dir = config::get_bin_dir()?;
let bin_dir = vp_shared::Dirs::get().bin_dir();
if corepack_path.parent() != Some(&bin_dir) {
return Ok(false);
}
Expand Down
61 changes: 16 additions & 45 deletions crates/vp_global_cli/src/commands/env/config.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
//! Configuration and version resolution for the env command.
//!
//! This module provides:
//! - VP_HOME path resolution
//! - Version resolution with priority order
//! - Config file management
//!
//! On-disk locations come from [`vp_shared::Dirs`].

use serde::{Deserialize, Serialize};
use vp_js_runtime::{
NodeProvider, VersionSource, is_valid_version, normalize_version, read_nvmrc_file,
read_package_json, resolve_node_version,
};
use vp_shared::Dirs;
use vt_path::{AbsolutePath, AbsolutePathBuf};

use crate::error::Error;

/// Config file name
const CONFIG_FILE: &str = "config.json";

/// Shim mode determines how shims resolve tools.
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -61,23 +60,6 @@ pub struct VersionResolution {
pub is_range: bool,
}

/// Get the VP_HOME directory path.
///
/// Uses `VP_HOME` environment variable if set, otherwise defaults to `~/.vite-plus`.
pub fn get_vp_home() -> Result<AbsolutePathBuf, Error> {
Ok(vp_shared::get_vp_home()?)
}

/// Get the bin directory path (~/.vite-plus/bin/).
pub fn get_bin_dir() -> Result<AbsolutePathBuf, Error> {
Ok(get_vp_home()?.join("bin"))
}

/// Get the packages directory path (~/.vite-plus/packages/).
pub fn get_packages_dir() -> Result<AbsolutePathBuf, Error> {
Ok(get_vp_home()?.join("packages"))
}

/// Get the node_modules directory path for a package.
///
/// npm uses different layouts on Unix vs Windows:
Expand Down Expand Up @@ -110,14 +92,9 @@ pub fn get_node_modules_dir(prefix: &AbsolutePath, package_name: &str) -> Absolu
}
}

/// Get the config file path.
pub fn get_config_path() -> Result<AbsolutePathBuf, Error> {
Ok(get_vp_home()?.join(CONFIG_FILE))
}

/// Load configuration from disk.
pub async fn load_config() -> Result<Config, Error> {
let config_path = get_config_path()?;
let config_path = Dirs::get().config_file();

if !tokio::fs::try_exists(&config_path).await.unwrap_or(false) {
return Ok(Config::default());
Expand All @@ -130,11 +107,11 @@ pub async fn load_config() -> Result<Config, Error> {

/// Save configuration to disk.
pub async fn save_config(config: &Config) -> Result<(), Error> {
let config_path = get_config_path()?;
let vite_plus_home = get_vp_home()?;
let dirs = Dirs::get();
let config_path = dirs.config_file();

// Ensure directory exists
tokio::fs::create_dir_all(&vite_plus_home).await?;
tokio::fs::create_dir_all(&dirs.config_dir()).await?;

let content = serde_json::to_string_pretty(config)?;
tokio::fs::write(&config_path, content).await?;
Expand All @@ -148,30 +125,25 @@ pub const VERSION_ENV_VAR: &str = vp_shared::env_vars::VP_NODE_VERSION;
/// Session version file name, written by `vp env use` so shims work without the shell eval wrapper.
pub const SESSION_VERSION_FILE: &str = ".session-node-version";

/// Get the path to the session version file (~/.vite-plus/.session-node-version).
pub fn get_session_version_path() -> Result<AbsolutePathBuf, Error> {
Ok(get_vp_home()?.join(SESSION_VERSION_FILE))
}

/// Read the session version file. Returns `None` if the file is missing or empty.
pub async fn read_session_version() -> Option<String> {
let path = get_session_version_path().ok()?;
let path = Dirs::get().session_node_version_file();
let content = tokio::fs::read_to_string(&path).await.ok()?;
let trimmed = content.trim().to_string();
if trimmed.is_empty() { None } else { Some(trimmed) }
}

/// Read the session version file synchronously. Returns `None` if the file is missing or empty.
pub fn read_session_version_sync() -> Option<String> {
let path = get_session_version_path().ok()?;
let path = Dirs::get().session_node_version_file();
let content = std::fs::read_to_string(path.as_path()).ok()?;
let trimmed = content.trim().to_string();
if trimmed.is_empty() { None } else { Some(trimmed) }
}

/// Write the resolved version to the session version file.
pub async fn write_session_version(version: &str) -> Result<(), Error> {
let path = get_session_version_path()?;
let path = Dirs::get().session_node_version_file();
// Ensure parent directory exists
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
Expand All @@ -182,7 +154,7 @@ pub async fn write_session_version(version: &str) -> Result<(), Error> {

/// Delete the session version file. Ignores "not found" errors.
pub async fn delete_session_version() -> Result<(), Error> {
let path = get_session_version_path()?;
let path = Dirs::get().session_node_version_file();
match tokio::fs::remove_file(&path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Expand Down Expand Up @@ -221,7 +193,7 @@ pub async fn resolve_version(cwd: &AbsolutePath) -> Result<VersionResolution, Er
return Ok(VersionResolution {
version: session_version,
source: SESSION_VERSION_FILE.into(),
source_path: get_session_version_path().ok(),
source_path: Some(Dirs::get().session_node_version_file()),
project_root: None,
is_range: false,
});
Expand Down Expand Up @@ -373,7 +345,7 @@ pub async fn resolve_version_from_files(cwd: &AbsolutePath) -> Result<VersionRes
version: resolved,
source: "default".into(),
// Don't set source_path for aliases (lts, latest) so cache can refresh
source_path: if is_alias { None } else { Some(get_config_path()?) },
source_path: if is_alias { None } else { Some(Dirs::get().config_file()) },
project_root: None,
is_range,
});
Expand Down Expand Up @@ -1102,7 +1074,7 @@ mod tests {
));

// Write empty content
let path = get_session_version_path().unwrap();
let path = Dirs::get().session_node_version_file();
tokio::fs::create_dir_all(path.parent().unwrap()).await.unwrap();
tokio::fs::write(&path, "").await.unwrap();

Expand All @@ -1125,7 +1097,7 @@ mod tests {
write_session_version("20.18.0").await.unwrap();

// Overwrite with whitespace-padded content
let path = get_session_version_path().unwrap();
let path = Dirs::get().session_node_version_file();
tokio::fs::write(&path, " 20.18.0 \n").await.unwrap();

assert_eq!(read_session_version().await.as_deref(), Some("20.18.0"));
Expand Down Expand Up @@ -1192,8 +1164,7 @@ mod tests {
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
let _guard = vp_shared::EnvConfig::test_guard(vp_shared::EnvConfig {
node_version: Some("24.0.0".into()),
vite_plus_home: Some(temp_dir.path().into()),
..vp_shared::EnvConfig::for_test()
..vp_shared::EnvConfig::for_test_with_home(temp_dir.path())
});

// Write session version file with different version
Expand Down
5 changes: 2 additions & 3 deletions crates/vp_global_cli/src/commands/env/current.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Err
let resolution = resolve_version(&cwd).await?;
let package_manager = resolve_package_manager_info(&cwd);

// Get the home directory for this version
let home_dir =
vp_shared::get_vp_home()?.join("js_runtime").join("node").join(&resolution.version);
// Get the install directory for this version
let home_dir = vp_shared::Dirs::get().js_runtime_dir().join("node").join(&resolution.version);

#[cfg(windows)]
let (node_path, npm_path, npx_path) =
Expand Down
4 changes: 2 additions & 2 deletions crates/vp_global_cli/src/commands/env/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::process::ExitStatus;

use vt_path::AbsolutePathBuf;

use super::config::{get_config_path, load_config, save_config};
use super::config::{load_config, save_config};
use crate::error::Error;

/// Execute the default command.
Expand All @@ -24,7 +24,7 @@ async fn show_default() -> Result<ExitStatus, Error> {
match config.default_node_version {
Some(version) => {
println!("Default Node.js version: {version}");
let config_path = get_config_path()?;
let config_path = vp_shared::Dirs::get().config_file();
println!(" Set via: {}", config_path.as_path().display());

// If it's an alias, also show the resolved version
Expand Down
Loading
Loading