tfv 4.0.4 → 5.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.
@@ -4,18 +4,25 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
 
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
7
11
  jobs:
8
12
  publish:
9
13
  runs-on: ubuntu-latest
14
+
10
15
  steps:
11
- - name: checkout
12
- uses: actions/checkout@v2
13
- - name: setup node
14
- uses: actions/setup-node@v1
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
15
19
  with:
16
- node-version: 12
20
+ node-version: 18
17
21
  registry-url: https://registry.npmjs.org
18
- - name: publish
19
- run: npm publish --access public
22
+ cache: 'npm'
23
+
24
+ - run: npm ci
25
+
26
+ - run: npm publish --access public
20
27
  env:
21
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -42,11 +42,28 @@ tfv -h
42
42
  tfv <command>
43
43
 
44
44
  Commands:
45
- tfv install <version> [option] Example: tfv install 1.0.11 [aliases: i]
46
- tfv list [option] Example: tfv list [aliases: ls]
47
- tfv remove <version> Example: tfv rm 1.0.11 [aliases: rm]
48
- tfv auto-switch Example: tfv as [aliases: as]
49
- tfv use <version> Example: tfv use 1.0.11
45
+
46
+ tfv install <version> [option] Install a terraform version [aliases: i]
47
+
48
+ tfv list [option] List installed or available terraform versions [aliases: ls]
49
+
50
+ tfv remove <version> Remove terraform versions from tfv store [aliases: rm]
51
+
52
+ tfv auto-switch Auto-detect and switch to your project terraform version [aliases: as]
53
+
54
+ tfv use <version> Switch to a specified terraform version
55
+
56
+ tfv apply Run terraform apply with optional file-based targets.
57
+ Accepts all terraform flags after --
58
+ Example: tfv apply --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"
59
+
60
+ tfv destroy Run terraform destroy with optional file-based targets.
61
+ Accepts all terraform flags after --
62
+ Example: tfv destroy --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"
63
+
64
+ tfv plan Run terraform plan with optional file-based targets.
65
+ Accepts all terraform flags after --
66
+ Example: tfv plan --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"
50
67
 
51
68
  Options:
52
69
  -h, --help Show help [boolean]
@@ -67,6 +84,9 @@ https://github.com/marcdomain/tfv/assets/25563661/fa44f0f2-2dca-4f22-9fea-c74e4b
67
84
  * [list](#list)
68
85
  * [remove](#remove)
69
86
  * [auto-switch](#auto-switch)
87
+ * [plan](#plan)
88
+ * [apply](#apply)
89
+ * [destroy](#destroy)
70
90
  <!--te-->
71
91
 
72
92
  ### Modules
@@ -162,3 +182,51 @@ Run with alias
162
182
  ```sh
163
183
  tfv as
164
184
  ```
185
+
186
+ - #### _PLAN_
187
+
188
+ Run terraform plan with optional file-based targets. Parses terraform files to extract resources, data sources, and modules as targets.
189
+
190
+ ```sh
191
+ tfv plan --file main.tf
192
+ ```
193
+
194
+ With multiple files
195
+
196
+ ```sh
197
+ tfv plan --file main.tf --file network.tf
198
+ ```
199
+
200
+ With extra terraform flags
201
+
202
+ ```sh
203
+ tfv plan --file main.tf -- -var="env=prod" -out=plan.out
204
+ ```
205
+
206
+ - #### _APPLY_
207
+
208
+ Run terraform apply with optional file-based targets.
209
+
210
+ ```sh
211
+ tfv apply --file main.tf
212
+ ```
213
+
214
+ With auto-approve
215
+
216
+ ```sh
217
+ tfv apply --file main.tf -- -auto-approve
218
+ ```
219
+
220
+ - #### _DESTROY_
221
+
222
+ Run terraform destroy with optional file-based targets.
223
+
224
+ ```sh
225
+ tfv destroy --file main.tf
226
+ ```
227
+
228
+ With auto-approve
229
+
230
+ ```sh
231
+ tfv destroy --file main.tf -- -auto-approve
232
+ ```
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const {runTerraformCommand} = require('../modules/terraform-command');
4
+
5
+ exports.command = 'apply'
6
+ exports.desc = 'Run terraform apply with optional file-based targets.\n'
7
+ exports.builder = (yargs) => {
8
+ return yargs
9
+ .option('file', {
10
+ alias: 'f',
11
+ describe: 'Terraform file(s) to extract targets from',
12
+ type: 'array',
13
+ })
14
+ .epilog('Accepts all terraform flags after --\nExample: tfv apply --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"')
15
+ }
16
+
17
+ exports.handler = async (argv) => {
18
+ const {file, _} = argv;
19
+ // Extra args come after -- in the command line
20
+ const extraArgs = _.slice(1);
21
+ await runTerraformCommand('apply', file, extraArgs);
22
+ }
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const {runTerraformCommand} = require('../modules/terraform-command');
4
+
5
+ exports.command = 'destroy'
6
+ exports.desc = 'Run terraform destroy with optional file-based targets.\n'
7
+ exports.builder = (yargs) => {
8
+ return yargs
9
+ .option('file', {
10
+ alias: 'f',
11
+ describe: 'Terraform file(s) to extract targets from',
12
+ type: 'array',
13
+ })
14
+ .epilog('Accepts all terraform flags after --\nExample: tfv destroy --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"')
15
+ }
16
+
17
+ exports.handler = async (argv) => {
18
+ const {file, _} = argv;
19
+ // Extra args come after -- in the command line
20
+ const extraArgs = _.slice(1);
21
+ await runTerraformCommand('destroy', file, extraArgs);
22
+ }
@@ -6,13 +6,15 @@ const {install} = require('../modules/install');
6
6
  exports.command = 'install <version> [option]'
7
7
  exports.aliases = ['i']
8
8
  exports.desc = 'Install a terraform version'
9
- exports.builder = {
10
- arch: {
11
- describe: 'Specify system architecture',
12
- alias: 'a',
13
- type: 'string',
14
- default: ''
15
- }
9
+ exports.builder = (yargs) => {
10
+ return yargs
11
+ .option('arch', {
12
+ alias: 'a',
13
+ describe: 'Specify system architecture. Defaults is the user system architecture',
14
+ type: 'string',
15
+ default: ''
16
+ })
17
+ .epilog('Version formats: latest, x.x.x (exact), x^ (latest major), x.x.^ (latest minor)\nExample: tfv install 1.5.7 --arch amd64')
16
18
  }
17
19
 
18
20
  exports.handler = async () => {
@@ -21,3 +23,4 @@ exports.handler = async () => {
21
23
 
22
24
  await install(version, arch);
23
25
  }
26
+
@@ -5,22 +5,25 @@ const {list} = require('../modules/list');
5
5
 
6
6
  exports.command = 'list [option]'
7
7
  exports.aliases = ['ls']
8
- exports.desc = 'Example: tfv list --local'
9
- exports.builder = {
10
- local: {
11
- alias: 'l',
12
- describe: 'List terraform versions you have installed using tfv',
13
- type: 'boolean',
14
- default: true,
15
- },
16
- remote: {
17
- alias: 'r',
18
- describe: 'List all available terraform versions',
19
- type: 'boolean',
20
- }
8
+ exports.desc = 'List installed or available terraform versions'
9
+ exports.builder = (yargs) => {
10
+ return yargs
11
+ .option('local', {
12
+ alias: 'l',
13
+ describe: 'List terraform versions you have installed using tfv',
14
+ type: 'boolean',
15
+ default: true,
16
+ })
17
+ .option('remote', {
18
+ alias: 'r',
19
+ describe: 'List all available terraform versions',
20
+ type: 'boolean',
21
+ })
22
+ .epilog('Examples:\n tfv ls List locally installed versions\n tfv ls --remote List all available remote versions')
21
23
  }
22
24
 
23
25
  exports.handler = async () => {
24
26
  const {local, remote} = yargs.argv;
25
27
  await list(local, remote);
26
28
  }
29
+
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const {runTerraformCommand} = require('../modules/terraform-command');
4
+
5
+ exports.command = 'plan'
6
+ exports.desc = 'Run terraform plan with optional file-based targets.\n'
7
+ exports.builder = (yargs) => {
8
+ return yargs
9
+ .option('file', {
10
+ alias: 'f',
11
+ describe: 'Terraform file(s) to extract targets from',
12
+ type: 'array',
13
+ })
14
+ .epilog('Accepts all terraform flags after --\nExample: tfv plan --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"')
15
+ }
16
+
17
+ exports.handler = async (argv) => {
18
+ const {file, _} = argv;
19
+ // Extra args come after -- in the command line
20
+ const extraArgs = _.slice(1);
21
+ await runTerraformCommand('plan', file, extraArgs);
22
+ }
@@ -5,12 +5,14 @@ const {remove} = require('../modules/remove');
5
5
 
6
6
  exports.command = 'remove <version>'
7
7
  exports.aliases = ['rm']
8
- exports.desc = 'Remove a list of versions managed by tfv'
9
- exports.builder = {
10
- verbose: {
11
- describe: 'Produce detailed output',
12
- type: 'boolean',
13
- }
8
+ exports.desc = 'Remove terraform versions from tfv store'
9
+ exports.builder = (yargs) => {
10
+ return yargs
11
+ .option('verbose', {
12
+ describe: 'Produce detailed output',
13
+ type: 'boolean',
14
+ })
15
+ .epilog('Example: tfv rm 1.5.7 1.4.6')
14
16
  }
15
17
 
16
18
  exports.handler = async () => {
@@ -4,12 +4,14 @@ const {autoSwitch} = require('../modules/switch');
4
4
 
5
5
  exports.command = 'auto-switch'
6
6
  exports.aliases = ['as']
7
- exports.desc = 'Example: tfv as'
8
- exports.builder = {
9
- verbose: {
10
- describe: 'Produce detailed output',
11
- type: 'boolean'
12
- }
7
+ exports.desc = 'Auto-detect and switch to your project terraform version'
8
+ exports.builder = (yargs) => {
9
+ return yargs
10
+ .option('verbose', {
11
+ describe: 'Produce detailed output',
12
+ type: 'boolean'
13
+ })
14
+ .epilog('Reads version from .terraform-version or required_version in .tf files\nExample: tfv auto-switch')
13
15
  }
14
16
 
15
17
  exports.handler = async () => {
@@ -4,12 +4,14 @@ const yargs = require('yargs');
4
4
  const {use} = require('../modules/use');
5
5
 
6
6
  exports.command = 'use <version>'
7
- exports.desc = 'Example: tfv use 1.0.11'
8
- exports.builder = {
9
- verbose: {
10
- describe: 'Produce detailed output',
11
- type: 'boolean',
12
- }
7
+ exports.desc = 'Switch to a specified terraform version\n'
8
+ exports.builder = (yargs) => {
9
+ return yargs
10
+ .option('verbose', {
11
+ describe: 'Produce detailed output',
12
+ type: 'boolean',
13
+ })
14
+ .epilog('Example: tfv use 1.5.7')
13
15
  }
14
16
 
15
17
  exports.handler = async () => {
@@ -17,3 +19,4 @@ exports.handler = async () => {
17
19
 
18
20
  await use(version);
19
21
  }
22
+
@@ -6,20 +6,98 @@ const {formatVersions} = require('../utils/formatVersions');
6
6
  const {P_END, P_OK} = require('../utils/colors');
7
7
  const {checkStore} = require('../utils/store');
8
8
 
9
+ const MAX_COLUMNS_PER_TABLE = 6;
10
+
11
+ /**
12
+ * Groups versions by their major.minor prefix
13
+ */
14
+ const groupVersionsByRelease = (versions) => {
15
+ const groups = {};
16
+
17
+ versions.forEach(version => {
18
+ const parts = version.split('.');
19
+ if (parts.length >= 2) {
20
+ const majorMinor = `${parts[0]}.${parts[1]}`;
21
+ if (!groups[majorMinor]) {
22
+ groups[majorMinor] = [];
23
+ }
24
+ groups[majorMinor].push(version);
25
+ }
26
+ });
27
+
28
+ return groups;
29
+ };
30
+
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)
39
+ 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
+ });
58
+ });
59
+
60
+ // Split into chunks of MAX_COLUMNS_PER_TABLE
61
+ 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));
64
+ }
65
+
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
72
+ const tableData = [];
73
+ for (let row = 0; row < maxRows; row++) {
74
+ 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
+ });
81
+ tableData.push(rowObj);
82
+ }
83
+
84
+ if (chunks.length > 1) {
85
+ console.log(`\n${P_OK}Table ${chunkIndex + 1} of ${chunks.length}${P_END}`);
86
+ }
87
+ console.table(tableData);
88
+ });
89
+ };
90
+
9
91
  exports.list = async (local, remote) => {
10
92
  try {
11
93
  let versions;
12
94
 
13
- const result = (message, v) => {
14
- console.log(`${P_OK}${message}${P_END}` + '\n');
15
- console.log(v.join('\n'));
16
- }
17
-
18
95
  if (remote) {
19
96
  const data = await fetchAllVersions();
20
97
  versions = formatVersions(data);
21
98
 
22
- return result('List of all available terraform versions', versions)
99
+ console.log(`${P_OK}List of all available terraform versions${P_END}\n`);
100
+ return displayVersionTable(versions);
23
101
  }
24
102
 
25
103
  if (local) {
@@ -0,0 +1,162 @@
1
+ const fs = require('fs');
2
+ const {spawn} = require('child_process');
3
+ const {P_END, P_OK, P_ERROR, P_WARN, P_INFO} = require('../utils/colors');
4
+
5
+ /**
6
+ * Removes comments from Terraform content
7
+ * Handles: # comments, // comments, and block comments
8
+ */
9
+ const removeComments = (content) => {
10
+ // Remove block comments /* */
11
+ content = content.replace(/\/\*[\s\S]*?\*\//g, '');
12
+
13
+ // Remove single line comments (# and //)
14
+ const lines = content.split('\n');
15
+ const cleanedLines = lines.map(line => {
16
+ // Find position of # or // that's not inside a string
17
+ let inString = false;
18
+ let stringChar = '';
19
+ let commentStart = -1;
20
+
21
+ for (let i = 0; i < line.length; i++) {
22
+ const char = line[i];
23
+ const nextChar = line[i + 1];
24
+
25
+ if (!inString && (char === '"' || char === "'")) {
26
+ inString = true;
27
+ stringChar = char;
28
+ } else if (inString && char === stringChar && line[i - 1] !== '\\') {
29
+ inString = false;
30
+ } else if (!inString) {
31
+ if (char === '#' || (char === '/' && nextChar === '/')) {
32
+ commentStart = i;
33
+ break;
34
+ }
35
+ }
36
+ }
37
+
38
+ if (commentStart !== -1) {
39
+ return line.substring(0, commentStart);
40
+ }
41
+ return line;
42
+ });
43
+
44
+ return cleanedLines.join('\n');
45
+ };
46
+
47
+ /**
48
+ * Extracts terraform targets from file content
49
+ * Returns array of target strings like "aws_instance.example", "module.vpc", etc.
50
+ */
51
+ const extractTargets = (content) => {
52
+ const targets = [];
53
+
54
+ // Remove comments first
55
+ const cleanContent = removeComments(content);
56
+
57
+ // Pattern for resource blocks: resource "type" "name"
58
+ const resourcePattern = /resource\s+"([^"]+)"\s+"([^"]+)"/g;
59
+ let match;
60
+ while ((match = resourcePattern.exec(cleanContent)) !== null) {
61
+ targets.push(`${match[1]}.${match[2]}`);
62
+ }
63
+
64
+ // Pattern for data blocks: data "type" "name"
65
+ const dataPattern = /data\s+"([^"]+)"\s+"([^"]+)"/g;
66
+ while ((match = dataPattern.exec(cleanContent)) !== null) {
67
+ targets.push(`data.${match[1]}.${match[2]}`);
68
+ }
69
+
70
+ // Pattern for module blocks: module "name"
71
+ const modulePattern = /module\s+"([^"]+)"/g;
72
+ while ((match = modulePattern.exec(cleanContent)) !== null) {
73
+ targets.push(`module.${match[1]}`);
74
+ }
75
+
76
+ return targets;
77
+ };
78
+
79
+ /**
80
+ * Extracts targets from one or more terraform files
81
+ */
82
+ const extractTargetsFromFiles = (files) => {
83
+ const allTargets = [];
84
+ const fileList = Array.isArray(files) ? files : [files];
85
+
86
+ fileList.forEach(filename => {
87
+ if (!fs.existsSync(filename)) {
88
+ console.log(`${P_WARN}Warning: File '${filename}' not found, skipping${P_END}`);
89
+ return;
90
+ }
91
+
92
+ const content = fs.readFileSync(filename, 'utf8');
93
+ const targets = extractTargets(content);
94
+
95
+ if (targets.length > 0) {
96
+ console.log(`${P_INFO}Found ${targets.length} target(s) in '${filename}':${P_END}`);
97
+ targets.forEach(target => {
98
+ console.log(` - ${target}`);
99
+ if (!allTargets.includes(target)) {
100
+ allTargets.push(target);
101
+ }
102
+ });
103
+ } else {
104
+ console.log(`${P_WARN}No targets found in '${filename}'${P_END}`);
105
+ }
106
+ });
107
+
108
+ return allTargets;
109
+ };
110
+
111
+ /**
112
+ * Runs a terraform command (plan, apply, destroy) with optional file-based targets
113
+ * @param {string} command - The terraform command (plan, apply, destroy)
114
+ * @param {string|string[]} files - Optional file(s) to extract targets from
115
+ * @param {string[]} extraArgs - Additional arguments to pass to terraform
116
+ */
117
+ exports.runTerraformCommand = async (command, files, extraArgs = []) => {
118
+ try {
119
+ const args = [command];
120
+
121
+ // Extract targets from files if provided
122
+ if (files && (Array.isArray(files) ? files.length > 0 : true)) {
123
+ console.log(`${P_OK}Extracting targets from file(s)...${P_END}\n`);
124
+ const targets = extractTargetsFromFiles(files);
125
+
126
+ if (targets.length > 0) {
127
+ console.log(`\n${P_OK}Total unique targets: ${targets.length}${P_END}\n`);
128
+ targets.forEach(target => {
129
+ args.push('-target', target);
130
+ });
131
+ } else {
132
+ console.log(`${P_WARN}No targets extracted from files. Running without file-based targets.${P_END}\n`);
133
+ }
134
+ }
135
+
136
+ // Add any extra arguments passed through
137
+ if (extraArgs && extraArgs.length > 0) {
138
+ args.push(...extraArgs);
139
+ }
140
+
141
+ console.log(`${P_OK}Running: terraform ${args.join(' ')}${P_END}\n`);
142
+
143
+ // Execute terraform command
144
+ const tf = spawn('terraform', args, {
145
+ stdio: 'inherit',
146
+ cwd: process.cwd()
147
+ });
148
+
149
+ tf.on('error', (err) => {
150
+ console.log(`${P_ERROR}Error executing terraform: ${err.message}${P_END}`);
151
+ process.exit(1);
152
+ });
153
+
154
+ tf.on('close', (code) => {
155
+ process.exit(code);
156
+ });
157
+
158
+ } catch (err) {
159
+ console.log(`${P_ERROR}Error: ${err.message}${P_END}`);
160
+ process.exit(1);
161
+ }
162
+ };
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "tfv",
3
- "version": "4.0.4",
3
+ "version": "5.0.0",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "provenance": true
7
+ },
4
8
  "description": "Terraform version manager",
5
9
  "main": "index.js",
6
10
  "directories": {