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/commands/use.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const {use} = require('../modules/use');
|
|
3
|
+
const { use } = require('../modules/use');
|
|
5
4
|
|
|
6
|
-
exports.command = 'use <
|
|
7
|
-
exports.desc = 'Switch to a specified terraform version
|
|
5
|
+
exports.command = 'use <ver>'
|
|
6
|
+
exports.desc = 'Switch to a specified terraform or opentofu version'
|
|
8
7
|
exports.builder = (yargs) => {
|
|
9
8
|
return yargs
|
|
10
|
-
.option('
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
.option('provider', {
|
|
10
|
+
alias: 'p',
|
|
11
|
+
describe: 'Provider: terraform (default) or tofu/opentofu',
|
|
12
|
+
type: 'string',
|
|
13
|
+
default: 'terraform',
|
|
13
14
|
})
|
|
14
|
-
.epilog(
|
|
15
|
+
.epilog([
|
|
16
|
+
'Examples:',
|
|
17
|
+
' tfv use 1.7.3',
|
|
18
|
+
' tfv use latest',
|
|
19
|
+
' tfv use 1.7.3 --provider tofu',
|
|
20
|
+
].join('\n'))
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
exports.handler = async () => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
await use(version);
|
|
23
|
+
exports.handler = async (argv) => {
|
|
24
|
+
const { ver, provider } = argv;
|
|
25
|
+
await use(ver, provider);
|
|
21
26
|
}
|
|
22
|
-
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { runTerraformCommand } = require('../modules/terraform-command');
|
|
4
|
+
|
|
5
|
+
exports.command = 'validate'
|
|
6
|
+
exports.desc = 'Run terraform validate. Accepts all terraform flags after --'
|
|
7
|
+
exports.builder = (yargs) => {
|
|
8
|
+
return yargs
|
|
9
|
+
.option('provider', {
|
|
10
|
+
alias: 'p',
|
|
11
|
+
describe: 'Provider: terraform (default) or tofu/opentofu',
|
|
12
|
+
type: 'string',
|
|
13
|
+
default: 'terraform',
|
|
14
|
+
})
|
|
15
|
+
.epilog('Example: tfv validate -- -json')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
exports.handler = async (argv) => {
|
|
19
|
+
const extraArgs = argv._.slice(1);
|
|
20
|
+
await runTerraformCommand('validate', null, extraArgs, argv.provider);
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const { existsSync, readFileSync } = require('fs');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
const { normalizeProvider, getCliName, getPaths, getBinTarget, checkPathConflict } = require('../utils/paths');
|
|
4
|
+
const { P_END, P_OK, P_INFO, P_WARN, P_ERROR } = require('../utils/colors');
|
|
5
|
+
|
|
6
|
+
exports.current = async (providerArg = 'terraform') => {
|
|
7
|
+
try {
|
|
8
|
+
const provider = normalizeProvider(providerArg);
|
|
9
|
+
const paths = getPaths();
|
|
10
|
+
|
|
11
|
+
let activeVersion = null;
|
|
12
|
+
if (existsSync(paths.active)) {
|
|
13
|
+
const active = JSON.parse(readFileSync(paths.active, 'utf-8'));
|
|
14
|
+
activeVersion = active[provider];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!activeVersion) {
|
|
18
|
+
console.log(`${P_WARN}No active ${provider} version set.${P_END}`);
|
|
19
|
+
console.log(`Run: ${P_OK}tfv use <version>${providerArg === 'terraform' ? '' : ` --provider ${providerArg}`}${P_END}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Confirm binary is present and get its real version output
|
|
24
|
+
const binary = getBinTarget(provider);
|
|
25
|
+
const cliName = getCliName(provider);
|
|
26
|
+
|
|
27
|
+
console.log(`${P_INFO}Active ${provider} version: ${P_OK}${activeVersion}${P_END}`);
|
|
28
|
+
console.log(`${P_INFO}Binary: ${binary}${P_END}`);
|
|
29
|
+
|
|
30
|
+
if (existsSync(binary)) {
|
|
31
|
+
const result = spawnSync(binary, ['version'], { stdio: 'pipe', encoding: 'utf-8' });
|
|
32
|
+
if (result.stdout) {
|
|
33
|
+
const firstLine = result.stdout.split('\n')[0];
|
|
34
|
+
console.log(`${P_INFO}Reported: ${firstLine}${P_END}`);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log(`${P_WARN}Binary not found at ${binary}. Re-run: tfv use ${activeVersion}${providerArg === 'terraform' ? '' : ` --provider ${providerArg}`}${P_END}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if a conflicting installation shadows tfv's binary
|
|
41
|
+
const conflict = checkPathConflict(provider);
|
|
42
|
+
if (conflict.ok) {
|
|
43
|
+
console.log(`${P_OK}PATH OK — '${cliName}' resolves to tfv-managed binary${P_END}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(`${P_WARN}PATH CONFLICT — '${cliName}' resolves to: ${conflict.resolved}${P_END}`);
|
|
46
|
+
console.log(`${P_WARN}Expected: ${conflict.expected}${P_END}`);
|
|
47
|
+
console.log(`${P_INFO}Run ${P_OK}tfv upgrade${P_END}${P_INFO} to re-anchor the PATH automatically.${P_END}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(`${P_ERROR}Error: ${err.message}${P_END}`);
|
|
51
|
+
}
|
|
52
|
+
};
|
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
|
+
};
|