diff --git a/README.md b/README.md index 3e73137..ba3ec47 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,19 @@ The base URL should point to a server that mirrors the structure of `https://mal - `/releases/npm.json` (JavaScript new packages list) - `/releases/pypi.json` (Python new packages list) +## Custom Install Directory + +By default, Safe Chain installs itself into `~/.safe-chain`. You can change this by setting `SAFE_CHAIN_DIR` before running the installer. This is useful for system-wide installations (e.g. inside a Docker image) or when you need to avoid conflicts with other tools. + +When set, all Safe Chain data (binary, shims, scripts) is placed under the custom directory instead of `~/.safe-chain`. + +```shell +export SAFE_CHAIN_DIR=/usr/local/.safe-chain +curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh +``` + +> **Note:** CLI argument and config file options are not supported for `SAFE_CHAIN_DIR`. The config file lives inside the Safe Chain directory itself, creating a chicken-and-egg problem, and passing a directory path as a flag to package manager commands (e.g. `npm install express --safe-chain-dir=...`) does not make sense. + # Usage in CI/CD You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. @@ -406,6 +419,7 @@ pipeline { environment { // Jenkins does not automatically persist PATH updates from setup-ci, // so add the shims + binary directory explicitly for all stages. + // If you set SAFE_CHAIN_DIR, replace ~/.safe-chain with that path here. PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}" } @@ -461,7 +475,7 @@ To add safe-chain in GitLab pipelines, you need to install it in the image runni # Install safe-chain RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - # Add safe-chain to PATH + # Add safe-chain to PATH (update paths if you set SAFE_CHAIN_DIR during install) ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}" ``` diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index ffe2505..f95fdfd 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -8,7 +8,8 @@ param( ) $Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set -$InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" +$SafeChainBase = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $env:USERPROFILE ".safe-chain" } +$InstallDir = Join-Path $SafeChainBase "bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" # Ensure TLS 1.2 is enabled for downloads diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 182cdad..f65b1d7 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -8,7 +8,8 @@ set -e # Exit on error # Configuration VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set -INSTALL_DIR="${HOME}/.safe-chain/bin" +SAFE_CHAIN_BASE="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}" +INSTALL_DIR="${SAFE_CHAIN_BASE}/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" # Colors for output diff --git a/install-scripts/uninstall-safe-chain.ps1 b/install-scripts/uninstall-safe-chain.ps1 index 3292cdd..32a27a5 100644 --- a/install-scripts/uninstall-safe-chain.ps1 +++ b/install-scripts/uninstall-safe-chain.ps1 @@ -4,7 +4,7 @@ # Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform) $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } -$DotSafeChain = Join-Path $HomeDir ".safe-chain" +$DotSafeChain = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $HomeDir ".safe-chain" } $InstallDir = Join-Path $DotSafeChain "bin" # Helper functions diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index dff6f31..fcb5153 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -7,7 +7,7 @@ set -e # Exit on error # Configuration -DOT_SAFE_CHAIN="${HOME}/.safe-chain" +DOT_SAFE_CHAIN="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}" # Colors for output RED='\033[0;31m' @@ -163,6 +163,7 @@ main() { else info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove." fi + } main "$@" diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 3fb0f21..1b978ea 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,6 +3,7 @@ import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; import { getEcoSystem } from "./settings.js"; +import { getSafeChainDir } from "./environmentVariables.js"; /** * @typedef {Object} SafeChainConfig @@ -304,8 +305,7 @@ function getConfigFilePath() { * @returns {string} */ export function getSafeChainDirectory() { - const homeDir = os.homedir(); - const safeChainDir = path.join(homeDir, ".safe-chain"); + const safeChainDir = getSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); if (!fs.existsSync(safeChainDir)) { fs.mkdirSync(safeChainDir, { recursive: true }); diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 7ccfd99..2d66d1d 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -4,6 +4,7 @@ import fs from "fs"; import path from "path"; import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; import { getSafeChainDir } from "../config/environmentVariables.js"; +export { getSafeChainDir }; import { safeSpawn } from "../utils/safeSpawn.js"; import { ui } from "../environment/userInteraction.js"; diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index 66c6533..120723a 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -122,7 +122,6 @@ function copyStartupFiles() { fs.mkdirSync(targetDir, { recursive: true }); } - // Use absolute path for source const sourcePath = path.join(dirname, "startup-scripts", file); fs.copyFileSync(sourcePath, targetPath); } diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.js index 364323e..4f04c5e 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -3,6 +3,7 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, getScriptsDir, + getSafeChainDir, } from "../helpers.js"; import { execSync, spawnSync } from "child_process"; import * as os from "os"; @@ -41,12 +42,27 @@ function teardown(tools) { eol ); + removeLinesMatchingPattern( + startupFile, + /^export\s+SAFE_CHAIN_DIR=.*#\s*Safe-chain/, + eol + ); + return true; } function setup() { const startupFile = getStartupFile(); + const customDir = getSafeChainDir(); + if (customDir) { + addLineToFile( + startupFile, + `export SAFE_CHAIN_DIR="${customDir}" # Safe-chain installation directory`, + eol + ); + } + addLineToFile( startupFile, `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain bash initialization script`, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js index f0a56d2..a8cd067 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js @@ -10,6 +10,7 @@ describe("Bash shell integration", () => { let bash; let windowsCygwinPath = ""; let platform = "linux"; + let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -20,6 +21,7 @@ describe("Bash shell integration", () => { namedExports: { doesExecutableExistOnSystem: () => true, getScriptsDir: () => "/test-home/.safe-chain/scripts", + getSafeChainDir: () => getSafeChainDirResult, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -89,6 +91,7 @@ describe("Bash shell integration", () => { // Reset mocks mock.reset(); platform = "linux"; + getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -200,6 +203,40 @@ describe("Bash shell integration", () => { }); }); + describe("SAFE_CHAIN_DIR", () => { + it("should write export line to rc file when custom dir is set", () => { + getSafeChainDirResult = "/custom/safe-chain"; + bash.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + content.includes('export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory') + ); + }); + + it("should not write export line when no custom dir is set", () => { + getSafeChainDirResult = undefined; + bash.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + + it("should remove export line on teardown", () => { + const initialContent = [ + '#!/bin/bash', + 'export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory', + 'source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script', + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + bash.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + }); + describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.js index 5f59826..bac8e7b 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.js @@ -3,6 +3,7 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, getScriptsDir, + getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -40,12 +41,27 @@ function teardown(tools) { eol ); + removeLinesMatchingPattern( + startupFile, + /^set\s+-gx\s+SAFE_CHAIN_DIR\s+.*#\s*Safe-chain/, + eol + ); + return true; } function setup() { const startupFile = getStartupFile(); + const customDir = getSafeChainDir(); + if (customDir) { + addLineToFile( + startupFile, + `set -gx SAFE_CHAIN_DIR "${customDir}" # Safe-chain installation directory`, + eol + ); + } + addLineToFile( startupFile, `source ${path.join(getScriptsDir(), "init-fish.fish")} # Safe-chain Fish initialization script`, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js index 0933b6e..c9918c5 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js @@ -8,6 +8,7 @@ import { knownAikidoTools } from "../helpers.js"; describe("Fish shell integration", () => { let mockStartupFile; let fish; + let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -18,6 +19,7 @@ describe("Fish shell integration", () => { namedExports: { doesExecutableExistOnSystem: () => true, getScriptsDir: () => "/test-home/.safe-chain/scripts", + getSafeChainDir: () => getSafeChainDirResult, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -53,6 +55,7 @@ describe("Fish shell integration", () => { // Reset mocks mock.reset(); + getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -153,6 +156,39 @@ describe("Fish shell integration", () => { }); }); + describe("SAFE_CHAIN_DIR", () => { + it("should write set line to config file when custom dir is set", () => { + getSafeChainDirResult = "/custom/safe-chain"; + fish.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + content.includes('set -gx SAFE_CHAIN_DIR "/custom/safe-chain" # Safe-chain installation directory') + ); + }); + + it("should not write set line when no custom dir is set", () => { + getSafeChainDirResult = undefined; + fish.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + + it("should remove set line on teardown", () => { + const initialContent = [ + 'set -gx SAFE_CHAIN_DIR "/custom/safe-chain" # Safe-chain installation directory', + "source /test-home/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + fish.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + }); + describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js index 59aee41..38b0b42 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -4,6 +4,7 @@ import { removeLinesMatchingPattern, validatePowerShellExecutionPolicy, getScriptsDir, + getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -38,6 +39,11 @@ function teardown(tools) { /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/, ); + removeLinesMatchingPattern( + startupFile, + /^\$env:SAFE_CHAIN_DIR\s*=.*#\s*Safe-chain/, + ); + return true; } @@ -52,6 +58,14 @@ async function setup() { const startupFile = getStartupFile(); + const customDir = getSafeChainDir(); + if (customDir) { + addLineToFile( + startupFile, + `$env:SAFE_CHAIN_DIR = '${customDir}' # Safe-chain installation directory`, + ); + } + addLineToFile( startupFile, `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js index 1d9f65c..97901f1 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js @@ -9,6 +9,7 @@ describe("PowerShell Core shell integration", () => { let mockStartupFile; let powershell; let executionPolicyResult; + let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -26,6 +27,7 @@ describe("PowerShell Core shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, + getSafeChainDir: () => getSafeChainDirResult, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -63,6 +65,7 @@ describe("PowerShell Core shell integration", () => { // Reset mocks mock.reset(); + getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -206,6 +209,40 @@ describe("PowerShell Core shell integration", () => { }); }); + describe("SAFE_CHAIN_DIR", () => { + it("should write $env:SAFE_CHAIN_DIR line to profile when custom dir is set", async () => { + getSafeChainDirResult = "C:\\custom\\safe-chain"; + await powershell.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + content.includes("$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory") + ); + }); + + it("should not write $env:SAFE_CHAIN_DIR line when no custom dir is set", async () => { + getSafeChainDirResult = undefined; + await powershell.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + + it("should remove $env:SAFE_CHAIN_DIR line on teardown", () => { + const initialContent = [ + "# PowerShell profile", + "$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory", + '. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script', + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + powershell.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + }); + describe("execution policy", () => { it(`should throw for restricted policies`, async () => { executionPolicyResult = { diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js index 36ab114..506b891 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -4,6 +4,7 @@ import { removeLinesMatchingPattern, validatePowerShellExecutionPolicy, getScriptsDir, + getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -38,6 +39,11 @@ function teardown(tools) { /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/, ); + removeLinesMatchingPattern( + startupFile, + /^\$env:SAFE_CHAIN_DIR\s*=.*#\s*Safe-chain/, + ); + return true; } @@ -52,6 +58,14 @@ async function setup() { const startupFile = getStartupFile(); + const customDir = getSafeChainDir(); + if (customDir) { + addLineToFile( + startupFile, + `$env:SAFE_CHAIN_DIR = '${customDir}' # Safe-chain installation directory`, + ); + } + addLineToFile( startupFile, `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js index 621b380..efb5cc3 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -9,6 +9,7 @@ describe("Windows PowerShell shell integration", () => { let mockStartupFile; let windowsPowershell; let executionPolicyResult; + let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -26,6 +27,7 @@ describe("Windows PowerShell shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, + getSafeChainDir: () => getSafeChainDirResult, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -63,6 +65,7 @@ describe("Windows PowerShell shell integration", () => { // Reset mocks mock.reset(); + getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -206,6 +209,40 @@ describe("Windows PowerShell shell integration", () => { }); }); + describe("SAFE_CHAIN_DIR", () => { + it("should write $env:SAFE_CHAIN_DIR line to profile when custom dir is set", async () => { + getSafeChainDirResult = "C:\\custom\\safe-chain"; + await windowsPowershell.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + content.includes("$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory") + ); + }); + + it("should not write $env:SAFE_CHAIN_DIR line when no custom dir is set", async () => { + getSafeChainDirResult = undefined; + await windowsPowershell.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + + it("should remove $env:SAFE_CHAIN_DIR line on teardown", () => { + const initialContent = [ + "# Windows PowerShell profile", + "$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory", + '. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script', + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + windowsPowershell.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + }); + describe("execution policy", () => { it(`should throw for restricted policies`, async () => { executionPolicyResult = { diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js index 369b445..a340424 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -3,6 +3,7 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, getScriptsDir, + getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -40,12 +41,27 @@ function teardown(tools) { eol ); + removeLinesMatchingPattern( + startupFile, + /^export\s+SAFE_CHAIN_DIR=.*#\s*Safe-chain/, + eol + ); + return true; } function setup() { const startupFile = getStartupFile(); + const customDir = getSafeChainDir(); + if (customDir) { + addLineToFile( + startupFile, + `export SAFE_CHAIN_DIR="${customDir}" # Safe-chain installation directory`, + eol + ); + } + addLineToFile( startupFile, `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain Zsh initialization script`, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js index 41e1bd1..4f1ca88 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js @@ -8,6 +8,7 @@ import { knownAikidoTools } from "../helpers.js"; describe("Zsh shell integration", () => { let mockStartupFile; let zsh; + let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -18,6 +19,7 @@ describe("Zsh shell integration", () => { namedExports: { doesExecutableExistOnSystem: () => true, getScriptsDir: () => "/test-home/.safe-chain/scripts", + getSafeChainDir: () => getSafeChainDirResult, addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -53,6 +55,7 @@ describe("Zsh shell integration", () => { // Reset mocks mock.reset(); + getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -171,6 +174,40 @@ describe("Zsh shell integration", () => { }); }); + describe("SAFE_CHAIN_DIR", () => { + it("should write export line to rc file when custom dir is set", () => { + getSafeChainDirResult = "/custom/safe-chain"; + zsh.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok( + content.includes('export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory') + ); + }); + + it("should not write export line when no custom dir is set", () => { + getSafeChainDirResult = undefined; + zsh.setup(); + + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + + it("should remove export line on teardown", () => { + const initialContent = [ + "#!/bin/zsh", + 'export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory', + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script", + ].join("\n"); + + fs.writeFileSync(mockStartupFile, initialContent, "utf-8"); + + zsh.teardown(knownAikidoTools); + const content = fs.readFileSync(mockStartupFile, "utf-8"); + assert.ok(!content.includes("SAFE_CHAIN_DIR")); + }); + }); + describe("integration tests", () => { it("should handle complete setup and teardown cycle", () => { const tools = [ diff --git a/packages/safe-chain/src/shell-integration/teardown.js b/packages/safe-chain/src/shell-integration/teardown.js index bcf6346..e5f149d 100644 --- a/packages/safe-chain/src/shell-integration/teardown.js +++ b/packages/safe-chain/src/shell-integration/teardown.js @@ -109,4 +109,5 @@ export async function teardownDirectories() { ); } } + }