@aikidosec/safe-chain 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +10 -8
  2. package/bin/aikido-pnpm.js +8 -0
  3. package/bin/aikido-pnpx.js +8 -0
  4. package/docs/shell-integration.md +4 -4
  5. package/package.json +3 -1
  6. package/src/packagemanager/_shared/matchesCommand.js +13 -0
  7. package/src/packagemanager/currentPackageManager.js +8 -0
  8. package/src/packagemanager/pnpm/createPackageManager.js +46 -0
  9. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +28 -0
  10. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +88 -0
  11. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +138 -0
  12. package/src/packagemanager/pnpm/runPnpmCommand.js +24 -0
  13. package/src/shell-integration/helpers.js +31 -31
  14. package/src/shell-integration/setup.js +26 -102
  15. package/src/shell-integration/shellDetection.js +17 -66
  16. package/src/shell-integration/supported-shells/bash.js +58 -0
  17. package/src/shell-integration/supported-shells/bash.spec.js +199 -0
  18. package/src/shell-integration/supported-shells/fish.js +61 -0
  19. package/src/shell-integration/supported-shells/fish.spec.js +199 -0
  20. package/src/shell-integration/supported-shells/powershell.js +61 -0
  21. package/src/shell-integration/supported-shells/powershell.spec.js +204 -0
  22. package/src/shell-integration/supported-shells/windowsPowershell.js +61 -0
  23. package/src/shell-integration/supported-shells/windowsPowershell.spec.js +204 -0
  24. package/src/shell-integration/supported-shells/zsh.js +58 -0
  25. package/src/shell-integration/supported-shells/zsh.spec.js +199 -0
  26. package/src/shell-integration/teardown.js +20 -99
  27. package/src/shell-integration/setup.spec.js +0 -304
  28. package/src/shell-integration/teardown.spec.js +0 -177
@@ -0,0 +1,199 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import path from "path";
6
+ import { knownAikidoTools } from "../helpers.js";
7
+
8
+ describe("Fish shell integration", () => {
9
+ let mockStartupFile;
10
+ let fish;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(tmpdir(), `test-fish-config-${Date.now()}`);
15
+
16
+ // Mock the helpers module
17
+ mock.module("../helpers.js", {
18
+ namedExports: {
19
+ doesExecutableExistOnSystem: () => true,
20
+ addLineToFile: (filePath, line) => {
21
+ if (!fs.existsSync(filePath)) {
22
+ fs.writeFileSync(filePath, "", "utf-8");
23
+ }
24
+ fs.appendFileSync(filePath, line + "\n", "utf-8");
25
+ },
26
+ removeLinesMatchingPattern: (filePath, pattern) => {
27
+ if (!fs.existsSync(filePath)) return;
28
+ const content = fs.readFileSync(filePath, "utf-8");
29
+ const lines = content.split("\n");
30
+ const filteredLines = lines.filter((line) => !pattern.test(line));
31
+ fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
32
+ },
33
+ },
34
+ });
35
+
36
+ // Mock child_process execSync
37
+ mock.module("child_process", {
38
+ namedExports: {
39
+ execSync: () => mockStartupFile,
40
+ },
41
+ });
42
+
43
+ // Import fish module after mocking
44
+ fish = (await import("./fish.js")).default;
45
+ });
46
+
47
+ afterEach(() => {
48
+ // Clean up test files
49
+ if (fs.existsSync(mockStartupFile)) {
50
+ fs.unlinkSync(mockStartupFile);
51
+ }
52
+
53
+ // Reset mocks
54
+ mock.reset();
55
+ });
56
+
57
+ describe("isInstalled", () => {
58
+ it("should return true when fish is installed", () => {
59
+ assert.strictEqual(fish.isInstalled(), true);
60
+ });
61
+
62
+ it("should call doesExecutableExistOnSystem with correct parameter", () => {
63
+ // Test that the method calls the helper with the right executable name
64
+ assert.strictEqual(fish.isInstalled(), true);
65
+ });
66
+ });
67
+
68
+ describe("setup", () => {
69
+ it("should add aliases for all provided tools", () => {
70
+ const tools = [
71
+ { tool: "npm", aikidoCommand: "aikido-npm" },
72
+ { tool: "npx", aikidoCommand: "aikido-npx" },
73
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
74
+ ];
75
+
76
+ const result = fish.setup(tools);
77
+ assert.strictEqual(result, true);
78
+
79
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
80
+ assert.ok(
81
+ content.includes('alias npm "aikido-npm" # Safe-chain alias for npm')
82
+ );
83
+ assert.ok(
84
+ content.includes('alias npx "aikido-npx" # Safe-chain alias for npx')
85
+ );
86
+ assert.ok(
87
+ content.includes('alias yarn "aikido-yarn" # Safe-chain alias for yarn')
88
+ );
89
+ });
90
+
91
+ it("should handle empty tools array", () => {
92
+ const result = fish.setup([]);
93
+ assert.strictEqual(result, true);
94
+
95
+ // File should be created during teardown call even if no tools are provided
96
+ if (fs.existsSync(mockStartupFile)) {
97
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
98
+ assert.strictEqual(content.trim(), "");
99
+ }
100
+ });
101
+ });
102
+
103
+ describe("teardown", () => {
104
+ it("should remove npm, npx, and yarn aliases", () => {
105
+ const initialContent = [
106
+ "#!/usr/bin/env fish",
107
+ "alias npm 'aikido-npm'",
108
+ "alias npx 'aikido-npx'",
109
+ "alias yarn 'aikido-yarn'",
110
+ "alias ls 'ls --color=auto'",
111
+ "alias grep 'grep --color=auto'",
112
+ ].join("\n");
113
+
114
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
115
+
116
+ const result = fish.teardown(knownAikidoTools);
117
+ assert.strictEqual(result, true);
118
+
119
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
120
+ assert.ok(!content.includes("alias npm "));
121
+ assert.ok(!content.includes("alias npx "));
122
+ assert.ok(!content.includes("alias yarn "));
123
+ assert.ok(content.includes("alias ls "));
124
+ assert.ok(content.includes("alias grep "));
125
+ });
126
+
127
+ it("should handle file that doesn't exist", () => {
128
+ if (fs.existsSync(mockStartupFile)) {
129
+ fs.unlinkSync(mockStartupFile);
130
+ }
131
+
132
+ const result = fish.teardown(knownAikidoTools);
133
+ assert.strictEqual(result, true);
134
+ });
135
+
136
+ it("should handle file with no relevant aliases", () => {
137
+ const initialContent = [
138
+ "#!/usr/bin/env fish",
139
+ "alias ls 'ls --color=auto'",
140
+ "set PATH $PATH ~/bin",
141
+ ].join("\n");
142
+
143
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
144
+
145
+ const result = fish.teardown(knownAikidoTools);
146
+ assert.strictEqual(result, true);
147
+
148
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
149
+ assert.ok(content.includes("alias ls "));
150
+ assert.ok(content.includes("set PATH "));
151
+ });
152
+ });
153
+
154
+ describe("shell properties", () => {
155
+ it("should have correct name", () => {
156
+ assert.strictEqual(fish.name, "Fish");
157
+ });
158
+
159
+ it("should expose all required methods", () => {
160
+ assert.ok(typeof fish.isInstalled === "function");
161
+ assert.ok(typeof fish.setup === "function");
162
+ assert.ok(typeof fish.teardown === "function");
163
+ assert.ok(typeof fish.name === "string");
164
+ });
165
+ });
166
+
167
+ describe("integration tests", () => {
168
+ it("should handle complete setup and teardown cycle", () => {
169
+ const tools = [
170
+ { tool: "npm", aikidoCommand: "aikido-npm" },
171
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
172
+ ];
173
+
174
+ // Setup
175
+ fish.setup(tools);
176
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
177
+ assert.ok(content.includes('alias npm "aikido-npm"'));
178
+ assert.ok(content.includes('alias yarn "aikido-yarn"'));
179
+
180
+ // Teardown
181
+ fish.teardown(tools);
182
+ content = fs.readFileSync(mockStartupFile, "utf-8");
183
+ assert.ok(!content.includes("alias npm "));
184
+ assert.ok(!content.includes("alias yarn "));
185
+ });
186
+
187
+ it("should handle multiple setup calls", () => {
188
+ const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
189
+
190
+ fish.setup(tools);
191
+ fish.teardown(tools);
192
+ fish.setup(tools);
193
+
194
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
195
+ const npmMatches = (content.match(/alias npm "/g) || []).length;
196
+ assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,61 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { execSync } from "child_process";
7
+
8
+ const shellName = "PowerShell Core";
9
+ const executableName = "pwsh";
10
+ const startupFileCommand = "echo $PROFILE";
11
+
12
+ function isInstalled() {
13
+ return doesExecutableExistOnSystem(executableName);
14
+ }
15
+
16
+ function teardown(tools) {
17
+ const startupFile = getStartupFile();
18
+
19
+ for (const { tool } of tools) {
20
+ // Remove any existing alias for the tool
21
+ removeLinesMatchingPattern(
22
+ startupFile,
23
+ new RegExp(`^Set-Alias\\s+${tool}\\s+`)
24
+ );
25
+ }
26
+
27
+ return true;
28
+ }
29
+
30
+ function setup(tools) {
31
+ const startupFile = getStartupFile();
32
+
33
+ for (const { tool, aikidoCommand } of tools) {
34
+ addLineToFile(
35
+ startupFile,
36
+ `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}`
37
+ );
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ function getStartupFile() {
44
+ try {
45
+ return execSync(startupFileCommand, {
46
+ encoding: "utf8",
47
+ shell: executableName,
48
+ }).trim();
49
+ } catch (error) {
50
+ throw new Error(
51
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
52
+ );
53
+ }
54
+ }
55
+
56
+ export default {
57
+ name: shellName,
58
+ isInstalled,
59
+ setup,
60
+ teardown,
61
+ };
@@ -0,0 +1,204 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import path from "path";
6
+ import { knownAikidoTools } from "../helpers.js";
7
+
8
+ describe("PowerShell Core shell integration", () => {
9
+ let mockStartupFile;
10
+ let powershell;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(
15
+ tmpdir(),
16
+ `test-powershell-profile-${Date.now()}.ps1`
17
+ );
18
+
19
+ // Mock the helpers module
20
+ mock.module("../helpers.js", {
21
+ namedExports: {
22
+ doesExecutableExistOnSystem: () => true,
23
+ addLineToFile: (filePath, line) => {
24
+ if (!fs.existsSync(filePath)) {
25
+ fs.writeFileSync(filePath, "", "utf-8");
26
+ }
27
+ fs.appendFileSync(filePath, line + "\n", "utf-8");
28
+ },
29
+ removeLinesMatchingPattern: (filePath, pattern) => {
30
+ if (!fs.existsSync(filePath)) return;
31
+ const content = fs.readFileSync(filePath, "utf-8");
32
+ const lines = content.split("\n");
33
+ const filteredLines = lines.filter((line) => !pattern.test(line));
34
+ fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
35
+ },
36
+ },
37
+ });
38
+
39
+ // Mock child_process execSync
40
+ mock.module("child_process", {
41
+ namedExports: {
42
+ execSync: () => mockStartupFile,
43
+ },
44
+ });
45
+
46
+ // Import powershell module after mocking
47
+ powershell = (await import("./powershell.js")).default;
48
+ });
49
+
50
+ afterEach(() => {
51
+ // Clean up test files
52
+ if (fs.existsSync(mockStartupFile)) {
53
+ fs.unlinkSync(mockStartupFile);
54
+ }
55
+
56
+ // Reset mocks
57
+ mock.reset();
58
+ });
59
+
60
+ describe("isInstalled", () => {
61
+ it("should return true when powershell is installed", () => {
62
+ assert.strictEqual(powershell.isInstalled(), true);
63
+ });
64
+
65
+ it("should call doesExecutableExistOnSystem with correct parameter", () => {
66
+ // Test that the method calls the helper with the right executable name
67
+ assert.strictEqual(powershell.isInstalled(), true);
68
+ });
69
+ });
70
+
71
+ describe("setup", () => {
72
+ it("should add aliases for all provided tools", () => {
73
+ const tools = [
74
+ { tool: "npm", aikidoCommand: "aikido-npm" },
75
+ { tool: "npx", aikidoCommand: "aikido-npx" },
76
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
77
+ ];
78
+
79
+ const result = powershell.setup(tools);
80
+ assert.strictEqual(result, true);
81
+
82
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
83
+ assert.ok(
84
+ content.includes("Set-Alias npm aikido-npm # Safe-chain alias for npm")
85
+ );
86
+ assert.ok(
87
+ content.includes("Set-Alias npx aikido-npx # Safe-chain alias for npx")
88
+ );
89
+ assert.ok(
90
+ content.includes(
91
+ "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn"
92
+ )
93
+ );
94
+ });
95
+
96
+ it("should handle empty tools array", () => {
97
+ const result = powershell.setup([]);
98
+ assert.strictEqual(result, true);
99
+
100
+ // File should be created during teardown call even if no tools are provided
101
+ if (fs.existsSync(mockStartupFile)) {
102
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
103
+ assert.strictEqual(content.trim(), "");
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("teardown", () => {
109
+ it("should remove npm, npx, and yarn aliases", () => {
110
+ const initialContent = [
111
+ "# PowerShell profile",
112
+ "Set-Alias npm aikido-npm",
113
+ "Set-Alias npx aikido-npx",
114
+ "Set-Alias yarn aikido-yarn",
115
+ "Set-Alias ls Get-ChildItem",
116
+ "Set-Alias grep Select-String",
117
+ ].join("\n");
118
+
119
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
120
+
121
+ const result = powershell.teardown(knownAikidoTools);
122
+ assert.strictEqual(result, true);
123
+
124
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
125
+ assert.ok(!content.includes("Set-Alias npm "));
126
+ assert.ok(!content.includes("Set-Alias npx "));
127
+ assert.ok(!content.includes("Set-Alias yarn "));
128
+ assert.ok(content.includes("Set-Alias ls "));
129
+ assert.ok(content.includes("Set-Alias grep "));
130
+ });
131
+
132
+ it("should handle file that doesn't exist", () => {
133
+ if (fs.existsSync(mockStartupFile)) {
134
+ fs.unlinkSync(mockStartupFile);
135
+ }
136
+
137
+ const result = powershell.teardown(knownAikidoTools);
138
+ assert.strictEqual(result, true);
139
+ });
140
+
141
+ it("should handle file with no relevant aliases", () => {
142
+ const initialContent = [
143
+ "# PowerShell profile",
144
+ "Set-Alias ls Get-ChildItem",
145
+ "$env:PATH += ';C:\\Tools'",
146
+ ].join("\n");
147
+
148
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
149
+
150
+ const result = powershell.teardown(knownAikidoTools);
151
+ assert.strictEqual(result, true);
152
+
153
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
154
+ assert.ok(content.includes("Set-Alias ls "));
155
+ assert.ok(content.includes("$env:PATH "));
156
+ });
157
+ });
158
+
159
+ describe("shell properties", () => {
160
+ it("should have correct name", () => {
161
+ assert.strictEqual(powershell.name, "PowerShell Core");
162
+ });
163
+
164
+ it("should expose all required methods", () => {
165
+ assert.ok(typeof powershell.isInstalled === "function");
166
+ assert.ok(typeof powershell.setup === "function");
167
+ assert.ok(typeof powershell.teardown === "function");
168
+ assert.ok(typeof powershell.name === "string");
169
+ });
170
+ });
171
+
172
+ describe("integration tests", () => {
173
+ it("should handle complete setup and teardown cycle", () => {
174
+ const tools = [
175
+ { tool: "npm", aikidoCommand: "aikido-npm" },
176
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
177
+ ];
178
+
179
+ // Setup
180
+ powershell.setup(tools);
181
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
182
+ assert.ok(content.includes("Set-Alias npm aikido-npm"));
183
+ assert.ok(content.includes("Set-Alias yarn aikido-yarn"));
184
+
185
+ // Teardown
186
+ powershell.teardown(tools);
187
+ content = fs.readFileSync(mockStartupFile, "utf-8");
188
+ assert.ok(!content.includes("Set-Alias npm "));
189
+ assert.ok(!content.includes("Set-Alias yarn "));
190
+ });
191
+
192
+ it("should handle multiple setup calls", () => {
193
+ const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
194
+
195
+ powershell.setup(tools);
196
+ powershell.teardown(tools);
197
+ powershell.setup(tools);
198
+
199
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
200
+ const npmMatches = (content.match(/Set-Alias npm /g) || []).length;
201
+ assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,61 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { execSync } from "child_process";
7
+
8
+ const shellName = "Windows PowerShell";
9
+ const executableName = "powershell";
10
+ const startupFileCommand = "echo $PROFILE";
11
+
12
+ function isInstalled() {
13
+ return doesExecutableExistOnSystem(executableName);
14
+ }
15
+
16
+ function teardown(tools) {
17
+ const startupFile = getStartupFile();
18
+
19
+ for (const { tool } of tools) {
20
+ // Remove any existing alias for the tool
21
+ removeLinesMatchingPattern(
22
+ startupFile,
23
+ new RegExp(`^Set-Alias\\s+${tool}\\s+`)
24
+ );
25
+ }
26
+
27
+ return true;
28
+ }
29
+
30
+ function setup(tools) {
31
+ const startupFile = getStartupFile();
32
+
33
+ for (const { tool, aikidoCommand } of tools) {
34
+ addLineToFile(
35
+ startupFile,
36
+ `Set-Alias ${tool} ${aikidoCommand} # Safe-chain alias for ${tool}`
37
+ );
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ function getStartupFile() {
44
+ try {
45
+ return execSync(startupFileCommand, {
46
+ encoding: "utf8",
47
+ shell: executableName,
48
+ }).trim();
49
+ } catch (error) {
50
+ throw new Error(
51
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
52
+ );
53
+ }
54
+ }
55
+
56
+ export default {
57
+ name: shellName,
58
+ isInstalled,
59
+ setup,
60
+ teardown,
61
+ };