File Coverage

blib/lib/NVMPL/Switcher.pm
Criterion Covered Total %
statement 85 133 63.9
branch 23 64 35.9
condition 5 23 21.7
subroutine 13 15 86.6
pod 0 3 0.0
total 126 238 52.9


line stmt bran cond sub pod time code
1             package NVMPL::Switcher;
2 4     4   138850 use strict;
  4         8  
  4         153  
3 4     4   21 use warnings;
  4         20  
  4         254  
4 4     4   28 use feature 'say';
  4         14  
  4         471  
5 4     4   41 use File::Spec;
  4         8  
  4         150  
6 4     4   511 use NVMPL::Config;
  4         18  
  4         156  
7 4     4   1182 use NVMPL::Utils qw(detect_platform);
  4         11  
  4         15258  
8              
9             # ---------------------------------------------------------
10             # Public entry point
11             # ---------------------------------------------------------
12              
13             sub _detect_shell {
14             # --- Windows shell detection first ---
15 1 50   1   7 if ($^O eq 'MSWin32') {
16             # PowerShell defines PSModulePath, CMD defines ComSpec
17 0 0 0     0 if ($ENV{PSModulePath}) {
    0          
18 0         0 return 'PowerShell';
19             } elsif ($ENV{ComSpec} && $ENV{ComSpec} =~ /cmd\.exe/i) {
20 0         0 return 'Cmd';
21             } else {
22 0         0 return 'PowerShell'; # safe default on Windows
23             }
24             }
25              
26             # --- Unix-like environments ---
27 1   50     9 my $shell = $ENV{SHELL} || '';
28 1 50       7 if ($shell =~ /zsh/i) {
    50          
29 0         0 return 'Zsh';
30             } elsif ($shell =~ /bash/i) {
31 0         0 return 'Bash';
32             } else {
33 1         5 return 'Bash'; # fallback for unknown shells
34             }
35             }
36              
37             sub use_version {
38 2     2 0 13896 my ($version) = @_;
39 2 50       13 unless ($version) {
40 0         0 say "Usage: nvm-pl use ";
41 0         0 return 1;
42             }
43              
44 2         10 $version =~ s/^V//;
45 2         7 my $vtag = "v$version";
46              
47 2         22 my $cfg = NVMPL::Config->load();
48 2         17 my $install_dir = $cfg->{install_dir};
49 2         27 my $versions_dir = File::Spec->catdir($install_dir, 'versions');
50 2         15 my $target_dir = File::Spec->catdir($versions_dir, $vtag);
51              
52             # --- Windows nested node folder fix ---
53 2 50       13 if ($^O eq 'MSWin32') {
54 0         0 my @matches = glob("$target_dir\\node-v*-win-x64");
55 0 0 0     0 if (@matches && -d $matches[0]) {
56 0         0 $target_dir = $matches[0];
57             }
58             }
59              
60 2         20 my $current_link = File::Spec->catfile($versions_dir, 'current');
61              
62 2 100       139 unless (-d $target_dir) {
63 1         79 say "[nvm-pl] Version $vtag is not installed.";
64 1         9 return 1;
65             }
66              
67 1 50 33     23 if (-l $current_link || -d $current_link) {
68 0 0       0 unlink $current_link or warn "[nvm-pl] Could not remove existing 'current': $!";
69             }
70              
71 1 50       8 if ($^O =~ /MSWin/) {
72 0         0 _win_junction($current_link, $target_dir);
73             } else {
74 1 50       101 symlink($target_dir, $current_link)
75             or die "[nvm-pl] Failed to create symlink: $!";
76             }
77              
78 1         21 _update_shell_config($current_link);
79              
80 1         46 say "[nvm-pl] Active version is now $vtag";
81              
82             # --- PowerShell live PATH update ---
83 1 0 33     10 if ($^O eq 'MSWin32' && $ENV{PSModulePath}) {
84 0         0 my $ps_path = $target_dir;
85 0         0 $ps_path =~ s#/#\\#g;
86              
87             # only update if node.exe isn't already on PATH
88 0         0 my $node_check = `where node 2> NUL`;
89 0 0       0 if ($node_check !~ /\Q$ps_path\E/i) {
90             # Detect which PowerShell to use
91 0         0 my $ps_exe = `where pwsh 2> NUL`;
92 0         0 chomp($ps_exe);
93 0 0       0 if (!$ps_exe) {
94 0         0 $ps_exe = `where powershell 2> NUL`;
95 0         0 chomp($ps_exe);
96             }
97              
98 0 0       0 if ($ps_exe) {
99 0         0 say "[nvm-pl] (PowerShell) Updating PATH for current session...";
100 0         0 system($ps_exe, "-NoLogo", "-NoProfile", "-Command",
101             "\$env:PATH = '$ps_path;' + \$env:PATH; Write-Host 'PATH updated for this session.'");
102             } else {
103 0         0 say "[nvm-pl] (PowerShell) Could not find PowerShell executable — skipping live PATH update.";
104             }
105             } else {
106 0         0 say "[nvm-pl] (PowerShell) Node path already active.";
107             }
108             }
109              
110 1         20 say "Restart your shell or run the appropriate source command for your shell.";
111 1         12 return 0;
112             }
113              
114              
115             sub _update_shell_config {
116 1     1   4 my ($current_link) = @_;
117 1         5 my $shell_type = _detect_shell();
118              
119             # Load the appropriate shell module dynamically
120 1         5 my $shell_module = "NVMPL::Shell::$shell_type";
121 1 50       123 eval "require $shell_module" or do {
122 0         0 warn "[nvm-pl] Could not load $shell_module: $@";
123 0         0 return;
124             };
125              
126 1         13 my $init_snippet = $shell_module->init_snippet();
127 1         6 my $config_file = _get_shell_config($shell_type);
128 1         5 _write_shell_config($config_file, $init_snippet, $shell_type);
129             }
130              
131             sub _get_shell_config {
132 1     1   5 my ($shell_type) = @_;
133 1   33     7 my $home = $ENV{HOME} // $ENV{USERPROFILE};
134              
135             return {
136             Bash => "$home/.bashrc",
137             Zsh => "$home/.zshrc",
138             Cmd => "$home/.cmdrc",
139             PowerShell => "$home/Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1"
140 1   33     17 }->{$shell_type} || "$home/.bashrc";
141             }
142              
143             sub _write_shell_config {
144 1     1   5 my ($config_file, $init_snippet, $shell_type) = @_;
145 1 50       4 return unless $config_file;
146              
147             # Read existing config
148 1         3 my @lines;
149 1 50       108 if (-f $config_file) {
150 1 50       57 open my $in, '<', $config_file or return;
151 1         128 @lines = <$in>;
152 1         22 close $in;
153             }
154              
155             # Remove ONLY the nvm-pl managed section (more precise)
156 1         3 my @clean_lines;
157 1         3 my $in_nvm_section = 0;
158              
159 1         3 foreach my $line (@lines) {
160             # Detect start of nvm-pl section
161 99 50       194 if ($line =~ /^# nvm-pl managed Node\.js path$/) {
162 0         0 $in_nvm_section = 1;
163 0         0 next;
164             }
165              
166             # Skip lines until we're out of the nvm-pl section
167 99 50       168 if ($in_nvm_section) {
168 0 0 0     0 if ($line =~ /^\s*$/ || $line =~ /^#/) {
169 0         0 $in_nvm_section = 0;
170             } else {
171 0         0 next;
172             }
173             }
174              
175 99 50       284 push @clean_lines, $line unless $in_nvm_section;
176             }
177              
178             # Write new config with nvm-pl snippet at the end
179 1 50       312 open my $out, '>', $config_file or return;
180 1         38 print $out @clean_lines;
181 1         3 print $out "\n# nvm-pl managed Node.js path\n";
182 1         4 print $out "$init_snippet\n";
183 1         54 close $out;
184              
185 1         68 say "[nvm-pl] Updated $config_file for $shell_type";
186             }
187              
188             # ---------------------------------------------------------
189             # Helpers
190             # ---------------------------------------------------------
191              
192             sub _win_junction {
193 0     0   0 my ($link, $target) = @_;
194 0         0 $link =~ s#/#\\#g;
195 0         0 $target =~ s#/#\\#g;
196 0         0 my $cmd = "cmd /C mklink /J \"$link\" \"$target\"";
197 0 0       0 system($cmd) == 0
198             or die "[nvm-pl] Failed to create junction: $!";
199             }
200              
201             sub list_installed {
202 1     1 0 170798 my $cfg = NVMPL::Config->load();
203 1         25 my $versions_dir = File::Spec->catdir($cfg->{install_dir}, 'versions');
204              
205 1 50       115 opendir(my $dh, $versions_dir) or die "Can't open $versions_dir: $!";
206 1 100       49 my @dirs = grep { /^v\d/ && -d File::Spec->catdir($versions_dir, $_) } readdir($dh);
  4         90  
207 1         73 closedir $dh;
208              
209 1 50       9 if (@dirs) {
210 1         71 say "[nvm-pl] Installed versions:";
211 1         85 say " $_" for sort @dirs;
212             } else {
213 0         0 say "[nvm-pl] No versions installed.";
214             }
215             }
216              
217             sub show_current {
218 1     1 0 10550 my $cfg = NVMPL::Config->load();
219 1         21 my $current = File::Spec->catfile($cfg->{install_dir}, 'versions', 'current');
220 1 50       45 if (-l $current) {
221 0         0 my $target = readlink($current);
222 0         0 say "[nvm-pl] Current version -> $target";
223             } else {
224 1         93 say "[nvm-pl] No active Node version.";
225             }
226             }
227              
228             sub _get_current_version {
229 0     0     my $cfg = NVMPL::Config->load();
230 0           my $current = File::Spec->catfile($cfg->{install_dir}, 'versions', 'current');
231 0 0         if (-l $current) {
232 0           my $target = readlink($current);
233 0           return (split('/', $target))[-1];
234             }
235 0           return undef;
236             }
237              
238             1;