node-version-use 1.9.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +121 -23
  2. package/dist/cjs/cli.js +25 -6
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/commands/default.d.cts +7 -0
  5. package/dist/cjs/commands/default.d.ts +7 -0
  6. package/dist/cjs/commands/default.js +57 -0
  7. package/dist/cjs/commands/default.js.map +1 -0
  8. package/dist/cjs/commands/index.d.cts +3 -0
  9. package/dist/cjs/commands/index.d.ts +3 -0
  10. package/dist/cjs/commands/index.js +54 -0
  11. package/dist/cjs/commands/index.js.map +1 -0
  12. package/dist/cjs/commands/install.d.cts +6 -0
  13. package/dist/cjs/commands/install.d.ts +6 -0
  14. package/dist/cjs/commands/install.js +69 -0
  15. package/dist/cjs/commands/install.js.map +1 -0
  16. package/dist/cjs/commands/list.d.cts +6 -0
  17. package/dist/cjs/commands/list.d.ts +6 -0
  18. package/dist/cjs/commands/list.js +93 -0
  19. package/dist/cjs/commands/list.js.map +1 -0
  20. package/dist/cjs/commands/local.d.cts +7 -0
  21. package/dist/cjs/commands/local.d.ts +7 -0
  22. package/dist/cjs/commands/local.js +69 -0
  23. package/dist/cjs/commands/local.js.map +1 -0
  24. package/dist/cjs/commands/setup.d.cts +7 -0
  25. package/dist/cjs/commands/setup.d.ts +7 -0
  26. package/dist/cjs/commands/setup.js +55 -0
  27. package/dist/cjs/commands/setup.js.map +1 -0
  28. package/dist/cjs/commands/teardown.d.cts +6 -0
  29. package/dist/cjs/commands/teardown.d.ts +6 -0
  30. package/dist/cjs/commands/teardown.js +66 -0
  31. package/dist/cjs/commands/teardown.js.map +1 -0
  32. package/dist/cjs/commands/uninstall.d.cts +6 -0
  33. package/dist/cjs/commands/uninstall.d.ts +6 -0
  34. package/dist/cjs/commands/uninstall.js +148 -0
  35. package/dist/cjs/commands/uninstall.js.map +1 -0
  36. package/dist/cjs/commands/which.d.cts +7 -0
  37. package/dist/cjs/commands/which.d.ts +7 -0
  38. package/dist/cjs/commands/which.js +117 -0
  39. package/dist/cjs/commands/which.js.map +1 -0
  40. package/dist/cjs/compat.d.cts +17 -0
  41. package/dist/cjs/compat.d.ts +17 -0
  42. package/dist/cjs/compat.js +47 -1
  43. package/dist/cjs/compat.js.map +1 -1
  44. package/dist/cjs/constants.js +1 -1
  45. package/dist/cjs/constants.js.map +1 -1
  46. package/dist/esm/cli.js +23 -4
  47. package/dist/esm/cli.js.map +1 -1
  48. package/dist/esm/commands/default.d.ts +7 -0
  49. package/dist/esm/commands/default.js +41 -0
  50. package/dist/esm/commands/default.js.map +1 -0
  51. package/dist/esm/commands/index.d.ts +3 -0
  52. package/dist/esm/commands/index.js +27 -0
  53. package/dist/esm/commands/index.js.map +1 -0
  54. package/dist/esm/commands/install.d.ts +6 -0
  55. package/dist/esm/commands/install.js +53 -0
  56. package/dist/esm/commands/install.js.map +1 -0
  57. package/dist/esm/commands/list.d.ts +6 -0
  58. package/dist/esm/commands/list.js +52 -0
  59. package/dist/esm/commands/list.js.map +1 -0
  60. package/dist/esm/commands/local.d.ts +7 -0
  61. package/dist/esm/commands/local.js +51 -0
  62. package/dist/esm/commands/local.js.map +1 -0
  63. package/dist/esm/commands/setup.d.ts +7 -0
  64. package/dist/esm/commands/setup.js +39 -0
  65. package/dist/esm/commands/setup.js.map +1 -0
  66. package/dist/esm/commands/teardown.d.ts +6 -0
  67. package/dist/esm/commands/teardown.js +33 -0
  68. package/dist/esm/commands/teardown.js.map +1 -0
  69. package/dist/esm/commands/uninstall.d.ts +6 -0
  70. package/dist/esm/commands/uninstall.js +132 -0
  71. package/dist/esm/commands/uninstall.js.map +1 -0
  72. package/dist/esm/commands/which.d.ts +7 -0
  73. package/dist/esm/commands/which.js +101 -0
  74. package/dist/esm/commands/which.js.map +1 -0
  75. package/dist/esm/compat.d.ts +17 -0
  76. package/dist/esm/compat.js +39 -2
  77. package/dist/esm/compat.js.map +1 -1
  78. package/dist/esm/constants.js +2 -1
  79. package/dist/esm/constants.js.map +1 -1
  80. package/package.json +11 -4
  81. package/scripts/postinstall.cjs +273 -0
  82. package/shim/Makefile +58 -0
  83. package/shim/go.mod +3 -0
  84. package/shim/main.go +302 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Postinstall script for node-version-use
3
+ *
4
+ * Downloads the platform-specific shim binary and installs it to ~/.nvu/bin/
5
+ * This enables transparent Node version switching via the shim.
6
+ *
7
+ * Compatible with Node.js 0.8+
8
+ */
9
+
10
+ var fs = require('fs');
11
+ var path = require('path');
12
+ var os = require('os');
13
+ var exit = require('exit');
14
+ var getRemote = require('get-remote');
15
+
16
+ // Polyfills for old Node versions
17
+ var mkdirp = require('mkdirp-classic');
18
+ var homedir = typeof os.homedir === 'function' ? os.homedir() : require('homedir-polyfill')();
19
+
20
+ // execSync doesn't exist in Node 0.8, use spawn
21
+ var spawn = require('child_process').spawn;
22
+
23
+ // Configuration
24
+ var GITHUB_REPO = 'kmalakoff/node-version-use';
25
+ var SHIM_VERSION = '1.0.2';
26
+
27
+ /**
28
+ * Get the platform-specific binary name
29
+ */
30
+ function getShimBinaryName() {
31
+ var platform = os.platform();
32
+ var arch = os.arch();
33
+
34
+ var platformMap = {
35
+ darwin: 'darwin',
36
+ linux: 'linux',
37
+ win32: 'win32',
38
+ };
39
+
40
+ var archMap = {
41
+ x64: 'x64',
42
+ arm64: 'arm64',
43
+ amd64: 'x64',
44
+ };
45
+
46
+ var platformName = platformMap[platform];
47
+ var archName = archMap[arch];
48
+
49
+ if (!platformName || !archName) {
50
+ return null;
51
+ }
52
+
53
+ var ext = platform === 'win32' ? '.exe' : '';
54
+ return 'nvu-shim-' + platformName + '-' + archName + ext;
55
+ }
56
+
57
+ /**
58
+ * Get the download URL for the shim binary
59
+ */
60
+ function getDownloadUrl(binaryName) {
61
+ var ext = os.platform() === 'win32' ? '.zip' : '.tar.gz';
62
+ return 'https://github.com/' + GITHUB_REPO + '/releases/download/shim-v' + SHIM_VERSION + '/' + binaryName + ext;
63
+ }
64
+
65
+ /**
66
+ * Copy file (compatible with Node 0.8)
67
+ */
68
+ function copyFileSync(src, dest) {
69
+ var content = fs.readFileSync(src);
70
+ fs.writeFileSync(dest, content);
71
+ }
72
+
73
+ /**
74
+ * Download a file from a URL (using get-remote for Node 0.8+ compatibility)
75
+ */
76
+ function downloadFile(url, destPath, callback) {
77
+ var writeStream = fs.createWriteStream(destPath);
78
+ getRemote(url).pipe(writeStream, callback);
79
+ }
80
+
81
+ /**
82
+ * Extract archive and install shims (callback-based)
83
+ */
84
+ function extractAndInstall(archivePath, destDir, binaryName, callback) {
85
+ var platform = os.platform();
86
+
87
+ if (platform === 'win32') {
88
+ // Windows: extract zip using PowerShell
89
+ var ps = spawn('powershell', ['-Command', "Expand-Archive -Path '" + archivePath + "' -DestinationPath '" + destDir + "' -Force"]);
90
+ ps.on('close', function (code) {
91
+ if (code !== 0) {
92
+ callback(new Error('Failed to extract archive'));
93
+ return;
94
+ }
95
+ var extractedPath = path.join(destDir, binaryName);
96
+ if (fs.existsSync(extractedPath)) {
97
+ try {
98
+ copyFileSync(extractedPath, path.join(destDir, 'node.exe'));
99
+ copyFileSync(extractedPath, path.join(destDir, 'npm.exe'));
100
+ copyFileSync(extractedPath, path.join(destDir, 'npx.exe'));
101
+ fs.unlinkSync(extractedPath);
102
+ } catch (err) {
103
+ callback(err);
104
+ return;
105
+ }
106
+ }
107
+ callback(null);
108
+ });
109
+ } else {
110
+ // Unix: extract tar.gz
111
+ var tar = spawn('tar', ['-xzf', archivePath, '-C', destDir]);
112
+ tar.on('close', function (code) {
113
+ if (code !== 0) {
114
+ callback(new Error('Failed to extract archive'));
115
+ return;
116
+ }
117
+ var extractedPath = path.join(destDir, binaryName);
118
+ if (fs.existsSync(extractedPath)) {
119
+ var nodePath = path.join(destDir, 'node');
120
+ var npmPath = path.join(destDir, 'npm');
121
+ var npxPath = path.join(destDir, 'npx');
122
+
123
+ try {
124
+ copyFileSync(extractedPath, nodePath);
125
+ copyFileSync(extractedPath, npmPath);
126
+ copyFileSync(extractedPath, npxPath);
127
+
128
+ fs.chmodSync(nodePath, 493); // 0755
129
+ fs.chmodSync(npmPath, 493);
130
+ fs.chmodSync(npxPath, 493);
131
+
132
+ fs.unlinkSync(extractedPath);
133
+ } catch (err) {
134
+ callback(err);
135
+ return;
136
+ }
137
+ }
138
+ callback(null);
139
+ });
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Print setup instructions
145
+ */
146
+ function printInstructions(installed) {
147
+ var nvuBinPath = path.join(homedir, '.nvu', 'bin');
148
+ var platform = os.platform();
149
+
150
+ console.log('');
151
+ console.log('============================================================');
152
+ if (installed) {
153
+ console.log(' nvu shims installed to ~/.nvu/bin/');
154
+ } else {
155
+ console.log(' nvu installed (shims not yet available)');
156
+ }
157
+ console.log('============================================================');
158
+ console.log('');
159
+ console.log('To enable transparent Node version switching, add to your shell profile:');
160
+ console.log('');
161
+
162
+ if (platform === 'win32') {
163
+ console.log(' PowerShell (add to $PROFILE):');
164
+ console.log(' $env:PATH = "' + nvuBinPath + ';$env:PATH"');
165
+ console.log('');
166
+ console.log(' CMD (run as administrator):');
167
+ console.log(' setx PATH "' + nvuBinPath + ';%PATH%"');
168
+ } else {
169
+ console.log(' # For bash (~/.bashrc):');
170
+ console.log(' export PATH="$HOME/.nvu/bin:$PATH"');
171
+ console.log('');
172
+ console.log(' # For zsh (~/.zshrc):');
173
+ console.log(' export PATH="$HOME/.nvu/bin:$PATH"');
174
+ console.log('');
175
+ console.log(' # For fish (~/.config/fish/config.fish):');
176
+ console.log(' set -gx PATH $HOME/.nvu/bin $PATH');
177
+ }
178
+
179
+ console.log('');
180
+ console.log('Then restart your terminal or source your shell profile.');
181
+ console.log('');
182
+ console.log("Without this, 'nvu 18 npm test' still works - you just won't have");
183
+ console.log("transparent 'node' command override.");
184
+ console.log('============================================================');
185
+ }
186
+
187
+ /**
188
+ * Get temp directory (compatible with Node 0.8)
189
+ */
190
+ function getTmpDir() {
191
+ return typeof os.tmpdir === 'function' ? os.tmpdir() : process.env.TMPDIR || process.env.TMP || process.env.TEMP || '/tmp';
192
+ }
193
+
194
+ /**
195
+ * Main installation function
196
+ */
197
+ function main() {
198
+ var binaryName = getShimBinaryName();
199
+
200
+ if (!binaryName) {
201
+ console.log('postinstall: Unsupported platform/architecture for shim binary.');
202
+ console.log('Platform: ' + os.platform() + ', Arch: ' + os.arch());
203
+ console.log('Shim not installed. You can still use nvu with explicit versions: nvu 18 npm test');
204
+ exit(0);
205
+ return;
206
+ }
207
+
208
+ var nvuDir = path.join(homedir, '.nvu');
209
+ var binDir = path.join(nvuDir, 'bin');
210
+
211
+ // Create directories
212
+ mkdirp.sync(nvuDir);
213
+ mkdirp.sync(binDir);
214
+
215
+ var downloadUrl = getDownloadUrl(binaryName);
216
+ var ext = os.platform() === 'win32' ? '.zip' : '.tar.gz';
217
+ var tempPath = path.join(getTmpDir(), 'nvu-shim-' + Date.now() + ext);
218
+
219
+ console.log('postinstall: Downloading shim binary for ' + os.platform() + '-' + os.arch() + '...');
220
+
221
+ downloadFile(downloadUrl, tempPath, function (downloadErr) {
222
+ if (downloadErr) {
223
+ // Clean up temp file if it exists
224
+ if (fs.existsSync(tempPath)) {
225
+ try {
226
+ fs.unlinkSync(tempPath);
227
+ } catch (_e) {}
228
+ }
229
+
230
+ if (downloadErr.message && downloadErr.message.indexOf('404') >= 0) {
231
+ console.log('postinstall: Shim binaries not yet published to GitHub releases.');
232
+ console.log('');
233
+ console.log('To build and install shims locally:');
234
+ console.log(' cd node_modules/node-version-use/shim');
235
+ console.log(' make install');
236
+ console.log('');
237
+ console.log('Or wait for the next release which will include pre-built binaries.');
238
+ } else {
239
+ console.log('postinstall warning: Failed to install shim: ' + (downloadErr.message || downloadErr));
240
+ console.log('You can still use nvu with explicit versions: nvu 18 npm test');
241
+ console.log('To install shims manually: cd node_modules/node-version-use/shim && make install');
242
+ }
243
+ printInstructions(false);
244
+ exit(0);
245
+ return;
246
+ }
247
+
248
+ console.log('postinstall: Extracting shim binary...');
249
+
250
+ extractAndInstall(tempPath, binDir, binaryName, function (extractErr) {
251
+ // Clean up temp file
252
+ if (fs.existsSync(tempPath)) {
253
+ try {
254
+ fs.unlinkSync(tempPath);
255
+ } catch (_e) {}
256
+ }
257
+
258
+ if (extractErr) {
259
+ console.log('postinstall warning: Failed to extract shim: ' + (extractErr.message || extractErr));
260
+ console.log('You can still use nvu with explicit versions: nvu 18 npm test');
261
+ printInstructions(false);
262
+ exit(0);
263
+ return;
264
+ }
265
+
266
+ console.log('postinstall: Shim installed successfully!');
267
+ printInstructions(true);
268
+ exit(0);
269
+ });
270
+ });
271
+ }
272
+
273
+ main();
package/shim/Makefile ADDED
@@ -0,0 +1,58 @@
1
+ # Makefile for building nvu shim binaries for all platforms
2
+
3
+ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
4
+ BUILD_DIR := build
5
+ BINARY_NAME := nvu-shim
6
+
7
+ # Build flags for smaller binaries
8
+ LDFLAGS := -s -w -X main.version=$(VERSION)
9
+
10
+ .PHONY: all clean darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64 windows-arm64 local install release
11
+
12
+ all: darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64 windows-arm64
13
+
14
+ $(BUILD_DIR):
15
+ mkdir -p $(BUILD_DIR)
16
+
17
+ darwin-arm64: $(BUILD_DIR)
18
+ GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 .
19
+
20
+ darwin-x64: $(BUILD_DIR)
21
+ GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-x64 .
22
+
23
+ linux-x64: $(BUILD_DIR)
24
+ GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-x64 .
25
+
26
+ linux-arm64: $(BUILD_DIR)
27
+ GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 .
28
+
29
+ windows-x64: $(BUILD_DIR)
30
+ GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-win32-x64.exe .
31
+
32
+ windows-arm64: $(BUILD_DIR)
33
+ GOOS=windows GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-win32-arm64.exe .
34
+
35
+ # Build for current platform (for local testing)
36
+ local: $(BUILD_DIR)
37
+ go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) .
38
+
39
+ # Install shims to ~/.nvu/bin for local testing
40
+ install: local
41
+ mkdir -p $(HOME)/.nvu/bin
42
+ cp $(BUILD_DIR)/$(BINARY_NAME) $(HOME)/.nvu/bin/node
43
+ cp $(BUILD_DIR)/$(BINARY_NAME) $(HOME)/.nvu/bin/npm
44
+ cp $(BUILD_DIR)/$(BINARY_NAME) $(HOME)/.nvu/bin/npx
45
+ @echo "Shims installed to ~/.nvu/bin/"
46
+ @echo "Add to your PATH: export PATH=\"\$$HOME/.nvu/bin:\$$PATH\""
47
+
48
+ clean:
49
+ rm -rf $(BUILD_DIR)
50
+
51
+ # Create release archives
52
+ release: all
53
+ cd $(BUILD_DIR) && tar -czf $(BINARY_NAME)-darwin-arm64.tar.gz $(BINARY_NAME)-darwin-arm64
54
+ cd $(BUILD_DIR) && tar -czf $(BINARY_NAME)-darwin-x64.tar.gz $(BINARY_NAME)-darwin-x64
55
+ cd $(BUILD_DIR) && tar -czf $(BINARY_NAME)-linux-x64.tar.gz $(BINARY_NAME)-linux-x64
56
+ cd $(BUILD_DIR) && tar -czf $(BINARY_NAME)-linux-arm64.tar.gz $(BINARY_NAME)-linux-arm64
57
+ cd $(BUILD_DIR) && zip $(BINARY_NAME)-win32-x64.zip $(BINARY_NAME)-win32-x64.exe
58
+ cd $(BUILD_DIR) && zip $(BINARY_NAME)-win32-arm64.zip $(BINARY_NAME)-win32-arm64.exe
package/shim/go.mod ADDED
@@ -0,0 +1,3 @@
1
+ module github.com/kmalakoff/node-version-use/shim
2
+
3
+ go 1.21
package/shim/main.go ADDED
@@ -0,0 +1,302 @@
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "runtime"
9
+ "strings"
10
+ "syscall"
11
+ )
12
+
13
+ // Version resolution priority:
14
+ // 1. .nvurc in current or parent directories
15
+ // 2. .nvmrc in current or parent directories
16
+ // 3. ~/.nvu/default (global default)
17
+
18
+ func main() {
19
+ // Determine which binary we're shimming based on the executable name
20
+ execName := filepath.Base(os.Args[0])
21
+ // Remove .exe suffix on Windows
22
+ execName = strings.TrimSuffix(execName, ".exe")
23
+
24
+ // Check if we're running the nvu CLI itself - if so, use any available version
25
+ // This prevents chicken-and-egg problems where nvu can't run because the
26
+ // configured version isn't installed yet
27
+ isNvuCli := isRunningNvuCli()
28
+
29
+ var version string
30
+ var err error
31
+
32
+ if isNvuCli {
33
+ // For nvu CLI, use any available installed version
34
+ version, err = findAnyInstalledVersion()
35
+ if err != nil {
36
+ fmt.Fprintf(os.Stderr, "nvu shim error: no Node versions installed\n")
37
+ fmt.Fprintf(os.Stderr, "\nInstall Node manually first, or use system Node to run:\n")
38
+ fmt.Fprintf(os.Stderr, " /usr/bin/node $(which nvu) install 20\n")
39
+ os.Exit(1)
40
+ }
41
+ } else {
42
+ // Resolve the Node version to use
43
+ version, err = resolveVersion()
44
+ if err != nil {
45
+ fmt.Fprintf(os.Stderr, "nvu shim error: %s\n", err)
46
+ fmt.Fprintf(os.Stderr, "\nTo fix this, either:\n")
47
+ fmt.Fprintf(os.Stderr, " 1. Create a .nvmrc file with a version: echo 20 > .nvmrc\n")
48
+ fmt.Fprintf(os.Stderr, " 2. Set a global default: nvu default 20\n")
49
+ os.Exit(1)
50
+ }
51
+ }
52
+
53
+ // Find the real binary path
54
+ binaryPath, err := findBinary(execName, version)
55
+ if err != nil {
56
+ fmt.Fprintf(os.Stderr, "nvu shim error: %s\n", err)
57
+ fmt.Fprintf(os.Stderr, "\nNode %s may not be installed. Run: nvu install %s\n", version, version)
58
+ os.Exit(1)
59
+ }
60
+
61
+ // Execute the real binary, replacing this process
62
+ err = execBinary(binaryPath, os.Args)
63
+ if err != nil {
64
+ fmt.Fprintf(os.Stderr, "nvu shim error: failed to exec %s: %s\n", binaryPath, err)
65
+ os.Exit(1)
66
+ }
67
+ }
68
+
69
+ // resolveVersion determines which Node version to use
70
+ func resolveVersion() (string, error) {
71
+ // 1. Check for .nvurc or .nvmrc in current directory and parents
72
+ cwd, err := os.Getwd()
73
+ if err != nil {
74
+ return "", fmt.Errorf("failed to get current directory: %w", err)
75
+ }
76
+
77
+ version := findVersionInParents(cwd)
78
+ if version != "" {
79
+ return version, nil
80
+ }
81
+
82
+ // 2. Check global default
83
+ homeDir, err := os.UserHomeDir()
84
+ if err != nil {
85
+ return "", fmt.Errorf("failed to get home directory: %w", err)
86
+ }
87
+
88
+ defaultPath := filepath.Join(homeDir, ".nvu", "default")
89
+ version, err = readVersionFile(defaultPath)
90
+ if err == nil && version != "" {
91
+ return version, nil
92
+ }
93
+
94
+ return "", fmt.Errorf("no Node version configured")
95
+ }
96
+
97
+ // findVersionInParents walks up the directory tree looking for version config files
98
+ func findVersionInParents(dir string) string {
99
+ for {
100
+ // Check .nvurc first (nvu-specific)
101
+ version, err := readVersionFile(filepath.Join(dir, ".nvurc"))
102
+ if err == nil && version != "" {
103
+ return version
104
+ }
105
+
106
+ // Check .nvmrc (ecosystem compatible)
107
+ version, err = readVersionFile(filepath.Join(dir, ".nvmrc"))
108
+ if err == nil && version != "" {
109
+ return version
110
+ }
111
+
112
+ // Move to parent directory
113
+ parent := filepath.Dir(dir)
114
+ if parent == dir {
115
+ // Reached root
116
+ break
117
+ }
118
+ dir = parent
119
+ }
120
+ return ""
121
+ }
122
+
123
+ // readVersionFile reads a version from a file, trimming whitespace
124
+ func readVersionFile(path string) (string, error) {
125
+ content, err := os.ReadFile(path)
126
+ if err != nil {
127
+ return "", err
128
+ }
129
+ version := strings.TrimSpace(string(content))
130
+ return version, nil
131
+ }
132
+
133
+ // findBinary locates the actual binary for the given command and version
134
+ func findBinary(name string, version string) (string, error) {
135
+ homeDir, err := os.UserHomeDir()
136
+ if err != nil {
137
+ return "", fmt.Errorf("failed to get home directory: %w", err)
138
+ }
139
+
140
+ versionsDir := filepath.Join(homeDir, ".nvu", "versions")
141
+
142
+ // Resolve version to an installed version directory
143
+ resolvedVersion, err := resolveInstalledVersion(versionsDir, version)
144
+ if err != nil {
145
+ return "", err
146
+ }
147
+
148
+ // The binary should be at ~/.nvu/versions/<version>/bin/<name>
149
+ var binaryPath string
150
+ if runtime.GOOS == "windows" {
151
+ // On Windows, look for .exe or .cmd
152
+ binaryPath = filepath.Join(versionsDir, resolvedVersion, "bin", name+".exe")
153
+ if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
154
+ // Try without bin/ (for node.exe which might be at root)
155
+ binaryPath = filepath.Join(versionsDir, resolvedVersion, name+".exe")
156
+ }
157
+ if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
158
+ // Try .cmd for npm/npx
159
+ binaryPath = filepath.Join(versionsDir, resolvedVersion, name+".cmd")
160
+ }
161
+ } else {
162
+ binaryPath = filepath.Join(versionsDir, resolvedVersion, "bin", name)
163
+ }
164
+
165
+ if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
166
+ return "", fmt.Errorf("binary not found: %s", binaryPath)
167
+ }
168
+
169
+ return binaryPath, nil
170
+ }
171
+
172
+ // resolveInstalledVersion finds the best matching installed version
173
+ func resolveInstalledVersion(versionsDir string, version string) (string, error) {
174
+ // Normalize version - remove 'v' prefix for comparison
175
+ normalizedVersion := strings.TrimPrefix(version, "v")
176
+
177
+ // Try exact matches first (with and without 'v' prefix)
178
+ exactMatches := []string{version, "v" + normalizedVersion, normalizedVersion}
179
+ for _, v := range exactMatches {
180
+ path := filepath.Join(versionsDir, v)
181
+ if info, err := os.Stat(path); err == nil && info.IsDir() {
182
+ return v, nil
183
+ }
184
+ }
185
+
186
+ // If no exact match, scan for partial version match (e.g., "20" matches "v20.19.6")
187
+ entries, err := os.ReadDir(versionsDir)
188
+ if err != nil {
189
+ return "", fmt.Errorf("failed to read versions directory: %w", err)
190
+ }
191
+
192
+ var bestMatch string
193
+ for _, entry := range entries {
194
+ if !entry.IsDir() {
195
+ continue
196
+ }
197
+ dirName := entry.Name()
198
+ dirVersion := strings.TrimPrefix(dirName, "v")
199
+
200
+ // Check if this version starts with our target
201
+ if strings.HasPrefix(dirVersion, normalizedVersion+".") || dirVersion == normalizedVersion {
202
+ // Prefer higher versions (simple string comparison works for semver)
203
+ if bestMatch == "" || dirName > bestMatch {
204
+ bestMatch = dirName
205
+ }
206
+ }
207
+ }
208
+
209
+ if bestMatch != "" {
210
+ return bestMatch, nil
211
+ }
212
+
213
+ return "", fmt.Errorf("no installed version matching %s", version)
214
+ }
215
+
216
+ // execBinary replaces the current process with the target binary
217
+ func execBinary(binaryPath string, args []string) error {
218
+ // On Unix, use syscall.Exec to replace the process
219
+ // On Windows, we need to use exec.Command and wait
220
+ if runtime.GOOS == "windows" {
221
+ return execWindows(binaryPath, args)
222
+ }
223
+ return execUnix(binaryPath, args)
224
+ }
225
+
226
+ func execUnix(binaryPath string, args []string) error {
227
+ // Replace args[0] with the actual binary path
228
+ args[0] = binaryPath
229
+ return syscall.Exec(binaryPath, args, os.Environ())
230
+ }
231
+
232
+ func execWindows(binaryPath string, args []string) error {
233
+ // On Windows, we can't use syscall.Exec, so we spawn and wait
234
+ cmd := exec.Command(binaryPath, args[1:]...)
235
+ cmd.Stdin = os.Stdin
236
+ cmd.Stdout = os.Stdout
237
+ cmd.Stderr = os.Stderr
238
+ cmd.Env = os.Environ()
239
+
240
+ err := cmd.Run()
241
+ if err != nil {
242
+ if exitError, ok := err.(*exec.ExitError); ok {
243
+ os.Exit(exitError.ExitCode())
244
+ }
245
+ return err
246
+ }
247
+ os.Exit(0)
248
+ return nil
249
+ }
250
+
251
+ // isRunningNvuCli checks if we're being used to run the nvu CLI
252
+ func isRunningNvuCli() bool {
253
+ if len(os.Args) < 2 {
254
+ return false
255
+ }
256
+
257
+ // Check if the script being run is the nvu CLI
258
+ script := os.Args[1]
259
+
260
+ // Check for common nvu CLI paths
261
+ if strings.Contains(script, "node-version-use") {
262
+ return true
263
+ }
264
+ if strings.HasSuffix(script, "/nvu") || strings.HasSuffix(script, "\\nvu") {
265
+ return true
266
+ }
267
+ if filepath.Base(script) == "nvu" || filepath.Base(script) == "nvu.js" {
268
+ return true
269
+ }
270
+
271
+ return false
272
+ }
273
+
274
+ // findAnyInstalledVersion returns any installed Node version
275
+ func findAnyInstalledVersion() (string, error) {
276
+ homeDir, err := os.UserHomeDir()
277
+ if err != nil {
278
+ return "", err
279
+ }
280
+
281
+ versionsDir := filepath.Join(homeDir, ".nvu", "versions")
282
+ entries, err := os.ReadDir(versionsDir)
283
+ if err != nil {
284
+ return "", err
285
+ }
286
+
287
+ // Return the first installed version we find
288
+ for _, entry := range entries {
289
+ if entry.IsDir() {
290
+ // Verify it has a node binary
291
+ nodePath := filepath.Join(versionsDir, entry.Name(), "bin", "node")
292
+ if runtime.GOOS == "windows" {
293
+ nodePath = filepath.Join(versionsDir, entry.Name(), "node.exe")
294
+ }
295
+ if _, err := os.Stat(nodePath); err == nil {
296
+ return entry.Name(), nil
297
+ }
298
+ }
299
+ }
300
+
301
+ return "", fmt.Errorf("no installed versions found")
302
+ }