1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
use crate::constants::{MULTISHELL_PATH_VAR, PHP_VERSION_FILE};
use crate::{fs, shell, update};
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use dialoguer::{Confirm, Select, theme::ColorfulTheme};
use std::path::Path;
/// Change PHP version
#[derive(Parser, Debug)]
pub struct Use {
/// The version to use (omit for interactive list)
pub version: Option<String>,
/// Skip interactive prompts when the requested version is missing (used by shell hooks).
#[arg(long, hide = true)]
pub silent: bool,
}
impl Use {
pub async fn call(self) -> Result<()> {
let mut version = match self.version {
Some(ref v) => match fs::try_resolve_local_version(v)? {
Some(resolved) => resolved,
None => {
if self.silent {
return Ok(());
}
let prompt = format!(
"PHP {} is not installed locally. Do you want to install it now?",
v.bold()
);
let install_now = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(&prompt)
.default(true)
.interact_opt()?
.unwrap_or(false);
if !install_now {
eprintln!("{} Operation cancelled.", "✗".red());
return Ok(());
}
// Skip install's own "use now?" prompt — we fall through to
// the activation path below with the freshly installed version.
match crate::commands::install::execute_install_with(v, false).await? {
Some(installed) => installed,
None => return Ok(()),
}
}
},
None => {
let mut resolved_version = None;
if let Ok(content) = std::fs::read_to_string(PHP_VERSION_FILE) {
let trimmed = content.trim().to_string();
if !trimmed.is_empty() {
match fs::try_resolve_local_version(&trimmed)? {
Some(resolved) => {
resolved_version = Some(resolved);
}
None => {
if !self.silent {
let prompt = format!(
"PHP {} (from {}) is not installed locally. Do you want to install it now?",
trimmed.bold(),
PHP_VERSION_FILE.bold()
);
let install_now =
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(&prompt)
.default(true)
.interact_opt()?
.unwrap_or(false);
if install_now {
if let Some(installed) =
crate::commands::install::execute_install_with(
&trimmed, false,
)
.await?
{
resolved_version = Some(installed);
}
} else {
eprintln!("{} Operation cancelled.", "✗".red());
return Ok(());
}
} else {
return Ok(());
}
}
}
}
}
if let Some(resolved) = resolved_version {
resolved
} else {
let items = fs::get_aliased_versions()?;
if items.is_empty() {
eprintln!("{} No PHP versions are currently installed.", "💡".yellow());
return Ok(());
}
let displays: Vec<String> = items.iter().map(|i| i.display.clone()).collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select a locally installed PHP version to use")
.default(0)
.items(&displays)
.interact_opt()?;
match selection {
Some(idx) => items[idx].version.clone(),
None => {
eprintln!("{} Operation cancelled.", "✗".red());
return Ok(());
}
}
}
}
};
if let Ok(Some(newer_version)) = update::check_for_updates(&version).await {
let prompt = format!(
"{} A new patch version is available: {} ➜ {}. Do you want to install and use it now?",
"💡".yellow(),
version.dimmed(),
newer_version.green().bold()
);
if Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(&prompt)
.default(true)
.interact_opt()?
.unwrap_or(false)
{
let install_cmd = crate::commands::install::Install {
version: Some(newer_version.clone()),
};
install_cmd.call().await?;
version = newer_version;
}
}
if !fs::is_version_installed(&version)? {
anyhow::bail!(
"PHP {} is not installed. Run 'pvm install {}' first.",
version,
version
);
}
// Smart prompt logic
if Path::new(PHP_VERSION_FILE).exists()
&& let Ok(current_file_ver) = std::fs::read_to_string(PHP_VERSION_FILE)
&& current_file_ver.trim() != version
{
let prompt = format!(
"A {} file is present ({}). Do you want to apply this change to the directory?",
PHP_VERSION_FILE,
current_file_ver.trim().yellow()
);
if Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(&prompt)
.default(false)
.interact_opt()?
.unwrap_or(false)
{
std::fs::write(PHP_VERSION_FILE, &version)
.with_context(|| format!("Failed to update {}", PHP_VERSION_FILE))?;
eprintln!(
"{} Updated {} to {}",
"✓".green(),
PHP_VERSION_FILE,
version.bold()
);
}
}
let bin_dir = fs::get_version_bin_dir(&version)?;
let s = shell::detect_shell();
// These evaluate in the user's shell hook via wrapper
let export_str1 = s.set_env_var(MULTISHELL_PATH_VAR, &bin_dir.to_string_lossy());
let export_str2 = s.path(&bin_dir);
let env_file = fs::get_env_update_path(None)?;
fs::write_env_file_locked(&env_file, &format!("{}\n{}", export_str1, export_str2))?;
// Note: process-global env is intentionally NOT mutated here. std::env::set_var
// is unsound in a multi-threaded tokio runtime, and the wrapper sources env_file
// into the parent shell on exit, so subsequent pvm invocations see the new PATH.
Ok(())
}
}