@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,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("Bash shell integration", () => {
9
+ let mockStartupFile;
10
+ let bash;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(tmpdir(), `test-bashrc-${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 bash module after mocking
44
+ bash = (await import("./bash.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 bash is installed", () => {
59
+ assert.strictEqual(bash.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(bash.isInstalled(), true);
65
+ });
66
+ });
67
+
68
+ describe("setup", () => {
69
+ it("should add source line for bash initialization script", () => {
70
+ const result = bash.setup();
71
+ assert.strictEqual(result, true);
72
+
73
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
74
+ assert.ok(
75
+ content.includes(
76
+ "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
77
+ )
78
+ );
79
+ });
80
+ });
81
+
82
+ describe("teardown", () => {
83
+ it("should remove npm, npx, and yarn aliases", () => {
84
+ const initialContent = [
85
+ "#!/bin/bash",
86
+ "alias npm='aikido-npm'",
87
+ "alias npx='aikido-npx'",
88
+ "alias yarn='aikido-yarn'",
89
+ "alias ls='ls --color=auto'",
90
+ "alias grep='grep --color=auto'",
91
+ ].join("\n");
92
+
93
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
94
+
95
+ const result = bash.teardown(knownAikidoTools);
96
+ assert.strictEqual(result, true);
97
+
98
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
99
+ assert.ok(!content.includes("alias npm="));
100
+ assert.ok(!content.includes("alias npx="));
101
+ assert.ok(!content.includes("alias yarn="));
102
+ assert.ok(content.includes("alias ls="));
103
+ assert.ok(content.includes("alias grep="));
104
+ });
105
+
106
+ it("should handle file that doesn't exist", () => {
107
+ if (fs.existsSync(mockStartupFile)) {
108
+ fs.unlinkSync(mockStartupFile);
109
+ }
110
+
111
+ const result = bash.teardown(knownAikidoTools);
112
+ assert.strictEqual(result, true);
113
+ });
114
+
115
+ it("should handle file with no relevant aliases", () => {
116
+ const initialContent = [
117
+ "#!/bin/bash",
118
+ "alias ls='ls --color=auto'",
119
+ "export PATH=$PATH:~/bin",
120
+ ].join("\n");
121
+
122
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
123
+
124
+ const result = bash.teardown(knownAikidoTools);
125
+ assert.strictEqual(result, true);
126
+
127
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
128
+ assert.ok(content.includes("alias ls="));
129
+ assert.ok(content.includes("export PATH="));
130
+ });
131
+ });
132
+
133
+ describe("shell properties", () => {
134
+ it("should have correct name", () => {
135
+ assert.strictEqual(bash.name, "Bash");
136
+ });
137
+
138
+ it("should expose all required methods", () => {
139
+ assert.ok(typeof bash.isInstalled === "function");
140
+ assert.ok(typeof bash.setup === "function");
141
+ assert.ok(typeof bash.teardown === "function");
142
+ assert.ok(typeof bash.name === "string");
143
+ });
144
+ });
145
+
146
+ describe("integration tests", () => {
147
+ it("should handle complete setup and teardown cycle", () => {
148
+ const tools = [
149
+ { tool: "npm", aikidoCommand: "aikido-npm" },
150
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
151
+ ];
152
+
153
+ // Setup
154
+ bash.setup(tools);
155
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
156
+ assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
157
+
158
+ // Teardown
159
+ bash.teardown(tools);
160
+ content = fs.readFileSync(mockStartupFile, "utf-8");
161
+ assert.ok(
162
+ !content.includes("source ~/.safe-chain/scripts/init-posix.sh")
163
+ );
164
+ });
165
+
166
+ it("should handle multiple setup calls", () => {
167
+ const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
168
+
169
+ bash.setup(tools);
170
+ bash.teardown(tools);
171
+ bash.setup(tools);
172
+
173
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
174
+ const sourceMatches = (content.match(/source.*init-posix\.sh/g) || [])
175
+ .length;
176
+ assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
177
+ });
178
+
179
+ it("should handle mixed content with aliases and source lines", () => {
180
+ const initialContent = [
181
+ "#!/bin/bash",
182
+ "alias npm='old-npm'",
183
+ "source ~/.safe-chain/scripts/init-posix.sh",
184
+ "alias ls='ls --color=auto'",
185
+ ].join("\n");
186
+
187
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
188
+
189
+ // Teardown should remove both aliases and source line
190
+ bash.teardown(knownAikidoTools);
191
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
192
+ assert.ok(!content.includes("alias npm="));
193
+ assert.ok(
194
+ !content.includes("source ~/.safe-chain/scripts/init-posix.sh")
195
+ );
196
+ assert.ok(content.includes("alias ls="));
197
+ });
198
+ });
199
+ });
@@ -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 = "Fish";
9
+ const executableName = "fish";
10
+ const startupFileCommand = "echo ~/.config/fish/config.fish";
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(`^alias\\s+${tool}\\s+`)
24
+ );
25
+ }
26
+
27
+ // Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish)
28
+ removeLinesMatchingPattern(
29
+ startupFile,
30
+ /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/
31
+ );
32
+
33
+ return true;
34
+ }
35
+
36
+ function setup() {
37
+ const startupFile = getStartupFile();
38
+
39
+ addLineToFile(
40
+ startupFile,
41
+ `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish 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,183 @@
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 source line for safe-chain fish initialization script", () => {
70
+ const result = fish.setup();
71
+ assert.strictEqual(result, true);
72
+
73
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
74
+ assert.ok(
75
+ content.includes('source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script')
76
+ );
77
+ });
78
+
79
+ it("should not duplicate source lines on multiple calls", () => {
80
+ fish.setup();
81
+ fish.setup();
82
+
83
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
84
+ const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length;
85
+ assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)");
86
+ });
87
+ });
88
+
89
+ describe("teardown", () => {
90
+ it("should remove npm, npx, yarn aliases and source line", () => {
91
+ const initialContent = [
92
+ "#!/usr/bin/env fish",
93
+ "alias npm 'aikido-npm'",
94
+ "alias npx 'aikido-npx'",
95
+ "alias yarn 'aikido-yarn'",
96
+ "source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script",
97
+ "alias ls 'ls --color=auto'",
98
+ "alias grep 'grep --color=auto'",
99
+ ].join("\n");
100
+
101
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
102
+
103
+ const result = fish.teardown(knownAikidoTools);
104
+ assert.strictEqual(result, true);
105
+
106
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
107
+ assert.ok(!content.includes("alias npm "));
108
+ assert.ok(!content.includes("alias npx "));
109
+ assert.ok(!content.includes("alias yarn "));
110
+ assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish"));
111
+ assert.ok(content.includes("alias ls "));
112
+ assert.ok(content.includes("alias grep "));
113
+ });
114
+
115
+ it("should handle file that doesn't exist", () => {
116
+ if (fs.existsSync(mockStartupFile)) {
117
+ fs.unlinkSync(mockStartupFile);
118
+ }
119
+
120
+ const result = fish.teardown(knownAikidoTools);
121
+ assert.strictEqual(result, true);
122
+ });
123
+
124
+ it("should handle file with no relevant aliases or source lines", () => {
125
+ const initialContent = [
126
+ "#!/usr/bin/env fish",
127
+ "alias ls 'ls --color=auto'",
128
+ "set PATH $PATH ~/bin",
129
+ ].join("\n");
130
+
131
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
132
+
133
+ const result = fish.teardown(knownAikidoTools);
134
+ assert.strictEqual(result, true);
135
+
136
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
137
+ assert.ok(content.includes("alias ls "));
138
+ assert.ok(content.includes("set PATH "));
139
+ });
140
+ });
141
+
142
+ describe("shell properties", () => {
143
+ it("should have correct name", () => {
144
+ assert.strictEqual(fish.name, "Fish");
145
+ });
146
+
147
+ it("should expose all required methods", () => {
148
+ assert.ok(typeof fish.isInstalled === "function");
149
+ assert.ok(typeof fish.setup === "function");
150
+ assert.ok(typeof fish.teardown === "function");
151
+ assert.ok(typeof fish.name === "string");
152
+ });
153
+ });
154
+
155
+ describe("integration tests", () => {
156
+ it("should handle complete setup and teardown cycle", () => {
157
+ const tools = [
158
+ { tool: "npm", aikidoCommand: "aikido-npm" },
159
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
160
+ ];
161
+
162
+ // Setup
163
+ fish.setup();
164
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
165
+ assert.ok(content.includes('source ~/.safe-chain/scripts/init-fish.fish'));
166
+
167
+ // Teardown
168
+ fish.teardown(tools);
169
+ content = fs.readFileSync(mockStartupFile, "utf-8");
170
+ assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish"));
171
+ });
172
+
173
+ it("should handle multiple setup calls", () => {
174
+ fish.setup();
175
+ fish.teardown(knownAikidoTools);
176
+ fish.setup();
177
+
178
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
179
+ const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length;
180
+ assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle");
181
+ });
182
+ });
183
+ });
@@ -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 = "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
+ // 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
+ };