@aikidosec/safe-chain 1.0.15 → 1.0.17

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 (34) 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/eslint.config.js +2 -1
  6. package/package.json +9 -4
  7. package/src/packagemanager/_shared/matchesCommand.js +13 -0
  8. package/src/packagemanager/currentPackageManager.js +8 -0
  9. package/src/packagemanager/pnpm/createPackageManager.js +46 -0
  10. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +28 -0
  11. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +88 -0
  12. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +138 -0
  13. package/src/packagemanager/pnpm/runPnpmCommand.js +24 -0
  14. package/src/shell-integration/helpers.js +31 -31
  15. package/src/shell-integration/setup.js +44 -95
  16. package/src/shell-integration/shellDetection.js +17 -66
  17. package/src/shell-integration/startup-scripts/init-fish.fish +58 -0
  18. package/src/shell-integration/startup-scripts/init-posix.sh +54 -0
  19. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +80 -0
  20. package/src/shell-integration/supported-shells/bash.js +62 -0
  21. package/src/shell-integration/supported-shells/bash.spec.js +199 -0
  22. package/src/shell-integration/supported-shells/fish.js +65 -0
  23. package/src/shell-integration/supported-shells/fish.spec.js +183 -0
  24. package/src/shell-integration/supported-shells/powershell.js +65 -0
  25. package/src/shell-integration/supported-shells/powershell.spec.js +200 -0
  26. package/src/shell-integration/supported-shells/windowsPowershell.js +65 -0
  27. package/src/shell-integration/supported-shells/windowsPowershell.spec.js +200 -0
  28. package/src/shell-integration/supported-shells/zsh.js +62 -0
  29. package/src/shell-integration/supported-shells/zsh.spec.js +226 -0
  30. package/src/shell-integration/teardown.js +20 -99
  31. package/.github/workflows/build-and-release.yml +0 -41
  32. package/.github/workflows/test-on-pr.yml +0 -28
  33. package/src/shell-integration/setup.spec.js +0 -304
  34. package/src/shell-integration/teardown.spec.js +0 -177
@@ -0,0 +1,200 @@
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 init-pwsh.ps1 source line", () => {
73
+ const result = powershell.setup();
74
+ assert.strictEqual(result, true);
75
+
76
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
77
+ assert.ok(
78
+ content.includes(
79
+ '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script'
80
+ )
81
+ );
82
+ });
83
+ });
84
+
85
+ describe("teardown", () => {
86
+ it("should remove init-pwsh.ps1 source line", () => {
87
+ const initialContent = [
88
+ "# PowerShell profile",
89
+ '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script',
90
+ "Set-Alias ls Get-ChildItem",
91
+ "Set-Alias grep Select-String",
92
+ ].join("\n");
93
+
94
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
95
+
96
+ const result = powershell.teardown(knownAikidoTools);
97
+ assert.strictEqual(result, true);
98
+
99
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
100
+ assert.ok(
101
+ !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
102
+ );
103
+ assert.ok(content.includes("Set-Alias ls "));
104
+ assert.ok(content.includes("Set-Alias grep "));
105
+ });
106
+
107
+ it("should remove old-style aliases from earlier versions", () => {
108
+ const initialContent = [
109
+ "# PowerShell profile",
110
+ "Set-Alias npm aikido-npm # Safe-chain alias for npm",
111
+ "Set-Alias npx aikido-npx # Safe-chain alias for npx",
112
+ "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn",
113
+ "Set-Alias ls Get-ChildItem",
114
+ "Set-Alias grep Select-String",
115
+ ].join("\n");
116
+
117
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
118
+
119
+ const result = powershell.teardown(knownAikidoTools);
120
+ assert.strictEqual(result, true);
121
+
122
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
123
+ assert.ok(!content.includes("Set-Alias npm "));
124
+ assert.ok(!content.includes("Set-Alias npx "));
125
+ assert.ok(!content.includes("Set-Alias yarn "));
126
+ assert.ok(content.includes("Set-Alias ls "));
127
+ assert.ok(content.includes("Set-Alias grep "));
128
+ });
129
+
130
+ it("should handle file that doesn't exist", () => {
131
+ if (fs.existsSync(mockStartupFile)) {
132
+ fs.unlinkSync(mockStartupFile);
133
+ }
134
+
135
+ const result = powershell.teardown(knownAikidoTools);
136
+ assert.strictEqual(result, true);
137
+ });
138
+
139
+ it("should handle file with no relevant content", () => {
140
+ const initialContent = [
141
+ "# PowerShell profile",
142
+ "Set-Alias ls Get-ChildItem",
143
+ "$env:PATH += ';C:\\Tools'",
144
+ ].join("\n");
145
+
146
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
147
+
148
+ const result = powershell.teardown(knownAikidoTools);
149
+ assert.strictEqual(result, true);
150
+
151
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
152
+ assert.ok(content.includes("Set-Alias ls "));
153
+ assert.ok(content.includes("$env:PATH "));
154
+ });
155
+ });
156
+
157
+ describe("shell properties", () => {
158
+ it("should have correct name", () => {
159
+ assert.strictEqual(powershell.name, "PowerShell Core");
160
+ });
161
+
162
+ it("should expose all required methods", () => {
163
+ assert.ok(typeof powershell.isInstalled === "function");
164
+ assert.ok(typeof powershell.setup === "function");
165
+ assert.ok(typeof powershell.teardown === "function");
166
+ assert.ok(typeof powershell.name === "string");
167
+ });
168
+ });
169
+
170
+ describe("integration tests", () => {
171
+ it("should handle complete setup and teardown cycle", () => {
172
+ // Setup
173
+ powershell.setup();
174
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
175
+ assert.ok(
176
+ content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
177
+ );
178
+
179
+ // Teardown
180
+ powershell.teardown(knownAikidoTools);
181
+ content = fs.readFileSync(mockStartupFile, "utf-8");
182
+ assert.ok(
183
+ !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
184
+ );
185
+ });
186
+
187
+ it("should handle multiple setup calls", () => {
188
+ powershell.setup();
189
+ powershell.teardown(knownAikidoTools);
190
+ powershell.setup();
191
+
192
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
193
+ const sourceMatches = (
194
+ content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) ||
195
+ []
196
+ ).length;
197
+ assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,65 @@
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
+ // Remove the line that sources the safe-chain PowerShell initialization script
28
+ removeLinesMatchingPattern(
29
+ startupFile,
30
+ /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/
31
+ );
32
+
33
+ return true;
34
+ }
35
+
36
+ function setup() {
37
+ const startupFile = getStartupFile();
38
+
39
+ addLineToFile(
40
+ startupFile,
41
+ `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`
42
+ );
43
+
44
+ return true;
45
+ }
46
+
47
+ function getStartupFile() {
48
+ try {
49
+ return execSync(startupFileCommand, {
50
+ encoding: "utf8",
51
+ shell: executableName,
52
+ }).trim();
53
+ } catch (error) {
54
+ throw new Error(
55
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
56
+ );
57
+ }
58
+ }
59
+
60
+ export default {
61
+ name: shellName,
62
+ isInstalled,
63
+ setup,
64
+ teardown,
65
+ };
@@ -0,0 +1,200 @@
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("Windows PowerShell shell integration", () => {
9
+ let mockStartupFile;
10
+ let windowsPowershell;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(
15
+ tmpdir(),
16
+ `test-windows-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 windowsPowershell module after mocking
47
+ windowsPowershell = (await import("./windowsPowershell.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 windows powershell is installed", () => {
62
+ assert.strictEqual(windowsPowershell.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(windowsPowershell.isInstalled(), true);
68
+ });
69
+ });
70
+
71
+ describe("setup", () => {
72
+ it("should add init-pwsh.ps1 source line", () => {
73
+ const result = windowsPowershell.setup();
74
+ assert.strictEqual(result, true);
75
+
76
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
77
+ assert.ok(
78
+ content.includes(
79
+ '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script'
80
+ )
81
+ );
82
+ });
83
+ });
84
+
85
+ describe("teardown", () => {
86
+ it("should remove init-pwsh.ps1 source line", () => {
87
+ const initialContent = [
88
+ "# Windows PowerShell profile",
89
+ '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script',
90
+ "Set-Alias ls Get-ChildItem",
91
+ "Set-Alias grep Select-String",
92
+ ].join("\n");
93
+
94
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
95
+
96
+ const result = windowsPowershell.teardown(knownAikidoTools);
97
+ assert.strictEqual(result, true);
98
+
99
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
100
+ assert.ok(
101
+ !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
102
+ );
103
+ assert.ok(content.includes("Set-Alias ls "));
104
+ assert.ok(content.includes("Set-Alias grep "));
105
+ });
106
+
107
+ it("should remove old-style aliases from earlier versions", () => {
108
+ const initialContent = [
109
+ "# Windows PowerShell profile",
110
+ "Set-Alias npm aikido-npm # Safe-chain alias for npm",
111
+ "Set-Alias npx aikido-npx # Safe-chain alias for npx",
112
+ "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn",
113
+ "Set-Alias ls Get-ChildItem",
114
+ "Set-Alias grep Select-String",
115
+ ].join("\n");
116
+
117
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
118
+
119
+ const result = windowsPowershell.teardown(knownAikidoTools);
120
+ assert.strictEqual(result, true);
121
+
122
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
123
+ assert.ok(!content.includes("Set-Alias npm "));
124
+ assert.ok(!content.includes("Set-Alias npx "));
125
+ assert.ok(!content.includes("Set-Alias yarn "));
126
+ assert.ok(content.includes("Set-Alias ls "));
127
+ assert.ok(content.includes("Set-Alias grep "));
128
+ });
129
+
130
+ it("should handle file that doesn't exist", () => {
131
+ if (fs.existsSync(mockStartupFile)) {
132
+ fs.unlinkSync(mockStartupFile);
133
+ }
134
+
135
+ const result = windowsPowershell.teardown(knownAikidoTools);
136
+ assert.strictEqual(result, true);
137
+ });
138
+
139
+ it("should handle file with no relevant content", () => {
140
+ const initialContent = [
141
+ "# Windows PowerShell profile",
142
+ "Set-Alias ls Get-ChildItem",
143
+ "$env:PATH += ';C:\\Tools'",
144
+ ].join("\n");
145
+
146
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
147
+
148
+ const result = windowsPowershell.teardown(knownAikidoTools);
149
+ assert.strictEqual(result, true);
150
+
151
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
152
+ assert.ok(content.includes("Set-Alias ls "));
153
+ assert.ok(content.includes("$env:PATH "));
154
+ });
155
+ });
156
+
157
+ describe("shell properties", () => {
158
+ it("should have correct name", () => {
159
+ assert.strictEqual(windowsPowershell.name, "Windows PowerShell");
160
+ });
161
+
162
+ it("should expose all required methods", () => {
163
+ assert.ok(typeof windowsPowershell.isInstalled === "function");
164
+ assert.ok(typeof windowsPowershell.setup === "function");
165
+ assert.ok(typeof windowsPowershell.teardown === "function");
166
+ assert.ok(typeof windowsPowershell.name === "string");
167
+ });
168
+ });
169
+
170
+ describe("integration tests", () => {
171
+ it("should handle complete setup and teardown cycle", () => {
172
+ // Setup
173
+ windowsPowershell.setup();
174
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
175
+ assert.ok(
176
+ content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
177
+ );
178
+
179
+ // Teardown
180
+ windowsPowershell.teardown(knownAikidoTools);
181
+ content = fs.readFileSync(mockStartupFile, "utf-8");
182
+ assert.ok(
183
+ !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
184
+ );
185
+ });
186
+
187
+ it("should handle multiple setup calls", () => {
188
+ windowsPowershell.setup();
189
+ windowsPowershell.teardown(knownAikidoTools);
190
+ windowsPowershell.setup();
191
+
192
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
193
+ const sourceMatches = (
194
+ content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) ||
195
+ []
196
+ ).length;
197
+ assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,62 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { execSync } from "child_process";
7
+
8
+ const shellName = "Zsh";
9
+ const executableName = "zsh";
10
+ const startupFileCommand = "echo ${ZDOTDIR:-$HOME}/.zshrc";
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(startupFile, new RegExp(`^alias\\s+${tool}=`));
22
+ }
23
+
24
+ // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-posix.sh)
25
+ removeLinesMatchingPattern(
26
+ startupFile,
27
+ /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/
28
+ );
29
+
30
+ return true;
31
+ }
32
+
33
+ function setup() {
34
+ const startupFile = getStartupFile();
35
+
36
+ addLineToFile(
37
+ startupFile,
38
+ `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script`
39
+ );
40
+
41
+ return true;
42
+ }
43
+
44
+ function getStartupFile() {
45
+ try {
46
+ return execSync(startupFileCommand, {
47
+ encoding: "utf8",
48
+ shell: executableName,
49
+ }).trim();
50
+ } catch (error) {
51
+ throw new Error(
52
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
53
+ );
54
+ }
55
+ }
56
+
57
+ export default {
58
+ name: shellName,
59
+ isInstalled,
60
+ setup,
61
+ teardown,
62
+ };