node-version-bridge 0.5.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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/nvb +290 -0
- package/hooks/nvb.bash +18 -0
- package/hooks/nvb.zsh +19 -0
- package/lib/alias.sh +140 -0
- package/lib/cache.sh +42 -0
- package/lib/config.sh +124 -0
- package/lib/detect.sh +26 -0
- package/lib/log.sh +27 -0
- package/lib/manager.sh +140 -0
- package/lib/parse.sh +121 -0
- package/lib/resolve.sh +32 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcos Reuquen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# node-version-bridge
|
|
2
|
+
|
|
3
|
+
Automatically detects the Node.js version declared in your project and applies it using your preferred version manager.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Many Node projects already have files like `.nvmrc` or `.node-version`, but if you use a different version manager than the rest of your team (asdf, nvm, fnm, mise, n), you end up maintaining duplicate files or running manual commands every time you switch projects.
|
|
8
|
+
|
|
9
|
+
## Solution
|
|
10
|
+
|
|
11
|
+
`nvb` reads the version files that already exist in your project and automatically applies the correct Node version when you enter the directory — regardless of which version manager you use.
|
|
12
|
+
|
|
13
|
+
## Supported managers
|
|
14
|
+
|
|
15
|
+
- **nvm** — `nvm use <version>`
|
|
16
|
+
- **fnm** — `fnm use <version>`
|
|
17
|
+
- **mise** — `mise shell node@<version>`
|
|
18
|
+
- **asdf** — `export ASDF_NODEJS_VERSION=<version>`
|
|
19
|
+
- **n** — `n <version>`
|
|
20
|
+
|
|
21
|
+
## Detected version files
|
|
22
|
+
|
|
23
|
+
By default, in this priority order:
|
|
24
|
+
|
|
25
|
+
1. `.nvmrc`
|
|
26
|
+
2. `.node-version`
|
|
27
|
+
3. `.tool-versions`
|
|
28
|
+
4. `package.json` (`engines.node`)
|
|
29
|
+
|
|
30
|
+
Aliases like `lts/*`, `lts/iron`, `node`, `stable`, and `latest` are automatically resolved to concrete versions via the nodejs.org API.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### Quick install (recommended)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/marcosreuquen/node-version-bridge.git
|
|
38
|
+
cd node-version-bridge
|
|
39
|
+
bash install.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The script will:
|
|
43
|
+
1. Copy `nvb` to `~/.local/share/nvb/`
|
|
44
|
+
2. Automatically add the shell hook to your `.zshrc` or `.bashrc`
|
|
45
|
+
|
|
46
|
+
Then restart your shell or run `source ~/.zshrc` (or `~/.bashrc`).
|
|
47
|
+
|
|
48
|
+
### Manual install
|
|
49
|
+
|
|
50
|
+
If you prefer full control over the installation:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/marcosreuquen/node-version-bridge.git
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then add the hook to your shell config:
|
|
57
|
+
|
|
58
|
+
**Zsh** — add to `~/.zshrc`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
source /path/to/node-version-bridge/hooks/nvb.zsh
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Bash** — add to `~/.bashrc`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
source /path/to/node-version-bridge/hooks/nvb.bash
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Verify installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
nvb doctor
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Uninstall
|
|
77
|
+
|
|
78
|
+
### With the script
|
|
79
|
+
|
|
80
|
+
From the repository directory:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bash uninstall.sh
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This removes installed files, the shell hook from your config, and the cache.
|
|
87
|
+
|
|
88
|
+
### Manual
|
|
89
|
+
|
|
90
|
+
1. Remove the `source ...nvb.zsh` (or `nvb.bash`) line from your `.zshrc`/`.bashrc`
|
|
91
|
+
2. Delete the install directory: `rm -rf ~/.local/share/nvb`
|
|
92
|
+
3. Optional — delete the cache: `rm -rf ~/.cache/node-version-bridge`
|
|
93
|
+
|
|
94
|
+
## Usage
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Show resolved version vs active version
|
|
98
|
+
nvb current
|
|
99
|
+
|
|
100
|
+
# Full diagnostics
|
|
101
|
+
nvb doctor
|
|
102
|
+
|
|
103
|
+
# Manage configuration
|
|
104
|
+
nvb config list
|
|
105
|
+
nvb config set NVB_AUTO_INSTALL true
|
|
106
|
+
|
|
107
|
+
# Show help
|
|
108
|
+
nvb help
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
Configuration can be set via environment variables or a config file at `~/.config/nvb/config` (or `$XDG_CONFIG_HOME/nvb/config`). Environment variables always take precedence over the config file.
|
|
114
|
+
|
|
115
|
+
### Config file
|
|
116
|
+
|
|
117
|
+
Create `~/.config/nvb/config` (or use `nvb config set`):
|
|
118
|
+
|
|
119
|
+
```ini
|
|
120
|
+
NVB_MANAGER=fnm
|
|
121
|
+
NVB_LOG_LEVEL=info
|
|
122
|
+
NVB_AUTO_INSTALL=true
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Manage config from the CLI
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
nvb config list # Show all config values and sources
|
|
129
|
+
nvb config set NVB_AUTO_INSTALL true # Enable auto-install
|
|
130
|
+
nvb config set NVB_MANAGER fnm # Force fnm
|
|
131
|
+
nvb config get NVB_LOG_LEVEL # Read a value
|
|
132
|
+
nvb config unset NVB_MANAGER # Remove a key
|
|
133
|
+
nvb config path # Show config file path
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Environment variables
|
|
137
|
+
|
|
138
|
+
| Variable | Description | Default |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `NVB_MANAGER` | Force a specific version manager | auto-detect |
|
|
141
|
+
| `NVB_LOG_LEVEL` | Log level: error, warn, info, debug | `error` |
|
|
142
|
+
| `NVB_PRIORITY` | File priority (comma-separated) | `.nvmrc,.node-version,.tool-versions,package.json` |
|
|
143
|
+
| `NVB_CACHE_DIR` | Cache directory | `$XDG_CACHE_HOME/node-version-bridge` |
|
|
144
|
+
| `NVB_AUTO_INSTALL` | Auto-install missing Node versions: true/false | `false` |
|
|
145
|
+
| `NVB_ALIAS_CACHE_TTL` | Alias cache TTL in seconds | `3600` |
|
|
146
|
+
|
|
147
|
+
## Tests
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
bash test/run.sh
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Expected outcome
|
|
154
|
+
|
|
155
|
+
- Less daily friction when switching between projects.
|
|
156
|
+
- No need to commit manager-specific files when `.nvmrc`/`.node-version` already exists.
|
|
157
|
+
- Works with any popular Node version manager.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Documentation
|
|
162
|
+
|
|
163
|
+
- [Product concept](./docs/concept.md)
|
|
164
|
+
- [Technical design](./docs/technical-design.md)
|
|
165
|
+
- [Roadmap](./docs/roadmap.md)
|
|
166
|
+
- [Implementation plan](./docs/implementation-plan.md)
|
|
167
|
+
- [Changelog](./CHANGELOG.md)
|
|
168
|
+
|
|
169
|
+
## Status
|
|
170
|
+
|
|
171
|
+
v0.5.0 — partial version resolution for asdf/mise, config file support, and auto-install. See [Changelog](./CHANGELOG.md).
|
|
172
|
+
|
|
173
|
+
**[Full Documentation](https://marcosreuquen.github.io/node-version-bridge/)**
|
package/bin/nvb
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — CLI entrypoint
|
|
3
|
+
# Detects Node.js version from project files and generates commands
|
|
4
|
+
# to apply it via the user's preferred version manager.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
NVB_VERSION="0.5.0"
|
|
9
|
+
|
|
10
|
+
# Resolve script location (handles symlinks on macOS/Linux)
|
|
11
|
+
_nvb_resolve_root() {
|
|
12
|
+
local source="${BASH_SOURCE[0]}"
|
|
13
|
+
while [[ -L "$source" ]]; do
|
|
14
|
+
local dir
|
|
15
|
+
dir="$(cd -P "$(dirname "$source")" && pwd)"
|
|
16
|
+
source="$(readlink "$source")"
|
|
17
|
+
[[ "$source" != /* ]] && source="${dir}/${source}"
|
|
18
|
+
done
|
|
19
|
+
cd -P "$(dirname "$source")/.." && pwd
|
|
20
|
+
}
|
|
21
|
+
NVB_ROOT="$(_nvb_resolve_root)"
|
|
22
|
+
|
|
23
|
+
# Source libraries
|
|
24
|
+
source "${NVB_ROOT}/lib/log.sh"
|
|
25
|
+
source "${NVB_ROOT}/lib/config.sh"
|
|
26
|
+
source "${NVB_ROOT}/lib/detect.sh"
|
|
27
|
+
source "${NVB_ROOT}/lib/alias.sh"
|
|
28
|
+
source "${NVB_ROOT}/lib/parse.sh"
|
|
29
|
+
source "${NVB_ROOT}/lib/resolve.sh"
|
|
30
|
+
source "${NVB_ROOT}/lib/cache.sh"
|
|
31
|
+
source "${NVB_ROOT}/lib/manager.sh"
|
|
32
|
+
|
|
33
|
+
# Load configuration file (env vars take precedence)
|
|
34
|
+
nvb_config_load
|
|
35
|
+
|
|
36
|
+
# --- Commands ---
|
|
37
|
+
|
|
38
|
+
nvb_cmd_refresh() {
|
|
39
|
+
# shellcheck disable=SC2119
|
|
40
|
+
nvb_resolve_version || true
|
|
41
|
+
local resolved_version="${NVB_RESOLVED_VERSION:-}"
|
|
42
|
+
local resolved_source="${NVB_RESOLVED_SOURCE:-}"
|
|
43
|
+
|
|
44
|
+
if [[ -z "$resolved_version" ]]; then
|
|
45
|
+
nvb_log debug "No version file found, nothing to do"
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check cache — skip if nothing changed
|
|
50
|
+
if nvb_cache_is_current "$PWD" "$resolved_version" "$resolved_source"; then
|
|
51
|
+
nvb_log debug "Cache hit: ${resolved_version} from ${resolved_source}"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
local manager
|
|
56
|
+
manager="$(nvb_detect_manager)" || {
|
|
57
|
+
echo "echo '[nvb] No supported version manager found.' >&2"
|
|
58
|
+
echo "echo '[nvb] Install one of: nvm, fnm, mise, asdf, n' >&2"
|
|
59
|
+
echo "echo '[nvb] See: https://github.com/marcosreuquen/node-version-bridge#supported-managers' >&2"
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
nvb_log info "Switching to Node ${resolved_version} (from ${resolved_source}) via ${manager}"
|
|
64
|
+
|
|
65
|
+
# Resolve partial version (e.g. "18" → "18.20.8") for managers that need exact versions
|
|
66
|
+
local apply_version
|
|
67
|
+
apply_version="$(nvb_resolve_installed_version "$manager" "$resolved_version")"
|
|
68
|
+
|
|
69
|
+
# Auto-install if enabled
|
|
70
|
+
local auto_install
|
|
71
|
+
auto_install="$(nvb_config_get NVB_AUTO_INSTALL false)"
|
|
72
|
+
if [[ "$auto_install" == "true" ]]; then
|
|
73
|
+
# If version couldn't be resolved to installed, install it first
|
|
74
|
+
if [[ "$apply_version" == "$resolved_version" ]] && ! _nvb_is_full_version "$resolved_version"; then
|
|
75
|
+
nvb_log debug "Auto-install enabled, no installed match for partial version ${resolved_version}"
|
|
76
|
+
nvb_adapter_install_cmd "$manager" "$resolved_version"
|
|
77
|
+
# Re-resolve after install (next hook trigger will pick it up if eval'd install succeeds)
|
|
78
|
+
apply_version="$(nvb_resolve_installed_version "$manager" "$resolved_version")"
|
|
79
|
+
else
|
|
80
|
+
nvb_log debug "Auto-install enabled, emitting install command"
|
|
81
|
+
nvb_adapter_install_cmd "$manager" "$apply_version"
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Output eval-able command for the shell hook
|
|
86
|
+
nvb_adapter_apply_cmd "$manager" "$apply_version"
|
|
87
|
+
|
|
88
|
+
# Update cache
|
|
89
|
+
nvb_cache_update "$PWD" "$resolved_version" "$resolved_source"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
nvb_cmd_current() {
|
|
93
|
+
# shellcheck disable=SC2119
|
|
94
|
+
nvb_resolve_version || true
|
|
95
|
+
local resolved_version="${NVB_RESOLVED_VERSION:-}"
|
|
96
|
+
local resolved_source="${NVB_RESOLVED_SOURCE:-}"
|
|
97
|
+
|
|
98
|
+
local active_version
|
|
99
|
+
active_version="$(node --version 2>/dev/null | sed 's/^v//')" || active_version="(not found)"
|
|
100
|
+
|
|
101
|
+
local manager
|
|
102
|
+
manager="$(nvb_detect_manager 2>/dev/null)" || manager="(none detected)"
|
|
103
|
+
|
|
104
|
+
# Resolve partial version to show what would actually be applied
|
|
105
|
+
local apply_version="${resolved_version}"
|
|
106
|
+
if [[ -n "$resolved_version" && "$manager" != "(none detected)" ]]; then
|
|
107
|
+
apply_version="$(nvb_resolve_installed_version "$manager" "$resolved_version" 2>/dev/null)" || apply_version="$resolved_version"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
echo "node-version-bridge v${NVB_VERSION}"
|
|
111
|
+
echo ""
|
|
112
|
+
echo " Active Node: ${active_version}"
|
|
113
|
+
if [[ -n "$resolved_version" && "$apply_version" != "$resolved_version" ]]; then
|
|
114
|
+
echo " Resolved: ${resolved_version} → ${apply_version}"
|
|
115
|
+
else
|
|
116
|
+
echo " Resolved: ${resolved_version:-(none)}"
|
|
117
|
+
fi
|
|
118
|
+
echo " Source: ${resolved_source:-(none)}"
|
|
119
|
+
echo " Manager: ${manager}"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
nvb_cmd_doctor() {
|
|
123
|
+
echo "node-version-bridge v${NVB_VERSION} — diagnostics"
|
|
124
|
+
echo ""
|
|
125
|
+
|
|
126
|
+
# Check version managers
|
|
127
|
+
echo "Version managers:"
|
|
128
|
+
local managers=("nvm" "fnm" "mise" "asdf" "n")
|
|
129
|
+
local active_manager
|
|
130
|
+
active_manager="$(nvb_detect_manager 2>/dev/null)" || active_manager=""
|
|
131
|
+
|
|
132
|
+
for m in "${managers[@]}"; do
|
|
133
|
+
if nvb_adapter_available "$m"; then
|
|
134
|
+
if [[ "$m" == "$active_manager" ]]; then
|
|
135
|
+
echo " ● ${m} (active)"
|
|
136
|
+
else
|
|
137
|
+
echo " ✓ ${m}"
|
|
138
|
+
fi
|
|
139
|
+
else
|
|
140
|
+
echo " ✗ ${m}"
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
echo ""
|
|
145
|
+
|
|
146
|
+
# Check version files
|
|
147
|
+
echo "Version files (from $PWD):"
|
|
148
|
+
local priority_str="${NVB_PRIORITY:-.nvmrc,.node-version,.tool-versions,package.json}"
|
|
149
|
+
IFS=',' read -ra priorities <<< "$priority_str"
|
|
150
|
+
|
|
151
|
+
for f in "${priorities[@]}"; do
|
|
152
|
+
f="$(echo "$f" | xargs)"
|
|
153
|
+
local found
|
|
154
|
+
found="$(nvb_detect_file "$f" 2>/dev/null)" || found=""
|
|
155
|
+
if [[ -n "$found" ]]; then
|
|
156
|
+
local ver
|
|
157
|
+
ver="$(nvb_parse_file "$f" "$found" 2>/dev/null)" || ver="(parse error)"
|
|
158
|
+
echo " ✓ ${found} → ${ver}"
|
|
159
|
+
else
|
|
160
|
+
echo " ✗ ${f} (not found)"
|
|
161
|
+
fi
|
|
162
|
+
done
|
|
163
|
+
|
|
164
|
+
echo ""
|
|
165
|
+
|
|
166
|
+
# Configuration
|
|
167
|
+
echo "Configuration:"
|
|
168
|
+
echo " NVB_MANAGER: ${NVB_MANAGER:-(auto-detect)}"
|
|
169
|
+
echo " NVB_LOG_LEVEL: ${NVB_LOG_LEVEL:-error}"
|
|
170
|
+
echo " NVB_PRIORITY: ${NVB_PRIORITY:-.nvmrc,.node-version,.tool-versions,package.json}"
|
|
171
|
+
echo " NVB_CACHE_DIR: ${NVB_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/node-version-bridge}"
|
|
172
|
+
echo " NVB_AUTO_INSTALL: $(nvb_config_get NVB_AUTO_INSTALL false)"
|
|
173
|
+
echo " NVB_ALIAS_CACHE_TTL: ${NVB_ALIAS_CACHE_TTL:-3600}"
|
|
174
|
+
echo " Config file: $(nvb_config_file)"
|
|
175
|
+
|
|
176
|
+
echo ""
|
|
177
|
+
local node_ver
|
|
178
|
+
node_ver="$(node --version 2>/dev/null)" || node_ver="(not found)"
|
|
179
|
+
echo "Active Node: ${node_ver}"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
nvb_cmd_config() {
|
|
183
|
+
local subcmd="${1:-list}"
|
|
184
|
+
shift 2>/dev/null || true
|
|
185
|
+
|
|
186
|
+
case "$subcmd" in
|
|
187
|
+
list|ls)
|
|
188
|
+
nvb_config_list
|
|
189
|
+
;;
|
|
190
|
+
get)
|
|
191
|
+
local key="${1:-}"
|
|
192
|
+
if [[ -z "$key" ]]; then
|
|
193
|
+
echo "Usage: nvb config get <KEY>" >&2
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
nvb_config_get "$key"
|
|
197
|
+
;;
|
|
198
|
+
set)
|
|
199
|
+
local key="${1:-}"
|
|
200
|
+
local value="${2:-}"
|
|
201
|
+
if [[ -z "$key" || -z "$value" ]]; then
|
|
202
|
+
echo "Usage: nvb config set <KEY> <VALUE>" >&2
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
nvb_config_set "$key" "$value"
|
|
206
|
+
echo "Set ${key}=${value} in $(nvb_config_file)"
|
|
207
|
+
;;
|
|
208
|
+
unset)
|
|
209
|
+
local key="${1:-}"
|
|
210
|
+
if [[ -z "$key" ]]; then
|
|
211
|
+
echo "Usage: nvb config unset <KEY>" >&2
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
nvb_config_unset "$key"
|
|
215
|
+
echo "Removed ${key} from $(nvb_config_file)"
|
|
216
|
+
;;
|
|
217
|
+
path)
|
|
218
|
+
nvb_config_file
|
|
219
|
+
;;
|
|
220
|
+
*)
|
|
221
|
+
echo "Usage: nvb config <list|get|set|unset|path>" >&2
|
|
222
|
+
echo "" >&2
|
|
223
|
+
echo " list Show all config values and their sources" >&2
|
|
224
|
+
echo " get <KEY> Get a config value" >&2
|
|
225
|
+
echo " set <KEY> <VALUE> Set a config value" >&2
|
|
226
|
+
echo " unset <KEY> Remove a config value" >&2
|
|
227
|
+
echo " path Show config file path" >&2
|
|
228
|
+
return 1
|
|
229
|
+
;;
|
|
230
|
+
esac
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
nvb_cmd_help() {
|
|
234
|
+
cat <<EOF
|
|
235
|
+
node-version-bridge v${NVB_VERSION}
|
|
236
|
+
|
|
237
|
+
Automatically applies the Node.js version declared in your project
|
|
238
|
+
using your preferred version manager.
|
|
239
|
+
|
|
240
|
+
Usage: nvb <command> [options]
|
|
241
|
+
|
|
242
|
+
Commands:
|
|
243
|
+
refresh Detect version and output shell commands to apply it (for eval)
|
|
244
|
+
current Show resolved version, source, and active Node version
|
|
245
|
+
doctor Check available managers and configuration
|
|
246
|
+
config Manage configuration (list, get, set, unset, path)
|
|
247
|
+
version Show nvb version
|
|
248
|
+
help Show this help
|
|
249
|
+
|
|
250
|
+
Config examples:
|
|
251
|
+
nvb config list Show all configuration
|
|
252
|
+
nvb config set NVB_AUTO_INSTALL true Enable auto-install
|
|
253
|
+
nvb config set NVB_MANAGER fnm Force fnm as manager
|
|
254
|
+
nvb config set NVB_LOG_LEVEL debug Enable debug logging
|
|
255
|
+
nvb config unset NVB_MANAGER Remove manager override
|
|
256
|
+
|
|
257
|
+
Supported version files: .nvmrc, .node-version, .tool-versions, package.json (engines.node)
|
|
258
|
+
Supported managers: nvm, fnm, mise, asdf, n
|
|
259
|
+
|
|
260
|
+
Shell integration (add to your shell config):
|
|
261
|
+
Zsh: source <path-to-nvb>/hooks/nvb.zsh
|
|
262
|
+
Bash: source <path-to-nvb>/hooks/nvb.bash
|
|
263
|
+
|
|
264
|
+
Environment variables:
|
|
265
|
+
NVB_MANAGER Force a specific version manager (nvm, fnm, mise, asdf, n)
|
|
266
|
+
NVB_LOG_LEVEL Log verbosity: error, warn, info, debug (default: error)
|
|
267
|
+
NVB_PRIORITY Comma-separated file priority (default: .nvmrc,.node-version,.tool-versions,package.json)
|
|
268
|
+
NVB_CACHE_DIR Override cache directory
|
|
269
|
+
NVB_AUTO_INSTALL Auto-install missing versions: true/false (default: false)
|
|
270
|
+
NVB_ALIAS_CACHE_TTL Alias cache TTL in seconds (default: 3600)
|
|
271
|
+
|
|
272
|
+
Configuration file: $(nvb_config_file)
|
|
273
|
+
EOF
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# --- Main dispatch ---
|
|
277
|
+
|
|
278
|
+
case "${1:-help}" in
|
|
279
|
+
refresh) nvb_cmd_refresh ;;
|
|
280
|
+
current) nvb_cmd_current ;;
|
|
281
|
+
doctor) nvb_cmd_doctor ;;
|
|
282
|
+
config) shift; nvb_cmd_config "$@" ;;
|
|
283
|
+
version|-v) echo "nvb ${NVB_VERSION}" ;;
|
|
284
|
+
help|--help|-h) nvb_cmd_help ;;
|
|
285
|
+
*)
|
|
286
|
+
nvb_log error "Unknown command: $1"
|
|
287
|
+
nvb_cmd_help >&2
|
|
288
|
+
exit 1
|
|
289
|
+
;;
|
|
290
|
+
esac
|
package/hooks/nvb.bash
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Bash hook
|
|
3
|
+
# Source this file in your .bashrc
|
|
4
|
+
|
|
5
|
+
# Resolve nvb binary path relative to this hook file
|
|
6
|
+
_NVB_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" && pwd)/nvb"
|
|
7
|
+
|
|
8
|
+
_nvb_hook() {
|
|
9
|
+
# Skip if directory hasn't changed (fast path, avoids fork)
|
|
10
|
+
[[ "${_NVB_LAST_DIR:-}" == "$PWD" ]] && return
|
|
11
|
+
_NVB_LAST_DIR="$PWD"
|
|
12
|
+
eval "$("${_NVB_BIN}" refresh 2>/dev/null)"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Append to PROMPT_COMMAND without duplicating
|
|
16
|
+
if [[ ";${PROMPT_COMMAND:-};" != *";_nvb_hook;"* ]]; then
|
|
17
|
+
PROMPT_COMMAND="_nvb_hook;${PROMPT_COMMAND:-}"
|
|
18
|
+
fi
|
package/hooks/nvb.zsh
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
# node-version-bridge — Zsh hook
|
|
3
|
+
# Source this file in your .zshrc
|
|
4
|
+
|
|
5
|
+
# Resolve nvb binary path relative to this hook file
|
|
6
|
+
_NVB_BIN="${0:A:h}/../bin/nvb"
|
|
7
|
+
|
|
8
|
+
_nvb_hook() {
|
|
9
|
+
# Skip if directory hasn't changed (fast path)
|
|
10
|
+
[[ "${_NVB_LAST_DIR:-}" == "$PWD" ]] && return
|
|
11
|
+
_NVB_LAST_DIR="$PWD"
|
|
12
|
+
eval "$("${_NVB_BIN}" refresh 2>/dev/null)"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
autoload -U add-zsh-hook
|
|
16
|
+
add-zsh-hook chpwd _nvb_hook
|
|
17
|
+
|
|
18
|
+
# Run once on shell start
|
|
19
|
+
_nvb_hook
|
package/lib/alias.sh
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Alias resolution
|
|
3
|
+
# Resolves Node.js aliases (lts/*, node, stable) to concrete versions
|
|
4
|
+
# using the Node.js release schedule API.
|
|
5
|
+
|
|
6
|
+
NVB_ALIAS_CACHE_TTL="${NVB_ALIAS_CACHE_TTL:-3600}" # 1 hour default
|
|
7
|
+
_NVB_NODE_INDEX_URL="https://nodejs.org/dist/index.json"
|
|
8
|
+
|
|
9
|
+
# Get the alias cache file path
|
|
10
|
+
_nvb_alias_cache_file() {
|
|
11
|
+
echo "${NVB_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/node-version-bridge}/node-index.json"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Fetch the Node.js version index (with caching)
|
|
15
|
+
_nvb_fetch_node_index() {
|
|
16
|
+
local cache_file
|
|
17
|
+
cache_file="$(_nvb_alias_cache_file)"
|
|
18
|
+
|
|
19
|
+
# Check if cache is fresh
|
|
20
|
+
if [[ -f "$cache_file" ]]; then
|
|
21
|
+
local mtime now age
|
|
22
|
+
if stat -f '%m' "$cache_file" &>/dev/null; then
|
|
23
|
+
# macOS
|
|
24
|
+
mtime="$(stat -f '%m' "$cache_file")"
|
|
25
|
+
else
|
|
26
|
+
# Linux
|
|
27
|
+
mtime="$(stat -c '%Y' "$cache_file")"
|
|
28
|
+
fi
|
|
29
|
+
now="$(date +%s)"
|
|
30
|
+
age=$(( now - mtime ))
|
|
31
|
+
if (( age < NVB_ALIAS_CACHE_TTL )); then
|
|
32
|
+
nvb_log debug "Using cached node index (age: ${age}s)"
|
|
33
|
+
cat "$cache_file"
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
nvb_log debug "Fetching node index from ${_NVB_NODE_INDEX_URL}"
|
|
39
|
+
|
|
40
|
+
local content
|
|
41
|
+
if command -v curl &>/dev/null; then
|
|
42
|
+
content="$(curl -sL --max-time 10 "$_NVB_NODE_INDEX_URL" 2>/dev/null)"
|
|
43
|
+
elif command -v wget &>/dev/null; then
|
|
44
|
+
content="$(wget -qO- --timeout=10 "$_NVB_NODE_INDEX_URL" 2>/dev/null)"
|
|
45
|
+
else
|
|
46
|
+
nvb_log warn "Neither curl nor wget available, cannot resolve aliases"
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [[ -z "$content" ]]; then
|
|
51
|
+
nvb_log warn "Failed to fetch node index"
|
|
52
|
+
# Fall back to stale cache if it exists
|
|
53
|
+
if [[ -f "$cache_file" ]]; then
|
|
54
|
+
nvb_log debug "Falling back to stale cache"
|
|
55
|
+
cat "$cache_file"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
mkdir -p "$(dirname "$cache_file")"
|
|
62
|
+
echo "$content" > "$cache_file"
|
|
63
|
+
echo "$content"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Resolve an alias to a concrete version
|
|
67
|
+
# Supported: lts/*, lts, lts/<codename>, node, stable, latest
|
|
68
|
+
nvb_resolve_alias() {
|
|
69
|
+
local alias="$1"
|
|
70
|
+
|
|
71
|
+
# Normalize
|
|
72
|
+
alias="$(echo "$alias" | tr '[:upper:]' '[:lower:]')"
|
|
73
|
+
|
|
74
|
+
local index
|
|
75
|
+
index="$(_nvb_fetch_node_index)" || return 1
|
|
76
|
+
|
|
77
|
+
local version=""
|
|
78
|
+
|
|
79
|
+
case "$alias" in
|
|
80
|
+
lts|lts/\*)
|
|
81
|
+
# Latest LTS version
|
|
82
|
+
version="$(_nvb_extract_latest_lts "$index")"
|
|
83
|
+
;;
|
|
84
|
+
lts/*)
|
|
85
|
+
# Specific LTS codename (e.g., lts/iron, lts/hydrogen)
|
|
86
|
+
local codename="${alias#lts/}"
|
|
87
|
+
version="$(_nvb_extract_lts_by_codename "$index" "$codename")"
|
|
88
|
+
;;
|
|
89
|
+
node|stable|latest)
|
|
90
|
+
# Latest current version
|
|
91
|
+
version="$(_nvb_extract_latest "$index")"
|
|
92
|
+
;;
|
|
93
|
+
*)
|
|
94
|
+
return 1
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
if [[ -n "$version" ]]; then
|
|
99
|
+
nvb_log debug "Resolved alias '${alias}' → ${version}"
|
|
100
|
+
echo "$version"
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
nvb_log warn "Could not resolve alias '${alias}'"
|
|
105
|
+
return 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Extract the latest LTS version from the index
|
|
109
|
+
_nvb_extract_latest_lts() {
|
|
110
|
+
local index="$1"
|
|
111
|
+
# index.json: array of objects with "version":"v22.x.x","lts":"Jod" or "lts":false
|
|
112
|
+
# First entry with lts !== false is the latest LTS
|
|
113
|
+
echo "$index" | grep -o '"version":"v[^"]*","date":"[^"]*","files":\[[^]]*\],"npm":"[^"]*","v8":"[^"]*","uv":"[^"]*","zlib":"[^"]*","openssl":"[^"]*","modules":"[^"]*","lts":"[^"]*"' \
|
|
114
|
+
| grep '"lts":"[^f]' | head -1 | grep -o '"version":"v[^"]*"' | head -1 | sed 's/"version":"v\([^"]*\)"/\1/'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Extract LTS version by codename
|
|
118
|
+
_nvb_extract_lts_by_codename() {
|
|
119
|
+
local index="$1"
|
|
120
|
+
local codename="$2"
|
|
121
|
+
# Case-insensitive match on codename
|
|
122
|
+
echo "$index" | grep -oi "\"version\":\"v[^\"]*\"[^}]*\"lts\":\"${codename}\"" \
|
|
123
|
+
| head -1 | grep -o '"version":"v[^"]*"' | sed 's/"version":"v\([^"]*\)"/\1/'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Extract the latest (current) version
|
|
127
|
+
_nvb_extract_latest() {
|
|
128
|
+
local index="$1"
|
|
129
|
+
echo "$index" | grep -o '"version":"v[^"]*"' | head -1 | sed 's/"version":"v\([^"]*\)"/\1/'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Check if a string looks like a known alias
|
|
133
|
+
nvb_is_alias() {
|
|
134
|
+
local value="$1"
|
|
135
|
+
value="$(echo "$value" | tr '[:upper:]' '[:lower:]')"
|
|
136
|
+
case "$value" in
|
|
137
|
+
lts|lts/*|node|stable|latest) return 0 ;;
|
|
138
|
+
*) return 1 ;;
|
|
139
|
+
esac
|
|
140
|
+
}
|
package/lib/cache.sh
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Cache management
|
|
3
|
+
# Avoids redundant version switches when directory/version hasn't changed
|
|
4
|
+
|
|
5
|
+
NVB_CACHE_DIR="${NVB_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/node-version-bridge}"
|
|
6
|
+
|
|
7
|
+
nvb_cache_file() {
|
|
8
|
+
echo "${NVB_CACHE_DIR}/state"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# Check if cached state matches current context
|
|
12
|
+
nvb_cache_is_current() {
|
|
13
|
+
local cwd="$1" version="$2" source="$3"
|
|
14
|
+
local cache_file
|
|
15
|
+
cache_file="$(nvb_cache_file)"
|
|
16
|
+
|
|
17
|
+
[[ -f "$cache_file" ]] || return 1
|
|
18
|
+
|
|
19
|
+
local cached_cwd cached_version cached_source
|
|
20
|
+
cached_cwd="$(sed -n '1p' "$cache_file")"
|
|
21
|
+
cached_version="$(sed -n '2p' "$cache_file")"
|
|
22
|
+
cached_source="$(sed -n '3p' "$cache_file")"
|
|
23
|
+
|
|
24
|
+
[[ "$cached_cwd" == "$cwd" && "$cached_version" == "$version" && "$cached_source" == "$source" ]]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Update cache with current state
|
|
28
|
+
nvb_cache_update() {
|
|
29
|
+
local cwd="$1" version="$2" source="$3"
|
|
30
|
+
local cache_file
|
|
31
|
+
cache_file="$(nvb_cache_file)"
|
|
32
|
+
|
|
33
|
+
mkdir -p "$(dirname "$cache_file")"
|
|
34
|
+
printf '%s\n%s\n%s\n' "$cwd" "$version" "$source" > "$cache_file"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Clear stored cache
|
|
38
|
+
nvb_cache_clear() {
|
|
39
|
+
local cache_file
|
|
40
|
+
cache_file="$(nvb_cache_file)"
|
|
41
|
+
rm -f "$cache_file"
|
|
42
|
+
}
|
package/lib/config.sh
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Configuration file support
|
|
3
|
+
# Loads and manages config from ~/.config/nvb/config
|
|
4
|
+
|
|
5
|
+
nvb_config_dir() {
|
|
6
|
+
echo "${XDG_CONFIG_HOME:-$HOME/.config}/nvb"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
nvb_config_file() {
|
|
10
|
+
echo "$(nvb_config_dir)/config"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# Load config file (environment variables take precedence)
|
|
14
|
+
nvb_config_load() {
|
|
15
|
+
local config_file
|
|
16
|
+
config_file="$(nvb_config_file)"
|
|
17
|
+
[[ -f "$config_file" ]] || return 0
|
|
18
|
+
|
|
19
|
+
local key value
|
|
20
|
+
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
|
21
|
+
key="$(echo "$key" | xargs)" 2>/dev/null || continue
|
|
22
|
+
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
23
|
+
value="$(echo "$value" | sed "s/^[\"']//;s/[\"']$//")"
|
|
24
|
+
if [[ -z "${!key:-}" ]]; then
|
|
25
|
+
export "$key=$value"
|
|
26
|
+
fi
|
|
27
|
+
done < "$config_file"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Get a config value (env > file > default)
|
|
31
|
+
nvb_config_get() {
|
|
32
|
+
local key="$1"
|
|
33
|
+
local default="${2:-}"
|
|
34
|
+
|
|
35
|
+
if [[ -n "${!key:-}" ]]; then
|
|
36
|
+
echo "${!key}"
|
|
37
|
+
return 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
local config_file
|
|
41
|
+
config_file="$(nvb_config_file)"
|
|
42
|
+
if [[ -f "$config_file" ]]; then
|
|
43
|
+
local file_value
|
|
44
|
+
file_value="$(grep "^${key}=" "$config_file" 2>/dev/null | tail -1 | cut -d'=' -f2- | sed "s/^[\"']//;s/[\"']$//")"
|
|
45
|
+
if [[ -n "$file_value" ]]; then
|
|
46
|
+
echo "$file_value"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "$default"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Set a config value in the file
|
|
55
|
+
nvb_config_set() {
|
|
56
|
+
local key="$1"
|
|
57
|
+
local value="$2"
|
|
58
|
+
local config_dir config_file
|
|
59
|
+
|
|
60
|
+
config_dir="$(nvb_config_dir)"
|
|
61
|
+
config_file="$(nvb_config_file)"
|
|
62
|
+
|
|
63
|
+
mkdir -p "$config_dir"
|
|
64
|
+
|
|
65
|
+
if [[ -f "$config_file" ]] && grep -q "^${key}=" "$config_file" 2>/dev/null; then
|
|
66
|
+
local tmp="${config_file}.tmp"
|
|
67
|
+
sed "s|^${key}=.*|${key}=${value}|" "$config_file" > "$tmp" && mv "$tmp" "$config_file"
|
|
68
|
+
else
|
|
69
|
+
echo "${key}=${value}" >> "$config_file"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Remove a config key
|
|
74
|
+
nvb_config_unset() {
|
|
75
|
+
local key="$1"
|
|
76
|
+
local config_file
|
|
77
|
+
config_file="$(nvb_config_file)"
|
|
78
|
+
[[ -f "$config_file" ]] || return 0
|
|
79
|
+
|
|
80
|
+
local tmp="${config_file}.tmp"
|
|
81
|
+
grep -v "^${key}=" "$config_file" > "$tmp" && mv "$tmp" "$config_file"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# List all config with source info
|
|
85
|
+
nvb_config_list() {
|
|
86
|
+
local config_file
|
|
87
|
+
config_file="$(nvb_config_file)"
|
|
88
|
+
|
|
89
|
+
echo "Configuration ($(nvb_config_file)):"
|
|
90
|
+
echo ""
|
|
91
|
+
|
|
92
|
+
local keys=("NVB_MANAGER" "NVB_LOG_LEVEL" "NVB_PRIORITY" "NVB_CACHE_DIR" "NVB_ALIAS_CACHE_TTL" "NVB_AUTO_INSTALL")
|
|
93
|
+
for key in "${keys[@]}"; do
|
|
94
|
+
local env_val="${!key:-}"
|
|
95
|
+
local file_val=""
|
|
96
|
+
local effective=""
|
|
97
|
+
local source=""
|
|
98
|
+
|
|
99
|
+
if [[ -f "$config_file" ]]; then
|
|
100
|
+
file_val="$(grep "^${key}=" "$config_file" 2>/dev/null | tail -1 | cut -d'=' -f2- | sed "s/^[\"']//;s/[\"']$//")" || true
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if [[ -n "$env_val" ]]; then
|
|
104
|
+
effective="$env_val"
|
|
105
|
+
if [[ -n "$file_val" ]]; then
|
|
106
|
+
source="env (overrides file: ${file_val})"
|
|
107
|
+
else
|
|
108
|
+
source="env"
|
|
109
|
+
fi
|
|
110
|
+
elif [[ -n "$file_val" ]]; then
|
|
111
|
+
effective="$file_val"
|
|
112
|
+
source="config file"
|
|
113
|
+
else
|
|
114
|
+
effective="(default)"
|
|
115
|
+
source=""
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if [[ -n "$source" ]]; then
|
|
119
|
+
echo " ${key}=${effective} <- ${source}"
|
|
120
|
+
else
|
|
121
|
+
echo " ${key}=${effective}"
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
}
|
package/lib/detect.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Version file detection
|
|
3
|
+
# Walks up directory tree to find version declaration files
|
|
4
|
+
|
|
5
|
+
# Find a specific file walking up from a directory
|
|
6
|
+
# Usage: nvb_detect_file <filename> [start_dir]
|
|
7
|
+
nvb_detect_file() {
|
|
8
|
+
local filename="$1"
|
|
9
|
+
local dir="${2:-$PWD}"
|
|
10
|
+
|
|
11
|
+
while [[ "$dir" != "/" ]]; do
|
|
12
|
+
if [[ -f "${dir}/${filename}" ]]; then
|
|
13
|
+
echo "${dir}/${filename}"
|
|
14
|
+
return 0
|
|
15
|
+
fi
|
|
16
|
+
dir="$(dirname "$dir")"
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
# Check filesystem root
|
|
20
|
+
if [[ -f "/${filename}" ]]; then
|
|
21
|
+
echo "/${filename}"
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
return 1
|
|
26
|
+
}
|
package/lib/log.sh
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Logging utilities
|
|
3
|
+
# All log output goes to stderr to keep stdout clean for eval-able commands
|
|
4
|
+
|
|
5
|
+
nvb_log() {
|
|
6
|
+
local level="$1"
|
|
7
|
+
shift
|
|
8
|
+
local configured="${NVB_LOG_LEVEL:-error}"
|
|
9
|
+
|
|
10
|
+
local level_num configured_num
|
|
11
|
+
level_num=$(_nvb_log_level_num "$level")
|
|
12
|
+
configured_num=$(_nvb_log_level_num "$configured")
|
|
13
|
+
|
|
14
|
+
if (( level_num <= configured_num )); then
|
|
15
|
+
echo "[nvb:${level}] $*" >&2
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_nvb_log_level_num() {
|
|
20
|
+
case "$1" in
|
|
21
|
+
error) echo 0 ;;
|
|
22
|
+
warn) echo 1 ;;
|
|
23
|
+
info) echo 2 ;;
|
|
24
|
+
debug) echo 3 ;;
|
|
25
|
+
*) echo 0 ;;
|
|
26
|
+
esac
|
|
27
|
+
}
|
package/lib/manager.sh
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Manager detection and adapter dispatch
|
|
3
|
+
# Detects the active Node version manager and generates apply commands
|
|
4
|
+
|
|
5
|
+
# Check if a version is a full semver (x.y.z)
|
|
6
|
+
_nvb_is_full_version() {
|
|
7
|
+
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# Resolve a partial version (e.g. "18" or "18.20") to the best matching
|
|
11
|
+
# installed version. For managers that handle partials natively (nvm, fnm, n),
|
|
12
|
+
# returns the version as-is. For asdf/mise, queries installed versions.
|
|
13
|
+
nvb_resolve_installed_version() {
|
|
14
|
+
local manager="$1"
|
|
15
|
+
local version="$2"
|
|
16
|
+
|
|
17
|
+
# Full semver doesn't need resolution
|
|
18
|
+
if _nvb_is_full_version "$version"; then
|
|
19
|
+
echo "$version"
|
|
20
|
+
return 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
local match=""
|
|
24
|
+
case "$manager" in
|
|
25
|
+
asdf)
|
|
26
|
+
match="$(asdf list nodejs 2>/dev/null | tr -d ' *' | grep -E "^${version}(\.|$)" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)"
|
|
27
|
+
;;
|
|
28
|
+
mise)
|
|
29
|
+
match="$(mise ls --installed node 2>/dev/null | awk '{print $2}' | grep -E "^${version}(\.|$)" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)"
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
# nvm, fnm, n handle partial versions natively
|
|
33
|
+
echo "$version"
|
|
34
|
+
return 0
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
if [[ -n "$match" ]]; then
|
|
39
|
+
nvb_log debug "Resolved partial version ${version} → ${match} for ${manager}"
|
|
40
|
+
echo "$match"
|
|
41
|
+
else
|
|
42
|
+
nvb_log debug "No installed version matching ${version} for ${manager}"
|
|
43
|
+
echo "$version"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Detect which version manager is available
|
|
48
|
+
# Respects NVB_MANAGER override, otherwise auto-detects
|
|
49
|
+
nvb_detect_manager() {
|
|
50
|
+
local manager="${NVB_MANAGER:-}"
|
|
51
|
+
if [[ -n "$manager" ]]; then
|
|
52
|
+
nvb_log debug "Using configured manager: ${manager}"
|
|
53
|
+
echo "$manager"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Auto-detect in common preference order
|
|
58
|
+
if [[ -n "${NVM_DIR:-}" ]]; then nvb_log debug "Detected nvm"; echo "nvm"; return 0; fi
|
|
59
|
+
if command -v fnm &>/dev/null; then nvb_log debug "Detected fnm"; echo "fnm"; return 0; fi
|
|
60
|
+
if command -v mise &>/dev/null; then nvb_log debug "Detected mise"; echo "mise"; return 0; fi
|
|
61
|
+
if command -v asdf &>/dev/null; then nvb_log debug "Detected asdf"; echo "asdf"; return 0; fi
|
|
62
|
+
if command -v n &>/dev/null; then nvb_log debug "Detected n"; echo "n"; return 0; fi
|
|
63
|
+
|
|
64
|
+
nvb_log error "No supported version manager detected"
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Check if a specific manager is available
|
|
69
|
+
nvb_adapter_available() {
|
|
70
|
+
local manager="$1"
|
|
71
|
+
case "$manager" in
|
|
72
|
+
nvm) [[ -n "${NVM_DIR:-}" ]] ;;
|
|
73
|
+
fnm) command -v fnm &>/dev/null ;;
|
|
74
|
+
mise) command -v mise &>/dev/null ;;
|
|
75
|
+
asdf) command -v asdf &>/dev/null ;;
|
|
76
|
+
n) command -v n &>/dev/null ;;
|
|
77
|
+
*) return 1 ;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Output eval-able shell command to switch Node version
|
|
82
|
+
# The output is designed to be eval'd in the user's interactive shell
|
|
83
|
+
nvb_adapter_apply_cmd() {
|
|
84
|
+
local manager="$1"
|
|
85
|
+
local version="$2"
|
|
86
|
+
|
|
87
|
+
case "$manager" in
|
|
88
|
+
nvm)
|
|
89
|
+
echo "nvm use '${version}' >/dev/null 2>&1 || echo '[nvb] Node ${version} not installed via nvm. Run: nvm install ${version}' >&2"
|
|
90
|
+
;;
|
|
91
|
+
fnm)
|
|
92
|
+
echo "fnm use '${version}' --log-level quiet 2>/dev/null || echo '[nvb] Node ${version} not installed via fnm. Run: fnm install ${version}' >&2"
|
|
93
|
+
;;
|
|
94
|
+
mise)
|
|
95
|
+
echo "eval \"\$(mise shell node@'${version}' 2>/dev/null)\" 2>/dev/null || echo '[nvb] Node ${version} not available via mise. Run: mise install node@${version}' >&2"
|
|
96
|
+
;;
|
|
97
|
+
asdf)
|
|
98
|
+
echo "export ASDF_NODEJS_VERSION='${version}'"
|
|
99
|
+
;;
|
|
100
|
+
n)
|
|
101
|
+
echo "n '${version}' >/dev/null 2>&1 || echo '[nvb] Node ${version} not installed via n. Run: n install ${version}' >&2"
|
|
102
|
+
;;
|
|
103
|
+
*)
|
|
104
|
+
nvb_log error "Unknown manager: ${manager}"
|
|
105
|
+
return 1
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Output eval-able shell command to install a Node version
|
|
111
|
+
nvb_adapter_install_cmd() {
|
|
112
|
+
local manager="$1"
|
|
113
|
+
local version="$2"
|
|
114
|
+
|
|
115
|
+
case "$manager" in
|
|
116
|
+
nvm)
|
|
117
|
+
echo "nvm install '${version}' >/dev/null 2>&1"
|
|
118
|
+
;;
|
|
119
|
+
fnm)
|
|
120
|
+
echo "fnm install '${version}' --log-level quiet 2>/dev/null"
|
|
121
|
+
;;
|
|
122
|
+
mise)
|
|
123
|
+
echo "mise install node@'${version}' >/dev/null 2>&1"
|
|
124
|
+
;;
|
|
125
|
+
asdf)
|
|
126
|
+
if _nvb_is_full_version "$version"; then
|
|
127
|
+
echo "asdf install nodejs '${version}' >/dev/null 2>&1"
|
|
128
|
+
else
|
|
129
|
+
echo "asdf install nodejs \"\$(asdf latest nodejs '${version}')\" >/dev/null 2>&1"
|
|
130
|
+
fi
|
|
131
|
+
;;
|
|
132
|
+
n)
|
|
133
|
+
echo "n '${version}' >/dev/null 2>&1"
|
|
134
|
+
;;
|
|
135
|
+
*)
|
|
136
|
+
nvb_log error "Unknown manager: ${manager}"
|
|
137
|
+
return 1
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
}
|
package/lib/parse.sh
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Version file parsing
|
|
3
|
+
# Extracts and normalizes Node.js version from various file formats
|
|
4
|
+
|
|
5
|
+
# Dispatch parsing based on file type
|
|
6
|
+
nvb_parse_file() {
|
|
7
|
+
local filename="$1"
|
|
8
|
+
local filepath="$2"
|
|
9
|
+
|
|
10
|
+
case "$(basename "$filename")" in
|
|
11
|
+
.nvmrc) nvb_parse_nvmrc "$filepath" ;;
|
|
12
|
+
.node-version) nvb_parse_node_version "$filepath" ;;
|
|
13
|
+
.tool-versions) nvb_parse_tool_versions "$filepath" ;;
|
|
14
|
+
package.json) nvb_parse_package_json "$filepath" ;;
|
|
15
|
+
*)
|
|
16
|
+
nvb_log warn "Unknown file format: ${filename}"
|
|
17
|
+
return 1
|
|
18
|
+
;;
|
|
19
|
+
esac
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Parse .nvmrc — strips leading 'v', validates semver-like pattern
|
|
23
|
+
nvb_parse_nvmrc() {
|
|
24
|
+
local filepath="$1"
|
|
25
|
+
local content
|
|
26
|
+
content="$(head -1 "$filepath" 2>/dev/null | tr -d '[:space:]')"
|
|
27
|
+
|
|
28
|
+
[[ -z "$content" ]] && return 1
|
|
29
|
+
|
|
30
|
+
# Strip leading 'v' or 'V'
|
|
31
|
+
content="${content#v}"
|
|
32
|
+
content="${content#V}"
|
|
33
|
+
|
|
34
|
+
# Resolve aliases (lts/*, node, stable, etc.)
|
|
35
|
+
if nvb_is_alias "$content"; then
|
|
36
|
+
local resolved
|
|
37
|
+
resolved="$(nvb_resolve_alias "$content")" || {
|
|
38
|
+
nvb_log warn "Could not resolve alias '${content}' in ${filepath} (network unavailable?)"
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
echo "$resolved"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Accept partial or full semver: 18, 18.19, 18.19.0
|
|
46
|
+
if [[ "$content" =~ ^[0-9]+(\.[0-9]+)?(\.[0-9]+)?$ ]]; then
|
|
47
|
+
echo "$content"
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
nvb_log warn "Could not parse version from ${filepath}: '${content}'"
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Parse .node-version — same format rules as .nvmrc
|
|
56
|
+
nvb_parse_node_version() {
|
|
57
|
+
nvb_parse_nvmrc "$1"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Parse .tool-versions — extract 'nodejs' or 'node' entry
|
|
61
|
+
nvb_parse_tool_versions() {
|
|
62
|
+
local filepath="$1"
|
|
63
|
+
local version
|
|
64
|
+
|
|
65
|
+
version="$(grep -E '^(nodejs|node)\s+' "$filepath" 2>/dev/null | head -1 | awk '{print $2}' | tr -d '[:space:]')"
|
|
66
|
+
|
|
67
|
+
[[ -z "$version" ]] && return 1
|
|
68
|
+
|
|
69
|
+
version="${version#v}"
|
|
70
|
+
version="${version#V}"
|
|
71
|
+
|
|
72
|
+
if [[ "$version" =~ ^[0-9]+(\.[0-9]+)?(\.[0-9]+)?$ ]]; then
|
|
73
|
+
echo "$version"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
nvb_log warn "Could not parse nodejs version from ${filepath}: '${version}'"
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Parse package.json — extract engines.node field
|
|
82
|
+
nvb_parse_package_json() {
|
|
83
|
+
local filepath="$1"
|
|
84
|
+
|
|
85
|
+
[[ -f "$filepath" ]] || return 1
|
|
86
|
+
|
|
87
|
+
local engines_node
|
|
88
|
+
# Extract "engines":{..."node":"<value>"...} using grep/sed (no jq dependency)
|
|
89
|
+
engines_node="$(grep -o '"engines"[[:space:]]*:[[:space:]]*{[^}]*}' "$filepath" 2>/dev/null \
|
|
90
|
+
| grep -o '"node"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
91
|
+
| head -1 \
|
|
92
|
+
| sed 's/"node"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')"
|
|
93
|
+
|
|
94
|
+
[[ -z "$engines_node" ]] && return 1
|
|
95
|
+
|
|
96
|
+
nvb_log debug "Found engines.node: '${engines_node}' in ${filepath}"
|
|
97
|
+
|
|
98
|
+
# Handle exact versions: "18.19.0", "v18.19.0", "18"
|
|
99
|
+
local clean="$engines_node"
|
|
100
|
+
clean="${clean#v}"
|
|
101
|
+
clean="${clean#V}"
|
|
102
|
+
|
|
103
|
+
if [[ "$clean" =~ ^[0-9]+(\.[0-9]+)?(\.[0-9]+)?$ ]]; then
|
|
104
|
+
echo "$clean"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Handle semver ranges: extract the base version
|
|
109
|
+
# ">=18.0.0", "^18.19.0", "~18.19.0", ">=18"
|
|
110
|
+
local base
|
|
111
|
+
base="$(echo "$clean" | sed 's/^[^0-9]*//' | grep -oE '^[0-9]+(\.[0-9]+)?(\.[0-9]+)?')"
|
|
112
|
+
|
|
113
|
+
if [[ -n "$base" ]]; then
|
|
114
|
+
nvb_log info "Interpreted engines.node '${engines_node}' as minimum version ${base}"
|
|
115
|
+
echo "$base"
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
nvb_log warn "Could not parse engines.node from ${filepath}: '${engines_node}'"
|
|
120
|
+
return 1
|
|
121
|
+
}
|
package/lib/resolve.sh
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# node-version-bridge — Version resolution
|
|
3
|
+
# Applies priority rules to select the target Node.js version
|
|
4
|
+
|
|
5
|
+
# Resolve the target version from available files
|
|
6
|
+
# Sets: NVB_RESOLVED_VERSION, NVB_RESOLVED_SOURCE
|
|
7
|
+
nvb_resolve_version() {
|
|
8
|
+
local dir="${1:-$PWD}"
|
|
9
|
+
local priority_str="${NVB_PRIORITY:-.nvmrc,.node-version,.tool-versions,package.json}"
|
|
10
|
+
|
|
11
|
+
NVB_RESOLVED_VERSION=""
|
|
12
|
+
NVB_RESOLVED_SOURCE=""
|
|
13
|
+
|
|
14
|
+
IFS=',' read -ra priorities <<< "$priority_str"
|
|
15
|
+
|
|
16
|
+
for filename in "${priorities[@]}"; do
|
|
17
|
+
filename="$(echo "$filename" | xargs)" # trim whitespace
|
|
18
|
+
local filepath
|
|
19
|
+
filepath="$(nvb_detect_file "$filename" "$dir")" || continue
|
|
20
|
+
|
|
21
|
+
local version
|
|
22
|
+
version="$(nvb_parse_file "$filename" "$filepath")" || continue
|
|
23
|
+
|
|
24
|
+
export NVB_RESOLVED_VERSION="$version"
|
|
25
|
+
export NVB_RESOLVED_SOURCE="$filepath"
|
|
26
|
+
nvb_log debug "Resolved ${version} from ${filepath}"
|
|
27
|
+
return 0
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
nvb_log debug "No version file found from ${dir}"
|
|
31
|
+
return 1
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-version-bridge",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Detect Node.js version from project files and apply it via any version manager",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nvb": "bin/nvb"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"lib/",
|
|
11
|
+
"hooks/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"node",
|
|
15
|
+
"version",
|
|
16
|
+
"nvm",
|
|
17
|
+
"fnm",
|
|
18
|
+
"mise",
|
|
19
|
+
"asdf",
|
|
20
|
+
"nvmrc"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Marcosreuquen/node-version-bridge.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Marcos Reuquen",
|
|
28
|
+
"os": [
|
|
29
|
+
"darwin",
|
|
30
|
+
"linux"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=14"
|
|
34
|
+
}
|
|
35
|
+
}
|