tfv 5.0.0 → 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 -129
  3. package/demo.gif +0 -0
  4. package/demo.tape +230 -0
  5. package/index.js +0 -4
  6. package/lib/commands/apply.js +9 -4
  7. package/lib/commands/current.js +25 -0
  8. package/lib/commands/destroy.js +9 -4
  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 +9 -4
  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,22 +1,26 @@
1
1
  'use strict'
2
2
 
3
- const yargs = require('yargs');
4
- const {use} = require('../modules/use');
3
+ const { use } = require('../modules/use');
5
4
 
6
- exports.command = 'use <version>'
7
- exports.desc = 'Switch to a specified terraform version\n'
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('verbose', {
11
- describe: 'Produce detailed output',
12
- type: 'boolean',
9
+ .option('provider', {
10
+ alias: 'p',
11
+ describe: 'Provider: terraform (default) or tofu/opentofu',
12
+ type: 'string',
13
+ default: 'terraform',
13
14
  })
14
- .epilog('Example: tfv use 1.5.7')
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 [,version] = yargs.argv._;
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
+ };
@@ -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
- existsSync,
6
- unlinkSync,
7
- readFileSync,
8
- writeFileSync,
9
- createWriteStream
7
+ chmodSync, existsSync, unlinkSync,
8
+ readFileSync, writeFileSync, createWriteStream
10
9
  } = require('fs');
11
- const {join} = require('path');
12
- const {arch} = process;
13
- const unzip = require('decompress');
14
- const {fetchAllVersions} = require('./remote');
15
- const {formatVersions} = require('../utils/formatVersions');
16
- const {P_END, P_ERROR, P_INFO, P_OK, P_WARN} = require('../utils/colors');
17
-
18
- exports.install = async (installVersion, sysArchitecture) => {
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
- const store = join(__dirname, '../..', 'store');
22
- const data = await fetchAllVersions();
23
- const result = formatVersions(data);
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
- [version] = result.filter(v => {
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
- [version] = result.filter(v => {
34
- const [, inTest] = v.split('-');
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
- if (!result.find(v => v === version)) {
41
- console.log(`${P_ERROR}Terraform ${version} not found.${P_END}`);
42
- console.log(`To view a list of available version, run ${P_OK}tfv list --remote${P_END}`);
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
- const getVersion = os.platform() === 'win32' ? `${version}.exe` : version;
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(`${store}/${getVersion}`)) {
49
- console.log(`${P_WARN}Terraform ${version} is already installed${P_END}`);
50
- return console.log(`To use this version Run: ${P_OK}tfv use ${version}${P_END}`)
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() === 'win32' ? 'windows' : 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
- if (os.platform() === 'darwin' && version.startsWith('0')) {
57
- sysArch = 'amd64'
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
- const url = `https://releases.hashicorp.com/terraform/${version}/terraform_${version}_${sysOs}_${archOption}.zip`;
121
+ await new Promise((resolve, reject) => {
122
+ console.log(`${P_INFO}Downloading ${provider} ${version} (${archOption})...${P_END}`);
63
123
 
64
- const manageTfArch = async (version) => {
65
- const archMap = `${store}/arch.json`;
66
- const archStore = readFileSync(`${archMap}`, 'utf8');
67
- const parseArchFile = JSON.parse(archStore);
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
- const fileName = join(__dirname, '../..', `${version}.zip`);
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
- const unzipFile = async (version) => {
75
- if (existsSync(fileName)) {
76
- await unzip(fileName, store, {
77
- map: file => {
78
- const [, ext] = `${file.path}`.split('.');
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
- unlinkSync(fileName);
85
- }
86
- }
153
+ req.on('error', reject);
154
+ });
87
155
 
88
- const makeExecutable = async (version) => {
89
- if (sysOs !== 'windows') {
90
- chmodSync(`${store}/${version}`, '755');
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
- return new Promise((resolve, reject) => {
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
- fileStream.on('close', async () => {
106
- await unzipFile(version);
107
- await manageTfArch(version);
108
- await makeExecutable(version);
109
- resolve(version);
110
- });
175
+ if (sysOs !== 'win32') {
176
+ chmodSync(storedFile, '755');
177
+ }
111
178
 
112
- fileStream.on('finish', () => {
113
- fileStream.close();
114
- console.log(`${P_OK}Successful!${P_END}`);
115
- console.log(`To use this version Run: ${P_OK}tfv use ${version}${P_END}`);
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
- req.on('error', (err) => {
120
- reject(console.log(`${P_ERROR}Error downloading the terraform ${version}${P_END}\n`, err))
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 ({message}) {
125
- console.log('ERROR:', message)
189
+ } catch (err) {
190
+ console.log(`${P_ERROR}Install failed: ${err.message}${P_END}`);
191
+ process.exit(1);
126
192
  }
127
- }
193
+ };
@@ -1,138 +1,99 @@
1
- const fs = require('fs');
2
- const {join} = require('path');
3
- const {spawnSync} = require('child_process')
4
- const {fetchAllVersions} = require('./remote');
5
- const {formatVersions} = require('../utils/formatVersions');
6
- const {P_END, P_OK} = require('../utils/colors');
7
- const {checkStore} = require('../utils/store');
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 MAX_COLUMNS_PER_TABLE = 6;
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
- versions.forEach(version => {
18
- const parts = version.split('.');
13
+ versions.forEach(v => {
14
+ const parts = v.split('.');
19
15
  if (parts.length >= 2) {
20
- const majorMinor = `${parts[0]}.${parts[1]}`;
21
- if (!groups[majorMinor]) {
22
- groups[majorMinor] = [];
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
- * Displays versions in table format with first version of each release as column header
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 [aMajor, aMinor] = a.split('.').map(Number);
41
- const [bMajor, bMinor] = b.split('.').map(Number);
42
- if (bMajor !== aMajor) return bMajor - aMajor;
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 += MAX_COLUMNS_PER_TABLE) {
63
- chunks.push(sortedKeys.slice(i, i + MAX_COLUMNS_PER_TABLE));
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
- // Display each chunk as a separate table
67
- chunks.forEach((chunkKeys, chunkIndex) => {
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(key => {
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
- let versions;
52
+ const provider = normalizeProvider(providerArg);
53
+ const cliName = getCliName(provider);
94
54
 
95
55
  if (remote) {
96
- const data = await fetchAllVersions();
97
- versions = formatVersions(data);
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
- if (local) {
104
- const store = join(__dirname, '../..', 'store');
61
+ // Default: local
62
+ checkStore(provider);
63
+ const store = getProviderStore(provider);
105
64
 
106
- checkStore(store);
107
-
108
- const tfVersion = spawnSync('terraform', ['version'], {stdio: 'pipe', encoding: 'utf-8'});
109
- const pattern = /v\d+\.\d+\.\d+/;
110
- const versionOutput = tfVersion.stdout.match(pattern);
111
-
112
- const archMap = `${store}/arch.json`;
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
- if (versionOutput && versionNumber === versionOutput[0].replace('v', '')) {
125
- vnObj['System Arch'] = `${parseArchFile[versionNumber]} 🚀`;
126
- }
127
- else vnObj['System Arch'] = parseArchFile[versionNumber];
128
- installedVersions.push(vnObj);
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
- console.log(`${P_OK}Terraform versions installed by tfv${P_END}`);
133
- return console.table(installedVersions);
134
- }
135
- } catch ({message}) {
136
- console.log(message)
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
+ };