@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,226 @@
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("Zsh shell integration", () => {
9
+ let mockStartupFile;
10
+ let zsh;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(tmpdir(), `test-zshrc-${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 zsh module after mocking
44
+ zsh = (await import("./zsh.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 zsh is installed", () => {
59
+ assert.strictEqual(zsh.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(zsh.isInstalled(), true);
65
+ });
66
+ });
67
+
68
+ describe("setup", () => {
69
+ it("should add source line for zsh initialization script", () => {
70
+ const result = zsh.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 Zsh initialization script"
77
+ )
78
+ );
79
+ });
80
+
81
+ it("should handle empty startup file", () => {
82
+ const result = zsh.setup();
83
+ assert.strictEqual(result, true);
84
+
85
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
86
+ assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
87
+ });
88
+ });
89
+
90
+ describe("teardown", () => {
91
+ it("should remove npm, npx, and yarn aliases", () => {
92
+ const initialContent = [
93
+ "#!/bin/zsh",
94
+ "alias npm='aikido-npm'",
95
+ "alias npx='aikido-npx'",
96
+ "alias yarn='aikido-yarn'",
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 = zsh.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("alias ls="));
111
+ assert.ok(content.includes("alias grep="));
112
+ });
113
+
114
+ it("should remove zsh initialization script source line", () => {
115
+ const initialContent = [
116
+ "#!/bin/zsh",
117
+ "source ~/.safe-chain/scripts/init-posix.sh",
118
+ "alias ls='ls --color=auto'",
119
+ ].join("\n");
120
+
121
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
122
+
123
+ const result = zsh.teardown(knownAikidoTools);
124
+ assert.strictEqual(result, true);
125
+
126
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
127
+ assert.ok(
128
+ !content.includes("source ~/.safe-chain/scripts/init-posix.sh")
129
+ );
130
+ assert.ok(content.includes("alias ls="));
131
+ });
132
+
133
+ it("should handle file that doesn't exist", () => {
134
+ if (fs.existsSync(mockStartupFile)) {
135
+ fs.unlinkSync(mockStartupFile);
136
+ }
137
+
138
+ const result = zsh.teardown(knownAikidoTools);
139
+ assert.strictEqual(result, true);
140
+ });
141
+
142
+ it("should handle file with no relevant aliases or source lines", () => {
143
+ const initialContent = [
144
+ "#!/bin/zsh",
145
+ "alias ls='ls --color=auto'",
146
+ "export PATH=$PATH:~/bin",
147
+ ].join("\n");
148
+
149
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
150
+
151
+ const result = zsh.teardown(knownAikidoTools);
152
+ assert.strictEqual(result, true);
153
+
154
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
155
+ assert.ok(content.includes("alias ls="));
156
+ assert.ok(content.includes("export PATH="));
157
+ });
158
+ });
159
+
160
+ describe("shell properties", () => {
161
+ it("should have correct name", () => {
162
+ assert.strictEqual(zsh.name, "Zsh");
163
+ });
164
+
165
+ it("should expose all required methods", () => {
166
+ assert.ok(typeof zsh.isInstalled === "function");
167
+ assert.ok(typeof zsh.setup === "function");
168
+ assert.ok(typeof zsh.teardown === "function");
169
+ assert.ok(typeof zsh.name === "string");
170
+ });
171
+ });
172
+
173
+ describe("integration tests", () => {
174
+ it("should handle complete setup and teardown cycle", () => {
175
+ const tools = [
176
+ { tool: "npm", aikidoCommand: "aikido-npm" },
177
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
178
+ ];
179
+
180
+ // Setup
181
+ zsh.setup();
182
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
183
+ assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
184
+
185
+ // Teardown
186
+ zsh.teardown(tools);
187
+ content = fs.readFileSync(mockStartupFile, "utf-8");
188
+ assert.ok(
189
+ !content.includes("source ~/.safe-chain/scripts/init-posix.sh")
190
+ );
191
+ });
192
+
193
+ it("should handle multiple setup calls", () => {
194
+ const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
195
+
196
+ zsh.setup(tools);
197
+ zsh.teardown(tools);
198
+ zsh.setup(tools);
199
+
200
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
201
+ const sourceMatches = (content.match(/source.*init-posix\.sh/g) || [])
202
+ .length;
203
+ assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
204
+ });
205
+
206
+ it("should handle mixed content with aliases and source lines", () => {
207
+ const initialContent = [
208
+ "#!/bin/zsh",
209
+ "alias npm='old-npm'",
210
+ "source ~/.safe-chain/scripts/init-posix.sh",
211
+ "alias ls='ls --color=auto'",
212
+ ].join("\n");
213
+
214
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
215
+
216
+ // Teardown should remove both aliases and source line
217
+ zsh.teardown(knownAikidoTools);
218
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
219
+ assert.ok(!content.includes("alias npm="));
220
+ assert.ok(
221
+ !content.includes("source ~/.safe-chain/scripts/init-posix.sh")
222
+ );
223
+ assert.ok(content.includes("alias ls="));
224
+ });
225
+ });
226
+ });
@@ -1,9 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
- import { getAliases } from "./helpers.js";
5
- import fs from "fs";
6
- import { EOL } from "os";
4
+ import { knownAikidoTools } from "./helpers.js";
7
5
 
8
6
  export async function teardown() {
9
7
  ui.writeInformation(
@@ -27,8 +25,26 @@ export async function teardown() {
27
25
 
28
26
  let updatedCount = 0;
29
27
  for (const shell of shells) {
30
- if (removeAliasesForShell(shell)) {
28
+ let success = false;
29
+ try {
30
+ success = shell.teardown(knownAikidoTools);
31
+ } catch {
32
+ success = false;
33
+ }
34
+
35
+ if (success) {
36
+ ui.writeInformation(
37
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.green(
38
+ "Teardown successful"
39
+ )}`
40
+ );
31
41
  updatedCount++;
42
+ } else {
43
+ ui.writeError(
44
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.red(
45
+ "Teardown failed"
46
+ )}. Please check your ${shell.name} configuration.`
47
+ );
32
48
  }
33
49
  }
34
50
 
@@ -43,98 +59,3 @@ export async function teardown() {
43
59
  return;
44
60
  }
45
61
  }
46
-
47
- /**
48
- * This function removes aliases for the given shell.
49
- * It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.),
50
- * and then removes the aliases for npm, npx, and yarn commands.
51
- * If the aliases don't exist, it will report that they were not found.
52
- * If the startup file does not exist, it will report that no aliases need to be removed.
53
- *
54
- * The shell startup script is loaded by the respective shell when it starts.
55
- * This means that the aliases will be removed from the shell after it is restarted.
56
- */
57
- function removeAliasesForShell(shell) {
58
- if (!shell.startupFile) {
59
- ui.writeError(
60
- `- ${chalk.bold(
61
- shell.name
62
- )}: no startup file found. Cannot remove aliases.`
63
- );
64
- return false;
65
- }
66
-
67
- if (!fs.existsSync(shell.startupFile)) {
68
- ui.writeInformation(
69
- `- ${chalk.bold(
70
- shell.name
71
- )}: startup file does not exist. No aliases to remove.`
72
- );
73
- return false;
74
- }
75
-
76
- const aliases = getAliases(shell.startupFile);
77
- const fileContent = fs.readFileSync(shell.startupFile, "utf-8");
78
- const { removedCount, notFoundCount } = removeAliasesFromFile(
79
- aliases,
80
- fileContent,
81
- shell.startupFile
82
- );
83
-
84
- let summary = "- " + chalk.bold(shell.name) + ": ";
85
-
86
- if (removedCount > 0) {
87
- summary += chalk.green(`${removedCount} aliases were removed`);
88
- }
89
- if (notFoundCount > 0) {
90
- if (removedCount > 0) {
91
- summary += ", ";
92
- }
93
- summary += chalk.yellow(`${notFoundCount} aliases were not found`);
94
- }
95
- if (removedCount === 0 && notFoundCount === 0) {
96
- summary += chalk.yellow("no aliases found to remove");
97
- }
98
-
99
- ui.writeInformation(summary);
100
- return removedCount > 0;
101
- }
102
-
103
- /**
104
- * This function removes the aliases from the startup file.
105
- * It searches for exact matches of each alias line and removes them.
106
- * eg: for bash it will remove 'alias npm="aikido-npm"' for npm from ~/.bashrc
107
- * @returns an object with the counts of removed and not found aliases.
108
- */
109
- export function removeAliasesFromFile(aliases, fileContent, startupFilePath) {
110
- let removedCount = 0;
111
- let notFoundCount = 0;
112
- let updatedContent = fileContent;
113
-
114
- for (const alias of aliases) {
115
- const lines = updatedContent.split(EOL);
116
- let aliasLineIndex = lines.findIndex((line) => line.trim() === alias);
117
-
118
- if (aliasLineIndex !== -1) {
119
- removedCount++;
120
-
121
- // Remove all occurrences of the alias line, in case it appears multiple times
122
- while (aliasLineIndex !== -1) {
123
- lines.splice(aliasLineIndex, 1);
124
- aliasLineIndex = lines.findIndex((line) => line.trim() === alias);
125
- }
126
- updatedContent = lines.join(EOL);
127
- } else {
128
- notFoundCount++;
129
- }
130
- }
131
-
132
- if (removedCount > 0) {
133
- fs.writeFileSync(startupFilePath, updatedContent, "utf-8");
134
- }
135
-
136
- return {
137
- removedCount,
138
- notFoundCount,
139
- };
140
- }
@@ -1,41 +0,0 @@
1
- name: Create Release
2
-
3
- on:
4
- push:
5
- tags:
6
- - "*"
7
-
8
- jobs:
9
- build:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v3
15
-
16
- - name: Set up Node.js
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: "lts/*"
20
- registry-url: "https://registry.npmjs.org/"
21
- env:
22
- NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
23
-
24
- - name: Set version number
25
- id: get_version
26
- run: |
27
- version="${{ github.ref_name }}"
28
- echo "tag=$version" >> $GITHUB_OUTPUT
29
-
30
- - name: Set the version
31
- run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }}
32
-
33
- - name: Install dependencies
34
- run: npm ci
35
-
36
- - name: Publish to npm
37
- run: |
38
- echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM"
39
- npm publish --access public
40
- env:
41
- NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
@@ -1,28 +0,0 @@
1
- name: Run Unit Tests
2
-
3
- on:
4
- pull_request:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- test:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v3
15
-
16
- - name: Set up Node.js
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: "lts/*"
20
-
21
- - name: Install dependencies
22
- run: npm ci
23
-
24
- - name: Run tests
25
- run: npm test
26
-
27
- - name: Run ESLint
28
- run: npm run lint