@aikidosec/safe-chain 1.0.16 → 1.0.18

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 (27) hide show
  1. package/eslint.config.js +2 -1
  2. package/package.json +7 -4
  3. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +3 -1
  4. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js +8 -0
  5. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +3 -1
  6. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js +8 -0
  7. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +8 -0
  8. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +3 -1
  9. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js +8 -0
  10. package/src/shell-integration/helpers.js +26 -2
  11. package/src/shell-integration/helpers.spec.js +113 -0
  12. package/src/shell-integration/setup.js +25 -0
  13. package/src/shell-integration/startup-scripts/init-fish.fish +58 -0
  14. package/src/shell-integration/startup-scripts/init-posix.sh +54 -0
  15. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +80 -0
  16. package/src/shell-integration/supported-shells/bash.js +11 -7
  17. package/src/shell-integration/supported-shells/bash.spec.js +32 -32
  18. package/src/shell-integration/supported-shells/fish.js +11 -7
  19. package/src/shell-integration/supported-shells/fish.spec.js +21 -37
  20. package/src/shell-integration/supported-shells/powershell.js +11 -7
  21. package/src/shell-integration/supported-shells/powershell.spec.js +43 -47
  22. package/src/shell-integration/supported-shells/windowsPowershell.js +11 -7
  23. package/src/shell-integration/supported-shells/windowsPowershell.spec.js +43 -47
  24. package/src/shell-integration/supported-shells/zsh.js +11 -7
  25. package/src/shell-integration/supported-shells/zsh.spec.js +57 -30
  26. package/.github/workflows/build-and-release.yml +0 -41
  27. package/.github/workflows/test-on-pr.yml +0 -28
package/eslint.config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import js from "@eslint/js";
2
- import { defineConfig } from "@eslint/config-helpers";
2
+ import { defineConfig, globalIgnores } from "@eslint/config-helpers";
3
3
  import globals from "globals";
4
4
  import importPlugin from "eslint-plugin-import";
5
5
 
@@ -22,4 +22,5 @@ export default defineConfig([
22
22
  },
23
23
  rules: {},
24
24
  },
25
+ globalIgnores(['test/e2e']),
25
26
  ]);
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "scripts": {
5
- "test": "node --test --experimental-test-module-mocks **/*.spec.js",
6
- "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js",
5
+ "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
+ "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
7
7
  "lint": "eslint ."
8
8
  },
9
9
  "repository": {
@@ -42,5 +42,8 @@
42
42
  "bugs": {
43
43
  "url": "https://github.com/AikidoSec/safe-chain/issues"
44
44
  },
45
- "homepage": "https://github.com/AikidoSec/safe-chain#readme"
45
+ "homepage": "https://github.com/AikidoSec/safe-chain#readme",
46
+ "overrides": {
47
+ "brace-expansion@<=2.0.2": "2.0.2"
48
+ }
46
49
  }
@@ -86,7 +86,9 @@ function parsePackagename(arg) {
86
86
  const lastAtIndex = arg.lastIndexOf("@");
87
87
 
88
88
  let name, version;
89
- if (lastAtIndex !== -1) {
89
+ // The index of the last "@" should be greater than 0
90
+ // If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
91
+ if (lastAtIndex > 0) {
90
92
  name = arg.slice(0, lastAtIndex);
91
93
  version = arg.slice(lastAtIndex + 1);
92
94
  } else {
@@ -19,6 +19,14 @@ describe("parsePackagesFromInstallArgs", () => {
19
19
  assert.deepEqual(result, [{ name: "@jest/transform", version: "29.7.0" }]);
20
20
  });
21
21
 
22
+ it("should return the package in the format @vercel/otel", () => {
23
+ const args = ["install", "@vercel/otel"];
24
+
25
+ const result = parsePackagesFromInstallArgs(args);
26
+
27
+ assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
28
+ });
29
+
22
30
  it("should return an array of changes for multiple packages", () => {
23
31
  const args = ["install", "express@4.17.1", "lodash@4.17.21"];
24
32
 
@@ -81,7 +81,9 @@ function parsePackagename(arg, defaultTag) {
81
81
  const lastAtIndex = arg.lastIndexOf("@");
82
82
 
83
83
  let name, version;
84
- if (lastAtIndex !== -1) {
84
+ // The index of the last "@" should be greater than 0
85
+ // If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
86
+ if (lastAtIndex > 0) {
85
87
  name = arg.slice(0, lastAtIndex);
86
88
  version = arg.slice(lastAtIndex + 1);
87
89
  } else {
@@ -19,6 +19,14 @@ describe("parsePackagesFromArguments", () => {
19
19
  assert.deepEqual(result, [{ name: "http-server", version: "14.1.1" }]);
20
20
  });
21
21
 
22
+ it("should return the package in the format @vercel/otel", () => {
23
+ const args = ["@vercel/otel"];
24
+
25
+ const result = parsePackagesFromArguments(args);
26
+
27
+ assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
28
+ });
29
+
22
30
  it("should return the package with latest tag if absent", () => {
23
31
  const args = ["http-server"];
24
32
 
@@ -27,6 +27,14 @@ describe("standardPnpmArgumentParser", () => {
27
27
  assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
28
28
  });
29
29
 
30
+ it("should return the package in the format @vercel/otel", () => {
31
+ const args = ["@vercel/otel"];
32
+
33
+ const result = parsePackagesFromArguments(args);
34
+
35
+ assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
36
+ });
37
+
30
38
  it("should return the package with latest tag if the version is absent and package starts with @", () => {
31
39
  const args = ["@aikidosec/package-name"];
32
40
 
@@ -77,7 +77,9 @@ function parsePackagename(arg, defaultTag) {
77
77
  const lastAtIndex = arg.lastIndexOf("@");
78
78
 
79
79
  let name, version;
80
- if (lastAtIndex !== -1) {
80
+ // The index of the last "@" should be greater than 0
81
+ // If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
82
+ if (lastAtIndex > 0) {
81
83
  name = arg.slice(0, lastAtIndex);
82
84
  version = arg.slice(lastAtIndex + 1);
83
85
  } else {
@@ -38,6 +38,14 @@ describe("standardYarnArgumentParser", () => {
38
38
  ]);
39
39
  });
40
40
 
41
+ it("should return the package in the format @vercel/otel", () => {
42
+ const args = ["add", "@vercel/otel"];
43
+
44
+ const result = parsePackagesFromArguments(args);
45
+
46
+ assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
47
+ });
48
+
41
49
  it("should ignore options with parameters and return an array of changes", () => {
42
50
  const args = ["add", "--proxy", "http://localhost", "axios@1.9.0"];
43
51
 
@@ -28,11 +28,35 @@ export function removeLinesMatchingPattern(filePath, pattern) {
28
28
  }
29
29
 
30
30
  const fileContent = fs.readFileSync(filePath, "utf-8");
31
- const lines = fileContent.split(os.EOL);
32
- const updatedLines = lines.filter((line) => !pattern.test(line));
31
+ const lines = fileContent.split(/[\r\n\u2028\u2029]+/);
32
+ const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
33
33
  fs.writeFileSync(filePath, updatedLines.join(os.EOL), "utf-8");
34
34
  }
35
35
 
36
+ const maxLineLength = 100;
37
+ function shouldRemoveLine(line, pattern) {
38
+ const isPatternMatch = pattern.test(line);
39
+
40
+ if (!isPatternMatch) {
41
+ return false;
42
+ }
43
+
44
+ if (line.length > maxLineLength) {
45
+ // safe-chain only adds lines shorter than maxLineLength
46
+ // so if the line is longer, it must be from a different
47
+ // source and could be dangerous to remove
48
+ return false;
49
+ }
50
+
51
+ if (line.includes("\n") || line.includes("\r") || line.includes("\u2028") || line.includes("\u2029")) {
52
+ // If the line contains newlines, something has gone wrong in splitting
53
+ // \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
54
+ return false;
55
+ }
56
+
57
+ return true;
58
+ }
59
+
36
60
  export function addLineToFile(filePath, line) {
37
61
  if (!fs.existsSync(filePath)) {
38
62
  fs.writeFileSync(filePath, "", "utf-8");
@@ -0,0 +1,113 @@
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
+
7
+ describe("removeLinesMatchingPatternTests", () => {
8
+ let testFile;
9
+
10
+ beforeEach(() => {
11
+ // Create temporary test file
12
+ testFile = path.join(tmpdir(), `test-helpers-${Date.now()}.txt`);
13
+
14
+ // Mock the os module to override EOL
15
+ mock.module("node:os", {
16
+ namedExports: {
17
+ EOL: "\r\n", // Simulate Windows line endings
18
+ tmpdir: tmpdir,
19
+ platform: () => "linux"
20
+ }
21
+ });
22
+ });
23
+
24
+ afterEach(() => {
25
+ // Clean up test files
26
+ if (fs.existsSync(testFile)) {
27
+ fs.unlinkSync(testFile);
28
+ }
29
+
30
+ // Reset mocks
31
+ mock.reset();
32
+ });
33
+
34
+
35
+ it("should handle mixed line endings without wiping entire file", async () => {
36
+ // Import helpers after setting up the mock
37
+ const { removeLinesMatchingPattern } = await import("./helpers.js");
38
+
39
+ // Create a file with Unix line endings but os.EOL expects Windows
40
+ const fileContent = [
41
+ "# keep this line",
42
+ "alias npm='remove-this'",
43
+ "# keep this line too",
44
+ "alias yarn='remove-this-too'",
45
+ "# final line to keep"
46
+ ].join("\n"); // File has Unix line endings
47
+
48
+ fs.writeFileSync(testFile, fileContent, "utf-8");
49
+
50
+ // Try to remove lines containing 'alias'
51
+ const pattern = /alias.*=/;
52
+ removeLinesMatchingPattern(testFile, pattern);
53
+
54
+ const result = fs.readFileSync(testFile, "utf-8");
55
+
56
+ // This test will fail because the function splits on '\r\n' but file uses '\n'
57
+ // So it treats the entire content as one line and if any part matches, removes everything
58
+ assert.ok(result.includes("keep this line"), "Should preserve non-matching lines");
59
+ assert.ok(result.includes("final line to keep"), "Should preserve final line");
60
+ });
61
+
62
+ it("should handle mixed line endings with short matching content", async () => {
63
+ // Import helpers after setting up the mock
64
+ const { removeLinesMatchingPattern } = await import("./helpers.js");
65
+
66
+ // Create a file with Unix line endings, but make the entire content short
67
+ // to bypass the maxLineLength protection
68
+ const fileContent = [
69
+ "# keep1",
70
+ "alias x=y", // Short alias line that should be removed
71
+ "# keep2"
72
+ ].join("\n"); // File has Unix line endings, total length < 100 chars
73
+
74
+ fs.writeFileSync(testFile, fileContent, "utf-8");
75
+
76
+ // Try to remove lines containing 'alias'
77
+ const pattern = /alias/;
78
+ removeLinesMatchingPattern(testFile, pattern);
79
+
80
+ const result = fs.readFileSync(testFile, "utf-8");
81
+
82
+ // This should now be protected by the newline detection
83
+ assert.ok(result.includes("keep1"), "Should preserve first line");
84
+ assert.ok(result.includes("keep2"), "Should preserve third line");
85
+ });
86
+
87
+ it("should handle Unicode line separators that bypass newline detection", async () => {
88
+ // Import helpers after setting up the mock
89
+ const { removeLinesMatchingPattern } = await import("./helpers.js");
90
+
91
+ // Use Unicode line separator (U+2028) and paragraph separator (U+2029)
92
+ // These are considered line breaks but aren't \n or \r
93
+ const fileContent = [
94
+ "keep this",
95
+ "alias test=value",
96
+ "keep that"
97
+ ].join("\u2028"); // Unicode line separator
98
+
99
+ fs.writeFileSync(testFile, fileContent, "utf-8");
100
+
101
+ // Try to remove lines containing 'alias'
102
+ const pattern = /alias/;
103
+ removeLinesMatchingPattern(testFile, pattern);
104
+
105
+ const result = fs.readFileSync(testFile, "utf-8");
106
+
107
+ // This could still wipe everything if split() treats it as one line
108
+ // but the content doesn't contain \n or \r so passes the newline check
109
+ assert.ok(result.includes("keep this"), "Should preserve first part");
110
+ assert.ok(result.includes("keep that"), "Should preserve last part");
111
+ });
112
+
113
+ });
@@ -2,6 +2,10 @@ import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
4
  import { knownAikidoTools } from "./helpers.js";
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
5
9
 
6
10
  /**
7
11
  * Loops over the detected shells and calls the setup function for each.
@@ -13,6 +17,8 @@ export async function setup() {
13
17
  );
14
18
  ui.emptyLine();
15
19
 
20
+ copyStartupFiles();
21
+
16
22
  try {
17
23
  const shells = detectShells();
18
24
  if (shells.length === 0) {
@@ -73,3 +79,22 @@ function setupShell(shell) {
73
79
 
74
80
  return success;
75
81
  }
82
+
83
+ function copyStartupFiles() {
84
+ const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
85
+
86
+ for (const file of startupFiles) {
87
+ const targetDir = path.join(os.homedir(), ".safe-chain", "scripts");
88
+ const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file);
89
+
90
+ if (!fs.existsSync(targetDir)) {
91
+ fs.mkdirSync(targetDir, { recursive: true });
92
+ }
93
+
94
+ // Use absolute path for source
95
+ const __filename = fileURLToPath(import.meta.url);
96
+ const __dirname = path.dirname(__filename);
97
+ const sourcePath = path.resolve(__dirname, "startup-scripts", file);
98
+ fs.copyFileSync(sourcePath, targetPath);
99
+ }
100
+ }
@@ -0,0 +1,58 @@
1
+ function printSafeChainWarning
2
+ set original_cmd $argv[1]
3
+
4
+ # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
5
+ set_color -b yellow black
6
+ printf "Warning:"
7
+ set_color normal
8
+ printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
9
+
10
+ # Cyan text for the install command
11
+ printf "Install safe-chain by using "
12
+ set_color cyan
13
+ printf "npm install -g @aikidosec/safe-chain"
14
+ set_color normal
15
+ printf ".\n"
16
+ end
17
+
18
+ function wrapSafeChainCommand
19
+ set original_cmd $argv[1]
20
+ set aikido_cmd $argv[2]
21
+ set cmd_args $argv[3..-1]
22
+
23
+ if type -q $aikido_cmd
24
+ # If the aikido command is available, just run it with the provided arguments
25
+ $aikido_cmd $cmd_args
26
+ else
27
+ # If the aikido command is not available, print a warning and run the original command
28
+ printSafeChainWarning $original_cmd
29
+ command $original_cmd $cmd_args
30
+ end
31
+ end
32
+
33
+ function npx
34
+ wrapSafeChainCommand "npx" "aikido-npx" $argv
35
+ end
36
+
37
+ function yarn
38
+ wrapSafeChainCommand "yarn" "aikido-yarn" $argv
39
+ end
40
+
41
+ function pnpm
42
+ wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
43
+ end
44
+
45
+ function pnpx
46
+ wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
47
+ end
48
+
49
+ function npm
50
+ if test (count $argv) -eq 1 -a \( "$argv[1]" = "-v" -o "$argv[1]" = "--version" \)
51
+ # If args is just -v or --version and nothing else, just run the npm version command
52
+ # This is because nvm uses this to check the version of npm
53
+ command npm $argv
54
+ return
55
+ end
56
+
57
+ wrapSafeChainCommand "npm" "aikido-npm" $argv
58
+ end
@@ -0,0 +1,54 @@
1
+
2
+ function printSafeChainWarning() {
3
+ # \033[43;30m is used to set the background color to yellow and text color to black
4
+ # \033[0m is used to reset the text formatting
5
+ printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1"
6
+ # \033[36m is used to set the text color to cyan
7
+ printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n"
8
+ }
9
+
10
+ function wrapSafeChainCommand() {
11
+ local original_cmd="$1"
12
+ local aikido_cmd="$2"
13
+
14
+ # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
15
+ # so that "$@" now contains only the arguments passed to the original command
16
+ shift 2
17
+
18
+ if command -v "$aikido_cmd" > /dev/null 2>&1; then
19
+ # If the aikido command is available, just run it with the provided arguments
20
+ "$aikido_cmd" "$@"
21
+ else
22
+ # If the aikido command is not available, print a warning and run the original command
23
+ printSafeChainWarning "$original_cmd"
24
+
25
+ command "$original_cmd" "$@"
26
+ fi
27
+ }
28
+
29
+ function npx() {
30
+ wrapSafeChainCommand "npx" "aikido-npx" "$@"
31
+ }
32
+
33
+ function yarn() {
34
+ wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
35
+ }
36
+
37
+ function pnpm() {
38
+ wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
39
+ }
40
+
41
+ function pnpx() {
42
+ wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
43
+ }
44
+
45
+ function npm() {
46
+ if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
47
+ # If args is just -v or --version and nothing else, just run the npm version command
48
+ # This is because nvm uses this to check the version of npm
49
+ command npm "$@"
50
+ return
51
+ fi
52
+
53
+ wrapSafeChainCommand "npm" "aikido-npm" "$@"
54
+ }
@@ -0,0 +1,80 @@
1
+ function Write-SafeChainWarning {
2
+ param([string]$Command)
3
+
4
+ # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:"
5
+ Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
6
+ Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it."
7
+
8
+ # Cyan text for the install command
9
+ Write-Host "Install safe-chain by using " -NoNewline
10
+ Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline
11
+ Write-Host "."
12
+ }
13
+
14
+ function Test-CommandAvailable {
15
+ param([string]$Command)
16
+
17
+ try {
18
+ Get-Command $Command -ErrorAction Stop | Out-Null
19
+ return $true
20
+ }
21
+ catch {
22
+ return $false
23
+ }
24
+ }
25
+
26
+ function Invoke-RealCommand {
27
+ param(
28
+ [string]$Command,
29
+ [string[]]$Arguments
30
+ )
31
+
32
+ # Find the real executable to avoid calling our wrapped functions
33
+ $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1
34
+ if ($realCommand) {
35
+ & $realCommand.Source @Arguments
36
+ }
37
+ }
38
+
39
+ function Invoke-WrappedCommand {
40
+ param(
41
+ [string]$OriginalCmd,
42
+ [string]$AikidoCmd,
43
+ [string[]]$Arguments
44
+ )
45
+
46
+ if (Test-CommandAvailable $AikidoCmd) {
47
+ & $AikidoCmd @Arguments
48
+ }
49
+ else {
50
+ Write-SafeChainWarning $OriginalCmd
51
+ Invoke-RealCommand $OriginalCmd $Arguments
52
+ }
53
+ }
54
+
55
+ function npx {
56
+ Invoke-WrappedCommand "npx" "aikido-npx" $args
57
+ }
58
+
59
+ function yarn {
60
+ Invoke-WrappedCommand "yarn" "aikido-yarn" $args
61
+ }
62
+
63
+ function pnpm {
64
+ Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
65
+ }
66
+
67
+ function pnpx {
68
+ Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
69
+ }
70
+
71
+ function npm {
72
+ # If args is just -v or --version and nothing else, just run the npm version command
73
+ # This is because nvm uses this to check the version of npm
74
+ if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
75
+ Invoke-RealCommand "npm" $args
76
+ return
77
+ }
78
+
79
+ Invoke-WrappedCommand "npm" "aikido-npm" $args
80
+ }
@@ -21,18 +21,22 @@ function teardown(tools) {
21
21
  removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
22
22
  }
23
23
 
24
+ // Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh)
25
+ removeLinesMatchingPattern(
26
+ startupFile,
27
+ /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/
28
+ );
29
+
24
30
  return true;
25
31
  }
26
32
 
27
- function setup(tools) {
33
+ function setup() {
28
34
  const startupFile = getStartupFile();
29
35
 
30
- for (const { tool, aikidoCommand } of tools) {
31
- addLineToFile(
32
- startupFile,
33
- `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}`
34
- );
35
- }
36
+ addLineToFile(
37
+ startupFile,
38
+ `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`
39
+ );
36
40
 
37
41
  return true;
38
42
  }
@@ -66,38 +66,17 @@ describe("Bash shell integration", () => {
66
66
  });
67
67
 
68
68
  describe("setup", () => {
69
- it("should add aliases for all provided tools", () => {
70
- const tools = [
71
- { tool: "npm", aikidoCommand: "aikido-npm" },
72
- { tool: "npx", aikidoCommand: "aikido-npx" },
73
- { tool: "yarn", aikidoCommand: "aikido-yarn" },
74
- ];
75
-
76
- const result = bash.setup(tools);
69
+ it("should add source line for bash initialization script", () => {
70
+ const result = bash.setup();
77
71
  assert.strictEqual(result, true);
78
72
 
79
73
  const content = fs.readFileSync(mockStartupFile, "utf-8");
80
74
  assert.ok(
81
- content.includes('alias npm="aikido-npm" # Safe-chain alias for npm')
82
- );
83
- assert.ok(
84
- content.includes('alias npx="aikido-npx" # Safe-chain alias for npx')
85
- );
86
- assert.ok(
87
- content.includes('alias yarn="aikido-yarn" # Safe-chain alias for yarn')
75
+ content.includes(
76
+ "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
77
+ )
88
78
  );
89
79
  });
90
-
91
- it("should handle empty tools array", () => {
92
- const result = bash.setup([]);
93
- assert.strictEqual(result, true);
94
-
95
- // File should be created during teardown call even if no tools are provided
96
- if (fs.existsSync(mockStartupFile)) {
97
- const content = fs.readFileSync(mockStartupFile, "utf-8");
98
- assert.strictEqual(content.trim(), "");
99
- }
100
- });
101
80
  });
102
81
 
103
82
  describe("teardown", () => {
@@ -174,14 +153,14 @@ describe("Bash shell integration", () => {
174
153
  // Setup
175
154
  bash.setup(tools);
176
155
  let content = fs.readFileSync(mockStartupFile, "utf-8");
177
- assert.ok(content.includes('alias npm="aikido-npm"'));
178
- assert.ok(content.includes('alias yarn="aikido-yarn"'));
156
+ assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
179
157
 
180
158
  // Teardown
181
159
  bash.teardown(tools);
182
160
  content = fs.readFileSync(mockStartupFile, "utf-8");
183
- assert.ok(!content.includes("alias npm="));
184
- assert.ok(!content.includes("alias yarn="));
161
+ assert.ok(
162
+ !content.includes("source ~/.safe-chain/scripts/init-posix.sh")
163
+ );
185
164
  });
186
165
 
187
166
  it("should handle multiple setup calls", () => {
@@ -192,8 +171,29 @@ describe("Bash shell integration", () => {
192
171
  bash.setup(tools);
193
172
 
194
173
  const content = fs.readFileSync(mockStartupFile, "utf-8");
195
- const npmMatches = (content.match(/alias npm="/g) || []).length;
196
- assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
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
197
  });
198
198
  });
199
199
  });