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.
- package/.github/workflows/publish.yml +27 -3
- package/README.md +190 -132
- package/demo.gif +0 -0
- package/demo.tape +230 -0
- package/index.js +0 -4
- package/lib/commands/apply.js +8 -3
- package/lib/commands/current.js +25 -0
- package/lib/commands/destroy.js +8 -3
- package/lib/commands/fmt.js +26 -0
- package/lib/commands/init.js +26 -0
- package/lib/commands/install.js +22 -13
- package/lib/commands/list.js +20 -11
- package/lib/commands/pin.js +26 -0
- package/lib/commands/plan.js +8 -3
- package/lib/commands/remove.js +17 -12
- package/lib/commands/shell-init.js +25 -0
- package/lib/commands/switch.js +28 -7
- package/lib/commands/upgrade.js +26 -0
- package/lib/commands/use.js +17 -13
- package/lib/commands/validate.js +21 -0
- package/lib/modules/current.js +52 -0
- package/lib/modules/install.js +155 -89
- package/lib/modules/list.js +66 -105
- package/lib/modules/pin.js +35 -0
- package/lib/modules/ps1.js +37 -29
- package/lib/modules/remote.js +68 -15
- package/lib/modules/remove.js +35 -21
- package/lib/modules/shell-init.js +113 -0
- package/lib/modules/switch.js +125 -41
- package/lib/modules/terraform-command.js +49 -67
- package/lib/modules/upgrade.js +93 -0
- package/lib/modules/use.js +58 -54
- package/lib/utils/formatVersions.js +57 -5
- package/lib/utils/paths.js +156 -0
- package/lib/utils/postInstall.js +37 -13
- package/lib/utils/store.js +17 -6
- package/package.json +11 -9
- package/test/extractTargets.test.js +75 -0
- package/test/formatVersions.test.js +126 -0
- package/test/moduleImports.test.js +45 -0
- package/test/paths.test.js +69 -0
- package/test/versionResolution.test.js +92 -0
package/lib/modules/ps1.js
CHANGED
|
@@ -1,38 +1,46 @@
|
|
|
1
|
-
const {spawnSync} = require(
|
|
2
|
-
const {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 (!
|
|
12
|
-
console.log(`${
|
|
13
|
-
|
|
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
|
|
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 (
|
|
22
|
-
const newSystemEnv = `${terraformPath};${systemEnv}`;
|
|
29
|
+
if (pathEntries.includes(BIN_DIR)) return; // Already in PATH
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|
package/lib/modules/remote.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
|
package/lib/modules/remove.js
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
|
-
const os = require('os')
|
|
2
|
-
const
|
|
3
|
-
const {join} = require('path');
|
|
4
|
-
const {
|
|
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 (
|
|
7
|
+
exports.remove = async (versions, providerArg = 'terraform') => {
|
|
7
8
|
try {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const provider = normalizeProvider(providerArg);
|
|
10
|
+
const store = getProviderStore(provider);
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
23
|
+
versions.forEach(v => {
|
|
24
|
+
const file = getVersionFile(provider, v);
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
package/lib/modules/switch.js
CHANGED
|
@@ -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 {
|
|
6
|
-
const {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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 (
|
|
57
|
-
console.log(
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (!options.silent) console.log(`${P_ERROR}auto-switch error: ${err.message}${P_END}`);
|
|
58
143
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
144
|
+
};
|