diff --git a/package-lock.json b/package-lock.json index 563bb9d..38cdeb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3947,15 +3947,16 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -10584,9 +10585,11 @@ "commander": "^9.4.0", "compare-versions": "^6.0.0", "enquirer": "^2.3.6", + "follow-redirects": "^1.15.6", "node-stream-zip": "^1.15.0", "pino": "^8.15.1", - "pino-pretty": "^10.0.0" + "pino-pretty": "^10.0.0", + "semver": "^7.6.3" }, "bin": { "tfvm": "lib/index.js" @@ -10602,6 +10605,18 @@ "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } + }, + "packages/cli/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } } } } diff --git a/packages/cli/README.md b/packages/cli/README.md index 67a0435..2a3a189 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -71,6 +71,8 @@ Run `tfvm` in any command line, followed by one of these commands: - `tfvm config disableErrors=true` - disables configuration warnings. - `tfvm config disableAWSWarnings=true` - disables AWS warnings that appear when using older terraform versions. - `tfvm config disableSettingPrompts=true` - disables prompts that show how to hide some error messages. + - `tfvm config useOpenTofu=true` - uses the open source version of Terraform, OpenTofu (experimental flag). This flag will also delete your terraform executable so you can only perfom tofu actions. When you switch back to `useOpenTofu=false`, the tofu executable will be deleted. This is so you don't perform any accidental commands in the wrong type of IAC. + - `tfvm config disableTofuWarnings=true` - disables warnings related to using Tofu (deleting executables, using Tofu instead of Terraform, etc.) - `help`: prints usage information. Run `tfvm help ` to see information about the other tfvm commands. ## FAQ diff --git a/packages/cli/lib/commands/config.js b/packages/cli/lib/commands/config.js index db07adc..a08ad51 100644 --- a/packages/cli/lib/commands/config.js +++ b/packages/cli/lib/commands/config.js @@ -4,6 +4,7 @@ import getSettings, { defaultSettings } from '../util/getSettings.js' import getErrorMessage from '../util/errorChecker.js' import { logger } from '../util/logger.js' import { getOS } from '../util/tfvmOS.js' +import deleteExecutable from '../util/deleteExecutable.js' const os = getOS() @@ -20,10 +21,15 @@ async function config (setting) { // we need to store logical true or false, not the string 'true' or 'false'. This converts to a boolean: settingsObj[settingKey] = value === 'true' await fs.writeFile(os.getSettingsDir(), JSON.stringify(settingsObj), 'utf8') + + if (settingKey === 'useOpenTofu') { + await deleteExecutable(settingsObj[settingKey]) + } } else { console.log(chalk.red.bold(`Invalid input for ${settingKey} setting. ` + `Use either 'tfvm config ${settingKey}=true' or 'tfvm config ${settingKey}=false'`)) } + console.log(chalk.cyan.bold(`Successfully set ${setting}`)) } else { logger.warn(`Invalid setting change attempt with setting=${setting}`) diff --git a/packages/cli/lib/commands/current.js b/packages/cli/lib/commands/current.js index 87b90f1..8e78a0e 100644 --- a/packages/cli/lib/commands/current.js +++ b/packages/cli/lib/commands/current.js @@ -3,20 +3,22 @@ import chalk from 'chalk' import getTerraformVersion from '../util/tfVersion.js' import getErrorMessage from '../util/errorChecker.js' import { logger } from '../util/logger.js' +import getSettings from '../util/getSettings.js' import { getOS } from '../util/tfvmOS.js' const os = getOS() async function current () { try { + const settings = await getSettings() const currentTFVersion = await getTerraformVersion() if (currentTFVersion !== null) { - console.log(chalk.white.bold('Current Terraform version:\n' + + console.log(chalk.white.bold(`Current ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} version:\n` + currentTFVersion + ` (Currently using ${os.getBitWidth()}-bit executable)`)) } else { - console.log(chalk.cyan.bold('It appears there is no terraform version running on your computer, or ' + + console.log(chalk.cyan.bold(`It appears there is no ${settings.useOpenTofu ? 'opentofu' : 'terraform'} version running on your computer, or ` + 'there was an error extracting the version.\n')) - console.log(chalk.green.bold('Run tfvm use to set your terraform version, ' + - 'or `terraform -v` to manually check the current version.')) + console.log(chalk.green.bold(`Run tfvm use to set your ${settings.useOpenTofu ? 'opentofu' : 'terraform'} version, ` + + `or ${settings.useOpenTofu ? 'tofu' : 'terraform'} -v to manually check the current version.`)) } } catch (error) { logger.fatal(error, 'Fatal error when running "current" command: ') diff --git a/packages/cli/lib/commands/detect.js b/packages/cli/lib/commands/detect.js index 0b78af6..060bb03 100644 --- a/packages/cli/lib/commands/detect.js +++ b/packages/cli/lib/commands/detect.js @@ -13,8 +13,10 @@ import getTerraformVersion from '../util/tfVersion.js' import { installNewVersion, switchVersionTo, useVersion } from './use.js' import { logger } from '../util/logger.js' import { TfvmFS } from '../util/TfvmFS.js' +import getSettings from '../util/getSettings.js' async function detect () { + const settings = await getSettings() // set of objects that contain the constraints and the file name const tfVersionConstraintSet = new Set() try { @@ -26,10 +28,10 @@ async function detect () { await satisfyConstraints(tfVersionConstraintSet) } else { // todo let the user select from list of frequently used versions instead of this disappointing message - console.log(chalk.white.bold('No terraform files containing any version constraints are found in this directory.')) + console.log(chalk.white.bold(`No ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} files containing any version constraints are found in this directory.`)) } } catch (error) { - logger.fatal(error, 'Fatal error when running "detect" command with these local terraform ' + + logger.fatal(error, `Fatal error when running "detect" command with these local ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} ` + `constraints: ${Array.from(tfVersionConstraintSet).map(c => JSON.stringify(c)).join('; ')}: `) getErrorMessage(error) } @@ -48,9 +50,10 @@ async function satisfyConstraints (tfVersionConstraintSet) { ? tfVersionConstraints // if the current version is null, then all the constraints are unmet : getUnmetConstraints(tfVersionConstraints, currentTfVersion) if (unmetVersionConstraints.length === 0) { + const settings = await getSettings() // exit quickly if the current tf version satisfies all required constraints - console.log(chalk.cyan.bold(`Your current terraform version (${currentTfVersion}) already ` + - 'satisfies the requirements of your local terraform files.')) + console.log(chalk.cyan.bold(`Your current ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} version (${currentTfVersion}) already ` + + `satisfies the requirements of your local ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} files.`)) } else if (unmetVersionConstraints.length === 1 && tfVersionConstraints.length === 1) { await satisfySingleConstraint(unmetVersionConstraints[0]) } else if (unmetVersionConstraints.length >= 1) { @@ -68,7 +71,8 @@ async function satisfyMultipleConstraints (tfVersionConstraints) { // if all the constraints are single versions, give them a dropdown list to select from await chooseAndUseVersionFrom(tfVersionConstraints) } else { - console.log(chalk.white.bold('There are multiple terraform version constraints in this directory:')) + const settings = await getSettings() + console.log(chalk.white.bold(`There are multiple ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} version constraints in this directory:`)) tfVersionConstraints.forEach(c => console.log(chalk.white.bold(` - ${c.displayVersion} (${c.fileName})`))) @@ -186,13 +190,14 @@ async function findLocalVersionConstraints (constraintSet) { const fileName = TfvmFS.getFileNameFromPath(filePath) const fileHclAsJson = parser.parse(content) if (fileHclAsJson.required_core) { + const settings = await getSettings() fileHclAsJson.required_core.forEach(version => { // terraform supports '!=' and `~>' in semver but the 'compare-versions' package does not for (const badOperator of ['!=']) { if (version.includes(badOperator)) { logger.error(`Failed to parse version ${version} because of '${badOperator}'.`) console.log(chalk.red.bold(`Ignoring constraint from ${fileName} ` + - `because tfvm doesn't support parsing versions with '${badOperator}' in the terraform required_version.`)) + `because tfvm doesn't support parsing versions with '${badOperator}' in the ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} required_version.`)) return // functional equivalent of 'continue' in forEach } } diff --git a/packages/cli/lib/commands/install.js b/packages/cli/lib/commands/install.js index f3200fe..60bc278 100644 --- a/packages/cli/lib/commands/install.js +++ b/packages/cli/lib/commands/install.js @@ -1,4 +1,5 @@ import chalk from 'chalk' +import deleteExecutable from '../util/deleteExecutable.js' import fs from 'node:fs/promises' import { versionRegEx } from '../util/constants.js' import getInstalledVersions from '../util/getInstalledVersions.js' @@ -8,20 +9,31 @@ import getErrorMessage from '../util/errorChecker.js' import getTerraformVersion from '../util/tfVersion.js' import getLatest from '../util/getLatest.js' import { logger } from '../util/logger.js' +import getSettings from '../util/getSettings.js' import { getOS, Mac } from '../util/tfvmOS.js' import { compare } from 'compare-versions' +import * as semver from 'semver' const os = getOS() const LAST_TF_VERSION_WITHOUT_ARM = '1.0.1' +const LOWEST_OTF_VERSION = '1.6.0' async function install (versionNum) { try { + const settings = await getSettings() const installVersion = 'v' + versionNum + const openTofuCheck = settings.useOpenTofu && semver.gte(versionNum, LOWEST_OTF_VERSION) + if (!versionRegEx.test(installVersion) && versionNum !== 'latest') { logger.warn(`invalid version attempted to install with version ${installVersion}`) console.log(chalk.red.bold('Invalid version syntax.')) - console.log(chalk.white.bold('Version should be formatted as \'vX.X.X\'\nGet a list of all current ' + + if (settings.useOpenTofu) { + console.log(chalk.white.bold('Version should be formatted as \'vX.X.X\'\nGet a list of all current ' + + 'opentofu versions here: https://github.com/opentofu/opentofu/releases')) + } else { + console.log(chalk.white.bold('Version should be formatted as \'vX.X.X\'\nGet a list of all current ' + 'terraform versions here: https://releases.hashicorp.com/terraform/')) + } } else if (versionNum === 'latest') { const installedVersions = await getInstalledVersions() const latest = await getLatest() @@ -29,11 +41,11 @@ async function install (versionNum) { if (latest) { const versionLatest = 'v' + latest if (installedVersions.includes(versionLatest) && currentVersion !== versionLatest) { - console.log(chalk.bold.cyan(`The latest terraform version is ${latest} and is ` + + console.log(chalk.bold.cyan(`The latest ${openTofuCheck ? 'opentofu' : 'terraform'} version is ${latest} and is ` + `already installed on your computer. Run 'tfvm use ${latest}' to use.`)) } else if (installedVersions.includes(versionLatest) && currentVersion === versionLatest) { const currentVersion = await getTerraformVersion() - console.log(chalk.bold.cyan(`The latest terraform version is ${currentVersion} and ` + + console.log(chalk.bold.cyan(`The latest ${openTofuCheck ? 'opentofu' : 'terraform'} version is ${currentVersion} and ` + 'is already installed and in use on your computer.')) } else { await installFromWeb(latest) @@ -42,7 +54,7 @@ async function install (versionNum) { } else { const installedVersions = await getInstalledVersions() if (installedVersions.includes(installVersion)) { - console.log(chalk.white.bold(`Terraform version ${installVersion} is already installed.`)) + console.log(chalk.white.bold(`${openTofuCheck ? 'OpenTofu' : 'Terraform'} version ${installVersion} is already installed.`)) } else { await installFromWeb(versionNum) } @@ -56,8 +68,17 @@ async function install (versionNum) { export default install export async function installFromWeb (versionNum, printMessage = true) { - const zipPath = os.getPath(os.getTfVersionsDir(), `v${versionNum}.zip`) - const newVersionDir = os.getPath(os.getTfVersionsDir(), 'v' + versionNum) + const settings = await getSettings() + const openTofuCheck = settings.useOpenTofu && semver.gte(versionNum, LOWEST_OTF_VERSION) + const openTofuCheckLessThan = settings.useOpenTofu && semver.lt(versionNum, LOWEST_OTF_VERSION) + if (openTofuCheckLessThan) { + await deleteExecutable(false) + } else if (openTofuCheck) { + await deleteExecutable(true) + } + let url + let zipPath + let newVersionDir let arch = os.getArchitecture() // Only newer terraform versions include a release for ARM (Apple Silicon) hardware, but their chips *can* @@ -68,7 +89,17 @@ export async function installFromWeb (versionNum, printMessage = true) { console.log(chalk.bold.yellow(`Warning: There is no available ARM release of Terraform for version ${versionNum}. Installing the amd64 version instead (should run without issue via Rosetta)...`)) } - const url = `https://releases.hashicorp.com/terraform/${versionNum}/terraform_${versionNum}_${os.getOSName()}_${arch}.zip` + + if (openTofuCheck) { + zipPath = os.getPath(os.getOtfVersionsDir(), `v${versionNum}.zip`) + newVersionDir = os.getPath(os.getOtfVersionsDir(), 'v' + versionNum) + url = `https://github.com/opentofu/opentofu/releases/download/v${versionNum}/tofu_${versionNum}_${os.getOSName()}_${arch}.zip` + } else { + zipPath = os.getPath(os.getTfVersionsDir(), `v${versionNum}.zip`) + newVersionDir = os.getPath(os.getTfVersionsDir(), 'v' + versionNum) + url = `https://releases.hashicorp.com/terraform/${versionNum}/terraform_${versionNum}_${os.getOSName()}_${arch}.zip` + } + await download(url, zipPath, versionNum) await fs.mkdir(newVersionDir) await unzipFile(zipPath, newVersionDir) diff --git a/packages/cli/lib/commands/list.js b/packages/cli/lib/commands/list.js index 3c03a1b..d36994a 100644 --- a/packages/cli/lib/commands/list.js +++ b/packages/cli/lib/commands/list.js @@ -5,8 +5,11 @@ import getInstalledVersions from '../util/getInstalledVersions.js' import getErrorMessage from '../util/errorChecker.js' import { logger } from '../util/logger.js' import { getOS } from '../util/tfvmOS.js' +import * as semver from 'semver' +import getSettings from '../util/getSettings.js' const os = getOS() +const LOWEST_OTF_VERSION = '1.6.0' async function list () { try { const printList = [] @@ -14,17 +17,38 @@ async function list () { if (tfList.length > 0) { const currentTFVersion = await getTerraformVersion() - console.log('\n') tfList.sort(compareVersions).reverse() for (const versionDir of tfList) { + const settings = await getSettings() + const version = versionDir.substring(1, versionDir.length) + + let type = '' + if (settings.useOpenTofu) { + // logic to get the correct spacing + const parsed = semver.parse(version) + type += (parsed.minor.toString().length === 1 && parsed.patch.toString().length === 1 ? ' ' : ' ') + if (semver.gte(version, LOWEST_OTF_VERSION)) { + type += '[OpenTofu]' + } else if (semver.lt(version, LOWEST_OTF_VERSION)) { + type += '[Terraform]' + } + } + if (versionDir === currentTFVersion) { let printVersion = ' * ' - printVersion = printVersion + versionDir.substring(1, versionDir.length) + printVersion += version + if (settings.useOpenTofu) { + printVersion += type + } printVersion = printVersion + ` (Currently using ${os.getBitWidth()}-bit executable)` printList.push(printVersion) } else { let printVersion = ' ' - printVersion = printVersion + versionDir.substring(1, versionDir.length) + printVersion += version + + if (settings.useOpenTofu) { + printVersion += type + } printList.push(printVersion) } } diff --git a/packages/cli/lib/commands/uninstall.js b/packages/cli/lib/commands/uninstall.js index a403764..42d37c2 100644 --- a/packages/cli/lib/commands/uninstall.js +++ b/packages/cli/lib/commands/uninstall.js @@ -1,25 +1,36 @@ import chalk from 'chalk' import { versionRegEx } from '../util/constants.js' import getInstalledVersions from '../util/getInstalledVersions.js' -import { TfvmFS } from '../util/TfvmFS.js' import getErrorMessage from '../util/errorChecker.js' import { logger } from '../util/logger.js' +import getSettings from '../util/getSettings.js' import { getOS } from '../util/tfvmOS.js' +import { TfvmFS } from '../util/TfvmFS.js' +import * as semver from 'semver' const os = getOS() +const LOWEST_OTF_VERSION = '1.6.0' async function uninstall (uninstallVersion) { try { uninstallVersion = 'v' + uninstallVersion if (!versionRegEx.test(uninstallVersion)) { console.log(chalk.red.bold('Invalid version syntax')) } else { + const settings = await getSettings() const installedVersions = await getInstalledVersions() + const semverCheck = semver.gte(uninstallVersion, LOWEST_OTF_VERSION) + const openTofuCheck = settings.useOpenTofu && semverCheck + if (!installedVersions.includes(uninstallVersion)) { - console.log(chalk.white.bold(`terraform ${uninstallVersion} is not installed. Type "tfvm list" to see what is installed.`)) + console.log(chalk.white.bold(`${openTofuCheck ? 'opentofu' : 'terraform'} ${uninstallVersion} is not installed. Type "tfvm list" to see what is installed.`)) } else { - console.log(chalk.white.bold(`Uninstalling terraform ${uninstallVersion}...`)) - await TfvmFS.deleteDirectory(os.getTfVersionsDir(), uninstallVersion) - console.log(chalk.cyan.bold(`Successfully uninstalled terraform ${uninstallVersion}`)) + console.log(chalk.white.bold(`Uninstalling ${openTofuCheck ? 'opentofu' : 'terraform'} ${uninstallVersion}...`)) + if (openTofuCheck) { + await TfvmFS.deleteDirectory(os.getOtfVersionsDir(), uninstallVersion) + } else { + await TfvmFS.deleteDirectory(os.getTfVersionsDir(), uninstallVersion) + } + console.log(chalk.cyan.bold(`Successfully uninstalled ${openTofuCheck ? 'opentofu' : 'terraform'} ${uninstallVersion}`)) } } } catch (error) { diff --git a/packages/cli/lib/commands/use.js b/packages/cli/lib/commands/use.js index 70bc2ef..47624db 100644 --- a/packages/cli/lib/commands/use.js +++ b/packages/cli/lib/commands/use.js @@ -1,4 +1,5 @@ import chalk from 'chalk' +import deleteExecutable from '../util/deleteExecutable.js' import fs from 'node:fs/promises' import enquirer from 'enquirer' import { versionRegEx } from '../util/constants.js' @@ -10,8 +11,11 @@ import getSettings from '../util/getSettings.js' import requiresOldAWSAuth from '../util/requiresOldAWSAuth.js' import { logger } from '../util/logger.js' import { getOS } from '../util/tfvmOS.js' +import * as semver from 'semver' const os = getOS() +const LOWEST_OTF_VERSION = '1.6.0' +let openTofuCheck = false async function use (version) { try { @@ -34,7 +38,15 @@ export async function useVersion (version) { if (!versionRegEx.test(versionWithV)) { console.log(chalk.red.bold('Invalid version syntax')) } else { - const installedVersions = await getInstalledVersions() + const installedVersions = await getInstalledVersions(version) + const settings = await getSettings() + openTofuCheck = settings.useOpenTofu && semver.gte(version, LOWEST_OTF_VERSION) + // delete tofu executable if using a version lower than 1.6.0 + if (settings.useOpenTofu && semver.lt(version, LOWEST_OTF_VERSION)) { + await deleteExecutable(false) + } else if (openTofuCheck) { + await deleteExecutable(true) + } if (!installedVersions.includes(versionWithV)) { const successfullyInstalled = await installNewVersion(version) if (!successfullyInstalled) return @@ -49,13 +61,15 @@ export async function useVersion (version) { * @returns {Promise} true if the user opted to install the version, false if they did not */ export async function installNewVersion (version) { - console.log(chalk.white.bold(`Terraform v${version} is not installed. Would you like to install it?`)) + const settings = await getSettings() + const openTofuCheck = settings.useOpenTofu && semver.gte(version, LOWEST_OTF_VERSION) + console.log(chalk.white.bold(`${openTofuCheck ? 'OpenTofu' : 'Terraform'} v${version} is not installed. Would you like to install it?`)) const installToggle = new enquirer.Toggle({ disabled: 'Yes', enabled: 'No' }) if (await installToggle.run()) { - console.log(chalk.white.bold(`No action taken. Use 'tfvm install ${version}' to install terraform v${version}`)) + console.log(chalk.white.bold(`No action taken. Use 'tfvm install ${version}' to install ${settings.useOpenTofu ? 'opentofu' : 'terraform'} v${version}`)) return false } else { await installFromWeb(version, false) @@ -69,17 +83,27 @@ export async function installNewVersion (version) { * @returns {Promise} */ export async function switchVersionTo (version) { + const settings = await getSettings() if (version[0] === 'v') version = version.substring(1) - await TfvmFS.createTfAppDataDir() - await TfvmFS.deleteCurrentTfExe() + if (openTofuCheck) { + await TfvmFS.createOtfAppDataDir() + await TfvmFS.deleteCurrentOtfExe() + await fs.copyFile( + os.getPath(os.getOtfVersionsDir(), 'v' + version, os.getOtfExecutableName()), // source file + os.getPath(os.getOpenTofuDir(), os.getOtfExecutableName()) // destination file + ) + } else { + await TfvmFS.createTfAppDataDir() + await TfvmFS.deleteCurrentTfExe() + await fs.copyFile( + os.getPath(os.getTfVersionsDir(), 'v' + version, os.getTFExecutableName()), // source file + os.getPath(os.getTerraformDir(), os.getTFExecutableName()) // destination file + ) + } + + console.log(chalk.cyan.bold(`Now using ${openTofuCheck ? 'opentofu' : 'terraform'} v${version} (${os.getBitWidth()}-bit)`)) - await fs.copyFile( - os.getPath(os.getTfVersionsDir(), 'v' + version, os.getTFExecutableName()), // source file - os.getPath(os.getTerraformDir(), os.getTFExecutableName()) // destination file - ) - console.log(chalk.cyan.bold(`Now using terraform v${version} (${os.getBitWidth()}-bit)`)) - const settings = await getSettings() if (requiresOldAWSAuth(version) && !settings.disableAWSWarnings) { console.log(chalk.yellow.bold('Warning: This tf version is not compatible with the newest ' + 'AWS CLI authentication methods (e.g. aws sso login). Use short-term credentials instead.')) diff --git a/packages/cli/lib/index.js b/packages/cli/lib/index.js index 566565b..fe55964 100755 --- a/packages/cli/lib/index.js +++ b/packages/cli/lib/index.js @@ -1,5 +1,4 @@ #! /usr/bin/env node - import { Command } from 'commander' import list from './commands/list.js' import current from './commands/current.js' @@ -68,7 +67,9 @@ program 'Here are all the available settings:\n' + 'disableErrors - Disables some recurrent warning messages\n' + 'disableAWSWarnings - Disables warnings about needing old AWS authentication with tf versions older than 0.14.6\n' + - 'disableSettingsPrompts - Disables prompts to turn off warnings by enabling these settings') + 'disableSettingsPrompts - Disables prompts to turn off warnings by enabling these settings\n' + + 'useOpenTofu - Uses OpenTofu instead of Terraform\n' + + 'disableTofuWarnings - Disables warnings related to using Tofu (deleting executables, using Tofu instead of Terraform, etc.)') program .command('install ') diff --git a/packages/cli/lib/scripts/addToPathLinuxOpenTofu.sh b/packages/cli/lib/scripts/addToPathLinuxOpenTofu.sh new file mode 100644 index 0000000..a91ef7e --- /dev/null +++ b/packages/cli/lib/scripts/addToPathLinuxOpenTofu.sh @@ -0,0 +1,25 @@ + +#!/bin/bash + +# Set the path to add +path2add="$HOME/.local/share/opentofu" + +# Get the user's PATH +userPath=$(echo $PATH) + +# Check if the path already contains the path to add +if [[ ! "$userPath" == *"$path2add"* ]]; then + # Add the path to the user's PATH + export PATH="$PATH:$path2add" + # Update .bashrc to persist the changes (also creates .bashrc if it doesn't already exist) + echo "export PATH=\"\$PATH:$path2add\"" >> ~/.bashrc + echo "OpenTofu path added to user PATH in .bashrc." + # Check if .zshrc exists + if [ -f "$HOME/.zshrc" ]; then + # Add the path to .zshrc + echo "export PATH=\"\$PATH:$path2add\"" >> ~/.zshrc + echo "OpenTofu path added to user PATH in .zshrc." + fi +else + echo "OpenTofu path already exists in user PATH." +fi diff --git a/packages/cli/lib/scripts/addToPathMacOpenTofu.sh b/packages/cli/lib/scripts/addToPathMacOpenTofu.sh new file mode 100644 index 0000000..0a5e9ed --- /dev/null +++ b/packages/cli/lib/scripts/addToPathMacOpenTofu.sh @@ -0,0 +1,25 @@ + +#!/bin/bash + +# Set the path to add +path2add="$HOME/Library/Application Support/opentofu" + +# Get the user's PATH +userPath=$(echo $PATH) + +# Check if the path already contains the path to add +if [[ ! "$userPath" == *"$path2add"* ]]; then + # Update .zshrc to persist the changes (also creates .zshrc if it doesn't already exist) + echo "export PATH=\"\$PATH:$path2add\"" >> ~/.zshrc + echo "OpenTofu path added to user PATH in .zshrc." + + + # Check if .bashrc exists (if the user happens to be using a non-zsh terminal, we want to support that) + if [ -f "$HOME/.bashrc" ]; then + # Add the path to .bashrc + echo "export PATH=\"\$PATH:$path2add\"" >> ~/.bashrc + echo "OpenTofu path added to user PATH in .bashrc." + fi +else + echo "OpenTofu path already exists in user PATH." +fi diff --git a/packages/cli/lib/scripts/addToPathWindowsOpenTofu.ps1 b/packages/cli/lib/scripts/addToPathWindowsOpenTofu.ps1 new file mode 100644 index 0000000..7227193 --- /dev/null +++ b/packages/cli/lib/scripts/addToPathWindowsOpenTofu.ps1 @@ -0,0 +1,14 @@ +# This script attempts to add opentofu to the system and local paths. +# It will not add it if it already exists. +# It will not add it to the system path if the shell it is being run from does not have admin rights + +$userName = $env:UserName +$path2addOpenTofu = ";C:\Users\$userName\AppData\Roaming\opentofu" +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User'); + +# attempts to add to local path +If (!$userPath.contains($path2addOpenTofu)) { + $userPath += $path2addOpenTofu + $userPath = $userPath -join ';' + [Environment]::SetEnvironmentVariable('Path', $userPath, 'User'); +} diff --git a/packages/cli/lib/util/TfvmFS.js b/packages/cli/lib/util/TfvmFS.js index 89ed144..5034f57 100644 --- a/packages/cli/lib/util/TfvmFS.js +++ b/packages/cli/lib/util/TfvmFS.js @@ -17,13 +17,38 @@ export class TfvmFS { } /** - * Deletes the terraform exe so that a new one can be copied in + * Creates opentofu directory of it doesn't already exist * @returns {Promise} */ + static async createOtfAppDataDir () { + if (!fs.existsSync(os.getOpenTofuDir())) fs.mkdirSync(os.getOpenTofuDir()) + } + + /** + * Deletes the terraform exe so that a new one can be copied in + * @returns {Promise} + */ static async deleteCurrentTfExe () { // if appdata/roaming/terraform/terraform.exe exists, delete it if ((await fsp.readdir(os.getTerraformDir())).includes(os.getTFExecutableName())) { await fsp.unlink(os.getTerraformDir() + path.sep + os.getTFExecutableName()) + return true + } else { + return false + } + } + + /** + * Deletes the opentofu exe so that a new one can be copied in + * @returns {Promise} + */ + static async deleteCurrentOtfExe () { + // if appdata/roaming/opentofu/tofu.exe exists, delete it + if ((await fsp.readdir(os.getOpenTofuDir())).includes(os.getOtfExecutableName())) { + await fsp.unlink(os.getOpenTofuDir() + path.sep + os.getOtfExecutableName()) + return true + } else { + return false } } diff --git a/packages/cli/lib/util/constants.js b/packages/cli/lib/util/constants.js index 3c584a7..d937407 100644 --- a/packages/cli/lib/util/constants.js +++ b/packages/cli/lib/util/constants.js @@ -1,4 +1,5 @@ -export const versionRegEx = /^v[0-9]+.{1}[0-9]+.{1}[0-9]+/ +export const versionRegEx = /^v[0-9]+\.[0-9]+\.[0-9]+/ // Uses positive-lookbehind to only match versions preceded by 'Terraform ' but without extracting 'Terraform ' export const tfCurrVersionRegEx = /(?<=Terraform )v[0-9]+.{1}[0-9]+.{1}[0-9]+/gm +export const openTofuCurrVersionRegEx = /(?<=OpenTofu )v[0-9]+.{1}[0-9]+.{1}[0-9]+/gm diff --git a/packages/cli/lib/util/deleteExecutable.js b/packages/cli/lib/util/deleteExecutable.js new file mode 100644 index 0000000..619cad5 --- /dev/null +++ b/packages/cli/lib/util/deleteExecutable.js @@ -0,0 +1,28 @@ +import getErrorMessage from './errorChecker.js' +import chalk from 'chalk' +import { logger } from './logger.js' +import { TfvmFS } from './TfvmFS.js' +import getSettings from './getSettings.js' + +async function deleteExecutable (useOpenTofu) { + try { + let successfulDeletion = false + if (useOpenTofu === true) { + successfulDeletion = await TfvmFS.deleteCurrentTfExe() + } else { + successfulDeletion = await TfvmFS.deleteCurrentOtfExe() + } + + logger.info(`Successfully deleted ${useOpenTofu ? 'Terraform' : 'OpenTofu'} executable`) + const settings = await getSettings() + if (!settings.disableTofuWarnings && successfulDeletion) { + console.log(chalk.magenta.bold(`Switching to ${useOpenTofu ? 'OpenTofu' : 'Terraform'}. Make sure to use the ${useOpenTofu ? 'tofu' : 'terraform'} command instead of ${useOpenTofu ? 'terraform' : 'tofu'}.`)) + return true + } + } catch (error) { + logger.fatal(error, `Fatal error when deleting executable when useOpenTofu=${useOpenTofu}`) + getErrorMessage(error) + } +} + +export default deleteExecutable diff --git a/packages/cli/lib/util/download.js b/packages/cli/lib/util/download.js index 2cede79..ebb14ce 100644 --- a/packages/cli/lib/util/download.js +++ b/packages/cli/lib/util/download.js @@ -1,6 +1,10 @@ import chalk from 'chalk' +import getSettings from './getSettings.js' import axios from 'axios' import fs from 'node:fs/promises' +import * as semver from 'semver' + +const LOWEST_OTF_VERSION = '1.6.0' const download = async (url, filePath, version) => { try { @@ -8,7 +12,8 @@ const download = async (url, filePath, version) => { const fileData = Buffer.from(response.data, 'binary') await fs.writeFile(filePath, fileData) } catch (err) { - console.log(chalk.red.bold(`Terraform ${version} is not yet released or available.`)) + const settings = await getSettings() + console.log(chalk.red.bold(`${settings.useOpenTofu && semver.gte(version, LOWEST_OTF_VERSION) ? 'OpenTofu' : 'Terraform'} ${version} is not yet released or available.`)) throw new Error() } } diff --git a/packages/cli/lib/util/getInstalledVersions.js b/packages/cli/lib/util/getInstalledVersions.js index bc4489c..245e876 100644 --- a/packages/cli/lib/util/getInstalledVersions.js +++ b/packages/cli/lib/util/getInstalledVersions.js @@ -1,30 +1,54 @@ import fs from 'node:fs/promises' import { versionRegEx } from './constants.js' import { logger } from './logger.js' +import getSettings from './getSettings.js' import { getOS } from './tfvmOS.js' +import * as semver from 'semver' const os = getOS() let installedVersions +const LOWEST_OTF_VERSION = '1.6.0' /** * Returns a list of installed tf versions. * @returns {Promise} */ -async function getInstalledVersions () { +async function getInstalledVersions (version = '') { + const settings = await getSettings() // return the list of installed versions if that is already cached if (!installedVersions) { - const tfList = [] + const versionsList = [] + let files + + let semverCheck = true + if (version !== '') { + semverCheck = semver.gte(version, LOWEST_OTF_VERSION) + } + if (settings.useOpenTofu && semverCheck) { + files = await fs.readdir(os.getOtfVersionsDir()) + const terraformFiles = await fs.readdir(os.getTfVersionsDir()) + terraformFiles.forEach(file => { + if (semver.lt(file, LOWEST_OTF_VERSION)) { + files.push(file) + } + }) + } else { + files = await fs.readdir(os.getTfVersionsDir()) + } - const files = await fs.readdir(os.getTfVersionsDir()) if (files && files.length) { files.forEach(file => { if (versionRegEx.test(file)) { - tfList.push(file) + versionsList.push(file) } }) - installedVersions = tfList + installedVersions = versionsList } else { - logger.debug(`Unable to find installed versions of terraform with directoriesObj=${JSON.stringify(os.getDirectories())} and files=${JSON.stringify(files)}`) + if (settings.useOpenTofu) { + logger.debug(`Unable to find installed versions of OpenTofu with directoriesObj=${JSON.stringify(os.getDirectories())} and files=${JSON.stringify(files)}`) + } else { + logger.debug(`Unable to find installed versions of Terraform with directoriesObj=${JSON.stringify(os.getDirectories())} and files=${JSON.stringify(files)}`) + } return [] } } diff --git a/packages/cli/lib/util/getLatest.js b/packages/cli/lib/util/getLatest.js index 186bc94..c167f80 100644 --- a/packages/cli/lib/util/getLatest.js +++ b/packages/cli/lib/util/getLatest.js @@ -1,12 +1,23 @@ import axios from 'axios' import { logger } from './logger.js' +import getSettings from './getSettings.js' export default async function () { + const settings = await getSettings() try { - const response = await axios.get('https://checkpoint-api.hashicorp.com/v1/check/terraform') - return response.data.current_version + if (settings.useOpenTofu) { + const response = await axios.get('https://api.github.com/repos/opentofu/opentofu/releases/latest') + return response.data.name.replace('v', '') + } else { + const response = await axios.get('https://checkpoint-api.hashicorp.com/v1/check/terraform') + return response.data.current_version + } } catch (e) { - logger.fatal(e, 'Error attempting to fetch latest terraform version with Checkpoint Hashicorp API:') + if (settings.useOpenTofu) { + logger.fatal(e, 'Error attempting to fetch latest opentofu version with GitHub API:') + } else { + logger.fatal(e, 'Error attempting to fetch latest terraform version with Checkpoint Hashicorp API:') + } return null } } diff --git a/packages/cli/lib/util/getSettings.js b/packages/cli/lib/util/getSettings.js index dc77880..1917588 100644 --- a/packages/cli/lib/util/getSettings.js +++ b/packages/cli/lib/util/getSettings.js @@ -9,7 +9,9 @@ let settings export const defaultSettings = { disableErrors: false, disableAWSWarnings: false, - disableSettingPrompts: false + disableSettingPrompts: false, + useOpenTofu: false, + disableTofuWarnings: false } /** diff --git a/packages/cli/lib/util/logger.js b/packages/cli/lib/util/logger.js index bd3ed9c..3735f58 100644 --- a/packages/cli/lib/util/logger.js +++ b/packages/cli/lib/util/logger.js @@ -9,6 +9,7 @@ const date = new Date().toISOString().split('T')[0] // TODO re-enable logging // before creating a log file, make sure the logs folder exists (and its parent tfvm folder) +if (!fs.existsSync(os.getOtfvmDir())) fs.mkdirSync(os.getOtfvmDir()) if (!fs.existsSync(os.getTfvmDir())) fs.mkdirSync(os.getTfvmDir()) if (!fs.existsSync(os.getLogsDir())) fs.mkdirSync(os.getLogsDir()) diff --git a/packages/cli/lib/util/tfVersion.js b/packages/cli/lib/util/tfVersion.js index 144f584..94b71c9 100644 --- a/packages/cli/lib/util/tfVersion.js +++ b/packages/cli/lib/util/tfVersion.js @@ -1,22 +1,40 @@ import runShell from './runShell.js' import { logger } from './logger.js' -import { tfCurrVersionRegEx } from './constants.js' +import { tfCurrVersionRegEx, openTofuCurrVersionRegEx } from './constants.js' +import getSettings from './getSettings.js' let currentTfVersion +let currentOtfVersion /** * Returns the current terraform version, if there is one. Returns null if there is no current version * @returns {Promise} */ async function getTerraformVersion () { + const settings = await getSettings() // cache current tf version during a single execution of tfvm - if (currentTfVersion) return currentTfVersion - const response = (await runShell('terraform -v')) + if (currentTfVersion && !settings.useOpenTofu) return currentTfVersion + if (currentOtfVersion && settings.useOpenTofu) return currentOtfVersion + + let response + if (settings.useOpenTofu) { + response = (await runShell('tofu -v')) + if (response === null) { + response = (await runShell('terraform -v')) + } + } else { + response = (await runShell('terraform -v')) + } + if (response == null) { logger.error('Error getting terraform version') return null } - const versionExtractionResult = Array.from(response.matchAll(tfCurrVersionRegEx)) + + let versionExtractionResult + if (response.includes('Terraform')) versionExtractionResult = Array.from(response.matchAll(tfCurrVersionRegEx)) + else versionExtractionResult = Array.from(response.matchAll(openTofuCurrVersionRegEx)) + if (versionExtractionResult.length === 0) { logger.error('Error extracting terraform version where this is the response from `terraform -v`:\n' + response) return null diff --git a/packages/cli/lib/util/tfvmOS.js b/packages/cli/lib/util/tfvmOS.js index 293247e..a3951fb 100644 --- a/packages/cli/lib/util/tfvmOS.js +++ b/packages/cli/lib/util/tfvmOS.js @@ -19,6 +19,7 @@ export class TfvmOS { logFolderName = 'logs' tfVersionsFolderName = 'versions' tfvmAppDataFolderName = 'tfvm' + otfvmAppDataFolderName = 'otfvm' static getOS () { const osClassBinding = { @@ -36,10 +37,18 @@ export class TfvmOS { return this.getAppDataDir().concat(sep + this.tfvmAppDataFolderName) } + getOtfvmDir () { + return this.getAppDataDir().concat(sep + this.otfvmAppDataFolderName) + } + getTfVersionsDir () { return this.getTfvmDir().concat(sep + this.tfVersionsFolderName) } + getOtfVersionsDir () { + return this.getOtfvmDir().concat(sep + this.tfVersionsFolderName) + } + getLogsDir () { return this.getTfvmDir().concat(sep + this.logFolderName) } @@ -48,6 +57,10 @@ export class TfvmOS { return this.getAppDataDir().concat(sep + 'terraform') } + getOpenTofuDir () { + return this.getAppDataDir().concat(sep + 'opentofu') + } + getSettingsDir () { return this.getTfvmDir().concat(sep + this.settingsFileName) } @@ -55,7 +68,8 @@ export class TfvmOS { /** * Returns arguments for the runShell() function and prepares the script for being run, if necessary */ - getAddToPathShellArgs () { throw new Error('Not implemented in parent class') } + getAddToPathShellArgsTerraform () { throw new Error('Not implemented in parent class') } + getAddToPathShellArgsOpenTofu () { throw new Error('Not implemented in parent class') } getArchitecture () { throw new Error('Not implemented in parent class') } getBitWidth () { throw new Error('Not implemented in parent class') } getPathCommand () { throw new Error('Not implemented in parent class') } @@ -64,7 +78,9 @@ export class TfvmOS { getAppDataDir () { throw new Error('Not implemented in parent class') } handleAddPathError () { throw new Error('Not implemented in parent class') } getTFExecutableName () { throw new Error('Not implemented in parent class') } + getOtfExecutableName () { throw new Error('Not implemented in parent class') } async prepareExecutable () { throw new Error('Not Implemented in parent class') } + async prepareTofuExecutable () { throw new Error('Not Implemented in parent class') } getOSName () { // This is whatever Terraform expects, not what node's process.platform returns throw new Error('Not implemented in parent class') @@ -74,8 +90,11 @@ export class TfvmOS { return { tfvmDir: this.getTfvmDir(), tfVersionsDir: this.getTfVersionsDir(), - logsDir: this.getLogsDir(), tfDir: this.getTerraformDir(), + otfvmDir: this.getOtfvmDir(), + otfVersionsDir: this.getOtfVersionsDir(), + otfDir: this.getOpenTofuDir(), + logsDir: this.getLogsDir(), settingsDir: this.getSettingsDir(), appDataDir: this.getAppDataDir() } @@ -98,12 +117,18 @@ export class Mac extends TfvmOS { return ':' } - async getAddToPathShellArgs () { + async getAddToPathShellArgsTerraform () { const scriptPath = resolve(__dirname, './../scripts/addToPathMac.sh') await fsp.chmod(scriptPath, EXECUTE_PERM_CODE) return [scriptPath, {}] } + async getAddToPathShellArgsOpenTofu () { + const scriptPath = resolve(__dirname, './../scripts/addToPathMacOpenTofu.sh') + await fsp.chmod(scriptPath, EXECUTE_PERM_CODE) + return [scriptPath, {}] + } + getAppDataDir () { return process.env.HOME + '/Library/Application Support' } @@ -129,10 +154,19 @@ export class Mac extends TfvmOS { return 'terraform' } + getOtfExecutableName () { + return 'tofu' + } + async prepareExecutable (version) { const exeLoc = resolve(this.getTfVersionsDir(), `v${version}/${this.getTFExecutableName()}`) await fsp.chmod(exeLoc, EXECUTE_PERM_CODE) } + + async prepareTofuExecutable (version) { + const exeLoc = resolve(this.getOtfVersionsDir(), `v${version}/${this.getOtfExecutableName()}`) + await fsp.chmod(exeLoc, EXECUTE_PERM_CODE) + } } export class Windows extends TfvmOS { @@ -144,10 +178,14 @@ export class Windows extends TfvmOS { return ';' } - async getAddToPathShellArgs () { + async getAddToPathShellArgsTerraform () { return [resolve(__dirname, './../scripts/addToPathWindows.ps1'), { shell: 'powershell.exe' }] } + async getAddToPathShellArgsOpenTofu () { + return [resolve(__dirname, './../scripts/addToPathWindowsOpenTofu.ps1'), { shell: 'powershell.exe' }] + } + getAppDataDir () { return process.env.APPDATA } @@ -173,7 +211,13 @@ export class Windows extends TfvmOS { return 'terraform.exe' } + getOtfExecutableName () { + return 'tofu.exe' + } + async prepareExecutable () {} + + async prepareTofuExecutable () {} } export class Linux extends TfvmOS { @@ -193,12 +237,18 @@ export class Linux extends TfvmOS { throw new Error('Bash script failed to add terraform directory to the path') } - async getAddToPathShellArgs () { + async getAddToPathShellArgsTerraform () { const scriptPath = resolve(__dirname, './../scripts/addToPathLinux.sh') await fsp.chmod(scriptPath, EXECUTE_PERM_CODE) return [scriptPath, {}] } + async getAddToPathShellArgsOpenTofu () { + const scriptPath = resolve(__dirname, './../scripts/addToPathLinuxOpenTofu.sh') + await fsp.chmod(scriptPath, EXECUTE_PERM_CODE) + return [scriptPath, {}] + } + getArchitecture () { // 'arm' or 'arm64', 'amd64', '386' const arches = { @@ -225,8 +275,17 @@ export class Linux extends TfvmOS { return 'terraform' } + getOtfExecutableName () { + return 'tofu' + } + async prepareExecutable (version) { const exeLoc = resolve(this.getTfVersionsDir(), `v${version}/${this.getTFExecutableName()}`) await fsp.chmod(exeLoc, EXECUTE_PERM_CODE) } + + async prepareTofuExecutable (version) { + const exeLoc = resolve(this.getOtfVersionsDir(), `v${version}/${this.getOtfExecutableName()}`) + await fsp.chmod(exeLoc, EXECUTE_PERM_CODE) + } } diff --git a/packages/cli/lib/util/verifySetup.js b/packages/cli/lib/util/verifySetup.js index 6552155..96284c7 100644 --- a/packages/cli/lib/util/verifySetup.js +++ b/packages/cli/lib/util/verifySetup.js @@ -9,6 +9,10 @@ async function verifySetup () { const os = getOS() // STEP 1: Check that the appdata/roaming/tfvm folder exists + if (!fs.existsSync(os.getOtfVersionsDir())) fs.mkdirSync(os.getOtfVersionsDir()) + + // STEP 2: Check that the path is set + const otfPaths = [] if (!fs.existsSync(os.getTfVersionsDir())) { fs.mkdirSync(os.getTfVersionsDir()) } @@ -24,27 +28,68 @@ async function verifySetup () { } // do we want to have an error here? const pathVars = PATH.split(os.getPathDelimiter()) let pathVarDoesntExist = true + let pathVarDoesntExistOpenTofu = true for (const variable of pathVars) { if (variable.replace(/[\r\n]/gm, '') === os.getTerraformDir()) pathVarDoesntExist = false if (variable.toLowerCase().includes('terraform') && variable.replace(/[\r\n]/gm, '') !== os.getTerraformDir()) { // strip newlines tfPaths.push(variable) } + if (variable.replace(/[\r\n]/gm, '') === os.getOpenTofuDir()) pathVarDoesntExistOpenTofu = false + if (variable.toLowerCase().includes('opentofu') && variable.replace(/[\r\n]/gm, '') !== os.getOpenTofuDir()) { // strip newlines + otfPaths.push(variable) + } } - if (pathVarDoesntExist) { + if (pathVarDoesntExist || pathVarDoesntExistOpenTofu) { // add to local paths logger.warn(`Couldn't find tfvm in path where this is the path: ${PATH}`) logger.debug('Attempting to run addToPath script...') - if (await runShell(...(await os.getAddToPathShellArgs())) == null) { - os.handleAddPathError() - } else { + let successfulAddToPath = false + if (pathVarDoesntExist) { + if ((await runShell(...(await os.getAddToPathShellArgsTerraform())) == null)) { + os.handleAddPathError() + } else successfulAddToPath = true + } else if (pathVarDoesntExistOpenTofu) { + if ((await runShell(...(await os.getAddToPathShellArgsOpenTofu())) == null)) { + os.handleAddPathError() + } else successfulAddToPath = true + } + + if (successfulAddToPath) { logger.debug('Successfully ran addToPath script, added to path.') console.log(chalk.red.bold('We couldn\'t find the right path variable for terraform, so we just added it.\n' + - 'Please restart your terminal, or open a new one, for terraform to work correctly.\n')) + 'Please restart your terminal, or open a new one, for terraform to work correctly.\n')) } return false } const settings = await getSettings() + // OpenTofu path check if (settings.disableErrors === 'false') { + if (otfPaths.length === 1) { + if (otfPaths[0] !== os.getOpenTofuDir()) { + logger.error(`Extra opentofu path in PATH: ${otfPaths[0]}.`) + console.log(chalk.red.bold(`It appears you have ${otfPaths[0]} in your Path system environmental variables.`)) + console.log(chalk.red.bold('This may stop tfvm from working correctly, so please remove this from the path.\n' + + 'If you make changes to the path, make sure to restart your terminal.')) + if (!settings.disableSettingPrompts) { + console.log(chalk.cyan.bold('To disable this error run \'tfvm config disableErrors=true\'')) + } + return false + } + } else if (otfPaths.length > 1) { + console.log(chalk.red.bold('Your Path environmental variable includes the following terraform paths:')) + for (const badPath of otfPaths) { + logger.warn(`It appears you have ${badPath} in your environmental variables, which may be bad.`) + console.log(chalk.red.bold(badPath)) + } + console.log(chalk.red.bold('This may stop tfvm from working correctly, so please remove these from the path.\n' + + 'If you make changes to the path, make sure to restart your terminal.')) + if (!settings.disableSettingPrompts) { + console.log(chalk.cyan.bold('To disable this error run \'tfvm config disableErrors=true\'')) + } + logger.trace('verifySetup exited unsuccessfully') + return false + } + // Terraform path check if (tfPaths.length === 1) { if (tfPaths[0] !== os.getTerraformDir()) { logger.error(`Extra terraform path in PATH: ${tfPaths[0]}.`) @@ -67,11 +112,11 @@ async function verifySetup () { if (!settings.disableSettingPrompts) { console.log(chalk.cyan.bold('To disable this error run \'tfvm config disableErrors=true\'')) } - logger.trace('verifySetup excited unsuccessfully') + logger.trace('verifySetup exited unsuccessfully') return false } } - logger.trace('verifySetup excited successfully') + logger.trace('verifySetup exited successfully') return true } diff --git a/packages/cli/package.json b/packages/cli/package.json index abd427e..bc4b0ec 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,9 +38,11 @@ "commander": "^9.4.0", "compare-versions": "^6.0.0", "enquirer": "^2.3.6", + "follow-redirects": "^1.15.6", "node-stream-zip": "^1.15.0", "pino": "^8.15.1", - "pino-pretty": "^10.0.0" + "pino-pretty": "^10.0.0", + "semver": "^7.6.3" }, "publishConfig": { "access": "public"