tfv 5.0.1 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +43 -3
- package/README.md +253 -122
- package/demo.gif +0 -0
- package/demo.tape +230 -0
- package/index.js +0 -4
- package/lib/commands/apply.js +8 -3
- package/lib/commands/current.js +25 -0
- package/lib/commands/destroy.js +8 -3
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/exec.js +33 -0
- package/lib/commands/fmt.js +26 -0
- package/lib/commands/init.js +26 -0
- package/lib/commands/install.js +22 -13
- package/lib/commands/list.js +20 -11
- package/lib/commands/pin.js +26 -0
- package/lib/commands/plan.js +8 -3
- package/lib/commands/prune.js +41 -0
- package/lib/commands/remove.js +17 -12
- package/lib/commands/shell-init.js +25 -0
- package/lib/commands/switch.js +28 -7
- package/lib/commands/upgrade.js +26 -0
- package/lib/commands/use.js +17 -13
- package/lib/commands/validate.js +21 -0
- package/lib/modules/current.js +52 -0
- package/lib/modules/doctor.js +160 -0
- package/lib/modules/exec.js +36 -0
- package/lib/modules/install.js +155 -89
- package/lib/modules/list.js +66 -105
- package/lib/modules/pin.js +35 -0
- package/lib/modules/prune.js +100 -0
- package/lib/modules/ps1.js +37 -29
- package/lib/modules/remote.js +68 -15
- package/lib/modules/remove.js +35 -21
- package/lib/modules/shell-init.js +226 -0
- package/lib/modules/switch.js +125 -41
- package/lib/modules/terraform-command.js +49 -67
- package/lib/modules/upgrade.js +93 -0
- package/lib/modules/use.js +58 -54
- package/lib/utils/formatVersions.js +57 -5
- package/lib/utils/paths.js +156 -0
- package/lib/utils/postInstall.js +37 -13
- package/lib/utils/store.js +17 -6
- package/package.json +11 -9
- package/test/extractTargets.test.js +75 -0
- package/test/formatVersions.test.js +126 -0
- package/test/moduleImports.test.js +45 -0
- package/test/paths.test.js +69 -0
- package/test/versionResolution.test.js +92 -0
package/lib/utils/postInstall.js
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
\\
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
const { platform } = require('os');
|
|
2
|
+
const { initDirs, getPaths, fixPathConflict } = require('./paths');
|
|
3
|
+
const { P_END, P_OK, P_WARN, P_INFO } = require('./colors');
|
|
4
|
+
|
|
5
|
+
const banner = `
|
|
6
|
+
_ ________ __
|
|
7
|
+
_| |__ / _____|\\ \\ / /
|
|
8
|
+
|_ ___\\ | |___ \\ \\ / /
|
|
9
|
+
| | | ___| \\ \\ / /
|
|
10
|
+
| |___ | | \\ \\/ /
|
|
11
|
+
\\______\\_| \\__/
|
|
12
|
+
|
|
13
|
+
${P_OK}Happy terraforming!${P_END} 😍🥂
|
|
14
|
+
---------------------------------
|
|
13
15
|
`;
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
try {
|
|
18
|
+
initDirs();
|
|
19
|
+
console.log(banner);
|
|
20
|
+
|
|
21
|
+
const { home } = getPaths();
|
|
22
|
+
console.log(`${P_INFO}tfv store: ${home}${P_END}`);
|
|
23
|
+
|
|
24
|
+
// Ensure ~/.tfv/bin is on PATH and positioned AFTER any system-level tool managers
|
|
25
|
+
// (brew, apt, etc.) so tfv always takes precedence. Safe to re-run on upgrade.
|
|
26
|
+
fixPathConflict();
|
|
27
|
+
|
|
28
|
+
if (platform() !== 'win32') {
|
|
29
|
+
console.log(`${P_OK}~/.tfv/bin added to PATH in your shell config files.${P_END}`);
|
|
30
|
+
console.log(`${P_WARN}Restart your terminal or run: source ~/.zshrc${P_END}`);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`${P_OK}~/.tfv/bin added to your User PATH.${P_END}`);
|
|
33
|
+
console.log(`${P_WARN}Restart your terminal for the change to take effect.${P_END}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\n${P_OK}Run ${P_END}${P_INFO}tfv install latest${P_END}${P_OK} to get started.${P_END}\n`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.log('postInstall error (non-fatal):', err.message);
|
|
39
|
+
}
|
package/lib/utils/store.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
const
|
|
2
|
-
const {
|
|
1
|
+
const { existsSync, readdirSync } = require('fs');
|
|
2
|
+
const { getProviderStore, initDirs } = require('./paths');
|
|
3
|
+
const { P_END, P_OK, P_ERROR } = require('./colors');
|
|
3
4
|
|
|
4
|
-
exports.checkStore = (
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
exports.checkStore = (provider = 'terraform') => {
|
|
6
|
+
initDirs();
|
|
7
|
+
const store = getProviderStore(provider);
|
|
8
|
+
|
|
9
|
+
if (!existsSync(store)) {
|
|
10
|
+
console.log(`${P_ERROR}Store not found for provider: ${provider}${P_END}`);
|
|
11
|
+
console.log(`For guidance, run ${P_OK}tfv -h${P_END}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const files = readdirSync(store).filter(f => f !== 'arch.json');
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
console.log(`${P_ERROR}You're yet to install ${provider} with tfv${P_END}`);
|
|
7
18
|
console.log(`For guidance, run ${P_OK}tfv -h${P_END}`);
|
|
8
19
|
process.exit(1);
|
|
9
20
|
}
|
|
10
|
-
}
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tfv",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
7
7
|
},
|
|
8
|
-
"description": "Terraform version manager",
|
|
8
|
+
"description": "Terraform & OpenTofu version manager",
|
|
9
9
|
"main": "index.js",
|
|
10
|
-
"directories": {
|
|
11
|
-
"lib": "lib",
|
|
12
|
-
"store": "store"
|
|
13
|
-
},
|
|
14
10
|
"scripts": {
|
|
15
|
-
"postinstall": "node lib/utils/postInstall.js"
|
|
11
|
+
"postinstall": "node lib/utils/postInstall.js",
|
|
12
|
+
"test": "jest --testPathPatterns=test/"
|
|
16
13
|
},
|
|
17
14
|
"bin": {
|
|
18
15
|
"tfv": "index.js"
|
|
19
16
|
},
|
|
20
17
|
"keywords": [
|
|
21
18
|
"Terraform",
|
|
22
|
-
"
|
|
19
|
+
"OpenTofu",
|
|
20
|
+
"version",
|
|
21
|
+
"manager"
|
|
23
22
|
],
|
|
24
23
|
"author": "Marcus Chukwuoma <marcus2cu@gmail.com> (https://github.com/marcdomain)",
|
|
25
24
|
"license": "MIT",
|
|
@@ -28,7 +27,10 @@
|
|
|
28
27
|
"url": "https://github.com/marcdomain/tfv.git"
|
|
29
28
|
},
|
|
30
29
|
"dependencies": {
|
|
31
|
-
"
|
|
30
|
+
"adm-zip": "^0.5.10",
|
|
32
31
|
"yargs": "^17.2.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"jest": "^30.4.2"
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const { extractTargets, removeComments } = require('../lib/modules/terraform-command');
|
|
2
|
+
|
|
3
|
+
describe('removeComments', () => {
|
|
4
|
+
test('removes # single-line comments', () => {
|
|
5
|
+
const result = removeComments('resource "aws_instance" "web" { # this is a comment\n}');
|
|
6
|
+
expect(result).not.toContain('this is a comment');
|
|
7
|
+
expect(result).toContain('resource "aws_instance" "web"');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('removes // single-line comments', () => {
|
|
11
|
+
const result = removeComments('// this is a comment\nresource "aws_s3_bucket" "bucket" {}');
|
|
12
|
+
expect(result).not.toContain('this is a comment');
|
|
13
|
+
expect(result).toContain('resource "aws_s3_bucket" "bucket"');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('removes /* */ block comments', () => {
|
|
17
|
+
const content = '/* block\ncomment */\nresource "aws_vpc" "main" {}';
|
|
18
|
+
const result = removeComments(content);
|
|
19
|
+
expect(result).not.toContain('block');
|
|
20
|
+
expect(result).toContain('resource "aws_vpc" "main"');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('does not remove # inside strings', () => {
|
|
24
|
+
const content = 'tags = { name = "web#1" } # this is a comment';
|
|
25
|
+
const result = removeComments(content);
|
|
26
|
+
expect(result).toContain('"web#1"');
|
|
27
|
+
expect(result).not.toContain('this is a comment');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('extractTargets', () => {
|
|
32
|
+
const tfContent = `
|
|
33
|
+
resource "aws_instance" "web" {
|
|
34
|
+
ami = "ami-12345"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resource "aws_s3_bucket" "logs" {}
|
|
38
|
+
|
|
39
|
+
data "aws_ami" "ubuntu" {}
|
|
40
|
+
|
|
41
|
+
module "vpc" {
|
|
42
|
+
source = "./modules/vpc"
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
test('extracts resource targets', () => {
|
|
47
|
+
const targets = extractTargets(tfContent);
|
|
48
|
+
expect(targets).toContain('aws_instance.web');
|
|
49
|
+
expect(targets).toContain('aws_s3_bucket.logs');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('extracts data source targets', () => {
|
|
53
|
+
const targets = extractTargets(tfContent);
|
|
54
|
+
expect(targets).toContain('data.aws_ami.ubuntu');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('extracts module targets', () => {
|
|
58
|
+
const targets = extractTargets(tfContent);
|
|
59
|
+
expect(targets).toContain('module.vpc');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('returns empty array for empty content', () => {
|
|
63
|
+
expect(extractTargets('')).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('ignores commented-out resources', () => {
|
|
67
|
+
const commented = `
|
|
68
|
+
# resource "aws_instance" "commented" {}
|
|
69
|
+
resource "aws_instance" "real" {}
|
|
70
|
+
`;
|
|
71
|
+
const targets = extractTargets(commented);
|
|
72
|
+
expect(targets).not.toContain('aws_instance.commented');
|
|
73
|
+
expect(targets).toContain('aws_instance.real');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const {
|
|
2
|
+
formatVersions, formatAllVersions,
|
|
3
|
+
formatOpenTofuVersions, formatAllOpenTofuVersions,
|
|
4
|
+
semverDesc,
|
|
5
|
+
} = require('../lib/utils/formatVersions');
|
|
6
|
+
|
|
7
|
+
describe('formatVersions (Terraform JSON API)', () => {
|
|
8
|
+
const mockApiResponse = {
|
|
9
|
+
versions: {
|
|
10
|
+
'1.7.3': { name: 'terraform', version: '1.7.3' },
|
|
11
|
+
'1.7.0': { name: 'terraform', version: '1.7.0' },
|
|
12
|
+
'1.6.6': { name: 'terraform', version: '1.6.6' },
|
|
13
|
+
'1.8.0-beta1': { name: 'terraform', version: '1.8.0-beta1' },
|
|
14
|
+
'0.14.0': { name: 'terraform', version: '0.14.0' },
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
test('parses versions from JSON object', () => {
|
|
19
|
+
const result = formatVersions(mockApiResponse);
|
|
20
|
+
expect(result).toContain('1.7.3');
|
|
21
|
+
expect(result).toContain('1.6.6');
|
|
22
|
+
expect(result).toContain('0.14.0');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('excludes pre-release versions', () => {
|
|
26
|
+
const result = formatVersions(mockApiResponse);
|
|
27
|
+
expect(result).not.toContain('1.8.0-beta1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('sorts versions descending', () => {
|
|
31
|
+
const result = formatVersions(mockApiResponse);
|
|
32
|
+
expect(result[0]).toBe('1.7.3');
|
|
33
|
+
expect(result[1]).toBe('1.7.0');
|
|
34
|
+
expect(result[2]).toBe('1.6.6');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('accepts a JSON string', () => {
|
|
38
|
+
const result = formatVersions(JSON.stringify(mockApiResponse));
|
|
39
|
+
expect(result).toContain('1.7.3');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('formatAllVersions (Terraform — includes pre-releases)', () => {
|
|
44
|
+
const mockApiResponse = {
|
|
45
|
+
versions: {
|
|
46
|
+
'1.8.0-beta1': {},
|
|
47
|
+
'1.7.3': {},
|
|
48
|
+
'1.7.0-rc1': {},
|
|
49
|
+
'1.6.6': {},
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
test('includes pre-release versions', () => {
|
|
54
|
+
const result = formatAllVersions(mockApiResponse);
|
|
55
|
+
expect(result).toContain('1.8.0-beta1');
|
|
56
|
+
expect(result).toContain('1.7.0-rc1');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('still includes stable versions', () => {
|
|
60
|
+
const result = formatAllVersions(mockApiResponse);
|
|
61
|
+
expect(result).toContain('1.7.3');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('sorts descending', () => {
|
|
65
|
+
const result = formatAllVersions(mockApiResponse);
|
|
66
|
+
expect(result[0]).toBe('1.8.0-beta1');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('formatOpenTofuVersions (GitHub Releases API)', () => {
|
|
71
|
+
const mockReleases = [
|
|
72
|
+
{ tag_name: 'v1.7.3', prerelease: false, draft: false },
|
|
73
|
+
{ tag_name: 'v1.7.0', prerelease: false, draft: false },
|
|
74
|
+
{ tag_name: 'v1.8.0-alpha1', prerelease: true, draft: false },
|
|
75
|
+
{ tag_name: 'v1.6.5', prerelease: false, draft: true },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
test('strips v prefix from versions', () => {
|
|
79
|
+
const result = formatOpenTofuVersions(mockReleases);
|
|
80
|
+
result.forEach(v => expect(v).not.toMatch(/^v/));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('excludes pre-releases and drafts', () => {
|
|
84
|
+
const result = formatOpenTofuVersions(mockReleases);
|
|
85
|
+
expect(result).not.toContain('1.8.0-alpha1');
|
|
86
|
+
expect(result).not.toContain('1.6.5');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('sorts versions descending', () => {
|
|
90
|
+
const result = formatOpenTofuVersions(mockReleases);
|
|
91
|
+
expect(result[0]).toBe('1.7.3');
|
|
92
|
+
expect(result[1]).toBe('1.7.0');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('formatAllOpenTofuVersions (includes pre-releases)', () => {
|
|
97
|
+
const mockReleases = [
|
|
98
|
+
{ tag_name: 'v1.8.0-alpha1', prerelease: true, draft: false },
|
|
99
|
+
{ tag_name: 'v1.7.3', prerelease: false, draft: false },
|
|
100
|
+
{ tag_name: 'v1.6.5', prerelease: false, draft: true }, // drafts still excluded
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
test('includes pre-release builds', () => {
|
|
104
|
+
const result = formatAllOpenTofuVersions(mockReleases);
|
|
105
|
+
expect(result).toContain('1.8.0-alpha1');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('excludes draft releases', () => {
|
|
109
|
+
const result = formatAllOpenTofuVersions(mockReleases);
|
|
110
|
+
expect(result).not.toContain('1.6.5');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('semverDesc', () => {
|
|
115
|
+
test('sorts major versions correctly', () => {
|
|
116
|
+
expect(['1.0.0', '2.0.0', '0.9.0'].sort(semverDesc)).toEqual(['2.0.0', '1.0.0', '0.9.0']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('sorts minor versions correctly', () => {
|
|
120
|
+
expect(['1.3.0', '1.10.0', '1.2.0'].sort(semverDesc)).toEqual(['1.10.0', '1.3.0', '1.2.0']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('sorts patch versions correctly', () => {
|
|
124
|
+
expect(['1.7.1', '1.7.3', '1.7.2'].sort(semverDesc)).toEqual(['1.7.3', '1.7.2', '1.7.1']);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Smoke-import tests — verifies every module loads without ReferenceErrors,
|
|
5
|
+
* missing requires, or other module-level exceptions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const modules = [
|
|
9
|
+
'../lib/modules/install',
|
|
10
|
+
'../lib/modules/use',
|
|
11
|
+
'../lib/modules/list',
|
|
12
|
+
'../lib/modules/remote',
|
|
13
|
+
'../lib/modules/remove',
|
|
14
|
+
'../lib/modules/switch',
|
|
15
|
+
'../lib/modules/current',
|
|
16
|
+
'../lib/modules/pin',
|
|
17
|
+
'../lib/modules/upgrade',
|
|
18
|
+
'../lib/modules/shell-init',
|
|
19
|
+
'../lib/modules/terraform-command',
|
|
20
|
+
'../lib/utils/paths',
|
|
21
|
+
'../lib/utils/store',
|
|
22
|
+
'../lib/utils/formatVersions',
|
|
23
|
+
'../lib/utils/colors',
|
|
24
|
+
'../lib/commands/install',
|
|
25
|
+
'../lib/commands/use',
|
|
26
|
+
'../lib/commands/list',
|
|
27
|
+
'../lib/commands/remove',
|
|
28
|
+
'../lib/commands/switch',
|
|
29
|
+
'../lib/commands/current',
|
|
30
|
+
'../lib/commands/pin',
|
|
31
|
+
'../lib/commands/upgrade',
|
|
32
|
+
'../lib/commands/shell-init',
|
|
33
|
+
'../lib/commands/plan',
|
|
34
|
+
'../lib/commands/apply',
|
|
35
|
+
'../lib/commands/destroy',
|
|
36
|
+
'../lib/commands/init',
|
|
37
|
+
'../lib/commands/validate',
|
|
38
|
+
'../lib/commands/fmt',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
describe('module imports', () => {
|
|
42
|
+
test.each(modules)('%s loads without error', (mod) => {
|
|
43
|
+
expect(() => require(mod)).not.toThrow();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const { join } = require('path');
|
|
2
|
+
const { homedir } = require('os');
|
|
3
|
+
const {
|
|
4
|
+
TFV_HOME,
|
|
5
|
+
getPaths,
|
|
6
|
+
normalizeProvider,
|
|
7
|
+
getCliName,
|
|
8
|
+
getProviderStore,
|
|
9
|
+
} = require('../lib/utils/paths');
|
|
10
|
+
|
|
11
|
+
describe('TFV_HOME', () => {
|
|
12
|
+
test('is under the user home directory', () => {
|
|
13
|
+
expect(TFV_HOME).toBe(join(homedir(), '.tfv'));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('getPaths', () => {
|
|
18
|
+
const paths = getPaths();
|
|
19
|
+
|
|
20
|
+
test('bin is inside TFV_HOME', () => {
|
|
21
|
+
expect(paths.bin).toBe(join(TFV_HOME, 'bin'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('store is inside TFV_HOME', () => {
|
|
25
|
+
expect(paths.store).toBe(join(TFV_HOME, 'store'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('terraform store is correct', () => {
|
|
29
|
+
expect(paths.terraform).toBe(join(TFV_HOME, 'store', 'terraform'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('opentofu store is correct', () => {
|
|
33
|
+
expect(paths.opentofu).toBe(join(TFV_HOME, 'store', 'opentofu'));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('active.json is in TFV_HOME', () => {
|
|
37
|
+
expect(paths.active).toBe(join(TFV_HOME, 'active.json'));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('normalizeProvider', () => {
|
|
42
|
+
test('terraform → terraform', () => expect(normalizeProvider('terraform')).toBe('terraform'));
|
|
43
|
+
test('tf → terraform', () => expect(normalizeProvider('tf')).toBe('terraform'));
|
|
44
|
+
test('tofu → opentofu', () => expect(normalizeProvider('tofu')).toBe('opentofu'));
|
|
45
|
+
test('opentofu → opentofu', () => expect(normalizeProvider('opentofu')).toBe('opentofu'));
|
|
46
|
+
test('default (undefined) → terraform', () => expect(normalizeProvider()).toBe('terraform'));
|
|
47
|
+
test('unknown → terraform', () => expect(normalizeProvider('unknown')).toBe('terraform'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getCliName', () => {
|
|
51
|
+
test('terraform → terraform', () => expect(getCliName('terraform')).toBe('terraform'));
|
|
52
|
+
test('tf → terraform', () => expect(getCliName('tf')).toBe('terraform'));
|
|
53
|
+
test('tofu → tofu', () => expect(getCliName('tofu')).toBe('tofu'));
|
|
54
|
+
test('opentofu → tofu', () => expect(getCliName('opentofu')).toBe('tofu'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getProviderStore', () => {
|
|
58
|
+
test('returns terraform store by default', () => {
|
|
59
|
+
expect(getProviderStore()).toBe(join(TFV_HOME, 'store', 'terraform'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('returns opentofu store for tofu alias', () => {
|
|
63
|
+
expect(getProviderStore('tofu')).toBe(join(TFV_HOME, 'store', 'opentofu'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('returns opentofu store', () => {
|
|
67
|
+
expect(getProviderStore('opentofu')).toBe(join(TFV_HOME, 'store', 'opentofu'));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Test the resolveConstraint logic from switch.js by extracting it
|
|
2
|
+
|
|
3
|
+
// Inline the function for isolated testing
|
|
4
|
+
const resolveConstraint = (constraint, versions) => {
|
|
5
|
+
const parts = constraint.split(',').map(s => s.trim());
|
|
6
|
+
|
|
7
|
+
return versions.find(v => {
|
|
8
|
+
return parts.every(part => {
|
|
9
|
+
const m = part.match(/^(~>|>=|<=|!=|>|<|=)?\s*(\d+(?:\.\d+)*)/);
|
|
10
|
+
if (!m) return false;
|
|
11
|
+
const [, op = '=', req] = m;
|
|
12
|
+
|
|
13
|
+
const vParts = v.split('.').map(n => parseInt(n, 10) || 0);
|
|
14
|
+
const rParts = req.split('.').map(n => parseInt(n, 10) || 0);
|
|
15
|
+
|
|
16
|
+
const cmp = () => {
|
|
17
|
+
for (let i = 0; i < 3; i++) {
|
|
18
|
+
const diff = (vParts[i] || 0) - (rParts[i] || 0);
|
|
19
|
+
if (diff !== 0) return diff;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
switch (op) {
|
|
25
|
+
case '~>': {
|
|
26
|
+
const segCount = rParts.length;
|
|
27
|
+
for (let i = 0; i < segCount - 1; i++) {
|
|
28
|
+
if ((vParts[i] || 0) !== (rParts[i] || 0)) return false;
|
|
29
|
+
}
|
|
30
|
+
return (vParts[segCount - 1] || 0) >= (rParts[segCount - 1] || 0);
|
|
31
|
+
}
|
|
32
|
+
case '>=': return cmp() >= 0;
|
|
33
|
+
case '>': return cmp() > 0;
|
|
34
|
+
case '<=': return cmp() <= 0;
|
|
35
|
+
case '<': return cmp() < 0;
|
|
36
|
+
case '!=': return cmp() !== 0;
|
|
37
|
+
default: return cmp() === 0;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}) || null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const versions = ['1.9.0', '1.8.5', '1.8.0', '1.7.3', '1.7.0', '1.6.6', '1.5.7', '0.15.0'];
|
|
44
|
+
|
|
45
|
+
describe('resolveConstraint', () => {
|
|
46
|
+
test('exact match =', () => {
|
|
47
|
+
expect(resolveConstraint('= 1.7.3', versions)).toBe('1.7.3');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('exact match (no operator)', () => {
|
|
51
|
+
expect(resolveConstraint('1.7.3', versions)).toBe('1.7.3');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('>= returns highest satisfying version', () => {
|
|
55
|
+
expect(resolveConstraint('>= 1.8.0', versions)).toBe('1.9.0');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('> strictly greater', () => {
|
|
59
|
+
expect(resolveConstraint('> 1.8.0', versions)).toBe('1.9.0');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('<= returns highest satisfying version', () => {
|
|
63
|
+
expect(resolveConstraint('<= 1.7.3', versions)).toBe('1.7.3');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('!= excludes version', () => {
|
|
67
|
+
expect(resolveConstraint('!= 1.9.0', versions)).toBe('1.8.5');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('~> 1.7 allows patch bumps only', () => {
|
|
71
|
+
// ~> 1.7 means >= 1.7, < 2.0 effectively
|
|
72
|
+
const result = resolveConstraint('~> 1.7', versions);
|
|
73
|
+
expect(result).toBe('1.9.0'); // highest with major=1
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('~> 1.7.0 allows patch bumps in 1.7.x', () => {
|
|
77
|
+
const result = resolveConstraint('~> 1.7.0', versions);
|
|
78
|
+
expect(['1.7.3', '1.7.0']).toContain(result);
|
|
79
|
+
// Should be 1.7.3 (highest 1.7.x)
|
|
80
|
+
expect(result).toBe('1.7.3');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('compound constraint >= X, < Y', () => {
|
|
84
|
+
// >= 1.7.0, < 1.9.0
|
|
85
|
+
const result = resolveConstraint('>= 1.7.0, < 1.9.0', versions);
|
|
86
|
+
expect(result).toBe('1.8.5');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('returns null when no version satisfies', () => {
|
|
90
|
+
expect(resolveConstraint('>= 2.0.0', versions)).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|