@aikidosec/safe-chain 1.0.10 → 1.0.12

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/README.md CHANGED
@@ -6,50 +6,52 @@ The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [n
6
6
 
7
7
  ## Installation
8
8
 
9
- To install the Aikido Safe Chain, you can use the following command:
9
+ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
10
10
 
11
- ```shell
12
- npm i -g @aikidosec/safe-chain
13
- ```
11
+ 1. **Install the Aikido Safe Chain package globally** using npm:
12
+ ```shell
13
+ npm install -g @aikidosec/safe-chain
14
+ ```
15
+ 2. **Setup the shell integration** by running:
16
+ ```shell
17
+ safe-chain setup
18
+ ```
19
+ 3. **Restart your terminal** to start using the Aikido Safe Chain.
14
20
 
15
- Now you should be able to use the `aikido-npm`, `aikido-npx`, or `aikido-yarn` command instead of `npm`, `npx`, or `yarn`. Example: `aikido-npm install axios`, `aikido-yarn add lodash`.
21
+ When running `npm`, `npx`, or `yarn` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
16
22
 
17
23
  ## Aliases in shell
18
24
 
19
- It is possible to create aliases in your shell startup script to make it easier to use the Aikido Safe Chain. This is useful if you want to use the Aikido Safe Chain as a drop-in replacement for **npm**, **npx**, or **yarn**. The aikido-npm, aikido-npx, and aikido-yarn commands will scan for malware and prompt you to exit if any is found. If not, they will run the original **npm**, **npx**, or **yarn** command.
25
+ The Aikido Safe Chain will setup aliases in your shell for the commands **npm**, **npx**, and **yarn**. This means that when you run these commands, they will be replaced with the Aikido Safe Chain commands **aikido-npm**, **aikido-npx**, and **aikido-yarn** respectively. The Aikido Safe Chain will do a pre-install check for malware before running the original command.
20
26
 
21
- ### Creating an alias
27
+ ### Creating shell aliases
22
28
 
23
- The `add-aikido-aliases` command will add the aliases for **npm**, **npx**, and **yarn** to your shell startup script.
29
+ The `setup` command will detect which shells are installed and add the aliases for **npm**, **npx**, and **yarn** to your shell startup scripts.
24
30
 
25
- To add aliases to your shell startup script, you can use the built-in command `aikido-npm add-aikido-aliases`:
31
+ Supported shells include:
26
32
 
27
- ```shell
28
- # Example for bash
29
- aikido-npm add-aikido-aliases ~/.bashrc
33
+ - ✅ **Bash**: `~/.bashrc`
34
+ - **Zsh**: `~/.zshrc`
35
+ - **Fish**: `~/.config/fish/config.fish`
36
+ - ✅ **Powershell**: `$PROFILE`
37
+ - ✅ **PowerShell Core**: `$PROFILE`
30
38
 
31
- # Example for zsh
32
- aikido-npm add-aikido-aliases ~/.zshrc
33
-
34
- # Example for powershell
35
- aikido-npm add-aikido-aliases $PROFILE
36
- ```
37
-
38
- This will create the aliases. The following table shows the aliases that will be created in the shell startup script:
39
+ After adding the alias, **the shell needs to restart in order to load the alias**.
39
40
 
40
- | Shell | Startup script | Npm Alias | Npx Alias | Yarn Alias |
41
- | -------------- | -------------------------- | --------------------------------------- | --------------------------------------- | ----------------------------------------- |
42
- | **Bash** | ~/.bashrc | `alias npm='aikido-npm'` | `alias npx='aikido-npx'` | `alias yarn='aikido-yarn'` |
43
- | **Zsh** | ~/.zshrc | `alias npm='aikido-npm'` | `alias npx='aikido-npx'` | `alias yarn='aikido-yarn'` |
44
- | **Ash** | ~/.profile, ~/.ashrc | `alias npm='aikido-npm'` | `alias npx='aikido-npx'` | `alias yarn='aikido-yarn'` |
45
- | **Fish** | ~/.config/fish/config.fish | `alias npm "aikido-npm"` | `alias npx "aikido-npx"` | `alias yarn "aikido-yarn"` |
46
- | **Powershell** | $PROFILE | `Set-Alias -Name npm -Value aikido-npm` | `Set-Alias -Name npx -Value aikido-npx` | `Set-Alias -Name yarn -Value aikido-yarn` |
41
+ ### Removing shell aliases
47
42
 
48
- After adding the alias, **the shell needs to restart in order to load the alias**.
43
+ The `teardown` command will remove the aliases for **npm**, **npx**, and **yarn** from your shell startup scripts. This is useful if you want to stop using the Aikido Safe Chain or if you want to switch to a different package manager.
49
44
 
50
- ### Removing the alias
45
+ ## Uninstallation
51
46
 
52
- To remove the added aliases, you can use the built-in commands of `aikido-npm`:
47
+ To uninstall the Aikido Safe Chain, you can run the following command:
53
48
 
54
- - `aikido-npm remove-aikido-aliases file_name` (eg `~/.bashrc`, `~/.zshrc`, etc.)
55
- This will remove the aliases if they are present in the file.
49
+ 1. **Remove all aliases from your shell** by running:
50
+ ```shell
51
+ safe-chain teardown
52
+ ```
53
+ 2. **Uninstall the Aikido Safe Chain package** using npm:
54
+ ```shell
55
+ npm uninstall -g @aikidosec/safe-chain
56
+ ```
57
+ 3. **Restart your terminal** to remove the aliases.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from "chalk";
4
+ import { ui } from "../src/environment/userInteraction.js";
5
+ import { setup } from "../src/shell-integration/setup.js";
6
+ import { teardown } from "../src/shell-integration/teardown.js";
7
+
8
+ if (process.argv.length < 3) {
9
+ ui.writeError("No command provided. Please provide a command to execute.");
10
+ ui.emptyLine();
11
+ writeHelp();
12
+ process.exit(1);
13
+ }
14
+
15
+ const command = process.argv[2];
16
+
17
+ if (command === "help" || command === "--help" || command === "-h") {
18
+ writeHelp();
19
+ process.exit(0);
20
+ }
21
+
22
+ if (command === "setup") {
23
+ setup();
24
+ } else if (command === "teardown") {
25
+ teardown();
26
+ } else {
27
+ ui.writeError(`Unknown command: ${command}.`);
28
+ ui.emptyLine();
29
+
30
+ writeHelp();
31
+
32
+ process.exit(1);
33
+ }
34
+
35
+ function writeHelp() {
36
+ ui.writeInformation(
37
+ chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
38
+ );
39
+ ui.emptyLine();
40
+ ui.writeInformation(
41
+ `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
42
+ "teardown"
43
+ )}, ${chalk.cyan("help")}`
44
+ );
45
+ ui.emptyLine();
46
+ ui.writeInformation(
47
+ `- ${chalk.cyan(
48
+ "safe-chain setup"
49
+ )}: This will setup your shell to wrap safe-chain around npm, npx and yarn.`
50
+ );
51
+ ui.writeInformation(
52
+ `- ${chalk.cyan(
53
+ "safe-chain teardown"
54
+ )}: This will remove safe-chain aliases from your shell configuration.`
55
+ );
56
+ ui.emptyLine();
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks **/*.spec.js",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js",
@@ -13,7 +13,8 @@
13
13
  "bin": {
14
14
  "aikido-npm": "bin/aikido-npm.js",
15
15
  "aikido-npx": "bin/aikido-npx.js",
16
- "aikido-yarn": "bin/aikido-yarn.js"
16
+ "aikido-yarn": "bin/aikido-yarn.js",
17
+ "safe-chain": "bin/safe-chain.js"
17
18
  },
18
19
  "type": "module",
19
20
  "keywords": [],
package/src/main.js CHANGED
@@ -1,24 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { scanCommand, shouldScanCommand } from "./scanning/index.js";
4
- import { isAddAliasCommand, addAlias } from "./shell-integration/addAlias.js";
5
- import {
6
- removeAlias,
7
- isRemoveAliasCommand,
8
- } from "./shell-integration/removeAlias.js";
9
4
  import { ui } from "./environment/userInteraction.js";
10
5
  import { getPackageManager } from "./packagemanager/currentPackageManager.js";
11
6
 
12
7
  export async function main(args) {
13
8
  try {
14
- if (isAddAliasCommand(args)) {
15
- addAlias(args);
16
- return;
17
- }
18
- if (isRemoveAliasCommand(args)) {
19
- removeAlias(args);
20
- return;
21
- }
22
9
  if (shouldScanCommand(args)) {
23
10
  await scanCommand(args);
24
11
  }
@@ -86,9 +86,7 @@ async function acceptRiskOrExit(message, defaultValue) {
86
86
  return;
87
87
  }
88
88
 
89
- ui.writeInformation(
90
- "Exiting without installing packages. Please check the output."
91
- );
89
+ ui.writeInformation("Exiting without installing packages.");
92
90
  ui.emptyLine();
93
91
  process.exit(1);
94
92
  }
@@ -0,0 +1,151 @@
1
+ import chalk from "chalk";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { detectShells } from "./shellDetection.js";
4
+ import { getAliases } from "./helpers.js";
5
+ import fs from "fs";
6
+ import { EOL } from "os";
7
+
8
+ export async function setup() {
9
+ ui.writeInformation(
10
+ chalk.bold("Setting up shell aliases.") +
11
+ " This will wrap safe-chain around npm, npx, and yarn commands."
12
+ );
13
+ ui.emptyLine();
14
+
15
+ try {
16
+ const shells = detectShells();
17
+ if (shells.length === 0) {
18
+ ui.writeError("No supported shells detected. Cannot set up aliases.");
19
+ return;
20
+ }
21
+
22
+ ui.writeInformation(
23
+ `Detected ${shells.length} supported shell(s): ${shells
24
+ .map((shell) => chalk.bold(shell.name))
25
+ .join(", ")}.`
26
+ );
27
+
28
+ let updatedCount = 0;
29
+ for (const shell of shells) {
30
+ if (setupAliasesForShell(shell)) {
31
+ updatedCount++;
32
+ }
33
+ }
34
+
35
+ if (updatedCount > 0) {
36
+ ui.emptyLine();
37
+ ui.writeInformation(`Please restart your terminal to apply the changes.`);
38
+ }
39
+ } catch (error) {
40
+ ui.writeError(
41
+ `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.`
42
+ );
43
+ return;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * This function sets up aliases for the given shell.
49
+ * It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.),
50
+ * and then appends the aliases for npm, npx, and yarn commands.
51
+ * If the aliases already exist, it will not add them again.
52
+ * If the startup file does not exist, it will create it.
53
+ *
54
+ * The shell startup script is loaded by the respective shell when it starts.
55
+ * This means that the aliases will be available in the shell after it is restarted.
56
+ */
57
+ function setupAliasesForShell(shell) {
58
+ if (!shell.startupFile) {
59
+ ui.writeError(
60
+ `- ${chalk.bold(
61
+ shell.name
62
+ )}: no startup file found. Cannot set up aliases.`
63
+ );
64
+ return false;
65
+ }
66
+
67
+ const aliases = getAliases(shell.startupFile);
68
+
69
+ if (aliases.length === 0) {
70
+ ui.writeError(`- ${chalk.bold(shell.name)}: could not generate aliases.`);
71
+ return false;
72
+ }
73
+
74
+ const fileContent = readOrCreateStartupFile(shell.startupFile);
75
+ const { addedCount, existingCount, failedCount } = appendAliasesToFile(
76
+ aliases,
77
+ fileContent,
78
+ shell.startupFile
79
+ );
80
+
81
+ let summary = "- " + chalk.bold(shell.name) + ": ";
82
+
83
+ if (addedCount > 0) {
84
+ summary += chalk.green(`${addedCount} aliases were added`);
85
+ }
86
+ if (existingCount > 0) {
87
+ if (addedCount > 0) {
88
+ summary += ", ";
89
+ }
90
+ summary += chalk.yellow(`${existingCount} aliases were already present`);
91
+ }
92
+ if (failedCount > 0) {
93
+ if (addedCount > 0 || existingCount > 0) {
94
+ summary += ", ";
95
+ }
96
+ summary += chalk.red(`${failedCount} aliases failed to add`);
97
+ }
98
+
99
+ // write summary in a single line
100
+ ui.writeInformation(summary);
101
+
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * This reads the content of the startup file.
107
+ * If the file does not exist, it creates an empty file and returns an empty string.
108
+ * The startup file is the shell's startup script (eg: ~/.bashrc, ~/.zshrc, etc.).
109
+ * It is used to set up the shell environment when it starts.
110
+ * Some shells may not have a startup file, in which case this function will create one.
111
+ */
112
+ export function readOrCreateStartupFile(filePath) {
113
+ if (!fs.existsSync(filePath)) {
114
+ fs.writeFileSync(filePath, "", "utf-8");
115
+ ui.writeInformation(`File ${filePath} created.`);
116
+ }
117
+ return fs.readFileSync(filePath, "utf-8");
118
+ }
119
+
120
+ /**
121
+ * This function appends the aliases to the startup file.
122
+ * eg: for bash it will append 'alias npm="aikido-npm"' for npm to ~/.bashrc
123
+ * @returns an object with the counts of added, existing, and failed aliases.
124
+ */
125
+ export function appendAliasesToFile(aliases, fileContent, startupFilePath) {
126
+ let addedCount = 0;
127
+ let existingCount = 0;
128
+ let failedCount = 0;
129
+
130
+ for (const alias of aliases) {
131
+ try {
132
+ if (fileContent.includes(alias)) {
133
+ existingCount++;
134
+ continue;
135
+ }
136
+
137
+ fs.appendFileSync(startupFilePath, `${EOL}${alias}`, "utf-8");
138
+
139
+ addedCount++;
140
+ } catch {
141
+ failedCount++;
142
+ continue;
143
+ }
144
+ }
145
+
146
+ return {
147
+ addedCount,
148
+ existingCount,
149
+ failedCount,
150
+ };
151
+ }
@@ -0,0 +1,304 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { EOL, tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import { getAliases } from "./helpers.js";
6
+ import { readOrCreateStartupFile, appendAliasesToFile } from "./setup.js";
7
+
8
+ describe("setupShell", () => {
9
+ function runSetupTestsForEnvironment(shell, startupExtension, expectedAliases) {
10
+ describe(`${shell} shell setup`, () => {
11
+ it(`should add aliases to ${shell} file`, () => {
12
+ const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
13
+ const filePath = createShellStartupScript(lines, startupExtension);
14
+
15
+ const aliases = getAliases(filePath);
16
+ const fileContent = fs.readFileSync(filePath, "utf-8");
17
+
18
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
19
+
20
+ assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
21
+ assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
22
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
23
+
24
+ const updatedContent = readAndDeleteFile(filePath);
25
+ for (const alias of expectedAliases) {
26
+ assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`);
27
+ }
28
+ assert.ok(updatedContent.includes("alias cls='clear'"), "Original aliases should remain");
29
+ });
30
+
31
+ it(`should not add aliases if they already exist in ${shell} file`, () => {
32
+ const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases];
33
+ const filePath = createShellStartupScript(lines, startupExtension);
34
+
35
+ const aliases = getAliases(filePath);
36
+ const fileContent = fs.readFileSync(filePath, "utf-8");
37
+
38
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
39
+
40
+ assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
41
+ assert.strictEqual(result.existingCount, 3, "Should find 3 existing aliases");
42
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
43
+
44
+ const updatedContent = readAndDeleteFile(filePath);
45
+ // Count occurrences to ensure no duplicates were added
46
+ for (const alias of expectedAliases) {
47
+ assert.strictEqual(countOccurrences(updatedContent, alias), 1, `Alias "${alias}" should appear exactly once`);
48
+ }
49
+ });
50
+
51
+ it(`should create file and add aliases if file does not exist for ${shell}`, () => {
52
+ const randomName = Math.random().toString(36).substring(2, 15);
53
+ const filePath = `${tmpdir()}/nonexistent-${randomName}${startupExtension}`;
54
+ if (fs.existsSync(filePath)) {
55
+ fs.rmSync(filePath, { force: true });
56
+ }
57
+
58
+ // Test readOrCreateStartupFile function
59
+ const fileContent = readOrCreateStartupFile(filePath);
60
+ assert.strictEqual(fileContent, "", "Should return empty string for new file");
61
+ assert.ok(fs.existsSync(filePath), "File should be created");
62
+
63
+ // Test adding aliases to the newly created file
64
+ const aliases = getAliases(filePath);
65
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
66
+
67
+ assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
68
+ assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
69
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
70
+
71
+ const updatedContent = readAndDeleteFile(filePath);
72
+ for (const alias of expectedAliases) {
73
+ assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`);
74
+ }
75
+ });
76
+
77
+ it(`should add aliases only once when called multiple times for ${shell}`, () => {
78
+ const lines = [`#!/usr/bin/env ${shell}`, ""];
79
+ const filePath = createShellStartupScript(lines, startupExtension);
80
+
81
+ const aliases = getAliases(filePath);
82
+
83
+ // First call - should add aliases
84
+ let fileContent = fs.readFileSync(filePath, "utf-8");
85
+ const result1 = appendAliasesToFile(aliases, fileContent, filePath);
86
+ assert.strictEqual(result1.addedCount, 3, "First call should add 3 aliases");
87
+
88
+ // Second call - should detect existing aliases
89
+ fileContent = fs.readFileSync(filePath, "utf-8");
90
+ const result2 = appendAliasesToFile(aliases, fileContent, filePath);
91
+ assert.strictEqual(result2.addedCount, 0, "Second call should add 0 aliases");
92
+ assert.strictEqual(result2.existingCount, 3, "Second call should find 3 existing aliases");
93
+
94
+ const updatedContent = readAndDeleteFile(filePath);
95
+ for (const alias of expectedAliases) {
96
+ assert.strictEqual(countOccurrences(updatedContent, alias), 1, `Alias "${alias}" should appear exactly once`);
97
+ }
98
+ });
99
+
100
+ it(`should use real getAliases() for ${shell} file`, () => {
101
+ const filePath = `${tmpdir()}/test${startupExtension}`;
102
+ const aliases = getAliases(filePath);
103
+
104
+ // Verify we get the expected aliases for this shell type
105
+ assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)");
106
+ for (let i = 0; i < aliases.length; i++) {
107
+ assert.strictEqual(aliases[i], expectedAliases[i], `Alias ${i} should match expected format`);
108
+ }
109
+ });
110
+
111
+ it(`should handle mixed scenario - some existing, some new for ${shell}`, () => {
112
+ const lines = [`#!/usr/bin/env ${shell}`, "", expectedAliases[0], "alias other='command'"];
113
+ const filePath = createShellStartupScript(lines, startupExtension);
114
+
115
+ const aliases = getAliases(filePath);
116
+ const fileContent = fs.readFileSync(filePath, "utf-8");
117
+
118
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
119
+
120
+ assert.strictEqual(result.addedCount, 2, "Should add 2 new aliases");
121
+ assert.strictEqual(result.existingCount, 1, "Should find 1 existing alias");
122
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
123
+
124
+ const updatedContent = readAndDeleteFile(filePath);
125
+ for (const alias of expectedAliases) {
126
+ assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be present`);
127
+ }
128
+ assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
129
+ });
130
+ });
131
+ }
132
+
133
+ // Test for each shell type using real getAliases() output
134
+ runSetupTestsForEnvironment("bash", ".bashrc", [
135
+ "alias npm='aikido-npm'",
136
+ "alias npx='aikido-npx'",
137
+ "alias yarn='aikido-yarn'"
138
+ ]);
139
+
140
+ runSetupTestsForEnvironment("zsh", ".zshrc", [
141
+ "alias npm='aikido-npm'",
142
+ "alias npx='aikido-npx'",
143
+ "alias yarn='aikido-yarn'"
144
+ ]);
145
+
146
+ runSetupTestsForEnvironment("fish", ".fish", [
147
+ 'alias npm "aikido-npm"',
148
+ 'alias npx "aikido-npx"',
149
+ 'alias yarn "aikido-yarn"'
150
+ ]);
151
+
152
+ runSetupTestsForEnvironment("pwsh", ".ps1", [
153
+ "Set-Alias npm aikido-npm",
154
+ "Set-Alias npx aikido-npx",
155
+ "Set-Alias yarn aikido-yarn"
156
+ ]);
157
+
158
+ describe("readOrCreateStartupFile", () => {
159
+ it("should read existing file content", () => {
160
+ const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
161
+ const filePath = createShellStartupScript(lines, ".bashrc");
162
+
163
+ const content = readOrCreateStartupFile(filePath);
164
+
165
+ assert.ok(content.includes("#!/usr/bin/env bash"), "Should contain shebang");
166
+ assert.ok(content.includes("alias test='echo test'"), "Should contain existing aliases");
167
+
168
+ // Cleanup
169
+ fs.rmSync(filePath, { force: true });
170
+ });
171
+
172
+ it("should create file if it doesn't exist", () => {
173
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
174
+ if (fs.existsSync(filePath)) {
175
+ fs.rmSync(filePath, { force: true });
176
+ }
177
+
178
+ const content = readOrCreateStartupFile(filePath);
179
+
180
+ assert.strictEqual(content, "", "Should return empty string for new file");
181
+ assert.ok(fs.existsSync(filePath), "File should be created");
182
+
183
+ // Cleanup
184
+ fs.rmSync(filePath, { force: true });
185
+ });
186
+
187
+ it("should handle empty existing file", () => {
188
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
189
+ fs.writeFileSync(filePath, "", "utf-8");
190
+
191
+ const content = readOrCreateStartupFile(filePath);
192
+
193
+ assert.strictEqual(content, "", "Should return empty string for empty file");
194
+ assert.ok(fs.existsSync(filePath), "File should still exist");
195
+
196
+ // Cleanup
197
+ fs.rmSync(filePath, { force: true });
198
+ });
199
+ });
200
+
201
+ describe("appendAliasesToFile edge cases", () => {
202
+ it("should handle empty aliases array", () => {
203
+ const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
204
+ const filePath = createShellStartupScript(lines, ".bashrc");
205
+ const fileContent = fs.readFileSync(filePath, "utf-8");
206
+
207
+ const result = appendAliasesToFile([], fileContent, filePath);
208
+
209
+ assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
210
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
211
+ assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
212
+
213
+ const updatedContent = readAndDeleteFile(filePath);
214
+ assert.ok(updatedContent.includes("alias test='echo test'"), "Original content should remain");
215
+ });
216
+
217
+ it("should handle partial substring matches correctly", () => {
218
+ const lines = [
219
+ "#!/usr/bin/env bash",
220
+ "",
221
+ "alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm='
222
+ "alias test='echo test'"
223
+ ];
224
+ const filePath = createShellStartupScript(lines, ".bashrc");
225
+ const fileContent = fs.readFileSync(filePath, "utf-8");
226
+
227
+ const aliases = ["alias npm='aikido-npm'"];
228
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
229
+
230
+ assert.strictEqual(result.addedCount, 1, "Should add 1 alias (npm)");
231
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
232
+ assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
233
+
234
+ const updatedContent = readAndDeleteFile(filePath);
235
+ assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "npm alias should be added");
236
+ assert.ok(updatedContent.includes("alias npmx='some-other-command'"), "npmx alias should remain");
237
+ });
238
+
239
+ it("should handle file with only whitespace", () => {
240
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
241
+ const fileContent = `${EOL}${EOL} ${EOL}`;
242
+ fs.writeFileSync(filePath, fileContent, "utf-8");
243
+
244
+ const aliases = ["alias npm='aikido-npm'"];
245
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
246
+
247
+ assert.strictEqual(result.addedCount, 1, "Should add 1 alias");
248
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
249
+ assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
250
+
251
+ const updatedContent = fs.readFileSync(filePath, "utf-8");
252
+ assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "Alias should be added");
253
+
254
+ // Cleanup
255
+ fs.rmSync(filePath, { force: true });
256
+ });
257
+ });
258
+
259
+ describe("appendAliasesToFile error handling", () => {
260
+ it("should handle file permission errors gracefully", () => {
261
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
262
+ fs.writeFileSync(filePath, "#!/usr/bin/env bash", "utf-8");
263
+
264
+ // Make file read-only to simulate permission error
265
+ fs.chmodSync(filePath, 0o444);
266
+
267
+ const aliases = ["alias npm='aikido-npm'"];
268
+ const fileContent = fs.readFileSync(filePath, "utf-8");
269
+
270
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
271
+
272
+ assert.strictEqual(result.addedCount, 0, "Should add 0 aliases due to permission error");
273
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
274
+ assert.strictEqual(result.failedCount, 1, "Should have 1 failed alias");
275
+
276
+ // Restore permissions and cleanup
277
+ fs.chmodSync(filePath, 0o644);
278
+ fs.rmSync(filePath, { force: true });
279
+ });
280
+ });
281
+ });
282
+
283
+ function createShellStartupScript(lines, fileExtension) {
284
+ const randomFileName = Math.random().toString(36).substring(2, 15);
285
+ const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`;
286
+ fs.writeFileSync(filePath, lines.join(EOL), "utf-8");
287
+ return filePath;
288
+ }
289
+
290
+ function readAndDeleteFile(filePath) {
291
+ const fileContent = fs.readFileSync(filePath, "utf-8");
292
+ fs.rmSync(filePath, { force: true });
293
+ return fileContent.split(EOL);
294
+ }
295
+
296
+ function countOccurrences(lines, searchString) {
297
+ let count = 0;
298
+ for (const line of lines) {
299
+ if (line.includes(searchString)) {
300
+ count++;
301
+ }
302
+ }
303
+ return count;
304
+ }
@@ -0,0 +1,75 @@
1
+ import * as os from "os";
2
+ import { execSync } from "child_process";
3
+
4
+ const shellList = {
5
+ bash: {
6
+ name: "Bash",
7
+ executable: "bash",
8
+ getStartupFileCommand: "echo ~/.bashrc",
9
+ },
10
+ zsh: {
11
+ name: "Zsh",
12
+ executable: "zsh",
13
+ getStartupFileCommand: "echo ${ZDOTDIR:-$HOME}/.zshrc",
14
+ },
15
+ fish: {
16
+ name: "Fish",
17
+ executable: "fish",
18
+ getStartupFileCommand: "echo ~/.config/fish/config.fish",
19
+ },
20
+ powershell: {
21
+ name: "PowerShell Core",
22
+ executable: "pwsh",
23
+ getStartupFileCommand: "echo $PROFILE",
24
+ },
25
+ windowsPowerShell: {
26
+ name: "Windows PowerShell",
27
+ executable: "powershell",
28
+ getStartupFileCommand: "echo $PROFILE",
29
+ },
30
+ };
31
+
32
+ export function detectShells() {
33
+ let availableShells = [];
34
+
35
+ for (const shellName of Object.keys(shellList)) {
36
+ const shell = shellList[shellName];
37
+
38
+ if (isShellAvailable(shell)) {
39
+ const startupFile = getShellStartupFile(shell);
40
+ availableShells.push({
41
+ name: shell.name,
42
+ executable: shell.executable,
43
+ startupFile: startupFile || null,
44
+ });
45
+ }
46
+ }
47
+
48
+ return availableShells;
49
+ }
50
+
51
+ function isShellAvailable(shell) {
52
+ try {
53
+ if (os.platform() === "win32") {
54
+ execSync(`where ${shell.executable}`, { stdio: "ignore" });
55
+ } else {
56
+ execSync(`which ${shell.executable}`, { stdio: "ignore" });
57
+ }
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function getShellStartupFile(shell) {
65
+ try {
66
+ const command = shell.getStartupFileCommand;
67
+ const output = execSync(command, {
68
+ encoding: "utf8",
69
+ shell: shell.executable,
70
+ }).trim();
71
+ return output;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
@@ -0,0 +1,140 @@
1
+ import chalk from "chalk";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { detectShells } from "./shellDetection.js";
4
+ import { getAliases } from "./helpers.js";
5
+ import fs from "fs";
6
+ import { EOL } from "os";
7
+
8
+ export async function teardown() {
9
+ ui.writeInformation(
10
+ chalk.bold("Removing shell aliases.") +
11
+ " This will remove safe-chain aliases for npm, npx, and yarn commands."
12
+ );
13
+ ui.emptyLine();
14
+
15
+ try {
16
+ const shells = detectShells();
17
+ if (shells.length === 0) {
18
+ ui.writeError("No supported shells detected. Cannot remove aliases.");
19
+ return;
20
+ }
21
+
22
+ ui.writeInformation(
23
+ `Detected ${shells.length} supported shell(s): ${shells
24
+ .map((shell) => chalk.bold(shell.name))
25
+ .join(", ")}.`
26
+ );
27
+
28
+ let updatedCount = 0;
29
+ for (const shell of shells) {
30
+ if (removeAliasesForShell(shell)) {
31
+ updatedCount++;
32
+ }
33
+ }
34
+
35
+ if (updatedCount > 0) {
36
+ ui.emptyLine();
37
+ ui.writeInformation(`Please restart your terminal to apply the changes.`);
38
+ }
39
+ } catch (error) {
40
+ ui.writeError(
41
+ `Failed to remove shell aliases: ${error.message}. Please check your shell configuration.`
42
+ );
43
+ return;
44
+ }
45
+ }
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
+ }
@@ -0,0 +1,177 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { EOL, tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import { getAliases } from "./helpers.js";
6
+ import { removeAliasesFromFile } from "./teardown.js";
7
+
8
+ describe("teardown", () => {
9
+ function runRemovalTestsForEnvironment(shell, startupExtension, expectedAliases) {
10
+ describe(`${shell} shell removal`, () => {
11
+ it(`should remove aliases from ${shell} file`, () => {
12
+ const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases, ""];
13
+ const filePath = createShellStartupScript(lines, startupExtension);
14
+
15
+ // Test the removeAliasesFromFile function directly
16
+ const aliases = getAliases(filePath);
17
+ const fileContent = fs.readFileSync(filePath, "utf-8");
18
+
19
+ const result = removeAliasesFromFile(aliases, fileContent, filePath);
20
+
21
+ assert.strictEqual(result.removedCount, 3, "Should remove 3 aliases");
22
+ assert.strictEqual(result.notFoundCount, 0, "Should find all aliases");
23
+
24
+ const updatedContent = readAndDeleteFile(filePath);
25
+ for (const alias of expectedAliases) {
26
+ assert.ok(!updatedContent.includes(alias), `Alias "${alias}" should be removed`);
27
+ }
28
+ });
29
+
30
+ it(`should handle file with no aliases for ${shell}`, () => {
31
+ const lines = [`#!/usr/bin/env ${shell}`, "", "alias other='command'", ""];
32
+ const filePath = createShellStartupScript(lines, startupExtension);
33
+
34
+ const aliases = getAliases(filePath);
35
+ const fileContent = fs.readFileSync(filePath, "utf-8");
36
+
37
+ const result = removeAliasesFromFile(aliases, fileContent, filePath);
38
+
39
+ assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases");
40
+ assert.strictEqual(result.notFoundCount, 3, "Should report 3 aliases not found");
41
+
42
+ const updatedContent = readAndDeleteFile(filePath);
43
+ assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain unchanged");
44
+ });
45
+
46
+ it(`should remove duplicate aliases from ${shell} file`, () => {
47
+ const lines = [
48
+ `#!/usr/bin/env ${shell}`,
49
+ "",
50
+ ...expectedAliases,
51
+ "alias other='command'",
52
+ ...expectedAliases, // duplicates
53
+ ""
54
+ ];
55
+ const filePath = createShellStartupScript(lines, startupExtension);
56
+
57
+ const aliases = getAliases(filePath);
58
+ const fileContent = fs.readFileSync(filePath, "utf-8");
59
+
60
+ const result = removeAliasesFromFile(aliases, fileContent, filePath);
61
+
62
+ assert.strictEqual(result.removedCount, 3, "Should remove 3 aliases (counting duplicates as single removal)");
63
+ assert.strictEqual(result.notFoundCount, 0, "Should find all aliases");
64
+
65
+ const updatedContent = readAndDeleteFile(filePath);
66
+ for (const alias of expectedAliases) {
67
+ assert.ok(!updatedContent.includes(alias), `Alias "${alias}" should be completely removed`);
68
+ }
69
+ assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
70
+ });
71
+
72
+ it(`should use real getAliases() for ${shell} file`, () => {
73
+ const filePath = `${tmpdir()}/test${startupExtension}`;
74
+ const aliases = getAliases(filePath);
75
+
76
+ // Verify we get the expected aliases for this shell type
77
+ assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)");
78
+ for (let i = 0; i < aliases.length; i++) {
79
+ assert.strictEqual(aliases[i], expectedAliases[i], `Alias ${i} should match expected format`);
80
+ }
81
+ });
82
+
83
+ it(`should handle partial alias matches for ${shell}`, () => {
84
+ const lines = [
85
+ `#!/usr/bin/env ${shell}`,
86
+ "",
87
+ expectedAliases[0], // Only first alias
88
+ "alias other='command'",
89
+ ""
90
+ ];
91
+ const filePath = createShellStartupScript(lines, startupExtension);
92
+
93
+ const aliases = getAliases(filePath);
94
+ const fileContent = fs.readFileSync(filePath, "utf-8");
95
+
96
+ const result = removeAliasesFromFile(aliases, fileContent, filePath);
97
+
98
+ assert.strictEqual(result.removedCount, 1, "Should remove 1 alias");
99
+ assert.strictEqual(result.notFoundCount, 2, "Should report 2 aliases not found");
100
+
101
+ const updatedContent = readAndDeleteFile(filePath);
102
+ assert.ok(!updatedContent.includes(expectedAliases[0]), "First alias should be removed");
103
+ assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
104
+ });
105
+ });
106
+ }
107
+
108
+ // Test for each shell type using real getAliases() output
109
+ runRemovalTestsForEnvironment("bash", ".bashrc", [
110
+ "alias npm='aikido-npm'",
111
+ "alias npx='aikido-npx'",
112
+ "alias yarn='aikido-yarn'"
113
+ ]);
114
+
115
+ runRemovalTestsForEnvironment("zsh", ".zshrc", [
116
+ "alias npm='aikido-npm'",
117
+ "alias npx='aikido-npx'",
118
+ "alias yarn='aikido-yarn'"
119
+ ]);
120
+
121
+ runRemovalTestsForEnvironment("fish", ".fish", [
122
+ 'alias npm "aikido-npm"',
123
+ 'alias npx "aikido-npx"',
124
+ 'alias yarn "aikido-yarn"'
125
+ ]);
126
+
127
+ runRemovalTestsForEnvironment("pwsh", ".ps1", [
128
+ "Set-Alias npm aikido-npm",
129
+ "Set-Alias npx aikido-npx",
130
+ "Set-Alias yarn aikido-yarn"
131
+ ]);
132
+
133
+ describe("removeAliasesFromFile edge cases", () => {
134
+ it("should handle empty file", () => {
135
+ const aliases = ["alias npm='aikido-npm'"];
136
+ const fileContent = "";
137
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
138
+ fs.writeFileSync(filePath, fileContent, "utf-8");
139
+
140
+ const result = removeAliasesFromFile(aliases, fileContent, filePath);
141
+
142
+ assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases from empty file");
143
+ assert.strictEqual(result.notFoundCount, 1, "Should report 1 alias not found");
144
+
145
+ // Cleanup
146
+ fs.rmSync(filePath, { force: true });
147
+ });
148
+
149
+ it("should handle file with only whitespace", () => {
150
+ const aliases = ["alias npm='aikido-npm'"];
151
+ const fileContent = `${EOL}${EOL} ${EOL}`;
152
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
153
+ fs.writeFileSync(filePath, fileContent, "utf-8");
154
+
155
+ const result = removeAliasesFromFile(aliases, fileContent, filePath);
156
+
157
+ assert.strictEqual(result.removedCount, 0, "Should remove 0 aliases from whitespace-only file");
158
+ assert.strictEqual(result.notFoundCount, 1, "Should report 1 alias not found");
159
+
160
+ // Cleanup
161
+ fs.rmSync(filePath, { force: true });
162
+ });
163
+ });
164
+ });
165
+
166
+ function createShellStartupScript(lines, fileExtension) {
167
+ const randomFileName = Math.random().toString(36).substring(2, 15);
168
+ const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`;
169
+ fs.writeFileSync(filePath, lines.join(EOL), "utf-8");
170
+ return filePath;
171
+ }
172
+
173
+ function readAndDeleteFile(filePath) {
174
+ const fileContent = fs.readFileSync(filePath, "utf-8");
175
+ fs.rmSync(filePath, { force: true });
176
+ return fileContent.split(EOL);
177
+ }
@@ -1,63 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { EOL } from "os";
4
- import { getAliases } from "./helpers.js";
5
- import { ui } from "../environment/userInteraction.js";
6
-
7
- export function isAddAliasCommand(args) {
8
- if (args[0] === "add-aikido-aliases") {
9
- return true;
10
- }
11
-
12
- if (args[0] === "add-aikido-npm-alias") {
13
- // not in the documenation anymore, but still here for backwards compatibility
14
- return true;
15
- }
16
-
17
- return false;
18
- }
19
-
20
- export function addAlias(args) {
21
- if (!isAddAliasCommand(args)) {
22
- return;
23
- }
24
-
25
- if (args.length < 2) {
26
- ui.writeError("Please specify the file to add the alias to.");
27
- return;
28
- }
29
-
30
- const filePath = path.resolve(args[1]);
31
- const aliases = getAliases(filePath);
32
-
33
- if (!fs.existsSync(filePath)) {
34
- fs.writeFileSync(filePath, "", "utf-8");
35
- ui.writeInformation(`File ${filePath} created.`);
36
- }
37
-
38
- const fileContent = fs.readFileSync(filePath, "utf-8");
39
-
40
- let missingAliases = [];
41
- for (const alias of aliases) {
42
- if (fileContent.includes(alias)) {
43
- ui.writeInformation(`Alias "${alias}" already exists in ${filePath}`);
44
- } else {
45
- missingAliases.push(alias);
46
- }
47
- }
48
-
49
- if (missingAliases.length === 0) {
50
- ui.writeInformation(`The aliases are already present in ${filePath}`);
51
- return;
52
- }
53
-
54
- const aliasLines = missingAliases.join(EOL);
55
- ui.writeInformation(`Adding alias "${aliasLines}" to ${filePath}...`);
56
-
57
- // Append the alias to the file
58
- fs.appendFileSync(filePath, `${EOL}${aliasLines}${EOL}`, "utf-8");
59
- ui.writeInformation(`Alias added to ${filePath}`);
60
- ui.writeInformation(
61
- `Please restart your terminal for the changes to take effect.`
62
- );
63
- }
@@ -1,61 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { getAliases } from "./helpers.js";
4
- import { ui } from "../environment/userInteraction.js";
5
- import { EOL } from "os";
6
-
7
- export function isRemoveAliasCommand(args) {
8
- if (args[0] === "remove-aikido-aliases") {
9
- return true;
10
- }
11
-
12
- if (args[0] === "remove-aikido-npm-alias") {
13
- // not in the documenation anymore, but still here for backwards compatibility
14
- return true;
15
- }
16
-
17
- return false;
18
- }
19
-
20
- export function removeAlias(args) {
21
- if (!isRemoveAliasCommand(args)) {
22
- return;
23
- }
24
-
25
- if (args.length < 2) {
26
- ui.writeError("Please specify the file to remove the alias from.");
27
- return;
28
- }
29
-
30
- const filePath = path.resolve(args[1]);
31
- const aliases = getAliases(filePath);
32
-
33
- if (!fs.existsSync(filePath)) {
34
- ui.writeError(`File ${filePath} does not exist.`);
35
- return;
36
- }
37
-
38
- for (const alias of aliases) {
39
- const fileContent = fs.readFileSync(filePath, "utf-8");
40
- if (!fileContent.includes(alias)) {
41
- ui.writeInformation(`Alias "${alias}" does not exist in ${filePath}`);
42
- continue;
43
- }
44
-
45
- ui.writeInformation(`Removing alias "${alias}" from ${filePath}...`);
46
-
47
- // Remove the alias from the file.
48
- // Make sure to remove the added newlines as well, but also make sure to remove aliases if they don't have newlines
49
- const updatedContent = fileContent
50
- .replaceAll(`${EOL}${alias}${EOL}`, "")
51
- .replaceAll(`${alias}${EOL}`, "")
52
- .replaceAll(`${EOL}${alias}`, "")
53
- .replaceAll(alias, "");
54
-
55
- fs.writeFileSync(filePath, updatedContent, "utf-8");
56
- ui.writeInformation(`Alias "${alias}" removed from ${filePath}`);
57
- }
58
- ui.writeInformation(
59
- `Please restart your terminal for the changes to take effect.`
60
- );
61
- }
@@ -1,172 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { EOL, tmpdir } from "node:os";
4
- import fs from "node:fs";
5
- import { addAlias } from "./addAlias.js";
6
- import { removeAlias } from "./removeAlias.js";
7
-
8
- describe("Add alias", () => {
9
- function runTestsForEnvironment(shell, startupExtension, aliases) {
10
- it(`should add alias to ${shell} file`, () => {
11
- const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
12
- const filePath = createShellStartupScript(lines, startupExtension);
13
-
14
- addAlias(["add-aikido-aliases", filePath]);
15
-
16
- const fileContent = readAndDeleteFile(filePath);
17
- for (const alias of aliases) {
18
- assert.ok(fileContent.includes(alias));
19
- }
20
- });
21
-
22
- it(`should not add alias if it already exists in ${shell} file`, () => {
23
- const lines = [`#!/usr/bin/env ${shell}`, "", ...aliases];
24
- const filePath = createShellStartupScript(lines, startupExtension);
25
-
26
- addAlias(["add-aikido-aliases", filePath]);
27
-
28
- const fileContent = readAndDeleteFile(filePath);
29
- assert.strictEqual(fileContent.length, lines.length);
30
- });
31
-
32
- it(`should create file and add alias if file does not exist`, () => {
33
- const filePath = `${tmpdir()}/nonexistent-file${startupExtension}`;
34
- if (fs.existsSync(filePath)) {
35
- fs.rmSync(filePath, { force: true });
36
- }
37
-
38
- addAlias(["add-aikido-aliases", filePath]);
39
-
40
- assert.ok(fs.existsSync(filePath));
41
- const fileContent = readAndDeleteFile(filePath);
42
- for (const alias of aliases) {
43
- assert.ok(fileContent.includes(alias));
44
- }
45
- });
46
-
47
- it(`should add the alias only once in ${shell} file`, () => {
48
- const lines = [`#!/usr/bin/env ${shell}`, ""];
49
- const filePath = createShellStartupScript(lines, startupExtension);
50
-
51
- addAlias(["add-aikido-aliases", filePath]);
52
- addAlias(["add-aikido-aliases", filePath]);
53
-
54
- const fileContent = readAndDeleteFile(filePath);
55
- for (const alias of aliases) {
56
- assert.strictEqual(countOccurences(fileContent, alias), 1);
57
- }
58
- });
59
-
60
- it(`should remove one alias from ${shell} file`, () => {
61
- const alias = aliases[Math.floor(Math.random() * aliases.length)];
62
- const lines = [`#!/usr/bin/env ${shell}`, "", alias];
63
- const filePath = createShellStartupScript(lines, startupExtension);
64
-
65
- removeAlias(["remove-aikido-npm-alias", filePath]);
66
-
67
- const fileContent = readAndDeleteFile(filePath);
68
- assert.ok(!fileContent.includes(alias));
69
- });
70
-
71
- it(`should remove all aliases from ${shell} file`, () => {
72
- const lines = [`#!/usr/bin/env ${shell}`, "", ...aliases, ""];
73
- const filePath = createShellStartupScript(lines, startupExtension);
74
-
75
- removeAlias(["remove-aikido-npm-alias", filePath]);
76
-
77
- const fileContent = readAndDeleteFile(filePath);
78
- for (const alias of aliases) {
79
- assert.ok(!fileContent.includes(alias));
80
- }
81
- });
82
-
83
- it(`should not remove alias if it does not exist in ${shell} file`, () => {
84
- const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
85
- const filePath = createShellStartupScript(lines, startupExtension);
86
-
87
- removeAlias(["remove-aikido-npm-alias", filePath]);
88
-
89
- const fileContent = readAndDeleteFile(filePath);
90
- assert.ok(fileContent.includes("alias cls='clear'"));
91
- });
92
-
93
- it(`should not remove alias if file does not exist`, () => {
94
- const filePath = `${tmpdir()}/nonexistent-file${startupExtension}`;
95
- if (fs.existsSync(filePath)) {
96
- fs.rmSync(filePath, { force: true });
97
- }
98
-
99
- removeAlias(["remove-aikido-npm-alias", filePath]);
100
-
101
- assert.ok(!fs.existsSync(filePath));
102
- });
103
-
104
- it(`should remove aliases from ${shell} file if duplicated`, () => {
105
- const lines = [
106
- `#!/usr/bin/env ${shell}`,
107
- "",
108
- ...aliases,
109
- "cls='clear'",
110
- ...aliases,
111
- ...aliases,
112
- "",
113
- ];
114
- const filePath = createShellStartupScript(lines, startupExtension);
115
-
116
- removeAlias(["remove-aikido-npm-alias", filePath]);
117
-
118
- const fileContent = readAndDeleteFile(filePath);
119
- for (const alias of aliases) {
120
- assert.ok(!fileContent.includes(alias));
121
- }
122
- });
123
- }
124
-
125
- runTestsForEnvironment("bash", ".bashrc", [
126
- "alias npm='aikido-npm'",
127
- "alias npx='aikido-npx'",
128
- "alias yarn='aikido-yarn'",
129
- ]);
130
- runTestsForEnvironment("zsh", ".zshrc", [
131
- "alias npm='aikido-npm'",
132
- "alias npx='aikido-npx'",
133
- "alias yarn='aikido-yarn'",
134
- ]);
135
- runTestsForEnvironment("fish", ".fish", [
136
- 'alias npm "aikido-npm"',
137
- 'alias npx "aikido-npx"',
138
- 'alias yarn "aikido-yarn"',
139
- ]);
140
- runTestsForEnvironment("pwsh", ".ps1", [
141
- "Set-Alias npm aikido-npm",
142
- "Set-Alias npx aikido-npx",
143
- "Set-Alias yarn aikido-yarn",
144
- ]);
145
- });
146
-
147
- function createShellStartupScript(lines, fileExtension) {
148
- var randomFileName = Math.random().toString(36).substring(2, 15);
149
- var filePath = `${tmpdir()}/${randomFileName}${fileExtension}`;
150
- fs.writeFileSync(filePath, lines.join(EOL), "utf-8");
151
- return filePath;
152
- }
153
-
154
- function readAndDeleteFile(filePath) {
155
- var fileContent = fs.readFileSync(filePath, "utf-8");
156
- fs.rm(filePath, { force: true }, (err) => {
157
- if (err) {
158
- console.error(`Error deleting file: ${err}`);
159
- }
160
- });
161
- return fileContent.split(EOL);
162
- }
163
-
164
- function countOccurences(lines, alias) {
165
- let count = 0;
166
- for (const line of lines) {
167
- if (line.includes(alias)) {
168
- count++;
169
- }
170
- }
171
- return count;
172
- }