UNPKG

tfvm

Version:

A fast, lightweight CLI tool to manage multiple Terraform versions

6 lines (5 loc) 13.6 kB
#! /usr/bin/env node var Xc=Object.create;var{getPrototypeOf:Zc,defineProperty:T,getOwnPropertyNames:Bc}=Object;var Yc=Object.prototype.hasOwnProperty;var L=(c,u,b)=>{b=c!=null?Xc(Zc(c)):{};let w=u||!c||!c.__esModule?T(b,"default",{value:c,enumerable:!0}):b;for(let $ of Bc(c))if(!Yc.call(w,$))T(w,$,{get:()=>c[$],enumerable:!0});return w};var X=require("citty");var I=L(require("os")),n=L(require("path")),K="https://releases.hashicorp.com/terraform",y=n.default.join(I.default.homedir(),".tfvm");var q=require("fs"),C=require("fs/promises"),Y=require("path"),yc=require("os");var N=L(require("picocolors")),v=(c,u="plain")=>{switch(u){case"success":console.log(N.default.green(c));break;case"error":console.log(N.default.red(c));break;case"info":console.log(N.default.blue(c));break;case"plain":default:console.log(c)}};var o=(c)=>v(c,"info");var H=(c)=>{if(!c||typeof c!=="string")return!1;return c.includes("/terraform/")&&/\/+terraform\/+[0-9]+\.[0-9]+\.[0-9]+(?:[-][a-zA-Z0-9\.\-]+)?(?:\/|$)/.test(c)},s=(c)=>{if(!c||typeof c!=="string")return null;let u=c.match(/.*\/terraform\/+([0-9]+\.[0-9]+\.[0-9]+(?:[-][a-zA-Z0-9\.\-]+)?)(?:\/|$)/);if(u&&u[1]){let b=u[1],[w]=b.split("-"),$=w.split(".");if($.length===3&&$.every((J)=>/^\d+$/.test(J)))return b}return null},A=(c)=>{if(!c||typeof c!=="string")return!1;return c.endsWith(".zip")&&c.length>4};var Q=require("fs/promises"),M=require("fs"),B=require("path"),j=require("os"),f=require("stream/promises"),d=require("node-html-parser"),_=require("fflate");var xc=()=>{let c=j.platform(),u=j.arch(),b={win32:"windows",darwin:"darwin",linux:"linux"},w={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"};return{platform:b[c]||null,arch:w[u]||null}},S=async(c)=>{let u=await fetch(c);if(!u.ok)throw Error(`HTTP ${u.status}: ${u.statusText}`);return u.text()},zc=async(c,u)=>{let b=await fetch(c);if(!b.ok)throw Error(`HTTP ${b.status}: ${b.statusText}`);if(!b.body)throw Error("No response body");let w=M.createWriteStream(u);await f.pipeline(b.body,w)},t=async()=>{try{let c=await S(K),u=d.parse(c),b=[];return u.querySelectorAll("a").forEach(($)=>{let J=$.getAttribute("href");if(H(J)){let Z=s(J);if(Z)b.push(Z)}}),b}catch(c){let u=c.code==="ENOTFOUND"?`Could not connect to ${K}. Check your internet connection!`:`Failed to fetch versions: ${c.message}`;throw Error(u)}},E=async(c)=>{try{let u=await S(`${K}/${c}/`),b=d.parse(u),w=[],{platform:$,arch:J}=xc(),Z=$===null||J===null;if(Z)v(`Unable to auto-detect platform (${j.platform()}) or architecture (${j.arch()})`,"info"),v("Showing all available packages for manual selection...","info");let U=Z?null:`${$}_${J}`;if(b.querySelectorAll("a").forEach((g)=>{let x=g.getAttribute("href");if(x&&H(x)&&A(x)){let z=B.basename(x);if(Z||U&&z.includes(U))w.push({name:z,value:x,short:z})}}),w.length===0)throw Error(`No Terraform packages found for version ${c}`);return w}catch(u){if(u.message.includes("HTTP 404"))throw Error(`Terraform version ${c} not found!`);if(u.code==="ENOTFOUND")throw Error(`Could not connect to ${K}. Check your internet connection!`);throw Error(`Failed to fetch executables: ${u.message}`)}},i=async(c,u)=>{let b=B.basename(c);if(v(`Downloading and extracting "${b}"...`,"info"),!M.existsSync(y))v(`Creating storage directory: ${y}`,"info"),await Q.mkdir(y,{recursive:!0});try{let w=B.join(y,b);await zc(c,w);let $=await Q.readFile(w),J=_.unzipSync(new Uint8Array($));for(let[g,x]of Object.entries(J)){let z=B.basename(c,".zip"),k=g;if(g==="terraform")k=z;else if(g==="terraform.exe")k=`${z}.exe`;let a=B.join(y,k);await Q.writeFile(a,x),await Q.chmod(a,493)}let Z=B.basename(c,".zip"),U=B.join(y,Z),m=B.join(y,"terraform");if(M.existsSync(U))await Q.writeFile(m,await Q.readFile(U)),await Q.chmod(m,493);await Q.unlink(w),v(`Successfully installed from ${b}!`,"success")}catch(w){throw Error(`Download failed: ${w.message}`)}};var W=L(require("@clack/prompts")),R=async(c)=>{let u=await W.select({message:"Which version do you want to download?",options:c.map((b)=>({value:b,label:b}))});if(W.isCancel(u))W.cancel("Operation cancelled"),process.exit(0);return u},r=async(c)=>{let u=await W.select({message:"Which package do you want to download?",options:c.map((b)=>({value:b.value,label:b.name,hint:b.short}))});if(W.isCancel(u))W.cancel("Operation cancelled"),process.exit(0);return u},e=async(c,u)=>{let b=await W.confirm({message:`Do you want to download ${u} for version ${c}?`,initialValue:!0});if(W.isCancel(b))W.cancel("Operation cancelled"),process.exit(0);return b},cc=async(c,u)=>{let b=await W.select({message:u,options:c.map((w)=>({value:w,label:w}))});if(W.isCancel(b))W.cancel("Operation cancelled"),process.exit(0);return b},uc=async(c)=>{let u=await W.confirm({message:`Do you want to remove all the terraform versions available at ${c}?`,initialValue:!1});if(W.isCancel(u))W.cancel("Operation cancelled"),process.exit(0);return u},bc=async(c)=>{let u=await W.select({message:"Select the terraform file you want to use:",options:c.map((b)=>({value:b,label:b}))});if(W.isCancel(u))W.cancel("Operation cancelled"),process.exit(0);return u},wc=async(c)=>{let u=await W.select({message:"Select the terraform file you want to remove:",options:c.map((b)=>({value:b,label:b}))});if(W.isCancel(u))W.cancel("Operation cancelled"),process.exit(0);return u};var jc=[".bashrc",".zshrc",".profile",".bash_profile"],vc="terraform",Uc="terraform_",gc=4,oc=1000,Mc=async(c,u)=>{try{return(await C.readFile(c,"utf-8")).includes(u)}catch{return!1}},P=()=>{if(!q.existsSync(y))return[];return q.readdirSync(y).filter((c)=>{if(!c.startsWith(Uc))return!1;if(c.split("_").length<gc)return!1;try{return(q.statSync(Y.join(y,c)).mode&73)!==0}catch{return!1}})},Lc=(c)=>{let u=Y.join(y,vc);if(!q.existsSync(u))return null;try{let b=q.statSync(u);for(let w of c){let $=q.statSync(Y.join(y,w));if(b.size===$.size&&Math.abs(b.mtimeMs-$.mtimeMs)<oc)return w}}catch{}return null},$c=async()=>{if(process.platform==="win32"){v("To use terraform globally on Windows:","info"),v("1. Open System Properties > Environment Variables","info"),v(`2. Add "${y}" to your PATH variable`,"info"),v(" OR run in PowerShell (Admin):","info"),v(` [Environment]::SetEnvironmentVariable("Path", $env:Path + ";${y}", "User")`,"info");return}let c=yc.homedir(),u=` # Added by tfvm export PATH="${y}:$PATH" `,b=jc.map((J)=>Y.join(c,J)).filter(q.existsSync),w=!1;for(let J of b){if(await Mc(J,y))continue;try{await C.appendFile(J,u),v(`Added ${y} to ${J}`,"success"),w=!0}catch(Z){v(`Failed to update ${J}: ${Z.message}`,"error")}}let $=w?`Restart your terminal or run: source ${process.platform==="darwin"?"~/.zshrc or ~/.bash_profile":"~/.bashrc"}`:`${y} is already in your PATH`;v($,"info")},l=async(c)=>{let u=await E(c),b=u.length===1?(v(`Auto-detected package: ${u[0].name}`,"info"),u[0].value):await r(u);await i(b,c),await $c()},D=async({remote:c}={})=>{if(c){try{let b=await t();v(`Terraform versions available at ${K}:`,"success");let w=await cc(b,"Select a version to download:");if(await e(w,"terraform"))await l(w)}catch(b){v(b.message,"error")}return}let u=P();if(u.length===0){v(`No terraform executables found at ${y}`,"error"),v("Use 'tfvm download' to install terraform versions","info");return}v(`Terraform executables at ${y}:`,"success"),u.forEach((b)=>v(`• ${b}`,"plain"))},G=async(c)=>{try{if(c)await l(c);else{let u=await t(),b=await R(u);await l(b)}}catch(u){v(u.message,"error")}},h=async({all:c}={})=>{if(!q.existsSync(y)){v(`Storage directory ${y} does not exist`,"error");return}let u=P();if(u.length===0){v("No terraform versions found","info");return}if(c){if(await uc(y))q.rmSync(y,{recursive:!0,force:!0}),v("Cleaned up entire .tfvm directory!","success"),v("All terraform versions and configurations have been removed","info");return}let b=await wc(u);if(Lc(u)===b){v(`Cannot remove ${b}: it is currently the active version`,"error"),v("Switch to a different version first using 'tfvm use'","info");return}q.rmSync(Y.join(y,b),{force:!0}),v(`Removed ${b}`,"success")},Wc=async()=>{let c=P();if(c.length===0){v(`No terraform executables at ${y}`,"error"),v("Use 'tfvm download' to install terraform versions","info");return}v(`Terraform executables available at ${y}:`,"success");let u=await bc(c),b=Y.join(y,u),w=Y.join(y,vc);q.copyFileSync(b,w),q.chmodSync(w,"755"),v(`Now using ${u} as 'terraform'!`,"success"),await $c()},qc=()=>{if(q.existsSync(y))v(`Storage directory: ${y}`,"success");else v("Storage directory does not exist","error"),v("It will be created automatically when you download terraform","info")};var V=require("fs/promises"),Jc=require("fs"),p=require("path"),Qc=require("os");var Nc=p.join(Qc.homedir(),".tfvm"),F=p.join(Nc,".version-check"),Cc=86400000,Vc="https://registry.npmjs.org/tfvm/latest",mc=async()=>{let c=await fetch(Vc);if(!c.ok)throw Error(`HTTP ${c.status}`);return(await c.json()).version},kc=async()=>{if(!Jc.existsSync(F))return!0;try{let c=await V.readFile(F,"utf-8"),u=JSON.parse(c);return Date.now()-u.lastCheck>Cc}catch{return!0}},Hc=async(c)=>{let u={lastCheck:Date.now(),latestVersion:c};await V.writeFile(F,JSON.stringify(u))},dc=(c,u)=>{let b=c.split(".").map(Number),w=u.split(".").map(Number);for(let $=0;$<3;$++){if(w[$]>b[$])return!0;if(w[$]<b[$])return!1}return!1},Kc=async(c)=>{if(!await kc())return;try{let u=await mc();if(await Hc(u),dc(c,u))console.log(""),o(`┌${"─".repeat(58)}┐`),o(`│ Update available: ${c}${u}${" ".repeat(30-c.length-u.length)}│`),o(`│ Run: npm install -g tfvm${" ".repeat(30)}│`),o(`└${"─".repeat(58)}┘`),console.log("")}catch{}};var O={name:"tfvm",version:"2.1.1",description:"A fast, lightweight CLI tool to manage multiple Terraform versions",author:{name:"Anindya Dey",url:"https://www.anindyadey.com"},license:"MIT",homepage:"https://github.com/anindya-dey/tfvm#readme",repository:{type:"git",url:"git+https://github.com/anindya-dey/tfvm.git"},bugs:{url:"https://github.com/anindya-dey/tfvm/issues"},keywords:["terraform","version-manager","cli","tfvm","terraform-version","terraform-switcher","devops","infrastructure"],bin:{tfvm:"./dist/tfvm.js"},files:["dist/","README.md","LICENSE"],engines:{node:">=24.0.0"},scripts:{build:"bun build ./src/cli.ts --outfile=dist/tfvm.js --target=node --format=cjs --minify --external picocolors --external node-html-parser --external citty --external @clack/prompts --external fflate",dev:"bun run --watch src/cli.ts",start:"bun run src/cli.ts",test:"bun test","test:coverage":"bun test --coverage","test:watch":"bun test --watch",clean:"rm -rf dist",prepublishOnly:"bun run clean && bun run test && bun run build",lint:"tsc --noEmit",version:"bun run build",postversion:"git push && git push --tags","release:patch":"npm version patch -m 'chore(release): v%s'","release:minor":"npm version minor -m 'chore(release): v%s'","release:major":"npm version major -m 'chore(release): v%s'","release:beta":"npm version prerelease --preid=beta -m 'chore(release): v%s'","release:dry":"npm version --dry-run",changelog:"./scripts/generate-changelog.sh",release:"./scripts/release.sh"},dependencies:{"@clack/prompts":"^0.11.0",citty:"^0.1.6",fflate:"^0.8.2",force:"^0.0.3","node-html-parser":"^7.0.1",picocolors:"^1.1.1"},devDependencies:{"@types/bun":"^1.3.5","@types/node":"^25.0.3",typescript:"^5.9.3"}};var lc=X.defineCommand({meta:{name:"tfvm",version:O.version,description:"Terraform Version Manager - A CLI tool to manage Terraform versions"},subCommands:{list:X.defineCommand({meta:{name:"list",description:`list all the downloaded versions of terraform or the ones available at ${K}`},args:{remote:{type:"boolean",alias:"r",description:`Displays a list of all terraform versions available at ${K}`}},run:async({args:c})=>{await D({remote:!!c.remote})}}),ls:X.defineCommand({meta:{name:"ls",description:"Alias for list command"},args:{remote:{type:"boolean",alias:"r",description:`Displays a list of all terraform versions available at ${K}`}},run:async({args:c})=>{await D({remote:!!c.remote})}}),download:X.defineCommand({meta:{name:"download",description:`downloads and extracts a specific package of terraform from ${K}`},args:{version:{type:"positional",description:"If provided, a list of all the terraform packages for this version would be displayed for the user to choose from",required:!1}},run:async({args:c})=>{await G(typeof c.version==="string"?c.version:void 0)}}),d:X.defineCommand({meta:{name:"d",description:"Alias for download command"},args:{version:{type:"positional",description:"If provided, a list of all the terraform packages for this version would be displayed for the user to choose from",required:!1}},run:async({args:c})=>{await G(typeof c.version==="string"?c.version:void 0)}}),remove:X.defineCommand({meta:{name:"remove",description:`removes a specific package or all packages of terraform saved locally at ${y}`},args:{all:{type:"boolean",alias:"a",description:`remove all versions of terraform from ${y}`}},run:async({args:c})=>{await h({all:!!c.all})}}),rm:X.defineCommand({meta:{name:"rm",description:"Alias for remove command"},args:{all:{type:"boolean",alias:"a",description:`remove all versions of terraform from ${y}`}},run:async({args:c})=>{await h({all:!!c.all})}}),use:X.defineCommand({meta:{name:"use",description:`sets a specific terraform release from ${y} as default which can be used directly in the terminal.`},run:async()=>{await Wc()}}),dir:X.defineCommand({meta:{name:"dir",description:`displays the directory where terraform executables are stored. Default directory is ${y}.`},run:async()=>{await qc()}})}}),Pc=async()=>{Kc(O.version),await X.runMain(lc)};Pc();