From d064d46668e2cfc16beca460842a90ddadb6a81f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 13 Apr 2026 11:01:45 -0700 Subject: [PATCH] Cleanup --- README.md | 17 ++-- install-scripts/install-safe-chain.ps1 | 46 +++++++--- install-scripts/install-safe-chain.sh | 76 ++++++++++++---- install-scripts/uninstall-safe-chain.ps1 | 76 ++++++++++++---- install-scripts/uninstall-safe-chain.sh | 89 +++++++++++++++---- packages/safe-chain/bin/safe-chain.js | 19 +++- packages/safe-chain/src/config/configFile.js | 4 +- .../src/config/environmentVariables.js | 11 --- .../src/config/environmentVariables.spec.js | 30 ------- .../safe-chain/src/config/safeChainDir.js | 10 +++ packages/safe-chain/src/installLocation.js | 39 ++++++++ .../safe-chain/src/installLocation.spec.js | 51 +++++++++++ .../safe-chain/src/registryProxy/certUtils.js | 6 +- .../src/registryProxy/certUtils.spec.js | 19 ++-- .../src/shell-integration/helpers.js | 9 +- .../src/shell-integration/helpers.spec.js | 43 +-------- .../templates/unix-wrapper.template.sh | 8 +- .../templates/windows-wrapper.template.cmd | 3 +- .../src/shell-integration/setup-ci.js | 6 +- .../startup-scripts/init-fish.fish | 7 +- .../startup-scripts/init-posix.sh | 24 +++-- .../startup-scripts/init-pwsh.ps1 | 3 +- .../supported-shells/bash.js | 23 +---- .../supported-shells/bash.spec.js | 24 ++--- .../supported-shells/fish.js | 23 +---- .../supported-shells/fish.spec.js | 24 ++--- .../supported-shells/powershell.js | 22 +---- .../supported-shells/powershell.spec.js | 24 ++--- .../supported-shells/windowsPowershell.js | 22 +---- .../windowsPowershell.spec.js | 24 ++--- .../shell-integration/supported-shells/zsh.js | 23 +---- .../supported-shells/zsh.spec.js | 24 ++--- 32 files changed, 429 insertions(+), 400 deletions(-) delete mode 100644 packages/safe-chain/src/config/environmentVariables.spec.js create mode 100644 packages/safe-chain/src/config/safeChainDir.js create mode 100644 packages/safe-chain/src/installLocation.js create mode 100644 packages/safe-chain/src/installLocation.spec.js diff --git a/README.md b/README.md index 0dc3f40..6a39aea 100644 --- a/README.md +++ b/README.md @@ -318,16 +318,21 @@ The base URL should point to a server that mirrors the structure of `https://mal ## 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. +By default, Safe Chain installs itself into `~/.safe-chain`. You can change this by passing an explicit install directory to 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, config) 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 +curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --install-dir /usr/local/.safe-chain ``` -This is a **one-time setting**. `safe-chain setup` automatically persists `SAFE_CHAIN_DIR` to your shell rc files (e.g. `~/.bashrc`, `~/.zshrc`) so that subsequent `safe-chain` commands (including teardown and re-setup) find the correct directory without needing the variable set again. +On Windows, use `-InstallDir`: + +```powershell +iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -InstallDir 'C:\ProgramData\safe-chain'" +``` + +This is a one-time installer choice. Runtime shell integration and uninstall now discover the installation from the installed scripts or binary and do not rely on an environment variable. # Usage in CI/CD @@ -419,7 +424,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. + // If you installed into a custom directory, replace ~/.safe-chain with that path here. PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}" } @@ -475,7 +480,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 (update paths if you set SAFE_CHAIN_DIR during install) + # Add safe-chain to PATH (update paths if you used a custom install dir) 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 4e77df4..ec0dcd6 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -4,25 +4,49 @@ param( [switch]$ci, - [switch]$includepython + [switch]$includepython, + [string]$InstallDir ) -$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set +function Test-InstallDir { + param([string]$Dir) -# Validate SAFE_CHAIN_DIR before use -if ($env:SAFE_CHAIN_DIR) { - if (-not [System.IO.Path]::IsPathRooted($env:SAFE_CHAIN_DIR)) { - Write-Host "[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: $($env:SAFE_CHAIN_DIR)" -ForegroundColor Red; exit 1 + if ([string]::IsNullOrWhiteSpace($Dir)) { + return @{ Ok = $true; Normalized = $null } } - if ($env:SAFE_CHAIN_DIR -match '\.\.') { - Write-Host "[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)" -ForegroundColor Red; exit 1 + + if (-not [System.IO.Path]::IsPathRooted($Dir)) { + return @{ Ok = $false; Reason = "-InstallDir must be an absolute path, got: $Dir" } } - if ($env:SAFE_CHAIN_DIR -match '^[A-Za-z]:[/\\]?$' -or $env:SAFE_CHAIN_DIR -eq '/') { - Write-Host "[ERROR] SAFE_CHAIN_DIR cannot be a root or drive-root directory" -ForegroundColor Red; exit 1 + + if ($Dir.Contains([System.IO.Path]::PathSeparator)) { + return @{ Ok = $false; Reason = "-InstallDir must not contain the PATH separator ($([System.IO.Path]::PathSeparator))" } } + + $normalized = [System.IO.Path]::GetFullPath($Dir) + $root = [System.IO.Path]::GetPathRoot($normalized) + if ($normalized.TrimEnd('\', '/') -eq $root.TrimEnd('\', '/')) { + return @{ Ok = $false; Reason = "-InstallDir cannot be a root or drive-root directory" } + } + + $segments = $normalized.Substring($root.Length).Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries) + if ($segments -contains "..") { + return @{ Ok = $false; Reason = "-InstallDir must not contain path traversal segments" } + } + + return @{ Ok = $true; Normalized = $normalized } } -$SafeChainBase = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $env:USERPROFILE ".safe-chain" } +$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set +$SafeChainBase = if ($InstallDir) { $InstallDir } else { Join-Path $env:USERPROFILE ".safe-chain" } + +$installDirValidation = Test-InstallDir -Dir $SafeChainBase +if (-not $installDirValidation.Ok) { + Write-Host "[ERROR] $($installDirValidation.Reason)" -ForegroundColor Red + exit 1 +} + +$SafeChainBase = $installDirValidation.Normalized $InstallDir = Join-Path $SafeChainBase "bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 03923d8..6a586e7 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -6,24 +6,50 @@ set -e # Exit on error +validate_install_dir() { + dir="$1" + + if [ -z "$dir" ]; then + return 0 + fi + + case "$dir" in + /*) ;; + *) + printf '[ERROR] --install-dir must be an absolute path, got: %s\n' "$dir" >&2 + exit 1 + ;; + esac + + case "$dir" in + *:*) + printf '[ERROR] --install-dir must not contain the PATH separator (:)\n' >&2 + exit 1 + ;; + esac + + if [ "$dir" = "/" ]; then + printf '[ERROR] --install-dir cannot be a root or drive-root directory\n' >&2 + exit 1 + fi + + old_ifs=$IFS + IFS='/' + set -- $dir + IFS=$old_ifs + + for segment in "$@"; do + if [ "$segment" = ".." ]; then + printf '[ERROR] --install-dir must not contain path traversal segments\n' >&2 + exit 1 + fi + done +} + # Configuration VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set +SAFE_CHAIN_BASE="${HOME}/.safe-chain" -# Validate SAFE_CHAIN_DIR before use -if [ -n "${SAFE_CHAIN_DIR}" ]; then - case "${SAFE_CHAIN_DIR}" in - /*) ;; - *) printf '[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: %s\n' "${SAFE_CHAIN_DIR}" >&2; exit 1 ;; - esac - case "${SAFE_CHAIN_DIR}" in - *../*|*/..*|..) printf '[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)\n' >&2; exit 1 ;; - esac - if [ "${SAFE_CHAIN_DIR}" = "/" ]; then - printf '[ERROR] SAFE_CHAIN_DIR cannot be the root directory\n' >&2; exit 1 - fi -fi - -SAFE_CHAIN_BASE="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}" INSTALL_DIR="${SAFE_CHAIN_BASE}/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" @@ -245,19 +271,33 @@ remove_nvm_installation() { # Parse command-line arguments parse_arguments() { - for arg in "$@"; do - case "$arg" in + while [ $# -gt 0 ]; do + case "$1" in --ci) USE_CI_SETUP=true ;; + --install-dir) + shift + if [ $# -eq 0 ]; then + error "Missing value for --install-dir" + fi + SAFE_CHAIN_BASE="$1" + ;; + --install-dir=*) + SAFE_CHAIN_BASE="${1#--install-dir=}" + ;; --include-python) warn "--include-python is deprecated and ignored. Python ecosystem is now included by default." ;; *) - error "Unknown argument: $arg" + error "Unknown argument: $1" ;; esac + shift done + + validate_install_dir "${SAFE_CHAIN_BASE}" + INSTALL_DIR="${SAFE_CHAIN_BASE}/bin" } # Main installation diff --git a/install-scripts/uninstall-safe-chain.ps1 b/install-scripts/uninstall-safe-chain.ps1 index 785e58a..2aa3798 100644 --- a/install-scripts/uninstall-safe-chain.ps1 +++ b/install-scripts/uninstall-safe-chain.ps1 @@ -5,22 +5,6 @@ # Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform) $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } -# Validate SAFE_CHAIN_DIR before use -if ($env:SAFE_CHAIN_DIR) { - if (-not [System.IO.Path]::IsPathRooted($env:SAFE_CHAIN_DIR)) { - Write-Host "[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: $($env:SAFE_CHAIN_DIR)" -ForegroundColor Red; exit 1 - } - if ($env:SAFE_CHAIN_DIR -match '\.\.') { - Write-Host "[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)" -ForegroundColor Red; exit 1 - } - if ($env:SAFE_CHAIN_DIR -match '^[A-Za-z]:[/\\]?$' -or $env:SAFE_CHAIN_DIR -eq '/') { - Write-Host "[ERROR] SAFE_CHAIN_DIR cannot be a root or drive-root directory" -ForegroundColor Red; exit 1 - } -} - -$DotSafeChain = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $HomeDir ".safe-chain" } -$InstallDir = Join-Path $DotSafeChain "bin" - # Helper functions function Write-Info { param([string]$Message) @@ -38,6 +22,64 @@ function Write-Error-Custom { exit 1 } +function Get-InstallDirFromBinaryPath { + param([string]$BinaryPath) + + if ([string]::IsNullOrWhiteSpace($BinaryPath)) { + return $null + } + + try { + $resolvedPath = (Resolve-Path -LiteralPath $BinaryPath -ErrorAction Stop).Path + } + catch { + $resolvedPath = [System.IO.Path]::GetFullPath($BinaryPath) + } + + $fileName = [System.IO.Path]::GetFileName($resolvedPath) + if (($fileName -ne "safe-chain") -and ($fileName -ne "safe-chain.exe")) { + return $null + } + + if ($resolvedPath -match '\.(js|cjs|mjs|cmd|ps1)$') { + return $null + } + + $binDir = Split-Path -Parent $resolvedPath + if ((Split-Path -Leaf $binDir) -ne "bin") { + return $null + } + + return (Split-Path -Parent $binDir) +} + +function Get-SafeChainInstallDir { + $command = Get-Command safe-chain -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($command) { + try { + $reportedInstallDir = & safe-chain get-install-dir 2>$null | Select-Object -First 1 + if ($reportedInstallDir) { + $reportedInstallDir = $reportedInstallDir.Trim() + } + if ($reportedInstallDir) { + return $reportedInstallDir + } + } + catch { + # Fall back to deriving the install dir from the discovered command path + } + } + + if ($command -and $command.Path) { + $discoveredInstallDir = Get-InstallDirFromBinaryPath -BinaryPath $command.Path + if ($discoveredInstallDir) { + return $discoveredInstallDir + } + } + + return (Join-Path $HomeDir ".safe-chain") +} + # Check and uninstall npm global package if present function Remove-NpmInstallation { # Check if npm is available @@ -90,6 +132,8 @@ function Remove-VoltaInstallation { # Main uninstallation function Uninstall-SafeChain { Write-Info "Uninstalling safe-chain..." + $DotSafeChain = Get-SafeChainInstallDir + $InstallDir = Join-Path $DotSafeChain "bin" # Run teardown if safe-chain is available # Check for both safe-chain.exe (Windows) and safe-chain (Unix) since PowerShell Core runs on all platforms diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index abde7ca..4169e1e 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -8,22 +8,6 @@ set -e # Exit on error # Configuration -# Validate SAFE_CHAIN_DIR before use -if [ -n "${SAFE_CHAIN_DIR}" ]; then - case "${SAFE_CHAIN_DIR}" in - /*) ;; - *) printf '[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: %s\n' "${SAFE_CHAIN_DIR}" >&2; exit 1 ;; - esac - case "${SAFE_CHAIN_DIR}" in - *../*|*/..*|..) printf '[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)\n' >&2; exit 1 ;; - esac - if [ "${SAFE_CHAIN_DIR}" = "/" ]; then - printf '[ERROR] SAFE_CHAIN_DIR cannot be the root directory\n' >&2; exit 1 - fi -fi - -DOT_SAFE_CHAIN="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}" - # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -49,6 +33,78 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +resolve_path() { + target="$1" + + while [ -L "$target" ]; do + link_target=$(readlink "$target" 2>/dev/null || echo "") + if [ -z "$link_target" ]; then + break + fi + + case "$link_target" in + /*) target="$link_target" ;; + *) + target="$(dirname "$target")/$link_target" + ;; + esac + done + + target_dir=$(dirname "$target") + target_name=$(basename "$target") + + if cd "$target_dir" 2>/dev/null; then + printf '%s/%s\n' "$(pwd -P)" "$target_name" + else + printf '%s\n' "$target" + fi +} + +derive_install_dir_from_binary() { + binary_path="$1" + + if [ -z "$binary_path" ]; then + return 1 + fi + + resolved_path=$(resolve_path "$binary_path") + binary_name=$(basename "$resolved_path") + case "$binary_name" in + safe-chain|safe-chain.exe) ;; + *) return 1 ;; + esac + + case "$resolved_path" in + *.js|*.cjs|*.mjs|*.cmd|*.ps1) return 1 ;; + esac + + binary_dir=$(dirname "$resolved_path") + if [ "$(basename "$binary_dir")" != "bin" ]; then + return 1 + fi + + dirname "$binary_dir" +} + +get_install_dir() { + if command_exists safe-chain; then + install_dir=$(safe-chain get-install-dir 2>/dev/null || true) + if [ -n "$install_dir" ]; then + printf '%s\n' "$install_dir" + return 0 + fi + + command_path=$(command -v safe-chain) + install_dir=$(derive_install_dir_from_binary "$command_path" || true) + if [ -n "$install_dir" ]; then + printf '%s\n' "$install_dir" + return 0 + fi + fi + + printf '%s\n' "${HOME}/.safe-chain" +} + # Check and uninstall npm global package if present remove_npm_installation() { if ! command_exists npm; then @@ -154,6 +210,7 @@ remove_nvm_installation() { # Main uninstallation main() { + DOT_SAFE_CHAIN=$(get_install_dir) SAFE_CHAIN_LOCATION="$DOT_SAFE_CHAIN/bin/safe-chain" if [ -x "$SAFE_CHAIN_LOCATION" ]; then diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 8d942e4..43819b9 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -16,6 +16,7 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import { knownAikidoTools } from "../src/shell-integration/helpers.js"; +import { getInstalledSafeChainDir } from "../src/installLocation.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -67,6 +68,17 @@ if (tool) { teardownDirectories(); } else if (command === "setup-ci") { setupCi(); +} else if (command === "get-install-dir") { + const installDir = getInstalledSafeChainDir(); + if (!installDir) { + ui.writeError( + "Install directory is only available for packaged safe-chain binaries.", + ); + process.exit(1); + } + + ui.writeInformation(installDir); + process.exit(0); } else if (command === "--version" || command === "-v" || command === "-v") { (async () => { ui.writeInformation(`Current safe-chain version: ${await getVersion()}`); @@ -88,7 +100,7 @@ function writeHelp() { ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( "teardown", - )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( + )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("get-install-dir")}, ${chalk.cyan("help")}, ${chalk.cyan( "--version", )}`, ); @@ -108,6 +120,11 @@ function writeHelp() { "safe-chain setup-ci", )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`, ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain get-install-dir", + )}: Print the install directory for packaged safe-chain binaries.`, + ); ui.writeInformation( `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan( "-v", diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 1b978ea..d340130 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,7 +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"; +import { getSafeChainBaseDir } from "./safeChainDir.js"; /** * @typedef {Object} SafeChainConfig @@ -305,7 +305,7 @@ function getConfigFilePath() { * @returns {string} */ export function getSafeChainDirectory() { - const safeChainDir = getSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); + const safeChainDir = getSafeChainBaseDir(); if (!fs.existsSync(safeChainDir)) { fs.mkdirSync(safeChainDir, { recursive: true }); diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index b76a413..932eff7 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -55,14 +55,3 @@ export function getMinimumPackageAgeExclusions() { export function getMalwareListBaseUrl() { return process.env.SAFE_CHAIN_MALWARE_LIST_BASE_URL; } - -/** - * Gets the safe-chain base directory from environment variable. - * When set, all safe-chain data (bin, shims, scripts) will be placed under this directory - * instead of the default ~/.safe-chain, enabling system-wide installations. - * Example: "/usr/local/.safe-chain" - * @returns {string | undefined} - */ -export function getSafeChainDir() { - return process.env.SAFE_CHAIN_DIR; -} diff --git a/packages/safe-chain/src/config/environmentVariables.spec.js b/packages/safe-chain/src/config/environmentVariables.spec.js deleted file mode 100644 index 2cbdd0f..0000000 --- a/packages/safe-chain/src/config/environmentVariables.spec.js +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, beforeEach, afterEach } from "node:test"; -import assert from "node:assert"; - -const { getSafeChainDir } = await import("./environmentVariables.js"); - -describe("getSafeChainDir", () => { - let original; - - beforeEach(() => { - original = process.env.SAFE_CHAIN_DIR; - }); - - afterEach(() => { - if (original !== undefined) { - process.env.SAFE_CHAIN_DIR = original; - } else { - delete process.env.SAFE_CHAIN_DIR; - } - }); - - it("returns undefined when SAFE_CHAIN_DIR is not set", () => { - delete process.env.SAFE_CHAIN_DIR; - assert.strictEqual(getSafeChainDir(), undefined); - }); - - it("returns the value of SAFE_CHAIN_DIR when set", () => { - process.env.SAFE_CHAIN_DIR = "/usr/local/.safe-chain"; - assert.strictEqual(getSafeChainDir(), "/usr/local/.safe-chain"); - }); -}); diff --git a/packages/safe-chain/src/config/safeChainDir.js b/packages/safe-chain/src/config/safeChainDir.js new file mode 100644 index 0000000..595300a --- /dev/null +++ b/packages/safe-chain/src/config/safeChainDir.js @@ -0,0 +1,10 @@ +import os from "os"; +import path from "path"; +import { getInstalledSafeChainDir } from "../installLocation.js"; + +/** + * @returns {string} + */ +export function getSafeChainBaseDir() { + return getInstalledSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); +} diff --git a/packages/safe-chain/src/installLocation.js b/packages/safe-chain/src/installLocation.js new file mode 100644 index 0000000..efe687a --- /dev/null +++ b/packages/safe-chain/src/installLocation.js @@ -0,0 +1,39 @@ +import path from "path"; + +/** + * @param {string} executablePath + * @returns {string | undefined} + */ +export function deriveInstallDirFromExecutablePath(executablePath) { + if (!executablePath) { + return undefined; + } + + const pathLibrary = executablePath.includes("\\") ? path.win32 : path.posix; + const executableDir = pathLibrary.dirname(executablePath); + if (pathLibrary.basename(executableDir) !== "bin") { + return undefined; + } + + return pathLibrary.dirname(executableDir); +} + +/** + * Returns the install directory for a packaged safe-chain binary. + * Custom installation directories only apply to packaged binary installs. + * For npm/global/dev-script executions this intentionally returns undefined, + * which causes callers to fall back to the default ~/.safe-chain layout. + * + * @param {{ isPackaged?: boolean, executablePath?: string }} [options] + * @returns {string | undefined} + */ +export function getInstalledSafeChainDir(options = {}) { + const isPackaged = options.isPackaged ?? Boolean(process.pkg); + if (!isPackaged) { + return undefined; + } + + return deriveInstallDirFromExecutablePath( + options.executablePath ?? process.execPath, + ); +} diff --git a/packages/safe-chain/src/installLocation.spec.js b/packages/safe-chain/src/installLocation.spec.js new file mode 100644 index 0000000..558a05f --- /dev/null +++ b/packages/safe-chain/src/installLocation.spec.js @@ -0,0 +1,51 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { + deriveInstallDirFromExecutablePath, + getInstalledSafeChainDir, +} from "./installLocation.js"; + +describe("deriveInstallDirFromExecutablePath", () => { + it("derives the install dir from a Unix binary path", () => { + assert.strictEqual( + deriveInstallDirFromExecutablePath("/usr/local/.safe-chain/bin/safe-chain"), + "/usr/local/.safe-chain", + ); + }); + + it("derives the install dir from a Windows binary path", () => { + assert.strictEqual( + deriveInstallDirFromExecutablePath("C:\\ProgramData\\safe-chain\\bin\\safe-chain.exe"), + "C:\\ProgramData\\safe-chain", + ); + }); + + it("returns undefined when the executable is not inside a bin directory", () => { + assert.strictEqual( + deriveInstallDirFromExecutablePath("/usr/local/.safe-chain/safe-chain"), + undefined, + ); + }); +}); + +describe("getInstalledSafeChainDir", () => { + it("returns undefined for non-packaged executions", () => { + assert.strictEqual( + getInstalledSafeChainDir({ + isPackaged: false, + executablePath: "/usr/local/.safe-chain/bin/safe-chain", + }), + undefined, + ); + }); + + it("returns the install dir for packaged executions", () => { + assert.strictEqual( + getInstalledSafeChainDir({ + isPackaged: true, + executablePath: "/usr/local/.safe-chain/bin/safe-chain", + }), + "/usr/local/.safe-chain", + ); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index a4bc0b1..50fad7b 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -1,16 +1,14 @@ import forge from "node-forge"; import path from "path"; import fs from "fs"; -import os from "os"; -import { getSafeChainDir } from "../config/environmentVariables.js"; +import { getSafeChainBaseDir } from "../config/safeChainDir.js"; const ca = loadCa(); const certCache = new Map(); function getCertFolder() { - const safeChainDir = getSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); - return path.join(safeChainDir, "certs"); + return path.join(getSafeChainBaseDir(), "certs"); } /** diff --git a/packages/safe-chain/src/registryProxy/certUtils.spec.js b/packages/safe-chain/src/registryProxy/certUtils.spec.js index ebf8dab..c715c8c 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.spec.js +++ b/packages/safe-chain/src/registryProxy/certUtils.spec.js @@ -2,24 +2,23 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; describe("certUtils", () => { - let originalSafeChainDir; + let installedSafeChainDir; beforeEach(() => { - originalSafeChainDir = process.env.SAFE_CHAIN_DIR; + installedSafeChainDir = undefined; + mock.module("../config/safeChainDir.js", { + namedExports: { + getSafeChainBaseDir: () => installedSafeChainDir ?? "/home/test/.safe-chain", + }, + }); }); afterEach(() => { - if (originalSafeChainDir === undefined) { - delete process.env.SAFE_CHAIN_DIR; - } else { - process.env.SAFE_CHAIN_DIR = originalSafeChainDir; - } - mock.reset(); }); - it("stores CA certificates in SAFE_CHAIN_DIR when configured", async () => { - process.env.SAFE_CHAIN_DIR = "/custom/safe-chain"; + it("stores CA certificates in the packaged install dir when available", async () => { + installedSafeChainDir = "/custom/safe-chain"; mock.module("fs", { defaultExport: { diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 2d66d1d..3dd73aa 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,8 +3,7 @@ import * as os from "os"; 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 { getSafeChainBaseDir } from "../config/safeChainDir.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { ui } from "../environment/userInteraction.js"; @@ -125,12 +124,10 @@ export function getPackageManagerList() { /** * Returns the safe-chain base directory. - * Uses SAFE_CHAIN_DIR environment variable when set, otherwise defaults to ~/.safe-chain. + * Uses the packaged binary location when available, otherwise defaults to ~/.safe-chain. * @returns {string} */ -export function getSafeChainBaseDir() { - return getSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); -} +export { getSafeChainBaseDir }; /** * @returns {string} diff --git a/packages/safe-chain/src/shell-integration/helpers.spec.js b/packages/safe-chain/src/shell-integration/helpers.spec.js index 8fd172b..8870451 100644 --- a/packages/safe-chain/src/shell-integration/helpers.spec.js +++ b/packages/safe-chain/src/shell-integration/helpers.spec.js @@ -185,64 +185,23 @@ describe("removeLinesMatchingPatternTests", () => { }); describe("getSafeChainBaseDir / getBinDir / getShimsDir / getScriptsDir", () => { - const customDir = "/usr/local/.safe-chain"; - - let originalSafeChainDir; - - beforeEach(() => { - originalSafeChainDir = process.env.SAFE_CHAIN_DIR; - delete process.env.SAFE_CHAIN_DIR; - }); - - afterEach(() => { - if (originalSafeChainDir !== undefined) { - process.env.SAFE_CHAIN_DIR = originalSafeChainDir; - } else { - delete process.env.SAFE_CHAIN_DIR; - } - }); - - it("defaults base dir to ~/.safe-chain when SAFE_CHAIN_DIR is not set", async () => { + it("defaults base dir to ~/.safe-chain when no packaged install dir is available", async () => { const { getSafeChainBaseDir } = await import("./helpers.js"); assert.strictEqual(getSafeChainBaseDir(), path.join(homedir(), ".safe-chain")); }); - it("uses SAFE_CHAIN_DIR as base dir when set", async () => { - process.env.SAFE_CHAIN_DIR = customDir; - const { getSafeChainBaseDir } = await import("./helpers.js"); - assert.strictEqual(getSafeChainBaseDir(), customDir); - }); - it("getBinDir returns ~/.safe-chain/bin by default", async () => { const { getBinDir } = await import("./helpers.js"); assert.strictEqual(getBinDir(), path.join(homedir(), ".safe-chain", "bin")); }); - it("getBinDir returns custom dir + /bin when SAFE_CHAIN_DIR is set", async () => { - process.env.SAFE_CHAIN_DIR = customDir; - const { getBinDir } = await import("./helpers.js"); - assert.strictEqual(getBinDir(), `${customDir}/bin`); - }); - it("getShimsDir returns ~/.safe-chain/shims by default", async () => { const { getShimsDir } = await import("./helpers.js"); assert.strictEqual(getShimsDir(), path.join(homedir(), ".safe-chain", "shims")); }); - it("getShimsDir returns custom dir + /shims when SAFE_CHAIN_DIR is set", async () => { - process.env.SAFE_CHAIN_DIR = customDir; - const { getShimsDir } = await import("./helpers.js"); - assert.strictEqual(getShimsDir(), `${customDir}/shims`); - }); - it("getScriptsDir returns ~/.safe-chain/scripts by default", async () => { const { getScriptsDir } = await import("./helpers.js"); assert.strictEqual(getScriptsDir(), path.join(homedir(), ".safe-chain", "scripts")); }); - - it("getScriptsDir returns custom dir + /scripts when SAFE_CHAIN_DIR is set", async () => { - process.env.SAFE_CHAIN_DIR = customDir; - const { getScriptsDir } = await import("./helpers.js"); - assert.strictEqual(getScriptsDir(), `${customDir}/scripts`); - }); }); diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh index 5635b1a..9275230 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -4,7 +4,7 @@ # Function to remove shim from PATH (POSIX-compliant) remove_shim_from_path() { - _safe_chain_shims="{{SHIMS_DIR}}" + _safe_chain_shims=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P) echo "$PATH" | sed "s|${_safe_chain_shims}:||g" } @@ -13,11 +13,7 @@ if command -v safe-chain >/dev/null 2>&1; then PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@" else # safe-chain is not reachable — warn the user so they know protection is inactive - if [ -n "$SAFE_CHAIN_DIR" ]; then - printf "\033[43;30mWarning:\033[0m safe-chain is not accessible. Check that '%s/bin' is readable and executable by the current user.\n" "$SAFE_CHAIN_DIR" >&2 - else - printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. {{PACKAGE_MANAGER}} will run without it.\n" >&2 - fi + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. {{PACKAGE_MANAGER}} will run without it.\n" >&2 # Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory) original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}}) diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd index 89f538f..b41fcfb 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd @@ -3,7 +3,8 @@ REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments REM Remove shim directory from PATH to prevent infinite loops -set "SHIM_DIR={{SHIMS_DIR}}" +set "SHIM_DIR=%~dp0" +if "%SHIM_DIR:~-1%"=="\" set "SHIM_DIR=%SHIM_DIR:~0,-1%" call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%" REM Check if aikido command is available with clean PATH diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 0dc32cf..1986bba 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -69,8 +69,7 @@ function createUnixShims(shimsDir) { for (const toolInfo of getToolsToSetup()) { const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) - .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand) - .replaceAll("{{SHIMS_DIR}}", shimsDir); + .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); const shimPath = path.join(shimsDir, toolInfo.tool); fs.writeFileSync(shimPath, shimContent, "utf-8"); @@ -109,8 +108,7 @@ function createWindowsShims(shimsDir) { for (const toolInfo of getToolsToSetup()) { const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) - .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand) - .replaceAll("{{SHIMS_DIR}}", shimsDir); + .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`; fs.writeFileSync(shimPath, shimContent, "utf-8"); diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index 11d1d55..e0cc9ec 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -1,8 +1,5 @@ -# Guard against PATH separator injection: reject SAFE_CHAIN_DIR values containing ':' -set -l safe_chain_base $HOME/.safe-chain -if set -q SAFE_CHAIN_DIR; and not string match -q '*:*' -- $SAFE_CHAIN_DIR - set safe_chain_base $SAFE_CHAIN_DIR -end +set -l safe_chain_script (status filename) +set -l safe_chain_base (path dirname (path dirname $safe_chain_script)) set -gx PATH $PATH $safe_chain_base/bin function npx diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index 45c6fd9..4235276 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -1,10 +1,22 @@ -# Guard against PATH separator injection: reject SAFE_CHAIN_DIR values containing ':' -case "${SAFE_CHAIN_DIR}" in - *:*) _sc_base="${HOME}/.safe-chain" ;; - *) _sc_base="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}" ;; -esac +_get_safe_chain_script_path() { + if [ -n "${BASH_SOURCE[0]:-}" ]; then + printf '%s\n' "${BASH_SOURCE[0]}" + return + fi + + if [ -n "${ZSH_VERSION:-}" ]; then + eval 'printf "%s\n" "${(%):-%N}"' + return + fi + + printf '%s\n' "$0" +} + +_sc_script_path="$(_get_safe_chain_script_path)" +_sc_scripts_dir=$(CDPATH= cd -- "$(dirname -- "$_sc_script_path")" 2>/dev/null && pwd -P) +_sc_base=$(dirname -- "$_sc_scripts_dir") export PATH="$PATH:${_sc_base}/bin" -unset _sc_base +unset _sc_base _sc_script_path _sc_scripts_dir function npx() { wrapSafeChainCommand "npx" "$@" diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index f814917..167e5d8 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -2,8 +2,7 @@ # $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell $isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true } $pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' } -# Guard against PATH separator injection: reject SAFE_CHAIN_DIR values containing the path separator -$safeChainBase = if ($env:SAFE_CHAIN_DIR -and -not $env:SAFE_CHAIN_DIR.Contains($pathSeparator)) { $env:SAFE_CHAIN_DIR } else { Join-Path $HOME '.safe-chain' } +$safeChainBase = Split-Path -Parent $PSScriptRoot $safeChainBin = Join-Path $safeChainBase 'bin' $env:PATH = "$env:PATH$pathSeparator$safeChainBin" 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 ff2266b..fc56025 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -3,7 +3,6 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, getScriptsDir, - getSafeChainDir, } from "../helpers.js"; import { execSync, spawnSync } from "child_process"; import * as os from "os"; @@ -54,15 +53,6 @@ function teardown(tools) { 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`, @@ -143,18 +133,7 @@ function cygpathw(path) { /** @param {string} preamble */ function buildManualInstructions(preamble) { - const customDir = getSafeChainDir(); - const instructions = [preamble]; - - if (customDir) { - instructions.push( - ` export SAFE_CHAIN_DIR="${customDir}"`, - ` source ${path.join(getScriptsDir(), "init-posix.sh")}`, - ); - } else { - instructions.push(` source ~/.safe-chain/scripts/init-posix.sh`); - } - + const instructions = [preamble, ` source ${path.join(getScriptsDir(), "init-posix.sh")}`]; instructions.push(`Then restart your terminal or run: source ~/.bashrc`); return instructions; } 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 4b25d4b..4eaaa6f 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,7 +10,6 @@ describe("Bash shell integration", () => { let bash; let windowsCygwinPath = ""; let platform = "linux"; - let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -21,7 +20,6 @@ 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"); @@ -91,7 +89,6 @@ describe("Bash shell integration", () => { // Reset mocks mock.reset(); platform = "linux"; - getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -203,26 +200,18 @@ 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"; + describe("custom install dir", () => { + it("writes only the source line to the rc file", () => { bash.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory') + content.includes("source /test-home/.safe-chain/scripts/init-posix.sh") ); - }); - - 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", () => { + it("removes legacy export lines on teardown", () => { const initialContent = [ '#!/bin/bash', 'export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory', @@ -236,12 +225,9 @@ describe("Bash shell integration", () => { assert.ok(!content.includes("SAFE_CHAIN_DIR")); }); - it("should show custom manual setup instructions when custom dir is set", () => { - getSafeChainDirResult = "/custom/safe-chain"; - + it("shows source-only manual setup instructions", () => { assert.deepStrictEqual(bash.getManualSetupInstructions(), [ "Add the following line to your ~/.bashrc file:", - ' export SAFE_CHAIN_DIR="/custom/safe-chain"', " source /test-home/.safe-chain/scripts/init-posix.sh", "Then restart your terminal or run: source ~/.bashrc", ]); 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 a6ffe1e..d5ea308 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.js @@ -3,7 +3,6 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, getScriptsDir, - getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -53,15 +52,6 @@ function teardown(tools) { 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`, @@ -86,18 +76,7 @@ function getStartupFile() { /** @param {string} preamble */ function buildManualInstructions(preamble) { - const customDir = getSafeChainDir(); - const instructions = [preamble]; - - if (customDir) { - instructions.push( - ` set -gx SAFE_CHAIN_DIR "${customDir}"`, - ` source ${path.join(getScriptsDir(), "init-fish.fish")}`, - ); - } else { - instructions.push(` source ~/.safe-chain/scripts/init-fish.fish`); - } - + const instructions = [preamble, ` source ${path.join(getScriptsDir(), "init-fish.fish")}`]; instructions.push( `Then restart your terminal or run: source ~/.config/fish/config.fish`, ); 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 29b6d6e..9a30f11 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,7 +8,6 @@ import { knownAikidoTools } from "../helpers.js"; describe("Fish shell integration", () => { let mockStartupFile; let fish; - let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -19,7 +18,6 @@ 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"); @@ -55,7 +53,6 @@ describe("Fish shell integration", () => { // Reset mocks mock.reset(); - getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -156,26 +153,18 @@ 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"; + describe("custom install dir", () => { + it("writes only the source line to the config file", () => { 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') + content.includes("source /test-home/.safe-chain/scripts/init-fish.fish") ); - }); - - 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", () => { + it("removes legacy set lines 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", @@ -188,12 +177,9 @@ describe("Fish shell integration", () => { assert.ok(!content.includes("SAFE_CHAIN_DIR")); }); - it("should show custom manual setup instructions when custom dir is set", () => { - getSafeChainDirResult = "/custom/safe-chain"; - + it("shows source-only manual setup instructions", () => { assert.deepStrictEqual(fish.getManualSetupInstructions(), [ "Add the following line to your ~/.config/fish/config.fish file:", - ' set -gx SAFE_CHAIN_DIR "/custom/safe-chain"', " source /test-home/.safe-chain/scripts/init-fish.fish", "Then restart your terminal or run: source ~/.config/fish/config.fish", ]); 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 906bedd..becc3db 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -4,7 +4,6 @@ import { removeLinesMatchingPattern, validatePowerShellExecutionPolicy, getScriptsDir, - getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -58,14 +57,6 @@ 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`, @@ -89,18 +80,7 @@ function getStartupFile() { /** @param {string} preamble */ function buildManualInstructions(preamble) { - const customDir = getSafeChainDir(); - const instructions = [preamble]; - - if (customDir) { - instructions.push( - ` $env:SAFE_CHAIN_DIR = '${customDir}'`, - ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`, - ); - } else { - instructions.push(` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`); - } - + const instructions = [preamble, ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`]; instructions.push(`Then restart your terminal or run: . $PROFILE`); return instructions; } 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 296abfa..16023b5 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,7 +9,6 @@ describe("PowerShell Core shell integration", () => { let mockStartupFile; let powershell; let executionPolicyResult; - let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -27,7 +26,6 @@ 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"); @@ -65,7 +63,6 @@ describe("PowerShell Core shell integration", () => { // Reset mocks mock.reset(); - getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -209,26 +206,18 @@ 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"; + describe("custom install dir", () => { + it("writes only the source line to the profile", async () => { 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") + content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"') ); - }); - - 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", () => { + it("removes legacy env lines on teardown", () => { const initialContent = [ "# PowerShell profile", "$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory", @@ -242,12 +231,9 @@ describe("PowerShell Core shell integration", () => { assert.ok(!content.includes("SAFE_CHAIN_DIR")); }); - it("should show custom manual setup instructions when custom dir is set", () => { - getSafeChainDirResult = "C:\\custom\\safe-chain"; - + it("shows source-only manual setup instructions", () => { assert.deepStrictEqual(powershell.getManualSetupInstructions(), [ 'Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):', - " $env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain'", ' . "/test-home/.safe-chain/scripts/init-pwsh.ps1"', "Then restart your terminal or run: . $PROFILE", ]); 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 e53891e..4a27fe9 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -4,7 +4,6 @@ import { removeLinesMatchingPattern, validatePowerShellExecutionPolicy, getScriptsDir, - getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -58,14 +57,6 @@ 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`, @@ -89,18 +80,7 @@ function getStartupFile() { /** @param {string} preamble */ function buildManualInstructions(preamble) { - const customDir = getSafeChainDir(); - const instructions = [preamble]; - - if (customDir) { - instructions.push( - ` $env:SAFE_CHAIN_DIR = '${customDir}'`, - ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`, - ); - } else { - instructions.push(` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`); - } - + const instructions = [preamble, ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`]; instructions.push(`Then restart your terminal or run: . $PROFILE`); return instructions; } 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 840f585..ac26ca7 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,7 +9,6 @@ describe("Windows PowerShell shell integration", () => { let mockStartupFile; let windowsPowershell; let executionPolicyResult; - let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -27,7 +26,6 @@ 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"); @@ -65,7 +63,6 @@ describe("Windows PowerShell shell integration", () => { // Reset mocks mock.reset(); - getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -209,26 +206,18 @@ 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"; + describe("custom install dir", () => { + it("writes only the source line to the profile", async () => { 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") + content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"') ); - }); - - 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", () => { + it("removes legacy env lines on teardown", () => { const initialContent = [ "# Windows PowerShell profile", "$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory", @@ -242,12 +231,9 @@ describe("Windows PowerShell shell integration", () => { assert.ok(!content.includes("SAFE_CHAIN_DIR")); }); - it("should show custom manual teardown instructions when custom dir is set", () => { - getSafeChainDirResult = "C:\\custom\\safe-chain"; - + it("shows source-only manual teardown instructions", () => { assert.deepStrictEqual(windowsPowershell.getManualTeardownInstructions(), [ 'Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):', - " $env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain'", ' . "/test-home/.safe-chain/scripts/init-pwsh.ps1"', "Then restart your terminal or run: . $PROFILE", ]); 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 9b87d86..3fa775c 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -3,7 +3,6 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, getScriptsDir, - getSafeChainDir, } from "../helpers.js"; import { execSync } from "child_process"; import path from "path"; @@ -53,15 +52,6 @@ function teardown(tools) { 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`, @@ -86,18 +76,7 @@ function getStartupFile() { /** @param {string} preamble */ function buildManualInstructions(preamble) { - const customDir = getSafeChainDir(); - const instructions = [preamble]; - - if (customDir) { - instructions.push( - ` export SAFE_CHAIN_DIR="${customDir}"`, - ` source ${path.join(getScriptsDir(), "init-posix.sh")}`, - ); - } else { - instructions.push(` source ~/.safe-chain/scripts/init-posix.sh`); - } - + const instructions = [preamble, ` source ${path.join(getScriptsDir(), "init-posix.sh")}`]; instructions.push(`Then restart your terminal or run: source ~/.zshrc`); return instructions; } 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 52e790f..caa85f4 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,7 +8,6 @@ import { knownAikidoTools } from "../helpers.js"; describe("Zsh shell integration", () => { let mockStartupFile; let zsh; - let getSafeChainDirResult = undefined; beforeEach(async () => { // Create temporary startup file for testing @@ -19,7 +18,6 @@ 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"); @@ -55,7 +53,6 @@ describe("Zsh shell integration", () => { // Reset mocks mock.reset(); - getSafeChainDirResult = undefined; }); describe("isInstalled", () => { @@ -174,26 +171,18 @@ 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"; + describe("custom install dir", () => { + it("writes only the source line to the rc file", () => { zsh.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory') + content.includes("source /test-home/.safe-chain/scripts/init-posix.sh") ); - }); - - 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", () => { + it("removes legacy export lines on teardown", () => { const initialContent = [ "#!/bin/zsh", 'export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory', @@ -207,12 +196,9 @@ describe("Zsh shell integration", () => { assert.ok(!content.includes("SAFE_CHAIN_DIR")); }); - it("should show custom manual teardown instructions when custom dir is set", () => { - getSafeChainDirResult = "/custom/safe-chain"; - + it("shows source-only manual teardown instructions", () => { assert.deepStrictEqual(zsh.getManualTeardownInstructions(), [ "Remove the following line from your ~/.zshrc file:", - ' export SAFE_CHAIN_DIR="/custom/safe-chain"', " source /test-home/.safe-chain/scripts/init-posix.sh", "Then restart your terminal or run: source ~/.zshrc", ]);