Skip to content

Commit f821e80

Browse files
authored
Merge branch 'main' into feat/agent-guide
2 parents a96563a + de5f8d9 commit f821e80

4 files changed

Lines changed: 252 additions & 28 deletions

File tree

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 213 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//! - ~/.vite-plus/current/ - Contains the actual vp CLI binary
66
//!
77
//! On Unix:
8-
//! - bin/vp is a symlink to ../current/bin/vp
9-
//! - bin/node, bin/npm, bin/npx are symlinks to ../current/bin/vp
8+
//! - bin/vp is a symlink to the active vp binary
9+
//! - bin/node, bin/npm, bin/npx are symlinks to the active vp binary
1010
//! - Symlinks preserve argv[0], allowing tool detection via the symlink name
1111
//!
1212
//! On Windows:
@@ -88,7 +88,7 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
8888
.map_err(|e| Error::ConfigError(format!("Cannot find current executable: {e}").into()))?;
8989

9090
// Create wrapper script in bin/
91-
setup_vp_wrapper(&bin_dir, refresh).await?;
91+
setup_vp_wrapper(&current_exe, &bin_dir, refresh).await?;
9292

9393
// Create shims for node, npm, npx
9494
let mut created = Vec::new();
@@ -144,30 +144,44 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
144144
Ok(ExitStatus::default())
145145
}
146146

147-
/// Create symlink in bin/ that points to current/bin/vp.
148-
async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> Result<(), Error> {
147+
/// Create symlink in bin/ that points to the active vp binary.
148+
async fn setup_vp_wrapper(
149+
current_exe: &std::path::Path,
150+
bin_dir: &vite_path::AbsolutePath,
151+
refresh: bool,
152+
) -> Result<(), Error> {
149153
#[cfg(unix)]
150154
{
151155
let bin_vp = bin_dir.join("vp");
152-
153-
// Create symlink bin/vp -> ../current/bin/vp
154-
let should_create_symlink = refresh
155-
|| !tokio::fs::try_exists(&bin_vp).await.unwrap_or(false)
156-
|| !is_symlink(&bin_vp).await; // Replace non-symlink with symlink
156+
let target = resolve_unix_vp_shim_target(current_exe, bin_dir).await?;
157+
let existing = tokio::fs::symlink_metadata(&bin_vp).await.ok();
158+
159+
let should_create_symlink = match existing.as_ref() {
160+
Some(metadata) if refresh || !metadata.file_type().is_symlink() => true,
161+
Some(_) => {
162+
let broken_symlink = !std::fs::exists(bin_vp.as_path()).unwrap_or(false);
163+
let wrong_target = tokio::fs::read_link(&bin_vp)
164+
.await
165+
.map(|existing_target| existing_target != target)
166+
.unwrap_or(true);
167+
broken_symlink || wrong_target
168+
}
169+
None => true,
170+
};
157171

158172
if should_create_symlink {
159173
// Remove existing if present (could be old wrapper script or file)
160-
if tokio::fs::try_exists(&bin_vp).await.unwrap_or(false) {
174+
if existing.is_some() {
161175
tokio::fs::remove_file(&bin_vp).await?;
162176
}
163-
// Create relative symlink
164-
tokio::fs::symlink("../current/bin/vp", &bin_vp).await?;
165-
tracing::debug!("Created symlink {:?} -> ../current/bin/vp", bin_vp);
177+
tokio::fs::symlink(&target, &bin_vp).await?;
178+
tracing::debug!("Created symlink {:?} -> {:?}", bin_vp, target);
166179
}
167180
}
168181

169182
#[cfg(windows)]
170183
{
184+
let _ = current_exe;
171185
let bin_vp_exe = bin_dir.join("vp.exe");
172186

173187
// Create trampoline bin/vp.exe that forwards to current\bin\vp.exe
@@ -195,13 +209,23 @@ async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> R
195209
Ok(())
196210
}
197211

198-
/// Check if a path is a symlink.
199212
#[cfg(unix)]
200-
async fn is_symlink(path: &vite_path::AbsolutePath) -> bool {
201-
match tokio::fs::symlink_metadata(path).await {
202-
Ok(m) => m.file_type().is_symlink(),
203-
Err(_) => false,
213+
pub(crate) async fn resolve_unix_vp_shim_target(
214+
current_exe: &std::path::Path,
215+
bin_dir: &vite_path::AbsolutePath,
216+
) -> Result<std::path::PathBuf, Error> {
217+
if let Some(vite_plus_home) = bin_dir.parent() {
218+
let standalone_vp = vite_plus_home.join("current").join("bin").join("vp");
219+
if tokio::fs::try_exists(&standalone_vp).await.unwrap_or(false) {
220+
let standalone_vp = tokio::fs::canonicalize(&standalone_vp).await.ok();
221+
let current_exe = tokio::fs::canonicalize(current_exe).await.ok();
222+
if standalone_vp.is_some() && standalone_vp == current_exe {
223+
return Ok(std::path::PathBuf::from("../current/bin/vp"));
224+
}
225+
}
204226
}
227+
228+
Ok(current_exe.to_path_buf())
205229
}
206230

207231
/// Create a single shim for node/npm/npx.
@@ -215,9 +239,31 @@ async fn create_shim(
215239
) -> Result<bool, Error> {
216240
let shim_path = bin_dir.join(shim_filename(tool));
217241

218-
// Check if shim already exists
219-
if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) {
220-
if !refresh {
242+
#[cfg(unix)]
243+
let desired_target = resolve_unix_vp_shim_target(source, bin_dir).await?;
244+
245+
let existing = tokio::fs::symlink_metadata(&shim_path).await.ok();
246+
if existing.is_some() {
247+
let should_replace = if refresh {
248+
true
249+
} else {
250+
#[cfg(unix)]
251+
{
252+
existing.as_ref().is_some_and(|metadata| metadata.file_type().is_symlink())
253+
&& (!std::fs::exists(shim_path.as_path()).unwrap_or(false)
254+
|| tokio::fs::read_link(&shim_path)
255+
.await
256+
.map(|existing_target| existing_target != desired_target)
257+
.unwrap_or(true))
258+
}
259+
260+
#[cfg(windows)]
261+
{
262+
false
263+
}
264+
};
265+
266+
if !should_replace {
221267
return Ok(false);
222268
}
223269
#[cfg(windows)]
@@ -255,19 +301,22 @@ fn shim_filename(tool: &str) -> String {
255301
}
256302
}
257303

258-
/// Create a Unix shim using symlink to ../current/bin/vp.
304+
/// Create a Unix shim using symlink to the active vp binary.
259305
///
260306
/// Symlinks preserve argv[0], allowing the vp binary to detect which tool
261307
/// was invoked. This is the same pattern used by Volta.
262308
#[cfg(unix)]
263309
async fn create_unix_shim(
264-
_source: &std::path::Path,
310+
source: &std::path::Path,
265311
shim_path: &vite_path::AbsolutePath,
266-
_tool: &str,
312+
tool: &str,
267313
) -> Result<(), Error> {
268-
// Create symlink to ../current/bin/vp (relative path)
269-
tokio::fs::symlink("../current/bin/vp", shim_path).await?;
270-
tracing::debug!("Created symlink shim at {:?} -> ../current/bin/vp", shim_path);
314+
let bin_dir = shim_path.parent().ok_or_else(|| {
315+
Error::ConfigError(format!("Cannot find parent directory for {tool} shim").into())
316+
})?;
317+
let target = resolve_unix_vp_shim_target(source, bin_dir).await?;
318+
tokio::fs::symlink(&target, shim_path).await?;
319+
tracing::debug!("Created symlink shim at {:?} -> {:?}", shim_path, target);
271320

272321
Ok(())
273322
}
@@ -1086,6 +1135,142 @@ mod tests {
10861135
assert!(fresh_home.join("env.ps1").exists(), "env.ps1 file should be created");
10871136
}
10881137

1138+
#[tokio::test]
1139+
#[cfg(unix)]
1140+
async fn test_unix_vp_shim_target_prefers_standalone_layout_for_current_exe() {
1141+
let temp_dir = TempDir::new().unwrap();
1142+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1143+
let bin_dir = home.join("bin");
1144+
let standalone_vp = home.join("current").join("bin").join("vp");
1145+
1146+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1147+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1148+
tokio::fs::write(&standalone_vp, b"vp").await.unwrap();
1149+
1150+
let target = resolve_unix_vp_shim_target(standalone_vp.as_path(), &bin_dir).await.unwrap();
1151+
1152+
assert_eq!(target, std::path::Path::new("../current/bin/vp"));
1153+
}
1154+
1155+
#[tokio::test]
1156+
#[cfg(unix)]
1157+
async fn test_unix_vp_shim_target_uses_current_exe_when_standalone_is_stale() {
1158+
let temp_dir = TempDir::new().unwrap();
1159+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1160+
let bin_dir = home.join("bin");
1161+
let standalone_vp = home.join("current").join("bin").join("vp");
1162+
let external_vp = temp_dir.path().join("external-vp");
1163+
1164+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1165+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1166+
tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap();
1167+
tokio::fs::write(&external_vp, b"active-vp").await.unwrap();
1168+
1169+
let target = resolve_unix_vp_shim_target(&external_vp, &bin_dir).await.unwrap();
1170+
1171+
assert_eq!(target, external_vp);
1172+
}
1173+
1174+
#[tokio::test]
1175+
#[cfg(unix)]
1176+
async fn test_unix_vp_shim_target_uses_current_exe_without_standalone_layout() {
1177+
let temp_dir = TempDir::new().unwrap();
1178+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1179+
let bin_dir = home.join("bin");
1180+
let external_vp = temp_dir.path().join("external-vp");
1181+
1182+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1183+
tokio::fs::write(&external_vp, b"vp").await.unwrap();
1184+
1185+
let target = resolve_unix_vp_shim_target(&external_vp, &bin_dir).await.unwrap();
1186+
1187+
assert_eq!(target, external_vp);
1188+
}
1189+
1190+
#[tokio::test]
1191+
#[cfg(unix)]
1192+
async fn test_create_shim_replaces_stale_unix_symlink_without_refresh() {
1193+
let temp_dir = TempDir::new().unwrap();
1194+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1195+
let bin_dir = home.join("bin");
1196+
let standalone_vp = home.join("current").join("bin").join("vp");
1197+
let external_vp = temp_dir.path().join("external-vp");
1198+
let node_shim = bin_dir.join("node");
1199+
1200+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1201+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1202+
tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap();
1203+
tokio::fs::write(&external_vp, b"active-vp").await.unwrap();
1204+
tokio::fs::symlink("../current/bin/vp", &node_shim).await.unwrap();
1205+
1206+
let created = create_shim(&external_vp, &bin_dir, "node", false).await.unwrap();
1207+
let target = tokio::fs::read_link(&node_shim).await.unwrap();
1208+
1209+
assert!(created, "stale shims should be recreated");
1210+
assert_eq!(target, external_vp);
1211+
}
1212+
1213+
#[tokio::test]
1214+
#[cfg(unix)]
1215+
async fn test_create_shim_replaces_broken_unix_symlink_without_refresh() {
1216+
let temp_dir = TempDir::new().unwrap();
1217+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1218+
let bin_dir = home.join("bin");
1219+
let external_vp = temp_dir.path().join("external-vp");
1220+
let node_shim = bin_dir.join("node");
1221+
1222+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1223+
tokio::fs::write(&external_vp, b"vp").await.unwrap();
1224+
tokio::fs::symlink("../current/bin/vp", &node_shim).await.unwrap();
1225+
1226+
let created = create_shim(&external_vp, &bin_dir, "node", false).await.unwrap();
1227+
let target = tokio::fs::read_link(&node_shim).await.unwrap();
1228+
1229+
assert!(created, "broken shims should be recreated");
1230+
assert_eq!(target, external_vp);
1231+
}
1232+
1233+
#[tokio::test]
1234+
#[cfg(unix)]
1235+
async fn test_setup_vp_wrapper_replaces_stale_unix_symlink_without_refresh() {
1236+
let temp_dir = TempDir::new().unwrap();
1237+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1238+
let bin_dir = home.join("bin");
1239+
let standalone_vp = home.join("current").join("bin").join("vp");
1240+
let external_vp = temp_dir.path().join("external-vp");
1241+
let vp_shim = bin_dir.join("vp");
1242+
1243+
tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap();
1244+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1245+
tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap();
1246+
tokio::fs::write(&external_vp, b"active-vp").await.unwrap();
1247+
tokio::fs::symlink("../current/bin/vp", &vp_shim).await.unwrap();
1248+
1249+
setup_vp_wrapper(&external_vp, &bin_dir, false).await.unwrap();
1250+
let target = tokio::fs::read_link(&vp_shim).await.unwrap();
1251+
1252+
assert_eq!(target, external_vp);
1253+
}
1254+
1255+
#[tokio::test]
1256+
#[cfg(unix)]
1257+
async fn test_setup_vp_wrapper_replaces_broken_unix_symlink_without_refresh() {
1258+
let temp_dir = TempDir::new().unwrap();
1259+
let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap();
1260+
let bin_dir = home.join("bin");
1261+
let external_vp = temp_dir.path().join("external-vp");
1262+
let vp_shim = bin_dir.join("vp");
1263+
1264+
tokio::fs::create_dir_all(&bin_dir).await.unwrap();
1265+
tokio::fs::write(&external_vp, b"vp").await.unwrap();
1266+
tokio::fs::symlink("../current/bin/vp", &vp_shim).await.unwrap();
1267+
1268+
setup_vp_wrapper(&external_vp, &bin_dir, false).await.unwrap();
1269+
let target = tokio::fs::read_link(&vp_shim).await.unwrap();
1270+
1271+
assert_eq!(target, external_vp);
1272+
}
1273+
10891274
#[tokio::test]
10901275
async fn test_create_env_files_contains_dynamic_completion() {
10911276
let temp_dir = TempDir::new().unwrap();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
const expected = path.resolve('external/vp');
5+
6+
for (const shim of ['vp', 'node', 'npm', 'npx', 'vpx', 'vpr']) {
7+
const shimPath = path.join('home', 'bin', shim);
8+
const target = fs.readlinkSync(shimPath);
9+
if (target !== expected) {
10+
throw new Error(`${shim} points to ${target}, expected ${expected}`);
11+
}
12+
}
13+
14+
console.log('all shims point to external vp');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
> mkdir -p external home # Prepare isolated external install and VP_HOME
2+
> cp "$VP_HOME/bin/vp" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME
3+
> printf '22.18.0\n' > .node-version # Project Node.js version
4+
> mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\necho vp-managed-node-22.18.0\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime
5+
> VP_HOME="$(pwd)/home" ./external/vp env setup # Setup shims from external vp
6+
> VP_BYPASS="$VP_HOME/bin" node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp
7+
all shims point to external vp
8+
9+
> VP_HOME="$(pwd)/home" PATH="$(pwd)/home/bin:$PATH" node -v # node shim uses the project version
10+
vp-managed-node-22.18.0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"ignoredPlatforms": ["win32"],
3+
"commands": [
4+
"mkdir -p external home # Prepare isolated external install and VP_HOME",
5+
"cp \"$VP_HOME/bin/vp\" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME",
6+
"printf '22.18.0\\n' > .node-version # Project Node.js version",
7+
"mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\\necho vp-managed-node-22.18.0\\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime",
8+
{
9+
"command": "VP_HOME=\"$(pwd)/home\" ./external/vp env setup # Setup shims from external vp",
10+
"ignoreOutput": true
11+
},
12+
"VP_BYPASS=\"$VP_HOME/bin\" node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp",
13+
"VP_HOME=\"$(pwd)/home\" PATH=\"$(pwd)/home/bin:$PATH\" node -v # node shim uses the project version"
14+
]
15+
}

0 commit comments

Comments
 (0)