tfv 5.0.1 → 6.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 (42) hide show
  1. package/.github/workflows/publish.yml +27 -3
  2. package/README.md +190 -132
  3. package/demo.gif +0 -0
  4. package/demo.tape +230 -0
  5. package/index.js +0 -4
  6. package/lib/commands/apply.js +8 -3
  7. package/lib/commands/current.js +25 -0
  8. package/lib/commands/destroy.js +8 -3
  9. package/lib/commands/fmt.js +26 -0
  10. package/lib/commands/init.js +26 -0
  11. package/lib/commands/install.js +22 -13
  12. package/lib/commands/list.js +20 -11
  13. package/lib/commands/pin.js +26 -0
  14. package/lib/commands/plan.js +8 -3
  15. package/lib/commands/remove.js +17 -12
  16. package/lib/commands/shell-init.js +25 -0
  17. package/lib/commands/switch.js +28 -7
  18. package/lib/commands/upgrade.js +26 -0
  19. package/lib/commands/use.js +17 -13
  20. package/lib/commands/validate.js +21 -0
  21. package/lib/modules/current.js +52 -0
  22. package/lib/modules/install.js +155 -89
  23. package/lib/modules/list.js +66 -105
  24. package/lib/modules/pin.js +35 -0
  25. package/lib/modules/ps1.js +37 -29
  26. package/lib/modules/remote.js +68 -15
  27. package/lib/modules/remove.js +35 -21
  28. package/lib/modules/shell-init.js +113 -0
  29. package/lib/modules/switch.js +125 -41
  30. package/lib/modules/terraform-command.js +49 -67
  31. package/lib/modules/upgrade.js +93 -0
  32. package/lib/modules/use.js +58 -54
  33. package/lib/utils/formatVersions.js +57 -5
  34. package/lib/utils/paths.js +156 -0
  35. package/lib/utils/postInstall.js +37 -13
  36. package/lib/utils/store.js +17 -6
  37. package/package.json +11 -9
  38. package/test/extractTargets.test.js +75 -0
  39. package/test/formatVersions.test.js +126 -0
  40. package/test/moduleImports.test.js +45 -0
  41. package/test/paths.test.js +69 -0
  42. package/test/versionResolution.test.js +92 -0
@@ -1,38 +1,46 @@
1
- const {spawnSync} = require("child_process");
2
- const {P_END, P_ERROR} = require('../utils/colors');
1
+ const { spawnSync } = require('child_process');
2
+ const { join } = require('path');
3
+ const { homedir } = require('os');
4
+ const { P_END, P_ERROR, P_WARN, P_OK } = require('../utils/colors');
5
+ const { TFV_HOME } = require('../utils/paths');
3
6
 
4
- exports.setWindowsTerraform = async () => {
5
- const getSystemEnv = spawnSync(
6
- "powershell",
7
- ["(Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Environment' -Name PATH).path"],
8
- {stdio: 'pipe'}
7
+ const BIN_DIR = join(TFV_HOME, 'bin');
8
+
9
+ /**
10
+ * Ensures ~/.tfv/bin is in the Windows User PATH (no admin required).
11
+ * Uses HKCU via [Environment]::SetEnvironmentVariable with "User" scope.
12
+ */
13
+ exports.ensureWindowsPath = () => {
14
+ const getUserPath = spawnSync(
15
+ 'powershell',
16
+ ['-NoProfile', '-NonInteractive', '-Command',
17
+ '[Environment]::GetEnvironmentVariable("PATH", "User")'],
18
+ { stdio: 'pipe', encoding: 'utf-8' }
9
19
  );
10
20
 
11
- if (!getSystemEnv || getSystemEnv.status === 1) {
12
- console.log(`${P_ERROR}Error ocurred while fetching windows environment${P_END}`);
13
- process.exit(1);
21
+ if (!getUserPath || getUserPath.status !== 0) {
22
+ console.log(`${P_WARN}Could not read Windows User PATH. Please add manually: ${BIN_DIR}${P_END}`);
23
+ return;
14
24
  }
15
25
 
16
- const systemEnv = getSystemEnv.stdout.toString().trim();
17
-
18
- const terraformPath = 'C:\\terraform';
19
- const getTerraformEnv = systemEnv.split(';').find(v => v === terraformPath);
26
+ const currentPath = (getUserPath.stdout || '').trim();
27
+ const pathEntries = currentPath.split(';').map(p => p.trim()).filter(Boolean);
20
28
 
21
- if (!getTerraformEnv) {
22
- const newSystemEnv = `${terraformPath};${systemEnv}`;
29
+ if (pathEntries.includes(BIN_DIR)) return; // Already in PATH
23
30
 
24
- const updateEnv = spawnSync(
25
- "powershell",
26
- [
27
- "-command",
28
- `start-process powershell -verb runas -argumentlist "Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Environment' -Name PATH -Value '${newSystemEnv}'"`
29
- ],
30
- {stdio: 'pipe'}
31
- );
31
+ const newPath = `${BIN_DIR};${currentPath}`;
32
+ const updateEnv = spawnSync(
33
+ 'powershell',
34
+ ['-NoProfile', '-NonInteractive', '-Command',
35
+ `[Environment]::SetEnvironmentVariable("PATH", "${newPath}", "User")`],
36
+ { stdio: 'pipe' }
37
+ );
32
38
 
33
- if (!updateEnv || updateEnv.status === 1) {
34
- console.log(`${P_ERROR}Error ocurred while setting windows Environment${P_END}`);
35
- process.exit(1);
36
- }
39
+ if (updateEnv && updateEnv.status === 0) {
40
+ console.log(`${P_OK}Added ${BIN_DIR} to your User PATH.${P_END}`);
41
+ console.log(`${P_WARN}Restart your terminal for the PATH change to take effect.${P_END}`);
42
+ } else {
43
+ console.log(`${P_ERROR}Could not update PATH automatically.${P_END}`);
44
+ console.log(`${P_WARN}Please add manually to your User PATH: ${BIN_DIR}${P_END}`);
37
45
  }
38
- }
46
+ };
@@ -1,18 +1,71 @@
1
1
  const https = require('https');
2
+ const { existsSync, readFileSync, writeFileSync } = require('fs');
3
+ const { join } = require('path');
4
+ const { getPaths, initDirs } = require('../utils/paths');
5
+ const { formatVersions, formatOpenTofuVersions } = require('../utils/formatVersions');
2
6
 
3
- exports.fetchAllVersions = async () => {
4
- try {
5
- return new Promise((resolve, reject) => {
6
- https.get('https://releases.hashicorp.com/terraform/', (resp) => {
7
- let data = '';
8
-
9
- resp.on('data', (chunk) => data += chunk);
10
- resp.on('end', () => resolve(data));
11
- }).on("error", (err) => {
12
- reject(err);
13
- });
14
- });
15
- } catch ({message}) {
16
- console.log('ERROR:', message);
17
- }
7
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
8
+
9
+ const URLS = {
10
+ terraform: 'https://releases.hashicorp.com/terraform/index.json',
11
+ opentofu: 'https://api.github.com/repos/opentofu/opentofu/releases?per_page=100',
12
+ };
13
+
14
+ const fetchUrl = (url) => new Promise((resolve, reject) => {
15
+ const options = { headers: { 'User-Agent': 'tfv-cli' } };
16
+ https.get(url, options, (res) => {
17
+ // Follow single redirect
18
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
19
+ return fetchUrl(res.headers.location).then(resolve).catch(reject);
20
+ }
21
+ let data = '';
22
+ res.on('data', chunk => { data += chunk; });
23
+ res.on('end', () => resolve(data));
24
+ }).on('error', reject);
25
+ });
26
+
27
+ const getCacheFile = (provider) => {
28
+ initDirs();
29
+ return join(getPaths().cache, `${provider}-versions.json`);
30
+ };
31
+
32
+ const readCache = (provider) => {
33
+ const cacheFile = getCacheFile(provider);
34
+ if (!existsSync(cacheFile)) return null;
35
+ try {
36
+ const { timestamp, data } = JSON.parse(readFileSync(cacheFile, 'utf-8'));
37
+ if (Date.now() - timestamp < CACHE_TTL_MS) return data;
38
+ } catch (_) { /* ignore corrupt cache */ }
39
+ return null;
40
+ };
41
+
42
+ const writeCache = (provider, data) => {
43
+ try {
44
+ writeFileSync(getCacheFile(provider), JSON.stringify({ timestamp: Date.now(), data }));
45
+ } catch (_) { /* non-fatal */ }
18
46
  };
47
+
48
+ /**
49
+ * Fetch all stable versions for a given provider.
50
+ * Returns sorted array of version strings (descending).
51
+ */
52
+ exports.fetchAllVersions = async (provider = 'terraform') => {
53
+ const normalized = provider === 'tofu' ? 'opentofu' : provider;
54
+
55
+ const cached = readCache(normalized);
56
+ if (cached) return cached;
57
+
58
+ const raw = await fetchUrl(URLS[normalized]);
59
+ const versions = normalized === 'opentofu'
60
+ ? formatOpenTofuVersions(raw)
61
+ : formatVersions(raw);
62
+
63
+ writeCache(normalized, versions);
64
+ return versions;
65
+ };
66
+
67
+ /**
68
+ * Fetch raw response (used for SHA256 sums and other direct URLs).
69
+ */
70
+ exports.fetchUrl = fetchUrl;
71
+
@@ -1,30 +1,44 @@
1
- const os = require('os')
2
- const fs = require('fs');
3
- const {join} = require('path');
4
- const {P_END, P_OK, P_ERROR} = require('../utils/colors');
1
+ const os = require('os');
2
+ const { existsSync, unlinkSync, readFileSync, writeFileSync } = require('fs');
3
+ const { join } = require('path');
4
+ const { normalizeProvider, getProviderStore, getVersionFile, getPaths } = require('../utils/paths');
5
+ const { P_END, P_OK, P_ERROR, P_WARN } = require('../utils/colors');
5
6
 
6
- exports.remove = async (version) => {
7
+ exports.remove = async (versions, providerArg = 'terraform') => {
7
8
  try {
8
- const store = join(__dirname, '../..', 'store');
9
- const arch = JSON.parse(fs.readFileSync(`${store}/arch.json`, 'utf-8'));
9
+ const provider = normalizeProvider(providerArg);
10
+ const store = getProviderStore(provider);
10
11
 
11
- version.forEach(v => {
12
- const tfVersion = os.platform() === 'win32' ? `${v}.exe` : v;
13
- const file = `${store}/${tfVersion}`;
12
+ const archFile = join(store, 'arch.json');
13
+ const archMap = existsSync(archFile) ? JSON.parse(readFileSync(archFile, 'utf-8')) : {};
14
14
 
15
- if (fs.existsSync(`${file}`)) {
16
- fs.unlinkSync(file);
15
+ // Get active version for warning
16
+ const paths = getPaths();
17
+ let activeVersion = null;
18
+ if (existsSync(paths.active)) {
19
+ const active = JSON.parse(readFileSync(paths.active, 'utf-8'));
20
+ activeVersion = active[provider];
21
+ }
17
22
 
18
- delete arch[v];
23
+ versions.forEach(v => {
24
+ const file = getVersionFile(provider, v);
19
25
 
20
- return console.log(`${P_OK}Terraform ${v} deleted from tfv store${P_END}`);
21
- } else {
22
- return console.log(`${P_ERROR}Terraform ${v} does not exist in tfv store${P_END}`);
26
+ if (!existsSync(file)) {
27
+ return console.log(`${P_ERROR}${provider} ${v} is not in tfv store${P_END}`);
23
28
  }
24
- })
25
29
 
26
- fs.writeFileSync(`${store}/arch.json`, JSON.stringify(arch, null, 2));
27
- } catch ({message}) {
28
- console.log('ERROR:', message)
30
+ if (v === activeVersion) {
31
+ console.log(`${P_WARN}Warning: ${provider} ${v} is currently active. Removing it will break the active symlink.${P_END}`);
32
+ console.log(`${P_WARN}Run ${P_OK}tfv use <other-version>${P_END}${P_WARN} first, or reinstall after removal.${P_END}`);
33
+ }
34
+
35
+ unlinkSync(file);
36
+ delete archMap[v];
37
+ console.log(`${P_OK}Removed ${provider} ${v} from tfv store${P_END}`);
38
+ });
39
+
40
+ writeFileSync(archFile, JSON.stringify(archMap, null, 2));
41
+ } catch (err) {
42
+ console.log(`${P_ERROR}Error: ${err.message}${P_END}`);
29
43
  }
30
- }
44
+ };
@@ -0,0 +1,113 @@
1
+ const { P_END, P_OK, P_ERROR, P_INFO } = require('../utils/colors');
2
+
3
+ const SUPPORTED_SHELLS = ['bash', 'zsh', 'fish', 'powershell', 'pwsh'];
4
+
5
+ const BASH_ZSH_HOOK = `
6
+ # tfv - terraform version manager shell integration
7
+ # Add this to your shell config by running: eval "$(tfv shell-init <shell>)"
8
+ _tfv_auto_switch() {
9
+ if [ -f ".terraform-version" ] || ls *.tf 2>/dev/null | head -1 >/dev/null 2>&1; then
10
+ tfv auto-switch --silent 2>/dev/null
11
+ fi
12
+ }
13
+
14
+ _tfv_cd() {
15
+ builtin cd "$@" && _tfv_auto_switch
16
+ }
17
+
18
+ alias cd='_tfv_cd'
19
+
20
+ # Run for the current directory on shell startup
21
+ _tfv_auto_switch
22
+ `.trim();
23
+
24
+ const FISH_HOOK = `
25
+ # tfv - terraform version manager shell integration
26
+ # Add to ~/.config/fish/config.fish: tfv shell-init fish | source
27
+
28
+ function _tfv_auto_switch
29
+ if test -f .terraform-version; or count *.tf >/dev/null 2>&1
30
+ tfv auto-switch --silent 2>/dev/null
31
+ end
32
+ end
33
+
34
+ function cd
35
+ builtin cd $argv
36
+ _tfv_auto_switch
37
+ end
38
+
39
+ _tfv_auto_switch
40
+ `.trim();
41
+
42
+ const POWERSHELL_HOOK = `
43
+ # tfv - terraform version manager shell integration
44
+ # Add to your PowerShell profile ($PROFILE):
45
+ # Invoke-Expression (tfv shell-init powershell | Out-String)
46
+
47
+ function global:Set-LocationWithTfv {
48
+ param([string]$Path = $PWD)
49
+ Set-Location $Path
50
+ if ((Test-Path ".terraform-version") -or (Get-ChildItem -Filter "*.tf" -ErrorAction SilentlyContinue)) {
51
+ tfv auto-switch --silent 2>$null
52
+ }
53
+ }
54
+
55
+ Remove-Item -Path Alias:cd -Force -ErrorAction SilentlyContinue
56
+ Set-Alias -Name cd -Value Set-LocationWithTfv -Scope Global -Force
57
+
58
+ # Run for current directory on shell startup
59
+ Set-LocationWithTfv -Path $PWD
60
+ `.trim();
61
+
62
+ const INSTALL_INSTRUCTIONS = {
63
+ bash: `
64
+ ${'\x1b[32m\x1b[1m'}Shell hook generated for bash.${'\x1b[0m'}
65
+ To activate, add this to your ~/.bashrc:
66
+ eval "$(tfv shell-init bash)"
67
+ Then restart your terminal or run: source ~/.bashrc
68
+ `,
69
+ zsh: `
70
+ ${'\x1b[32m\x1b[1m'}Shell hook generated for zsh.${'\x1b[0m'}
71
+ To activate, add this to your ~/.zshrc:
72
+ eval "$(tfv shell-init zsh)"
73
+ Then restart your terminal or run: source ~/.zshrc
74
+ `,
75
+ fish: `
76
+ ${'\x1b[32m\x1b[1m'}Shell hook generated for fish.${'\x1b[0m'}
77
+ To activate, add this to your ~/.config/fish/config.fish:
78
+ tfv shell-init fish | source
79
+ Then restart your terminal or run: source ~/.config/fish/config.fish
80
+ `,
81
+ powershell: `
82
+ ${'\x1b[32m\x1b[1m'}Shell hook generated for PowerShell.${'\x1b[0m'}
83
+ To activate, add this to your $PROFILE:
84
+ Invoke-Expression (tfv shell-init powershell | Out-String)
85
+ Then restart PowerShell or run: . $PROFILE
86
+ `,
87
+ };
88
+
89
+ exports.shellInit = (shell) => {
90
+ const s = (shell || '').toLowerCase().trim();
91
+
92
+ if (!SUPPORTED_SHELLS.includes(s)) {
93
+ console.error(`${P_ERROR}Unsupported shell: ${shell}${P_END}`);
94
+ console.error(`Supported shells: ${SUPPORTED_SHELLS.join(', ')}`);
95
+ process.exit(1);
96
+ }
97
+
98
+ // Print the hook script to stdout (intended to be eval'd)
99
+ if (s === 'fish') {
100
+ process.stdout.write(FISH_HOOK + '\n');
101
+ } else if (s === 'powershell' || s === 'pwsh') {
102
+ process.stdout.write(POWERSHELL_HOOK + '\n');
103
+ } else {
104
+ // bash and zsh use the same hook
105
+ process.stdout.write(BASH_ZSH_HOOK + '\n');
106
+ }
107
+
108
+ // Print install instructions to stderr so they don't get eval'd
109
+ const key = s === 'pwsh' ? 'powershell' : s;
110
+ if (INSTALL_INSTRUCTIONS[key]) {
111
+ process.stderr.write(INSTALL_INSTRUCTIONS[key] + '\n');
112
+ }
113
+ };
@@ -1,60 +1,144 @@
1
1
  const fs = require('fs');
2
- const {join} = require('path');
3
- const {use} = require('./use');
4
- const {install} = require('./install');
5
- const {P_END, P_ERROR, P_INFO} = require('../utils/colors');
6
- const {checkStore} = require('../utils/store');
2
+ const { join } = require('path');
3
+ const { use } = require('./use');
4
+ const { install } = require('./install');
5
+ const { normalizeProvider, getProviderStore, initDirs } = require('../utils/paths');
6
+ const { P_END, P_ERROR, P_INFO } = require('../utils/colors');
7
+
8
+ /**
9
+ * Resolve required_version constraint to an exact version number.
10
+ * Handles: =, ==, >=, >, ~>, and compound constraints (">= 1.3, < 2.0")
11
+ */
12
+ const resolveConstraint = (constraint, versions) => {
13
+ // Normalize: remove spaces around operators
14
+ const parts = constraint.split(',').map(s => s.trim());
15
+
16
+ return versions.find(v => {
17
+ return parts.every(part => {
18
+ const m = part.match(/^(~>|>=|<=|!=|>|<|=)?\s*(\d+(?:\.\d+)*)/);
19
+ if (!m) return false;
20
+ const [, op = '=', req] = m;
21
+
22
+ const vParts = v.split('.').map(n => parseInt(n, 10) || 0);
23
+ const rParts = req.split('.').map(n => parseInt(n, 10) || 0);
24
+
25
+ const cmp = () => {
26
+ for (let i = 0; i < 3; i++) {
27
+ const diff = (vParts[i] || 0) - (rParts[i] || 0);
28
+ if (diff !== 0) return diff;
29
+ }
30
+ return 0;
31
+ };
32
+
33
+ switch (op) {
34
+ case '~>': {
35
+ // pessimistic: allow patch or minor bumps only at the last specified segment
36
+ const segCount = rParts.length;
37
+ // major.minor segments must match, only patch can increase
38
+ for (let i = 0; i < segCount - 1; i++) {
39
+ if ((vParts[i] || 0) !== (rParts[i] || 0)) return false;
40
+ }
41
+ return (vParts[segCount - 1] || 0) >= (rParts[segCount - 1] || 0);
42
+ }
43
+ case '>=': return cmp() >= 0;
44
+ case '>': return cmp() > 0;
45
+ case '<=': return cmp() <= 0;
46
+ case '<': return cmp() < 0;
47
+ case '!=': return cmp() !== 0;
48
+ default: return cmp() === 0;
49
+ }
50
+ });
51
+ }) || null;
52
+ };
53
+
54
+ exports.autoSwitch = async (options = {}, providerArg = 'terraform') => {
55
+ const { silent = false } = options;
56
+ const log = silent ? () => {} : console.log;
7
57
 
8
- exports.autoSwitch = async () => {
9
58
  try {
59
+ initDirs();
60
+ const provider = normalizeProvider(providerArg);
61
+
10
62
  let versionFile;
11
63
  let tfVersion;
12
- const tfState = `terraform.tfstate`;
13
64
 
14
- if (fs.existsSync(tfState)) {
15
- versionFile = tfState;
16
- tfVersion = JSON.parse(fs.readFileSync(tfState, 'utf-8'))['terraform_version'];
17
- } else {
18
- const getAllFiles = fs.readdirSync(process.cwd());
19
- const likelyVersionFiles = ['main.tf', 'provider.tf', 'providers.tf', 'versions.tf', 'version.tf', 'backend.tf', 'terraform.tf'];
20
- const existingFiles = getAllFiles.filter(f => likelyVersionFiles.indexOf(f) !== -1);
21
- const checkVersionIn = existingFiles.concat(getAllFiles.filter(f => existingFiles.indexOf(f) === -1)).filter(f => f.endsWith('.tf'));
22
-
23
- for (let i = 0; i < checkVersionIn.length; i++) {
24
- versionFile = checkVersionIn[i];
25
- const requiredVersion = fs.readFileSync(versionFile, 'utf-8');
26
- const pattern = /^([\s]{0,}required_version)[\s]{0,}=[\s]{0,}"(>=|=|>|~>)[\s]{0,}\d+.*"/m;
27
- const specified = requiredVersion.match(pattern);
28
-
29
- if (specified) {
30
- const vPattern = /\d+(\.\d+)?(\.\d+)?/;
31
- [tfVersion] = specified[0].trim().match(vPattern);
65
+ // 1. Check .terraform-version file first (tfenv compatibility)
66
+ const dotVersionFile = join(process.cwd(), '.terraform-version');
67
+ if (fs.existsSync(dotVersionFile)) {
68
+ const pinned = fs.readFileSync(dotVersionFile, 'utf-8').trim();
69
+ if (pinned) {
70
+ versionFile = '.terraform-version';
71
+ tfVersion = pinned;
72
+ }
73
+ }
74
+
75
+ // 2. Check terraform.tfstate for version
76
+ if (!tfVersion) {
77
+ const tfState = 'terraform.tfstate';
78
+ if (fs.existsSync(tfState)) {
79
+ versionFile = tfState;
80
+ tfVersion = JSON.parse(fs.readFileSync(tfState, 'utf-8'))['terraform_version'];
81
+ }
82
+ }
83
+
84
+ // 3. Scan .tf files for required_version
85
+ if (!tfVersion) {
86
+ const allFiles = fs.readdirSync(process.cwd());
87
+ const priority = ['main.tf', 'provider.tf', 'providers.tf', 'versions.tf', 'version.tf', 'backend.tf', 'terraform.tf'];
88
+ const tfFiles = [
89
+ ...allFiles.filter(f => priority.includes(f)),
90
+ ...allFiles.filter(f => f.endsWith('.tf') && !priority.includes(f)),
91
+ ];
92
+
93
+ const pattern = /required_version\s*=\s*"([^"]+)"/m;
94
+
95
+ for (const file of tfFiles) {
96
+ const content = fs.readFileSync(file, 'utf-8');
97
+ const match = content.match(pattern);
98
+ if (match) {
99
+ versionFile = file;
100
+ tfVersion = match[1].trim();
32
101
  break;
33
102
  }
34
103
  }
35
104
  }
36
105
 
37
- if (tfVersion) {
38
- console.log('required_version was found in:', versionFile);
39
- const store = join(__dirname, '../..', 'store');
106
+ if (!tfVersion) {
107
+ log(`${P_ERROR}No terraform version found.${P_END}`);
108
+ log(`Create a .terraform-version file or set required_version in a .tf file.`);
109
+ return;
110
+ }
111
+
112
+ log(`${P_INFO}Found version constraint "${tfVersion}" in: ${versionFile}${P_END}`);
40
113
 
41
- checkStore(store);
114
+ const store = getProviderStore(provider);
115
+ const installedFiles = fs.readdirSync(store).filter(f => f !== 'arch.json');
116
+ const installedVersions = installedFiles.map(f => f.replace('.exe', ''));
42
117
 
43
- const inStore = fs.readdirSync(store).find(v => v.startsWith(tfVersion));
118
+ // If exact version (no operators), use directly
119
+ let resolved = tfVersion;
120
+ if (/[><=~!,]/.test(tfVersion)) {
121
+ const { fetchAllVersions } = require('./remote');
122
+ const allVersions = await fetchAllVersions(provider);
123
+ resolved = resolveConstraint(tfVersion, allVersions);
44
124
 
45
- if (inStore) {
46
- await use(inStore.replace('.exe', ''));
47
- } else {
48
- const version = await install(`${tfVersion}^`);
49
- await use(version);
125
+ if (!resolved) {
126
+ log(`${P_ERROR}No ${provider} version satisfies constraint: ${tfVersion}${P_END}`);
127
+ return;
50
128
  }
129
+ log(`${P_INFO}Resolved to: ${resolved}${P_END}`);
130
+ }
131
+
132
+ const inStore = installedVersions.find(v => v === resolved || v.startsWith(resolved));
133
+ if (inStore) {
134
+ await use(inStore, provider);
51
135
  } else {
52
- console.log(`${P_ERROR}NOT FOUND:${P_END}${P_INFO}required_version${P_END}`);
53
- console.log(`${P_INFO}check in your remote state file${P_END}`);
136
+ log(`${P_INFO}${provider} ${resolved} not in store. Installing...${P_END}`);
137
+ const installed = await install(resolved, null, provider);
138
+ if (installed) await use(installed, provider);
54
139
  }
55
140
 
56
- } catch ({message}) {
57
- console.log('ERROR:', message)
141
+ } catch (err) {
142
+ if (!options.silent) console.log(`${P_ERROR}auto-switch error: ${err.message}${P_END}`);
58
143
  }
59
- }
60
-
144
+ };