tfv 5.0.1 → 6.1.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 +43 -3
- package/README.md +253 -122
- 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/doctor.js +20 -0
- package/lib/commands/exec.js +33 -0
- 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/prune.js +41 -0
- 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/doctor.js +160 -0
- package/lib/modules/exec.js +36 -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/prune.js +100 -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 +226 -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/install.js
CHANGED
|
@@ -1,127 +1,193 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { join } = path;
|
|
3
6
|
const {
|
|
4
|
-
chmodSync,
|
|
5
|
-
|
|
6
|
-
unlinkSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
createWriteStream
|
|
7
|
+
chmodSync, existsSync, unlinkSync,
|
|
8
|
+
readFileSync, writeFileSync, createWriteStream
|
|
10
9
|
} = require('fs');
|
|
11
|
-
const
|
|
12
|
-
const {
|
|
13
|
-
const
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
const AdmZip = require('adm-zip');
|
|
11
|
+
const { fetchAllVersions, fetchUrl } = require('./remote');
|
|
12
|
+
const { normalizeProvider, getProviderStore, getVersionFile, initDirs } = require('../utils/paths');
|
|
13
|
+
const { P_END, P_ERROR, P_INFO, P_OK, P_WARN } = require('../utils/colors');
|
|
14
|
+
|
|
15
|
+
const getDownloadUrl = (provider, version, sysOs, archOption) => {
|
|
16
|
+
if (provider === 'opentofu') {
|
|
17
|
+
const osName = sysOs === 'darwin' ? 'darwin' : sysOs === 'win32' ? 'windows' : 'linux';
|
|
18
|
+
return `https://github.com/opentofu/opentofu/releases/download/v${version}/tofu_${version}_${osName}_${archOption}.zip`;
|
|
19
|
+
}
|
|
20
|
+
const osName = sysOs === 'win32' ? 'windows' : sysOs;
|
|
21
|
+
return `https://releases.hashicorp.com/terraform/${version}/terraform_${version}_${osName}_${archOption}.zip`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const getSha256Url = (provider, version, sysOs, archOption) => {
|
|
25
|
+
if (provider === 'opentofu') {
|
|
26
|
+
return `https://github.com/opentofu/opentofu/releases/download/v${version}/tofu_${version}_SHA256SUMS`;
|
|
27
|
+
}
|
|
28
|
+
return `https://releases.hashicorp.com/terraform/${version}/terraform_${version}_SHA256SUMS`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getZipName = (provider, version, sysOs, archOption) => {
|
|
32
|
+
const osName = sysOs === 'win32' ? 'windows' : sysOs === 'darwin' ? 'darwin' : 'linux';
|
|
33
|
+
const prefix = provider === 'opentofu' ? 'tofu' : 'terraform';
|
|
34
|
+
return `${prefix}_${version}_${osName}_${archOption}.zip`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const verifyChecksum = async (filePath, provider, version, sysOs, archOption) => {
|
|
38
|
+
const sumsUrl = getSha256Url(provider, version, sysOs, archOption);
|
|
39
|
+
const zipName = getZipName(provider, version, sysOs, archOption);
|
|
40
|
+
|
|
41
|
+
const sumsData = await fetchUrl(sumsUrl);
|
|
42
|
+
const matchLine = sumsData.split('\n').find(l => l.includes(zipName));
|
|
43
|
+
|
|
44
|
+
if (!matchLine) throw new Error(`Checksum entry not found for ${zipName}`);
|
|
45
|
+
|
|
46
|
+
const [expectedHash] = matchLine.trim().split(/\s+/);
|
|
47
|
+
const fileBuffer = readFileSync(filePath);
|
|
48
|
+
const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
49
|
+
|
|
50
|
+
if (actualHash !== expectedHash) {
|
|
51
|
+
throw new Error(`SHA256 mismatch!\n Expected: ${expectedHash}\n Got: ${actualHash}`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const showProgress = (received, total) => {
|
|
56
|
+
if (!total) return;
|
|
57
|
+
const pct = Math.min(100, Math.floor((received / total) * 100));
|
|
58
|
+
const filled = Math.floor(pct / 2);
|
|
59
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(50 - filled);
|
|
60
|
+
process.stdout.write(`\r [${bar}] ${pct}% (${(received / 1024 / 1024).toFixed(1)} MB)`);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const providerFlag = (providerArg) => providerArg === 'terraform' ? '' : ` --provider ${providerArg}`;
|
|
64
|
+
|
|
65
|
+
exports.install = async (installVersion, sysArchitecture, providerArg = 'terraform') => {
|
|
19
66
|
try {
|
|
67
|
+
initDirs();
|
|
68
|
+
|
|
69
|
+
const provider = normalizeProvider(providerArg);
|
|
20
70
|
let version = installVersion;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
|
|
72
|
+
// isExplicitPreRelease: user typed e.g. "1.8.0-beta1" intentionally.
|
|
73
|
+
// In that case we skip stable-only validation and let the download confirm existence.
|
|
74
|
+
const isExplicitPreRelease = version !== 'latest'
|
|
75
|
+
&& !`${version}`.endsWith('^')
|
|
76
|
+
&& `${version}`.includes('-');
|
|
77
|
+
|
|
78
|
+
// Stable versions only — used for latest/^ resolution and validation of stable requests.
|
|
79
|
+
const stableVersions = isExplicitPreRelease ? [] : await fetchAllVersions(provider);
|
|
24
80
|
|
|
25
81
|
if (version === 'latest') {
|
|
26
|
-
|
|
27
|
-
const [, inTest] = v.split('-');
|
|
28
|
-
if (!inTest) return v;
|
|
29
|
-
});
|
|
82
|
+
version = stableVersions[0];
|
|
30
83
|
}
|
|
31
84
|
|
|
32
85
|
if (`${version}`.endsWith('^')) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (v.startsWith(version.replace('^', '')) && !inTest) return v
|
|
36
|
-
});
|
|
37
|
-
version = version ? version : installVersion.replace('^', '');
|
|
86
|
+
const prefix = version.replace('^', '');
|
|
87
|
+
version = stableVersions.find(v => v.startsWith(prefix)) || prefix;
|
|
38
88
|
}
|
|
39
89
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(
|
|
90
|
+
// Validate explicit stable version exists; skip check for explicit pre-releases.
|
|
91
|
+
if (!isExplicitPreRelease && !stableVersions.includes(version)) {
|
|
92
|
+
console.log(`${P_ERROR}${provider} ${version} not found.${P_END}`);
|
|
93
|
+
console.log(`To view available versions, run ${P_OK}tfv list --remote${providerFlag(providerArg)}${P_END}`);
|
|
43
94
|
process.exit(1);
|
|
44
95
|
}
|
|
45
96
|
|
|
46
|
-
|
|
97
|
+
if (isExplicitPreRelease) {
|
|
98
|
+
console.log(`${P_WARN}Installing pre-release version ${version}. Use stable versions for production.${P_END}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const store = getProviderStore(provider);
|
|
102
|
+
const storedFile = getVersionFile(provider, version);
|
|
47
103
|
|
|
48
|
-
if (existsSync(
|
|
49
|
-
console.log(`${P_WARN}
|
|
50
|
-
return console.log(`To use this version
|
|
104
|
+
if (existsSync(storedFile)) {
|
|
105
|
+
console.log(`${P_WARN}${provider} ${version} is already installed${P_END}`);
|
|
106
|
+
return console.log(`To use this version run: ${P_OK}tfv use ${version}${providerFlag(providerArg)}${P_END}`);
|
|
51
107
|
}
|
|
52
108
|
|
|
53
|
-
const sysOs = os.platform()
|
|
54
|
-
let sysArch = arch === 'x64' ? 'amd64' : arch;
|
|
109
|
+
const sysOs = os.platform();
|
|
110
|
+
let sysArch = process.arch === 'x64' ? 'amd64' : process.arch;
|
|
55
111
|
|
|
56
|
-
|
|
57
|
-
|
|
112
|
+
// Older terraform versions on macOS only had amd64 builds
|
|
113
|
+
if (provider === 'terraform' && sysOs === 'darwin' && version.startsWith('0')) {
|
|
114
|
+
sysArch = 'amd64';
|
|
58
115
|
}
|
|
59
116
|
|
|
60
117
|
const archOption = sysArchitecture || sysArch;
|
|
118
|
+
const url = getDownloadUrl(provider, version, sysOs, archOption);
|
|
119
|
+
const tmpZip = join(os.tmpdir(), `tfv-${provider}-${version}.zip`);
|
|
61
120
|
|
|
62
|
-
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
console.log(`${P_INFO}Downloading ${provider} ${version} (${archOption})...${P_END}`);
|
|
63
123
|
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
parseArchFile[version] = archOption;
|
|
69
|
-
writeFileSync(`${archMap}`, JSON.stringify(parseArchFile, null, 2));
|
|
70
|
-
}
|
|
124
|
+
// Defined before use to avoid temporal dead zone
|
|
125
|
+
const handleResponse = (res) => {
|
|
126
|
+
const total = parseInt(res.headers['content-length'], 10);
|
|
127
|
+
let received = 0;
|
|
71
128
|
|
|
72
|
-
|
|
129
|
+
const fileStream = createWriteStream(tmpZip);
|
|
130
|
+
res.on('data', chunk => {
|
|
131
|
+
received += chunk.length;
|
|
132
|
+
showProgress(received, total);
|
|
133
|
+
});
|
|
134
|
+
res.pipe(fileStream);
|
|
73
135
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
file.path = ext ? `${version}.${ext}` : `${version}`;
|
|
80
|
-
return file;
|
|
81
|
-
}
|
|
136
|
+
fileStream.on('error', reject);
|
|
137
|
+
fileStream.on('finish', () => {
|
|
138
|
+
fileStream.close();
|
|
139
|
+
process.stdout.write('\n');
|
|
140
|
+
resolve();
|
|
82
141
|
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const req = https.get(url, { headers: { 'User-Agent': 'tfv-cli' } }, (res) => {
|
|
145
|
+
// Follow redirect — GitHub releases redirect to S3/CDN
|
|
146
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
147
|
+
return https.get(res.headers.location, { headers: { 'User-Agent': 'tfv-cli' } }, handleResponse)
|
|
148
|
+
.on('error', reject);
|
|
149
|
+
}
|
|
150
|
+
handleResponse(res);
|
|
151
|
+
});
|
|
83
152
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
153
|
+
req.on('error', reject);
|
|
154
|
+
});
|
|
87
155
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
156
|
+
console.log(`${P_INFO}Verifying SHA256 checksum...${P_END}`);
|
|
157
|
+
await verifyChecksum(tmpZip, provider, version, sysOs, archOption);
|
|
158
|
+
console.log(`${P_OK}Checksum verified.${P_END}`);
|
|
159
|
+
|
|
160
|
+
console.log(`${P_INFO}Extracting...${P_END}`);
|
|
161
|
+
const zip = new AdmZip(tmpZip);
|
|
162
|
+
for (const entry of zip.getEntries()) {
|
|
163
|
+
if (entry.isDirectory) continue;
|
|
164
|
+
const ext = path.extname(entry.entryName).toLowerCase(); // '' or '.exe'
|
|
165
|
+
if (!ext || ext === '.exe') {
|
|
166
|
+
// Rename binary to the version number, preserving .exe on Windows
|
|
167
|
+
const destName = ext ? `${version}${ext}` : version;
|
|
168
|
+
writeFileSync(path.join(store, destName), entry.getData());
|
|
91
169
|
}
|
|
170
|
+
// All other entries (LICENSE.txt, CHANGELOG.md, etc.) are simply skipped
|
|
92
171
|
}
|
|
93
172
|
|
|
94
|
-
|
|
95
|
-
const req = https.get(url, (res) => {
|
|
96
|
-
console.log(`${P_INFO}Installing terraform ${version} (for ${archOption} architecture) ${P_END}`);
|
|
97
|
-
|
|
98
|
-
const fileStream = createWriteStream(fileName);
|
|
99
|
-
res.pipe(fileStream);
|
|
100
|
-
|
|
101
|
-
fileStream.on('error', (err) => {
|
|
102
|
-
console.log(`${P_ERROR}Error writing stream ${P_END}\n`, err);
|
|
103
|
-
});
|
|
173
|
+
unlinkSync(tmpZip);
|
|
104
174
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await makeExecutable(version);
|
|
109
|
-
resolve(version);
|
|
110
|
-
});
|
|
175
|
+
if (sysOs !== 'win32') {
|
|
176
|
+
chmodSync(storedFile, '755');
|
|
177
|
+
}
|
|
111
178
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
179
|
+
// Save arch mapping
|
|
180
|
+
const archFile = join(store, 'arch.json');
|
|
181
|
+
const archMap = JSON.parse(readFileSync(archFile, 'utf-8'));
|
|
182
|
+
archMap[version] = archOption;
|
|
183
|
+
writeFileSync(archFile, JSON.stringify(archMap, null, 2));
|
|
118
184
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
185
|
+
console.log(`${P_OK}Installed ${provider} ${version} successfully!${P_END}`);
|
|
186
|
+
console.log(`To use this version run: ${P_OK}tfv use ${version}${providerFlag(providerArg)}${P_END}`);
|
|
187
|
+
return version;
|
|
123
188
|
|
|
124
|
-
} catch (
|
|
125
|
-
console.log(
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.log(`${P_ERROR}Install failed: ${err.message}${P_END}`);
|
|
191
|
+
process.exit(1);
|
|
126
192
|
}
|
|
127
|
-
}
|
|
193
|
+
};
|
package/lib/modules/list.js
CHANGED
|
@@ -1,138 +1,99 @@
|
|
|
1
|
-
const
|
|
2
|
-
const {join} = require('path');
|
|
3
|
-
const {spawnSync} = require('child_process')
|
|
4
|
-
const {fetchAllVersions} = require('./remote');
|
|
5
|
-
const {
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
1
|
+
const { existsSync, readdirSync, readFileSync } = require('fs');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const { fetchAllVersions } = require('./remote');
|
|
5
|
+
const { normalizeProvider, getProviderStore, getCliName, getPaths } = require('../utils/paths');
|
|
6
|
+
const { checkStore } = require('../utils/store');
|
|
7
|
+
const { P_END, P_OK, P_WARN } = require('../utils/colors');
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const MAX_COLUMNS = 6;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
* Groups versions by their major.minor prefix
|
|
13
|
-
*/
|
|
14
|
-
const groupVersionsByRelease = (versions) => {
|
|
11
|
+
const groupByRelease = (versions) => {
|
|
15
12
|
const groups = {};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const parts = version.split('.');
|
|
13
|
+
versions.forEach(v => {
|
|
14
|
+
const parts = v.split('.');
|
|
19
15
|
if (parts.length >= 2) {
|
|
20
|
-
const
|
|
21
|
-
if (!groups[
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
groups[majorMinor].push(version);
|
|
16
|
+
const key = `${parts[0]}.${parts[1]}`;
|
|
17
|
+
if (!groups[key]) groups[key] = [];
|
|
18
|
+
groups[key].push(v);
|
|
25
19
|
}
|
|
26
20
|
});
|
|
27
|
-
|
|
28
21
|
return groups;
|
|
29
22
|
};
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* Breaks into multiple tables if columns exceed MAX_COLUMNS_PER_TABLE
|
|
34
|
-
*/
|
|
35
|
-
const displayVersionTable = (versions) => {
|
|
36
|
-
const groups = groupVersionsByRelease(versions);
|
|
37
|
-
|
|
38
|
-
// Sort release groups by version (descending)
|
|
24
|
+
const displayRemoteTable = (versions) => {
|
|
25
|
+
const groups = groupByRelease(versions);
|
|
39
26
|
const sortedKeys = Object.keys(groups).sort((a, b) => {
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
|
|
43
|
-
return bMinor - aMinor;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Sort versions within each group (descending)
|
|
47
|
-
sortedKeys.forEach(key => {
|
|
48
|
-
groups[key].sort((a, b) => {
|
|
49
|
-
const aParts = a.split('.').map(n => parseInt(n) || 0);
|
|
50
|
-
const bParts = b.split('.').map(n => parseInt(n) || 0);
|
|
51
|
-
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
52
|
-
const aVal = aParts[i] || 0;
|
|
53
|
-
const bVal = bParts[i] || 0;
|
|
54
|
-
if (bVal !== aVal) return bVal - aVal;
|
|
55
|
-
}
|
|
56
|
-
return 0;
|
|
57
|
-
});
|
|
27
|
+
const [aMaj, aMin] = a.split('.').map(Number);
|
|
28
|
+
const [bMaj, bMin] = b.split('.').map(Number);
|
|
29
|
+
return bMaj !== aMaj ? bMaj - aMaj : bMin - aMin;
|
|
58
30
|
});
|
|
59
31
|
|
|
60
|
-
// Split into chunks of MAX_COLUMNS_PER_TABLE
|
|
61
32
|
const chunks = [];
|
|
62
|
-
for (let i = 0; i < sortedKeys.length; i +=
|
|
63
|
-
chunks.push(sortedKeys.slice(i, i +
|
|
33
|
+
for (let i = 0; i < sortedKeys.length; i += MAX_COLUMNS) {
|
|
34
|
+
chunks.push(sortedKeys.slice(i, i + MAX_COLUMNS));
|
|
64
35
|
}
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Find max rows needed for this chunk
|
|
69
|
-
const maxRows = Math.max(...chunkKeys.map(key => groups[key].length));
|
|
70
|
-
|
|
71
|
-
// Build table data
|
|
37
|
+
chunks.forEach((chunkKeys, idx) => {
|
|
38
|
+
const maxRows = Math.max(...chunkKeys.map(k => groups[k].length));
|
|
72
39
|
const tableData = [];
|
|
73
40
|
for (let row = 0; row < maxRows; row++) {
|
|
74
41
|
const rowObj = {};
|
|
75
|
-
chunkKeys.forEach(
|
|
76
|
-
// Use major.minor.x format as header (e.g., "1.5.x")
|
|
77
|
-
const header = `${key}.x`;
|
|
78
|
-
const value = groups[key][row] || '';
|
|
79
|
-
rowObj[header] = value;
|
|
80
|
-
});
|
|
42
|
+
chunkKeys.forEach(k => { rowObj[`${k}.x`] = groups[k][row] || ''; });
|
|
81
43
|
tableData.push(rowObj);
|
|
82
44
|
}
|
|
83
|
-
|
|
84
|
-
if (chunks.length > 1) {
|
|
85
|
-
console.log(`\n${P_OK}Table ${chunkIndex + 1} of ${chunks.length}${P_END}`);
|
|
86
|
-
}
|
|
45
|
+
if (chunks.length > 1) console.log(`\n${P_OK}Table ${idx + 1} of ${chunks.length}${P_END}`);
|
|
87
46
|
console.table(tableData);
|
|
88
47
|
});
|
|
89
48
|
};
|
|
90
49
|
|
|
91
|
-
exports.list = async (local, remote) => {
|
|
50
|
+
exports.list = async (local, remote, providerArg = 'terraform') => {
|
|
92
51
|
try {
|
|
93
|
-
|
|
52
|
+
const provider = normalizeProvider(providerArg);
|
|
53
|
+
const cliName = getCliName(provider);
|
|
94
54
|
|
|
95
55
|
if (remote) {
|
|
96
|
-
const
|
|
97
|
-
versions
|
|
98
|
-
|
|
99
|
-
console.log(`${P_OK}List of all available terraform versions${P_END}\n`);
|
|
100
|
-
return displayVersionTable(versions);
|
|
56
|
+
const versions = await fetchAllVersions(provider);
|
|
57
|
+
console.log(`${P_OK}Available ${provider} versions (${versions.length} total)${P_END}\n`);
|
|
58
|
+
return displayRemoteTable(versions);
|
|
101
59
|
}
|
|
102
60
|
|
|
103
|
-
|
|
104
|
-
|
|
61
|
+
// Default: local
|
|
62
|
+
checkStore(provider);
|
|
63
|
+
const store = getProviderStore(provider);
|
|
105
64
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const archStore = fs.readFileSync(`${archMap}`, 'utf8');
|
|
114
|
-
const parseArchFile = JSON.parse(archStore);
|
|
115
|
-
const installedVersions = [];
|
|
116
|
-
|
|
117
|
-
versions = fs.readdirSync(store).forEach(f => {
|
|
118
|
-
if (f !== 'arch.json') {
|
|
119
|
-
const versionNumber = f.replace('.exe', '');
|
|
120
|
-
const vnObj = {};
|
|
121
|
-
|
|
122
|
-
vnObj['Terraform Version'] = versionNumber;
|
|
65
|
+
// Get currently active version
|
|
66
|
+
const paths = getPaths();
|
|
67
|
+
let activeVersion = null;
|
|
68
|
+
if (existsSync(paths.active)) {
|
|
69
|
+
const active = JSON.parse(readFileSync(paths.active, 'utf-8'));
|
|
70
|
+
activeVersion = active[provider];
|
|
71
|
+
}
|
|
123
72
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
73
|
+
const archFile = join(store, 'arch.json');
|
|
74
|
+
const archMap = existsSync(archFile) ? JSON.parse(readFileSync(archFile, 'utf-8')) : {};
|
|
75
|
+
|
|
76
|
+
const rows = readdirSync(store)
|
|
77
|
+
.filter(f => f !== 'arch.json')
|
|
78
|
+
.map(f => f.replace('.exe', ''))
|
|
79
|
+
.sort((a, b) => {
|
|
80
|
+
const aParts = a.split('.').map(n => parseInt(n, 10) || 0);
|
|
81
|
+
const bParts = b.split('.').map(n => parseInt(n, 10) || 0);
|
|
82
|
+
for (let i = 0; i < 3; i++) {
|
|
83
|
+
const diff = (bParts[i] || 0) - (aParts[i] || 0);
|
|
84
|
+
if (diff !== 0) return diff;
|
|
129
85
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
86
|
+
return 0;
|
|
87
|
+
})
|
|
88
|
+
.map(v => ({
|
|
89
|
+
'Version': v === activeVersion ? `${v} 🚀` : v,
|
|
90
|
+
'Arch': archMap[v] || 'unknown',
|
|
91
|
+
'Status': v === activeVersion ? 'active' : '',
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
console.log(`${P_OK}Installed ${provider} versions${P_END}`);
|
|
95
|
+
console.table(rows);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.log(err.message);
|
|
137
98
|
}
|
|
138
|
-
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { existsSync, readFileSync, writeFileSync } = require('fs');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const { normalizeProvider, getPaths } = require('../utils/paths');
|
|
4
|
+
const { P_END, P_OK, P_WARN, P_ERROR } = require('../utils/colors');
|
|
5
|
+
|
|
6
|
+
exports.pin = async (version, providerArg = 'terraform') => {
|
|
7
|
+
try {
|
|
8
|
+
const provider = normalizeProvider(providerArg);
|
|
9
|
+
let pinVersion = version;
|
|
10
|
+
|
|
11
|
+
if (!pinVersion || pinVersion === 'current') {
|
|
12
|
+
const paths = getPaths();
|
|
13
|
+
if (!existsSync(paths.active)) {
|
|
14
|
+
console.log(`${P_ERROR}No active ${provider} version found.${P_END}`);
|
|
15
|
+
console.log(`Run: ${P_OK}tfv use <version>${providerArg === 'terraform' ? '' : ` --provider ${providerArg}`}${P_END}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const active = JSON.parse(readFileSync(paths.active, 'utf-8'));
|
|
19
|
+
pinVersion = active[provider];
|
|
20
|
+
if (!pinVersion) {
|
|
21
|
+
console.log(`${P_ERROR}No active ${provider} version set.${P_END}`);
|
|
22
|
+
console.log(`Run: ${P_OK}tfv use <version>${providerArg === 'terraform' ? '' : ` --provider ${providerArg}`}${P_END}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const targetFile = join(process.cwd(), '.terraform-version');
|
|
28
|
+
writeFileSync(targetFile, pinVersion);
|
|
29
|
+
|
|
30
|
+
console.log(`${P_OK}Pinned ${provider} ${pinVersion} to .terraform-version${P_END}`);
|
|
31
|
+
console.log(`${P_OK}Other team members can run: tfv auto-switch${P_END}`);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.log(`${P_ERROR}Error: ${err.message}${P_END}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const { existsSync, readdirSync, unlinkSync, readFileSync, writeFileSync } = require('fs');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { normalizeProvider, getProviderStore, getVersionFile, getPaths } = require('../utils/paths');
|
|
5
|
+
const { P_END, P_OK, P_ERROR, P_WARN, P_INFO } = require('../utils/colors');
|
|
6
|
+
|
|
7
|
+
const sortVersionsDesc = (versions) => versions.sort((a, b) => {
|
|
8
|
+
const ap = a.split('.').map(n => parseInt(n, 10) || 0);
|
|
9
|
+
const bp = b.split('.').map(n => parseInt(n, 10) || 0);
|
|
10
|
+
for (let i = 0; i < 3; i++) {
|
|
11
|
+
const d = (bp[i] || 0) - (ap[i] || 0);
|
|
12
|
+
if (d !== 0) return d;
|
|
13
|
+
}
|
|
14
|
+
return 0;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const confirm = (question) => new Promise(resolve => {
|
|
18
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
rl.question(question, ans => { rl.close(); resolve(ans.trim().toLowerCase()); });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
exports.prune = async (providerArg = 'terraform', keep = 0, yes = false) => {
|
|
23
|
+
try {
|
|
24
|
+
const provider = normalizeProvider(providerArg);
|
|
25
|
+
const store = getProviderStore(provider);
|
|
26
|
+
|
|
27
|
+
if (!existsSync(store)) {
|
|
28
|
+
console.log(`${P_ERROR}No store found for ${provider}.${P_END}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const archFile = join(store, 'arch.json');
|
|
33
|
+
const archMap = existsSync(archFile) ? JSON.parse(readFileSync(archFile, 'utf-8')) : {};
|
|
34
|
+
|
|
35
|
+
const paths = getPaths();
|
|
36
|
+
let activeVersion = null;
|
|
37
|
+
if (existsSync(paths.active)) {
|
|
38
|
+
const active = JSON.parse(readFileSync(paths.active, 'utf-8'));
|
|
39
|
+
activeVersion = active[provider];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const installed = sortVersionsDesc(
|
|
43
|
+
readdirSync(store)
|
|
44
|
+
.filter(f => f !== 'arch.json')
|
|
45
|
+
.map(f => f.replace(/\.exe$/, ''))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (installed.length === 0) {
|
|
49
|
+
console.log(`${P_WARN}No ${provider} versions installed.${P_END}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build the keep set: active version is always kept; top N by semver if --keep N
|
|
54
|
+
const keepSet = new Set();
|
|
55
|
+
if (activeVersion) keepSet.add(activeVersion);
|
|
56
|
+
if (keep > 0) installed.slice(0, keep).forEach(v => keepSet.add(v));
|
|
57
|
+
|
|
58
|
+
const toRemove = installed.filter(v => !keepSet.has(v));
|
|
59
|
+
|
|
60
|
+
if (toRemove.length === 0) {
|
|
61
|
+
console.log(`${P_OK}Nothing to prune for ${provider}.${P_END}`);
|
|
62
|
+
if (activeVersion) console.log(`${P_INFO}Active version ${activeVersion} is kept.${P_END}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`${P_INFO}Versions to remove (${provider}):${P_END}`);
|
|
67
|
+
toRemove.forEach(v => console.log(` ${P_WARN}${v}${P_END}`));
|
|
68
|
+
console.log();
|
|
69
|
+
if (activeVersion) console.log(`${P_OK}Keeping (active): ${activeVersion}${P_END}`);
|
|
70
|
+
if (keep > 0) {
|
|
71
|
+
const kept = installed.filter(v => keepSet.has(v) && v !== activeVersion);
|
|
72
|
+
if (kept.length) console.log(`${P_OK}Keeping (--keep ${keep}): ${kept.join(', ')}${P_END}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!yes) {
|
|
76
|
+
const ans = await confirm(`\nRemove ${toRemove.length} version(s)? [y/N] `);
|
|
77
|
+
if (ans !== 'y' && ans !== 'yes') {
|
|
78
|
+
console.log(`${P_WARN}Aborted.${P_END}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log();
|
|
84
|
+
toRemove.forEach(v => {
|
|
85
|
+
const file = getVersionFile(provider, v);
|
|
86
|
+
if (existsSync(file)) {
|
|
87
|
+
unlinkSync(file);
|
|
88
|
+
delete archMap[v];
|
|
89
|
+
console.log(`${P_OK}Removed ${provider} ${v}${P_END}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
writeFileSync(archFile, JSON.stringify(archMap, null, 2));
|
|
94
|
+
console.log(`\n${P_OK}Done. Pruned ${toRemove.length} version(s).${P_END}`);
|
|
95
|
+
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.log(`${P_ERROR}Error: ${err.message}${P_END}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
};
|