@aikidosec/safe-chain 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +10 -8
  2. package/bin/aikido-pnpm.js +8 -0
  3. package/bin/aikido-pnpx.js +8 -0
  4. package/docs/shell-integration.md +4 -4
  5. package/package.json +3 -1
  6. package/src/packagemanager/_shared/matchesCommand.js +13 -0
  7. package/src/packagemanager/currentPackageManager.js +8 -0
  8. package/src/packagemanager/pnpm/createPackageManager.js +46 -0
  9. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +28 -0
  10. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +88 -0
  11. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +138 -0
  12. package/src/packagemanager/pnpm/runPnpmCommand.js +24 -0
  13. package/src/shell-integration/helpers.js +31 -31
  14. package/src/shell-integration/setup.js +26 -102
  15. package/src/shell-integration/shellDetection.js +17 -66
  16. package/src/shell-integration/supported-shells/bash.js +58 -0
  17. package/src/shell-integration/supported-shells/bash.spec.js +199 -0
  18. package/src/shell-integration/supported-shells/fish.js +61 -0
  19. package/src/shell-integration/supported-shells/fish.spec.js +199 -0
  20. package/src/shell-integration/supported-shells/powershell.js +61 -0
  21. package/src/shell-integration/supported-shells/powershell.spec.js +204 -0
  22. package/src/shell-integration/supported-shells/windowsPowershell.js +61 -0
  23. package/src/shell-integration/supported-shells/windowsPowershell.spec.js +204 -0
  24. package/src/shell-integration/supported-shells/zsh.js +58 -0
  25. package/src/shell-integration/supported-shells/zsh.spec.js +199 -0
  26. package/src/shell-integration/teardown.js +20 -99
  27. package/src/shell-integration/setup.spec.js +0 -304
  28. package/src/shell-integration/teardown.spec.js +0 -177
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Aikido Safe Chain
2
2
 
3
- The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, or yarn.
3
+ The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx.
4
4
 
5
- The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), and [yarn](https://yarnpkg.com/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, or yarn from downloading or running the malware.
5
+ The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm or pnpx from downloading or running the malware.
6
6
 
7
7
  ![demo](https://aikido-production-staticfiles-public.s3.eu-west-1.amazonaws.com/safe-pkg.gif)
8
8
 
@@ -11,7 +11,9 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi
11
11
  - ✅ **npm**
12
12
  - ✅ **npx**
13
13
  - ✅ **yarn**
14
- - 🚧 **pnpm** Coming soon
14
+ - **pnpm**
15
+ - ✅ **pnpx**
16
+ - 🚧 **bun** Coming soon
15
17
 
16
18
  # Usage
17
19
 
@@ -28,20 +30,20 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
28
30
  safe-chain setup
29
31
  ```
30
32
  3. **❗Restart your terminal** to start using the Aikido Safe Chain.
31
- - This step is crucial as it ensures that the shell aliases for npm, npx, and yarn are loaded correctly. If you do not restart your terminal, the aliases will not be available.
33
+ - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm and pnpx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
32
34
  4. **Verify the installation** by running:
33
35
  ```shell
34
- npm install eslint-js
36
+ npm install safe-chain-test
35
37
  ```
36
38
  - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
37
39
 
38
- 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.
40
+ When running `npm`, `npx`, `yarn`, `pnpm` or `pnpx` 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.
39
41
 
40
42
  ## How it works
41
43
 
42
- The Aikido Safe Chain works by intercepting the npm, npx, and yarn commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**.
44
+ The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm and pnpx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**.
43
45
 
44
- The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, and yarn commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support:
46
+ The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm and pnpx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support:
45
47
 
46
48
  - ✅ **Bash**
47
49
  - ✅ **Zsh**
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "pnpm";
7
+ initializePackageManager(packageManagerName, process.versions.node);
8
+ await main(process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "pnpx";
7
+ initializePackageManager(packageManagerName, process.versions.node);
8
+ await main(process.argv.slice(2));
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents.
5
+ The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents.
6
6
 
7
7
  ## Supported Shells
8
8
 
@@ -27,7 +27,7 @@ safe-chain setup
27
27
  This command:
28
28
 
29
29
  - Detects all supported shells on your system
30
- - Adds aliases for `npm`, `npx`, and `yarn` to each shell's startup file
30
+ - Adds aliases for `npm`, `npx`, `yarn`, `pnpm` and `pnpx` to each shell's startup file
31
31
 
32
32
  ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the aliases are loaded correctly.
33
33
 
@@ -75,7 +75,7 @@ The system modifies the following files based on your shell configuration:
75
75
  This means the aliases are working but the Aikido commands aren't installed or available in your PATH:
76
76
 
77
77
  - Make sure Aikido Safe Chain is properly installed on your system
78
- - Verify the `aikido-npm`, `aikido-npx`, and `aikido-yarn` commands exist
78
+ - Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` and `aikido-pnpx` commands exist
79
79
  - Check that these commands are in your system's PATH
80
80
 
81
81
  ### Manual Verification
@@ -105,4 +105,4 @@ To verify the integration is working, follow these steps:
105
105
 
106
106
  3. **If you need to remove aliases manually:**
107
107
 
108
- Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, or `aikido-yarn`.
108
+ Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` or `aikido-pnpx`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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",
@@ -14,6 +14,8 @@
14
14
  "aikido-npm": "bin/aikido-npm.js",
15
15
  "aikido-npx": "bin/aikido-npx.js",
16
16
  "aikido-yarn": "bin/aikido-yarn.js",
17
+ "aikido-pnpm": "bin/aikido-pnpm.js",
18
+ "aikido-pnpx": "bin/aikido-pnpx.js",
17
19
  "safe-chain": "bin/safe-chain.js"
18
20
  },
19
21
  "type": "module",
@@ -0,0 +1,13 @@
1
+ export function matchesCommand(args, ...commandArgs) {
2
+ if (args.length < commandArgs.length) {
3
+ return false;
4
+ }
5
+
6
+ for (var i = 0; i < commandArgs.length; i++) {
7
+ if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ return true;
13
+ }
@@ -1,5 +1,9 @@
1
1
  import { createNpmPackageManager } from "./npm/createPackageManager.js";
2
2
  import { createNpxPackageManager } from "./npx/createPackageManager.js";
3
+ import {
4
+ createPnpmPackageManager,
5
+ createPnpxPackageManager,
6
+ } from "./pnpm/createPackageManager.js";
3
7
  import { createYarnPackageManager } from "./yarn/createPackageManager.js";
4
8
 
5
9
  const state = {
@@ -13,6 +17,10 @@ export function initializePackageManager(packageManagerName, version) {
13
17
  state.packageManagerName = createNpxPackageManager();
14
18
  } else if (packageManagerName === "yarn") {
15
19
  state.packageManagerName = createYarnPackageManager();
20
+ } else if (packageManagerName === "pnpm") {
21
+ state.packageManagerName = createPnpmPackageManager();
22
+ } else if (packageManagerName === "pnpx") {
23
+ state.packageManagerName = createPnpxPackageManager();
16
24
  } else {
17
25
  throw new Error("Unsupported package manager: " + packageManagerName);
18
26
  }
@@ -0,0 +1,46 @@
1
+ import { matchesCommand } from "../_shared/matchesCommand.js";
2
+ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
3
+ import { runPnpmCommand } from "./runPnpmCommand.js";
4
+
5
+ const scanner = commandArgumentScanner();
6
+
7
+ export function createPnpmPackageManager() {
8
+ return {
9
+ getWarningMessage: () => null,
10
+ runCommand: (args) => runPnpmCommand(args, "pnpm"),
11
+ isSupportedCommand: (args) =>
12
+ matchesCommand(args, "add") ||
13
+ matchesCommand(args, "update") ||
14
+ matchesCommand(args, "upgrade") ||
15
+ matchesCommand(args, "up") ||
16
+ // dlx does not always come in the first position
17
+ // eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
18
+ // documentation: https://pnpm.io/cli/dlx#--package-name
19
+ args.includes("dlx"),
20
+ getDependencyUpdatesForCommand: (args) =>
21
+ getDependencyUpdatesForCommand(args, false),
22
+ };
23
+ }
24
+
25
+ export function createPnpxPackageManager() {
26
+ return {
27
+ getWarningMessage: () => null,
28
+ runCommand: (args) => runPnpmCommand(args, "pnpx"),
29
+ isSupportedCommand: () => true,
30
+ getDependencyUpdatesForCommand: (args) =>
31
+ getDependencyUpdatesForCommand(args, true),
32
+ };
33
+ }
34
+
35
+ function getDependencyUpdatesForCommand(args, isPnpx) {
36
+ if (isPnpx) {
37
+ return scanner.scan(args);
38
+ }
39
+ if (args.includes("dlx")) {
40
+ // dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`)
41
+ // so we need to filter it out instead of slicing the array
42
+ // documentation: https://pnpm.io/cli/dlx#--package-name
43
+ return scanner.scan(args.filter((arg) => arg !== "dlx"));
44
+ }
45
+ return scanner.scan(args.slice(1));
46
+ }
@@ -0,0 +1,28 @@
1
+ import { resolvePackageVersion } from "../../../api/npmApi.js";
2
+ import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
3
+
4
+ export function commandArgumentScanner() {
5
+ return {
6
+ scan: (args) => scanDependencies(args),
7
+ shouldScan: () => true, // There's no dry run for pnpm, so we always scan
8
+ };
9
+ }
10
+
11
+ async function scanDependencies(args) {
12
+ const changes = [];
13
+ const packageUpdates = parsePackagesFromArguments(args);
14
+
15
+ for (const packageUpdate of packageUpdates) {
16
+ var exactVersion = await resolvePackageVersion(
17
+ packageUpdate.name,
18
+ packageUpdate.version
19
+ );
20
+ if (exactVersion) {
21
+ packageUpdate.version = exactVersion;
22
+ }
23
+
24
+ changes.push({ ...packageUpdate, type: "add" });
25
+ }
26
+
27
+ return changes;
28
+ }
@@ -0,0 +1,88 @@
1
+ export function parsePackagesFromArguments(args) {
2
+ const changes = [];
3
+ let defaultTag = "latest";
4
+
5
+ for (let i = 0; i < args.length; i++) {
6
+ const arg = args[i];
7
+ const option = getOption(arg);
8
+
9
+ if (option) {
10
+ // If the option has a parameter, skip the next argument as well
11
+ i += option.numberOfParameters;
12
+
13
+ continue;
14
+ }
15
+
16
+ const packageDetails = parsePackagename(arg, defaultTag);
17
+ if (packageDetails) {
18
+ changes.push(packageDetails);
19
+ }
20
+ }
21
+
22
+ return changes;
23
+ }
24
+
25
+ function getOption(arg) {
26
+ if (isOptionWithParameter(arg)) {
27
+ return {
28
+ name: arg,
29
+ numberOfParameters: 1,
30
+ };
31
+ }
32
+
33
+ // Arguments starting with "-" or "--" are considered options
34
+ // except for "--package=" which contains the package name
35
+ if (arg.startsWith("-") && !arg.startsWith("--package=")) {
36
+ return {
37
+ name: arg,
38
+ numberOfParameters: 0,
39
+ };
40
+ }
41
+
42
+ return undefined;
43
+ }
44
+
45
+ function isOptionWithParameter(arg) {
46
+ const optionsWithParameters = ["--C", "--dir"];
47
+
48
+ return optionsWithParameters.includes(arg);
49
+ }
50
+
51
+ function parsePackagename(arg, defaultTag) {
52
+ // format can be --package=name@version
53
+ // in that case, we need to remove the --package= part
54
+ if (arg.startsWith("--package=")) {
55
+ arg = arg.slice(10);
56
+ }
57
+
58
+ arg = removeAlias(arg);
59
+
60
+ // Split at the last "@" to separate the package name and version
61
+ const lastAtIndex = arg.lastIndexOf("@");
62
+
63
+ let name, version;
64
+ // The index of the last "@" should be greater than 0
65
+ // If the index is 0, it means the package name starts with "@" (eg: "@aikidosec/package-name")
66
+ if (lastAtIndex > 0) {
67
+ name = arg.slice(0, lastAtIndex);
68
+ version = arg.slice(lastAtIndex + 1);
69
+ } else {
70
+ name = arg;
71
+ version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
72
+ }
73
+
74
+ return {
75
+ name,
76
+ version,
77
+ };
78
+ }
79
+
80
+ function removeAlias(arg) {
81
+ // removes the alias.
82
+ // Eg.: server@npm:http-server@latest becomes http-server@latest
83
+ const aliasIndex = arg.indexOf("@npm:");
84
+ if (aliasIndex !== -1) {
85
+ return arg.slice(aliasIndex + 5);
86
+ }
87
+ return arg;
88
+ }
@@ -0,0 +1,138 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js";
4
+
5
+ describe("standardPnpmArgumentParser", () => {
6
+ it("should return an empty array for no changes", () => {
7
+ const args = [];
8
+
9
+ const result = parsePackagesFromArguments(args);
10
+
11
+ assert.deepEqual(result, []);
12
+ });
13
+
14
+ it("should return an array of changes for one package", () => {
15
+ const args = ["axios@1.9.0"];
16
+
17
+ const result = parsePackagesFromArguments(args);
18
+
19
+ assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
20
+ });
21
+
22
+ it("should return the package with latest tag if absent", () => {
23
+ const args = ["axios"];
24
+
25
+ const result = parsePackagesFromArguments(args);
26
+
27
+ assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
28
+ });
29
+
30
+ it("should return the package with latest tag if the version is absent and package starts with @", () => {
31
+ const args = ["@aikidosec/package-name"];
32
+
33
+ const result = parsePackagesFromArguments(args);
34
+
35
+ assert.deepEqual(result, [
36
+ { name: "@aikidosec/package-name", version: "latest" },
37
+ ]);
38
+ });
39
+
40
+ it("should return the package with the specified tag if the package starts with @ and includes the version", () => {
41
+ const args = ["@aikidosec/package-name@1.0.0"];
42
+
43
+ const result = parsePackagesFromArguments(args);
44
+
45
+ assert.deepEqual(result, [
46
+ { name: "@aikidosec/package-name", version: "1.0.0" },
47
+ ]);
48
+ });
49
+
50
+ it("should only return all packages", () => {
51
+ const args = ["axios", "jest"];
52
+
53
+ const result = parsePackagesFromArguments(args);
54
+
55
+ assert.deepEqual(result, [
56
+ { name: "axios", version: "latest" },
57
+ { name: "jest", version: "latest" },
58
+ ]);
59
+ });
60
+
61
+ it("should ignore options with parameters and return an array of changes", () => {
62
+ const args = ["--C", "/Users/johnsmith/dev/project", "axios@1.9.0"];
63
+
64
+ const result = parsePackagesFromArguments(args);
65
+
66
+ assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
67
+ });
68
+
69
+ it("should parse version even for aliased packages", () => {
70
+ const args = ["server@npm:axios@1.9.0"];
71
+
72
+ const result = parsePackagesFromArguments(args);
73
+
74
+ assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
75
+ });
76
+
77
+ it("should parse scoped packages", () => {
78
+ const args = ["@scope/package@1.0.0"];
79
+
80
+ const result = parsePackagesFromArguments(args);
81
+
82
+ assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
83
+ });
84
+
85
+ it("should parse packages with version ranges", () => {
86
+ const args = ["axios@^1.9.0"];
87
+
88
+ const result = parsePackagesFromArguments(args);
89
+
90
+ assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]);
91
+ });
92
+
93
+ it("should parse package folders", () => {
94
+ const args = ["./local-package"];
95
+
96
+ const result = parsePackagesFromArguments(args);
97
+
98
+ assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
99
+ });
100
+
101
+ it("should parse tarballs", () => {
102
+ const args = ["file:./local-package.tgz"];
103
+
104
+ const result = parsePackagesFromArguments(args);
105
+
106
+ assert.deepEqual(result, [
107
+ { name: "file:./local-package.tgz", version: "latest" },
108
+ ]);
109
+ });
110
+
111
+ it("should parse tarball URLs", () => {
112
+ const args = ["https://example.com/local-package.tgz"];
113
+
114
+ const result = parsePackagesFromArguments(args);
115
+
116
+ assert.deepEqual(result, [
117
+ { name: "https://example.com/local-package.tgz", version: "latest" },
118
+ ]);
119
+ });
120
+
121
+ it("should parse git URLs", () => {
122
+ const args = ["git://github.com/http-party/http-server"];
123
+
124
+ const result = parsePackagesFromArguments(args);
125
+
126
+ assert.deepEqual(result, [
127
+ { name: "git://github.com/http-party/http-server", version: "latest" },
128
+ ]);
129
+ });
130
+
131
+ it("should parse packages with --package={packageName}", () => {
132
+ const args = ["--package=axios@1.9.0"];
133
+
134
+ const result = parsePackagesFromArguments(args);
135
+
136
+ assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
137
+ });
138
+ });
@@ -0,0 +1,24 @@
1
+ import { spawnSync } from "child_process";
2
+ import { ui } from "../../environment/userInteraction.js";
3
+
4
+ export function runPnpmCommand(args, toolName = "pnpm") {
5
+ try {
6
+ let result;
7
+
8
+ if (toolName === "pnpm") {
9
+ result = spawnSync("pnpm", args, { stdio: "inherit" });
10
+ } else if (toolName === "pnpx") {
11
+ result = spawnSync("pnpx", args, { stdio: "inherit" });
12
+ } else {
13
+ throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
14
+ }
15
+
16
+ if (result.status !== null) {
17
+ return { status: result.status };
18
+ }
19
+ } catch (error) {
20
+ ui.writeError("Error executing command:", error.message);
21
+ return { status: 1 };
22
+ }
23
+ return { status: 0 };
24
+ }
@@ -1,44 +1,44 @@
1
- const knownAikidoTools = [
1
+ import { spawnSync } from "child_process";
2
+ import * as os from "os";
3
+ import fs from "fs";
4
+
5
+ export const knownAikidoTools = [
2
6
  { tool: "npm", aikidoCommand: "aikido-npm" },
3
7
  { tool: "npx", aikidoCommand: "aikido-npx" },
4
8
  { tool: "yarn", aikidoCommand: "aikido-yarn" },
5
- // When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js)
9
+ { tool: "pnpm", aikidoCommand: "aikido-pnpm" },
10
+ { tool: "pnpx", aikidoCommand: "aikido-pnpx" },
11
+ // When adding a new tool here, also update the expected alias in the tests (setup.spec.js, teardown.spec.js)
6
12
  // and add the documentation for the new tool in the README.md
7
13
  ];
8
14
 
9
- export function getAliases(fileName) {
10
- const fileExtension = fileName.split(".").pop().toLowerCase();
11
-
12
- let createAlias = pickCreateAliasFunction(fileExtension);
15
+ export function doesExecutableExistOnSystem(executableName) {
16
+ if (os.platform() === "win32") {
17
+ const result = spawnSync("where", [executableName], { stdio: "ignore" });
18
+ return result.status === 0;
19
+ } else {
20
+ const result = spawnSync("which", [executableName], { stdio: "ignore" });
21
+ return result.status === 0;
22
+ }
23
+ }
13
24
 
14
- const aliases = knownAikidoTools.map(({ tool, aikidoCommand }) =>
15
- createAlias(tool, aikidoCommand)
16
- );
25
+ export function removeLinesMatchingPattern(filePath, pattern) {
26
+ if (!fs.existsSync(filePath)) {
27
+ return;
28
+ }
17
29
 
18
- return aliases;
30
+ const fileContent = fs.readFileSync(filePath, "utf-8");
31
+ const lines = fileContent.split(os.EOL);
32
+ const updatedLines = lines.filter((line) => !pattern.test(line));
33
+ fs.writeFileSync(filePath, updatedLines.join(os.EOL), "utf-8");
19
34
  }
20
35
 
21
- function pickCreateAliasFunction(fileExtension) {
22
- let createAlias;
23
- switch (fileExtension) {
24
- case "ps1":
25
- createAlias = createGeneralPowershellAlias;
26
- break;
27
- case "fish":
28
- createAlias = createGeneralFishAlias;
29
- break;
30
- default:
31
- createAlias = createGeneralPosixAlias;
36
+ export function addLineToFile(filePath, line) {
37
+ if (!fs.existsSync(filePath)) {
38
+ fs.writeFileSync(filePath, "", "utf-8");
32
39
  }
33
- return createAlias;
34
- }
35
40
 
36
- function createGeneralPosixAlias(tool, aikidoCommand) {
37
- return `alias ${tool}='${aikidoCommand}'`;
38
- }
39
- function createGeneralPowershellAlias(tool, aikidoCommand) {
40
- return `Set-Alias ${tool} ${aikidoCommand}`;
41
- }
42
- function createGeneralFishAlias(tool, aikidoCommand) {
43
- return `alias ${tool} "${aikidoCommand}"`;
41
+ const fileContent = fs.readFileSync(filePath, "utf-8");
42
+ const updatedContent = fileContent + os.EOL + line;
43
+ fs.writeFileSync(filePath, updatedContent, "utf-8");
44
44
  }