mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #411 from AikidoSec/feat/dynamic-install-dir
Add support for custom install directory
This commit is contained in:
commit
782af8e789
34 changed files with 1120 additions and 302 deletions
21
README.md
21
README.md
|
|
@ -317,6 +317,24 @@ The base URL should point to a server that mirrors the structure of `https://mal
|
||||||
- `/releases/npm.json` (JavaScript new packages list)
|
- `/releases/npm.json` (JavaScript new packages list)
|
||||||
- `/releases/pypi.json` (Python 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 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`.
|
||||||
|
|
||||||
|
### Unix/Linux/macOS
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --install-dir /usr/local/.safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -InstallDir 'C:\ProgramData\safe-chain'"
|
||||||
|
```
|
||||||
|
|
||||||
# Usage in CI/CD
|
# 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.
|
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.
|
||||||
|
|
@ -407,6 +425,7 @@ pipeline {
|
||||||
environment {
|
environment {
|
||||||
// Jenkins does not automatically persist PATH updates from setup-ci,
|
// Jenkins does not automatically persist PATH updates from setup-ci,
|
||||||
// so add the shims + binary directory explicitly for all stages.
|
// so add the shims + binary directory explicitly for all stages.
|
||||||
|
// 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}"
|
PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,7 +481,7 @@ To add safe-chain in GitLab pipelines, you need to install it in the image runni
|
||||||
# Install safe-chain
|
# Install safe-chain
|
||||||
RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
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 used a custom install dir)
|
||||||
ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}"
|
ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,52 @@
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[switch]$ci,
|
[switch]$ci,
|
||||||
[switch]$includepython
|
[switch]$includepython,
|
||||||
|
[string]$InstallDir
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validates and normalizes the requested install directory.
|
||||||
|
# Rejects non-absolute, root, PATH-like, and traversal-containing paths.
|
||||||
|
function Test-InstallDir {
|
||||||
|
param([string]$Dir)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Dir)) {
|
||||||
|
return @{ Ok = $true; Normalized = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($Dir)) {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir must be an absolute path, got: $Dir" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Dir.Contains([System.IO.Path]::PathSeparator)) {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir must not contain the PATH separator ($([System.IO.Path]::PathSeparator))" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputSegments = $Dir.Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries)
|
||||||
|
if ($inputSegments -contains "..") {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir must not contain path traversal segments" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$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" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{ Ok = $true; Normalized = $normalized }
|
||||||
|
}
|
||||||
|
|
||||||
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
|
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
|
||||||
$InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin"
|
$SafeChainBase = if ($InstallDir) { $InstallDir } else { Join-Path $HOME ".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"
|
$RepoUrl = "https://github.com/AikidoSec/safe-chain"
|
||||||
|
|
||||||
# Ensure TLS 1.2 is enabled for downloads
|
# Ensure TLS 1.2 is enabled for downloads
|
||||||
|
|
@ -98,6 +139,59 @@ function Get-Architecture {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Emits the deprecation warning for SAFE_CHAIN_VERSION and prints the version-pinned install command.
|
||||||
|
# Returns immediately when no version was provided through the environment.
|
||||||
|
function Write-VersionDeprecationWarning {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:SAFE_CHAIN_VERSION)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
||||||
|
Write-Warn ""
|
||||||
|
Write-Warn "Please use direct download URLs for version pinning instead:"
|
||||||
|
Write-Warn ""
|
||||||
|
if ($ci) {
|
||||||
|
Write-Warn " iex `"& { `$(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1' -UseBasicParsing) } -ci`""
|
||||||
|
} else {
|
||||||
|
Write-Warn " iex (iwr `"https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1`" -UseBasicParsing)"
|
||||||
|
}
|
||||||
|
Write-Warn ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Builds the Windows release binary filename for the detected architecture.
|
||||||
|
# Centralizes binary name generation for the download step.
|
||||||
|
function Get-BinaryName {
|
||||||
|
param([string]$Architecture)
|
||||||
|
|
||||||
|
return "safe-chain-win-$Architecture.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs safe-chain setup or setup-ci after the binary is installed.
|
||||||
|
# Temporarily appends the install directory to PATH and downgrades setup failures to warnings.
|
||||||
|
function Invoke-SafeChainSetup {
|
||||||
|
param(
|
||||||
|
[string]$BinaryPath,
|
||||||
|
[string]$InstallDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
$setupCmd = if ($ci) { "setup-ci" } else { "setup" }
|
||||||
|
|
||||||
|
Write-Info "Running safe-chain $setupCmd..."
|
||||||
|
try {
|
||||||
|
$env:Path = "$env:Path;$InstallDirectory"
|
||||||
|
& $BinaryPath $setupCmd
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warn "safe-chain was installed but setup encountered issues."
|
||||||
|
Write-Warn "You can run 'safe-chain $setupCmd' manually later."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warn "safe-chain was installed but setup encountered issues: $_"
|
||||||
|
Write-Warn "You can run 'safe-chain $setupCmd' manually later."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Check and uninstall npm global package if present
|
# Check and uninstall npm global package if present
|
||||||
function Remove-NpmInstallation {
|
function Remove-NpmInstallation {
|
||||||
# Check if npm is available
|
# Check if npm is available
|
||||||
|
|
@ -149,19 +243,7 @@ function Remove-VoltaInstallation {
|
||||||
|
|
||||||
# Main installation
|
# Main installation
|
||||||
function Install-SafeChain {
|
function Install-SafeChain {
|
||||||
# Show deprecation warning if SAFE_CHAIN_VERSION is set
|
Write-VersionDeprecationWarning
|
||||||
if (-not [string]::IsNullOrWhiteSpace($env:SAFE_CHAIN_VERSION)) {
|
|
||||||
Write-Warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
|
||||||
Write-Warn ""
|
|
||||||
Write-Warn "Please use direct download URLs for version pinning instead:"
|
|
||||||
Write-Warn ""
|
|
||||||
if ($ci) {
|
|
||||||
Write-Warn " iex `"& { `$(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1' -UseBasicParsing) } -ci`""
|
|
||||||
} else {
|
|
||||||
Write-Warn " iex (iwr `"https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1`" -UseBasicParsing)"
|
|
||||||
}
|
|
||||||
Write-Warn ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fetch latest version if VERSION is not set
|
# Fetch latest version if VERSION is not set
|
||||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
|
@ -192,7 +274,7 @@ function Install-SafeChain {
|
||||||
|
|
||||||
# Detect platform
|
# Detect platform
|
||||||
$arch = Get-Architecture
|
$arch = Get-Architecture
|
||||||
$binaryName = "safe-chain-win-$arch.exe"
|
$binaryName = Get-BinaryName -Architecture $arch
|
||||||
|
|
||||||
Write-Info "Detected architecture: $arch"
|
Write-Info "Detected architecture: $arch"
|
||||||
|
|
||||||
|
|
@ -238,31 +320,7 @@ function Install-SafeChain {
|
||||||
|
|
||||||
Write-Info "Binary installed to: $finalFile"
|
Write-Info "Binary installed to: $finalFile"
|
||||||
|
|
||||||
# Build setup command based on parameters
|
Invoke-SafeChainSetup -BinaryPath $finalFile -InstallDirectory $InstallDir
|
||||||
$setupCmd = if ($ci) { "setup-ci" } else { "setup" }
|
|
||||||
$setupArgs = @()
|
|
||||||
|
|
||||||
# Execute safe-chain setup
|
|
||||||
Write-Info "Running safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })..."
|
|
||||||
try {
|
|
||||||
$env:Path = "$env:Path;$InstallDir"
|
|
||||||
|
|
||||||
if ($setupArgs) {
|
|
||||||
& $finalFile $setupCmd $setupArgs
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
& $finalFile $setupCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Warn "safe-chain was installed but setup encountered issues."
|
|
||||||
Write-Warn "You can run 'safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })' manually later."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Warn "safe-chain was installed but setup encountered issues: $_"
|
|
||||||
Write-Warn "You can run 'safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })' manually later."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run installation
|
# Run installation
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,53 @@
|
||||||
|
|
||||||
set -e # Exit on error
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Validates a user-provided install dir and exits on unsafe values.
|
||||||
|
# Rejects relative paths, root paths, PATH separators, and traversal segments.
|
||||||
|
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
|
# Configuration
|
||||||
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
|
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
|
||||||
INSTALL_DIR="${HOME}/.safe-chain/bin"
|
SAFE_CHAIN_BASE="${HOME}/.safe-chain"
|
||||||
|
|
||||||
|
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
|
||||||
REPO_URL="https://github.com/AikidoSec/safe-chain"
|
REPO_URL="https://github.com/AikidoSec/safe-chain"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
|
|
@ -126,6 +170,75 @@ download() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prints the deprecation warning for SAFE_CHAIN_VERSION and the replacement install command.
|
||||||
|
# Returns immediately when no version was pinned through the environment.
|
||||||
|
warn_deprecated_version_env() {
|
||||||
|
if [ -z "$SAFE_CHAIN_VERSION" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
||||||
|
warn ""
|
||||||
|
warn "Please use direct download URLs for version pinning instead:"
|
||||||
|
warn ""
|
||||||
|
if [ "$USE_CI_SETUP" = "true" ]; then
|
||||||
|
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh -s -- --ci"
|
||||||
|
else
|
||||||
|
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh"
|
||||||
|
fi
|
||||||
|
warn ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensures VERSION is populated before installation continues.
|
||||||
|
# Fetches the latest release only when no explicit version was provided.
|
||||||
|
ensure_version() {
|
||||||
|
if [ -n "$VERSION" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Fetching latest release version..."
|
||||||
|
VERSION=$(fetch_latest_version)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Constructs platform-specific binary filename to match GitHub release asset naming convention.
|
||||||
|
get_binary_name() {
|
||||||
|
os="$1"
|
||||||
|
arch="$2"
|
||||||
|
|
||||||
|
if [ "$os" = "win" ]; then
|
||||||
|
printf 'safe-chain-%s-%s.exe\n' "$os" "$arch"
|
||||||
|
else
|
||||||
|
printf 'safe-chain-%s-%s\n' "$os" "$arch"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the final installation path for the downloaded safe-chain binary.
|
||||||
|
# Uses INSTALL_DIR and the platform-specific executable name.
|
||||||
|
get_final_binary_path() {
|
||||||
|
os="$1"
|
||||||
|
|
||||||
|
if [ "$os" = "win" ]; then
|
||||||
|
printf '%s/safe-chain.exe\n' "$INSTALL_DIR"
|
||||||
|
else
|
||||||
|
printf '%s/safe-chain\n' "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_setup_command() {
|
||||||
|
final_file="$1"
|
||||||
|
|
||||||
|
setup_cmd="setup"
|
||||||
|
if [ "$USE_CI_SETUP" = "true" ]; then
|
||||||
|
setup_cmd="setup-ci"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Running safe-chain $setup_cmd..."
|
||||||
|
if ! "$final_file" "$setup_cmd"; then
|
||||||
|
warn "safe-chain was installed but setup encountered issues."
|
||||||
|
warn "You can run 'safe-chain $setup_cmd' manually later."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Check and uninstall npm global package if present
|
# Check and uninstall npm global package if present
|
||||||
remove_npm_installation() {
|
remove_npm_installation() {
|
||||||
if ! command_exists npm; then
|
if ! command_exists npm; then
|
||||||
|
|
@ -229,19 +342,39 @@ remove_nvm_installation() {
|
||||||
|
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
parse_arguments() {
|
parse_arguments() {
|
||||||
for arg in "$@"; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$arg" in
|
case "$1" in
|
||||||
--ci)
|
--ci)
|
||||||
USE_CI_SETUP=true
|
USE_CI_SETUP=true
|
||||||
;;
|
;;
|
||||||
|
--install-dir)
|
||||||
|
shift
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
error "Missing value for --install-dir"
|
||||||
|
fi
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
error "--install-dir must not be empty"
|
||||||
|
fi
|
||||||
|
SAFE_CHAIN_BASE="$1"
|
||||||
|
;;
|
||||||
|
--install-dir=*)
|
||||||
|
SAFE_CHAIN_BASE="${1#--install-dir=}"
|
||||||
|
if [ -z "$SAFE_CHAIN_BASE" ]; then
|
||||||
|
error "--install-dir must not be empty"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
--include-python)
|
--include-python)
|
||||||
warn "--include-python is deprecated and ignored. Python ecosystem is now included by default."
|
warn "--include-python is deprecated and ignored. Python ecosystem is now included by default."
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
error "Unknown argument: $arg"
|
error "Unknown argument: $1"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
validate_install_dir "${SAFE_CHAIN_BASE}"
|
||||||
|
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main installation
|
# Main installation
|
||||||
|
|
@ -252,25 +385,9 @@ main() {
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
parse_arguments "$@"
|
parse_arguments "$@"
|
||||||
|
|
||||||
# Show deprecation warning if SAFE_CHAIN_VERSION is set
|
warn_deprecated_version_env
|
||||||
if [ -n "$SAFE_CHAIN_VERSION" ]; then
|
|
||||||
warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
|
||||||
warn ""
|
|
||||||
warn "Please use direct download URLs for version pinning instead:"
|
|
||||||
warn ""
|
|
||||||
if [ "$USE_CI_SETUP" = "true" ]; then
|
|
||||||
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh -s -- --ci"
|
|
||||||
else
|
|
||||||
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh"
|
|
||||||
fi
|
|
||||||
warn ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fetch latest version if VERSION is not set
|
ensure_version
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
info "Fetching latest release version..."
|
|
||||||
VERSION=$(fetch_latest_version)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the requested version is already installed
|
# Check if the requested version is already installed
|
||||||
if is_version_installed "$VERSION"; then
|
if is_version_installed "$VERSION"; then
|
||||||
|
|
@ -294,11 +411,7 @@ main() {
|
||||||
# Detect platform
|
# Detect platform
|
||||||
OS=$(detect_os)
|
OS=$(detect_os)
|
||||||
ARCH=$(detect_arch)
|
ARCH=$(detect_arch)
|
||||||
if [ "$OS" = "win" ]; then
|
BINARY_NAME=$(get_binary_name "$OS" "$ARCH")
|
||||||
BINARY_NAME="safe-chain-${OS}-${ARCH}.exe"
|
|
||||||
else
|
|
||||||
BINARY_NAME="safe-chain-${OS}-${ARCH}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Detected platform: ${OS}-${ARCH}"
|
info "Detected platform: ${OS}-${ARCH}"
|
||||||
|
|
||||||
|
|
@ -316,11 +429,7 @@ main() {
|
||||||
download "$DOWNLOAD_URL" "$TEMP_FILE"
|
download "$DOWNLOAD_URL" "$TEMP_FILE"
|
||||||
|
|
||||||
# Rename and make executable
|
# Rename and make executable
|
||||||
if [ "$OS" = "win" ]; then
|
FINAL_FILE=$(get_final_binary_path "$OS")
|
||||||
FINAL_FILE="${INSTALL_DIR}/safe-chain.exe"
|
|
||||||
else
|
|
||||||
FINAL_FILE="${INSTALL_DIR}/safe-chain"
|
|
||||||
fi
|
|
||||||
mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE"
|
mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE"
|
||||||
if [ "$OS" != "win" ]; then
|
if [ "$OS" != "win" ]; then
|
||||||
chmod +x "$FINAL_FILE" || error "Failed to make binary executable"
|
chmod +x "$FINAL_FILE" || error "Failed to make binary executable"
|
||||||
|
|
@ -328,20 +437,7 @@ main() {
|
||||||
|
|
||||||
info "Binary installed to: $FINAL_FILE"
|
info "Binary installed to: $FINAL_FILE"
|
||||||
|
|
||||||
# Build setup command based on arguments
|
run_setup_command "$FINAL_FILE"
|
||||||
SETUP_CMD="setup"
|
|
||||||
SETUP_ARGS=""
|
|
||||||
|
|
||||||
if [ "$USE_CI_SETUP" = "true" ]; then
|
|
||||||
SETUP_CMD="setup-ci"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Execute safe-chain setup
|
|
||||||
info "Running safe-chain $SETUP_CMD $SETUP_ARGS..."
|
|
||||||
if ! "$FINAL_FILE" $SETUP_CMD $SETUP_ARGS; then
|
|
||||||
warn "safe-chain was installed but setup encountered issues."
|
|
||||||
warn "You can run 'safe-chain $SETUP_CMD $SETUP_ARGS' manually later."
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
|
|
||||||
# Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform)
|
# Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform)
|
||||||
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
||||||
$DotSafeChain = Join-Path $HomeDir ".safe-chain"
|
|
||||||
$InstallDir = Join-Path $DotSafeChain "bin"
|
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
function Write-Info {
|
function Write-Info {
|
||||||
|
|
@ -24,6 +22,146 @@ function Write-Error-Custom {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Derives the safe-chain base install directory from a resolved binary path.
|
||||||
|
# Rejects wrapper scripts and paths that do not match the packaged bin layout.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the first safe-chain command found on PATH, if any.
|
||||||
|
# Used as the starting point for install-dir discovery.
|
||||||
|
function Get-SafeChainCommand {
|
||||||
|
return Get-Command safe-chain -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the safe-chain command path only when it points to a valid packaged binary install.
|
||||||
|
# Prevents teardown from invoking arbitrary wrappers or scripts from PATH.
|
||||||
|
function Get-ValidatedSafeChainCommandPath {
|
||||||
|
$command = Get-SafeChainCommand
|
||||||
|
if (-not $command -or [string]::IsNullOrWhiteSpace($command.Path)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$installDir = Get-InstallDirFromBinaryPath -BinaryPath $command.Path
|
||||||
|
if (-not $installDir) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $command.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Invokes the validated safe-chain binary with get-install-dir and returns the reported base directory.
|
||||||
|
# Safely returns $null when the command is unavailable or the lookup fails.
|
||||||
|
function Get-ReportedInstallDir {
|
||||||
|
$safeChainPath = Get-ValidatedSafeChainCommandPath
|
||||||
|
if (-not $safeChainPath) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$reportedInstallDir = & $safeChainPath get-install-dir 2>$null | Select-Object -First 1
|
||||||
|
if ($reportedInstallDir) {
|
||||||
|
$reportedInstallDir = $reportedInstallDir.Trim()
|
||||||
|
}
|
||||||
|
if ($reportedInstallDir) {
|
||||||
|
return $reportedInstallDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determines the safe-chain base install directory for uninstall.
|
||||||
|
# Prefers the binary-reported location, then derives it from PATH, then falls back to the default home-dir layout.
|
||||||
|
function Get-SafeChainInstallDir {
|
||||||
|
$reportedInstallDir = Get-ReportedInstallDir
|
||||||
|
if ($reportedInstallDir) {
|
||||||
|
return $reportedInstallDir
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = Get-SafeChainCommand
|
||||||
|
if ($command -and $command.Path) {
|
||||||
|
$discoveredInstallDir = Get-InstallDirFromBinaryPath -BinaryPath $command.Path
|
||||||
|
if ($discoveredInstallDir) {
|
||||||
|
return $discoveredInstallDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Join-Path $HomeDir ".safe-chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Finds the installed safe-chain binary inside the resolved install directory.
|
||||||
|
# Falls back to a validated safe-chain command when the expected file is missing.
|
||||||
|
function Find-SafeChainBinary {
|
||||||
|
param([string]$DotSafeChain)
|
||||||
|
|
||||||
|
$safeChainExe = Join-Path $DotSafeChain "bin/safe-chain.exe"
|
||||||
|
$safeChainBin = Join-Path $DotSafeChain "bin/safe-chain"
|
||||||
|
|
||||||
|
if (Test-Path $safeChainExe) {
|
||||||
|
return $safeChainExe
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $safeChainBin) {
|
||||||
|
return $safeChainBin
|
||||||
|
}
|
||||||
|
|
||||||
|
return Get-ValidatedSafeChainCommandPath
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs safe-chain teardown before removing the installation directory.
|
||||||
|
# Converts teardown failures into warnings so uninstall can still complete.
|
||||||
|
function Invoke-SafeChainTeardown {
|
||||||
|
param([string]$SafeChainPath)
|
||||||
|
|
||||||
|
if (-not $SafeChainPath) {
|
||||||
|
Write-Warn "safe-chain command not found. Proceeding with uninstallation."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Running safe-chain teardown..."
|
||||||
|
try {
|
||||||
|
& $SafeChainPath teardown
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warn "safe-chain teardown encountered issues: $_"
|
||||||
|
Write-Warn "Continuing with uninstallation..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Check and uninstall npm global package if present
|
# Check and uninstall npm global package if present
|
||||||
function Remove-NpmInstallation {
|
function Remove-NpmInstallation {
|
||||||
# Check if npm is available
|
# Check if npm is available
|
||||||
|
|
@ -76,49 +214,9 @@ function Remove-VoltaInstallation {
|
||||||
# Main uninstallation
|
# Main uninstallation
|
||||||
function Uninstall-SafeChain {
|
function Uninstall-SafeChain {
|
||||||
Write-Info "Uninstalling safe-chain..."
|
Write-Info "Uninstalling safe-chain..."
|
||||||
|
$DotSafeChain = Get-SafeChainInstallDir
|
||||||
# Run teardown if safe-chain is available
|
$safeChainPath = Find-SafeChainBinary -DotSafeChain $DotSafeChain
|
||||||
# Check for both safe-chain.exe (Windows) and safe-chain (Unix) since PowerShell Core runs on all platforms
|
Invoke-SafeChainTeardown -SafeChainPath $safeChainPath
|
||||||
$safeChainExe = Join-Path $InstallDir "safe-chain.exe"
|
|
||||||
$safeChainBin = Join-Path $InstallDir "safe-chain"
|
|
||||||
|
|
||||||
$safeChainPath = $null
|
|
||||||
if (Test-Path $safeChainExe) {
|
|
||||||
$safeChainPath = $safeChainExe
|
|
||||||
}
|
|
||||||
elseif (Test-Path $safeChainBin) {
|
|
||||||
$safeChainPath = $safeChainBin
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($safeChainPath) {
|
|
||||||
Write-Info "Running safe-chain teardown..."
|
|
||||||
try {
|
|
||||||
& $safeChainPath teardown
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Warn "safe-chain teardown encountered issues: $_"
|
|
||||||
Write-Warn "Continuing with uninstallation..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elseif (Get-Command safe-chain -ErrorAction SilentlyContinue) {
|
|
||||||
Write-Info "Running safe-chain teardown..."
|
|
||||||
try {
|
|
||||||
safe-chain teardown
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Warn "safe-chain teardown encountered issues: $_"
|
|
||||||
Write-Warn "Continuing with uninstallation..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warn "safe-chain command not found. Proceeding with uninstallation."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove npm and Volta installations
|
# Remove npm and Volta installations
|
||||||
Remove-NpmInstallation
|
Remove-NpmInstallation
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
set -e # Exit on error
|
set -e # Exit on error
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
DOT_SAFE_CHAIN="${HOME}/.safe-chain"
|
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -34,6 +33,159 @@ command_exists() {
|
||||||
command -v "$1" >/dev/null 2>&1
|
command -v "$1" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolves a path to its canonical filesystem location when possible.
|
||||||
|
# Follows symlinks so binary validation can inspect the real installed path.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Derives the safe-chain base install directory from a packaged binary path.
|
||||||
|
# Rejects wrapper scripts and paths that do not match the expected bin layout.
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determines the installed safe-chain base directory for uninstall.
|
||||||
|
# Prefers the binary-reported location, then infers it from PATH, then falls back to ~/.safe-chain.
|
||||||
|
get_install_dir() {
|
||||||
|
reported_install_dir=$(get_reported_install_dir || true)
|
||||||
|
if [ -n "$reported_install_dir" ]; then
|
||||||
|
printf '%s\n' "$reported_install_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command_path=$(get_safe_chain_command_path || true)
|
||||||
|
install_dir=$(derive_install_dir_from_binary "$command_path" || true)
|
||||||
|
if [ -n "$install_dir" ]; then
|
||||||
|
printf '%s\n' "$install_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${HOME}/.safe-chain"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the current safe-chain command path from PATH.
|
||||||
|
# Fails when safe-chain is not currently resolvable.
|
||||||
|
get_safe_chain_command_path() {
|
||||||
|
if ! command_exists safe-chain; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v safe-chain
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the safe-chain command path only when it resolves to a valid packaged binary install.
|
||||||
|
# Prevents the uninstaller from invoking arbitrary PATH entries.
|
||||||
|
get_validated_safe_chain_command_path() {
|
||||||
|
command_path=$(get_safe_chain_command_path || true)
|
||||||
|
if [ -z "$command_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_dir=$(derive_install_dir_from_binary "$command_path" || true)
|
||||||
|
if [ -z "$install_dir" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$command_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Asks the validated safe-chain binary for its install directory via get-install-dir.
|
||||||
|
# Returns nothing if the command is unavailable or the lookup fails.
|
||||||
|
get_reported_install_dir() {
|
||||||
|
safe_chain_path=$(get_validated_safe_chain_command_path || true)
|
||||||
|
if [ -z "$safe_chain_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_dir=$("$safe_chain_path" get-install-dir 2>/dev/null || true)
|
||||||
|
if [ -n "$install_dir" ]; then
|
||||||
|
printf '%s\n' "$install_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Locates the installed safe-chain binary to use for teardown.
|
||||||
|
# Checks the discovered install dir first, then falls back to a validated PATH entry.
|
||||||
|
find_installed_safe_chain_binary() {
|
||||||
|
dot_safe_chain="$1"
|
||||||
|
|
||||||
|
safe_chain_location="$dot_safe_chain/bin/safe-chain"
|
||||||
|
if [ -x "$safe_chain_location" ]; then
|
||||||
|
printf '%s\n' "$safe_chain_location"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command_path=$(get_validated_safe_chain_command_path || true)
|
||||||
|
if [ -n "$command_path" ]; then
|
||||||
|
printf '%s\n' "$command_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs safe-chain teardown before removing files.
|
||||||
|
# Continues with uninstall even if teardown is unavailable or fails.
|
||||||
|
run_safe_chain_teardown() {
|
||||||
|
safe_chain_command="$1"
|
||||||
|
|
||||||
|
if [ -z "$safe_chain_command" ]; then
|
||||||
|
warn "safe-chain command not found. Proceeding with uninstallation."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Running safe-chain teardown..."
|
||||||
|
"$safe_chain_command" teardown || warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
||||||
|
}
|
||||||
|
|
||||||
# Check and uninstall npm global package if present
|
# Check and uninstall npm global package if present
|
||||||
remove_npm_installation() {
|
remove_npm_installation() {
|
||||||
if ! command_exists npm; then
|
if ! command_exists npm; then
|
||||||
|
|
@ -139,17 +291,9 @@ remove_nvm_installation() {
|
||||||
|
|
||||||
# Main uninstallation
|
# Main uninstallation
|
||||||
main() {
|
main() {
|
||||||
SAFE_CHAIN_LOCATION="$DOT_SAFE_CHAIN/bin/safe-chain"
|
DOT_SAFE_CHAIN=$(get_install_dir)
|
||||||
|
SAFE_CHAIN_COMMAND=$(find_installed_safe_chain_binary "$DOT_SAFE_CHAIN" || true)
|
||||||
if [ -x "$SAFE_CHAIN_LOCATION" ]; then
|
run_safe_chain_teardown "$SAFE_CHAIN_COMMAND"
|
||||||
info "Running safe-chain teardown..."
|
|
||||||
"$SAFE_CHAIN_LOCATION" teardown || warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
|
||||||
elif command_exists safe-chain; then
|
|
||||||
info "Running safe-chain teardown..."
|
|
||||||
safe-chain teardown || warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
|
||||||
else
|
|
||||||
warn "safe-chain command not found. Proceeding with uninstallation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for existing safe-chain installation through nvm, volta, or npm
|
# Check for existing safe-chain installation through nvm, volta, or npm
|
||||||
remove_npm_installation
|
remove_npm_installation
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
|
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
|
||||||
|
import { getInstalledSafeChainDir } from "../src/installLocation.js";
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
// This checks the current file's dirname in a way that's compatible with:
|
// This checks the current file's dirname in a way that's compatible with:
|
||||||
|
|
@ -67,6 +68,17 @@ if (tool) {
|
||||||
teardownDirectories();
|
teardownDirectories();
|
||||||
} else if (command === "setup-ci") {
|
} else if (command === "setup-ci") {
|
||||||
setupCi();
|
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") {
|
} else if (command === "--version" || command === "-v" || command === "-v") {
|
||||||
(async () => {
|
(async () => {
|
||||||
ui.writeInformation(`Current safe-chain version: ${await getVersion()}`);
|
ui.writeInformation(`Current safe-chain version: ${await getVersion()}`);
|
||||||
|
|
@ -88,7 +100,7 @@ function writeHelp() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
||||||
"teardown",
|
"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",
|
"--version",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
@ -108,6 +120,11 @@ function writeHelp() {
|
||||||
"safe-chain setup-ci",
|
"safe-chain setup-ci",
|
||||||
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`,
|
)}: 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(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
|
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
|
||||||
"-v",
|
"-v",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { getEcoSystem } from "./settings.js";
|
import { getEcoSystem } from "./settings.js";
|
||||||
|
import { getSafeChainBaseDir } from "./safeChainDir.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} SafeChainConfig
|
* @typedef {Object} SafeChainConfig
|
||||||
|
|
@ -304,8 +305,7 @@ function getConfigFilePath() {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function getSafeChainDirectory() {
|
export function getSafeChainDirectory() {
|
||||||
const homeDir = os.homedir();
|
const safeChainDir = getSafeChainBaseDir();
|
||||||
const safeChainDir = path.join(homeDir, ".safe-chain");
|
|
||||||
|
|
||||||
if (!fs.existsSync(safeChainDir)) {
|
if (!fs.existsSync(safeChainDir)) {
|
||||||
fs.mkdirSync(safeChainDir, { recursive: true });
|
fs.mkdirSync(safeChainDir, { recursive: true });
|
||||||
|
|
|
||||||
71
packages/safe-chain/src/config/safeChainDir.js
Normal file
71
packages/safe-chain/src/config/safeChainDir.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { getInstalledSafeChainDir } from "../installLocation.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getSafeChainBaseDir() {
|
||||||
|
return getInstalledSafeChainDir() ?? path.join(os.homedir(), ".safe-chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getBinDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getShimsDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "shims");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getScriptsDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "scripts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getCertsDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "certs");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the directory of the calling module.
|
||||||
|
* Falls back to __dirname when import.meta.url is unavailable (pkg CJS binary).
|
||||||
|
* @param {string | undefined} moduleUrl
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function resolveModuleDir(moduleUrl) {
|
||||||
|
if (moduleUrl) {
|
||||||
|
return path.dirname(fileURLToPath(moduleUrl));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | undefined} moduleUrl
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getStartupScriptSourcePath(moduleUrl, fileName) {
|
||||||
|
return path.join(resolveModuleDir(moduleUrl), "startup-scripts", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | undefined} moduleUrl
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getPathWrapperTemplatePath(moduleUrl, fileName) {
|
||||||
|
return path.join(resolveModuleDir(moduleUrl), "path-wrappers", "templates", fileName);
|
||||||
|
}
|
||||||
42
packages/safe-chain/src/installLocation.js
Normal file
42
packages/safe-chain/src/installLocation.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/** @type {NodeJS.Process & { pkg?: unknown }} */
|
||||||
|
const processWithPkg = process;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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(processWithPkg.pkg);
|
||||||
|
if (!isPackaged) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deriveInstallDirFromExecutablePath(
|
||||||
|
options.executablePath ?? process.execPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
51
packages/safe-chain/src/installLocation.spec.js
Normal file
51
packages/safe-chain/src/installLocation.spec.js
Normal file
|
|
@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import forge from "node-forge";
|
import forge from "node-forge";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import os from "os";
|
import { getCertsDir } from "../config/safeChainDir.js";
|
||||||
|
|
||||||
const certFolder = path.join(os.homedir(), ".safe-chain", "certs");
|
|
||||||
const ca = loadCa();
|
const ca = loadCa();
|
||||||
|
|
||||||
const certCache = new Map();
|
const certCache = new Map();
|
||||||
|
|
@ -20,7 +19,7 @@ function createKeyIdentifier(publicKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCaCertPath() {
|
export function getCaCertPath() {
|
||||||
return path.join(certFolder, "ca-cert.pem");
|
return path.join(getCertsDir(), "ca-cert.pem");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,6 +111,7 @@ export function generateCertForHost(hostname) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCa() {
|
function loadCa() {
|
||||||
|
const certFolder = getCertsDir();
|
||||||
const keyPath = path.join(certFolder, "ca-key.pem");
|
const keyPath = path.join(certFolder, "ca-key.pem");
|
||||||
const certPath = path.join(certFolder, "ca-cert.pem");
|
const certPath = path.join(certFolder, "ca-cert.pem");
|
||||||
|
|
||||||
|
|
|
||||||
71
packages/safe-chain/src/registryProxy/certUtils.spec.js
Normal file
71
packages/safe-chain/src/registryProxy/certUtils.spec.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("certUtils", () => {
|
||||||
|
let installedSafeChainDir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
installedSafeChainDir = undefined;
|
||||||
|
mock.module("../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
|
getSafeChainBaseDir: () => installedSafeChainDir ?? "/home/test/.safe-chain",
|
||||||
|
getCertsDir: () => `${installedSafeChainDir ?? "/home/test/.safe-chain"}/certs`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores CA certificates in the packaged install dir when available", async () => {
|
||||||
|
installedSafeChainDir = "/custom/safe-chain";
|
||||||
|
|
||||||
|
mock.module("fs", {
|
||||||
|
defaultExport: {
|
||||||
|
existsSync: () => false,
|
||||||
|
mkdirSync: () => {},
|
||||||
|
writeFileSync: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("node-forge", {
|
||||||
|
defaultExport: {
|
||||||
|
pki: {
|
||||||
|
getPublicKeyFingerprint: () => "fingerprint",
|
||||||
|
rsa: {
|
||||||
|
generateKeyPair: () => ({
|
||||||
|
publicKey: "public-key",
|
||||||
|
privateKey: "private-key",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
createCertificate: () => ({
|
||||||
|
publicKey: null,
|
||||||
|
serialNumber: "",
|
||||||
|
validity: {
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(),
|
||||||
|
},
|
||||||
|
setSubject: () => {},
|
||||||
|
setIssuer: () => {},
|
||||||
|
setExtensions: () => {},
|
||||||
|
sign: () => {},
|
||||||
|
}),
|
||||||
|
privateKeyToPem: () => "private-key-pem",
|
||||||
|
certificateToPem: () => "certificate-pem",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
sha1: { create: () => "sha1" },
|
||||||
|
sha256: { create: () => "sha256" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getCaCertPath } = await import("./certUtils.js");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
getCaCertPath(),
|
||||||
|
"/custom/safe-chain/certs/ca-cert.pem",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -127,20 +127,6 @@ export function getPackageManagerList() {
|
||||||
return `${tools.join(", ")}, and ${lastTool} commands`;
|
return `${tools.join(", ")}, and ${lastTool} commands`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function getShimsDir() {
|
|
||||||
return path.join(os.homedir(), ".safe-chain", "shims");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function getScriptsDir() {
|
|
||||||
return path.join(os.homedir(), ".safe-chain", "scripts");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} executableName
|
* @param {string} executableName
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir, homedir } from "node:os";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
|
@ -15,6 +15,7 @@ describe("removeLinesMatchingPatternTests", () => {
|
||||||
mock.module("node:os", {
|
mock.module("node:os", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
EOL: "\r\n", // Simulate Windows line endings
|
EOL: "\r\n", // Simulate Windows line endings
|
||||||
|
homedir,
|
||||||
tmpdir: tmpdir,
|
tmpdir: tmpdir,
|
||||||
platform: () => "linux",
|
platform: () => "linux",
|
||||||
},
|
},
|
||||||
|
|
@ -182,3 +183,30 @@ describe("removeLinesMatchingPatternTests", () => {
|
||||||
assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines");
|
assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getSafeChainBaseDir / getBinDir / getShimsDir / getScriptsDir", () => {
|
||||||
|
it("defaults base dir to ~/.safe-chain when no packaged install dir is available", async () => {
|
||||||
|
const { getSafeChainBaseDir } = await import("../config/safeChainDir.js");
|
||||||
|
assert.strictEqual(getSafeChainBaseDir(), path.join(homedir(), ".safe-chain"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getBinDir returns ~/.safe-chain/bin by default", async () => {
|
||||||
|
const { getBinDir } = await import("../config/safeChainDir.js");
|
||||||
|
assert.strictEqual(getBinDir(), path.join(homedir(), ".safe-chain", "bin"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getShimsDir returns ~/.safe-chain/shims by default", async () => {
|
||||||
|
const { getShimsDir } = await import("../config/safeChainDir.js");
|
||||||
|
assert.strictEqual(getShimsDir(), path.join(homedir(), ".safe-chain", "shims"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getScriptsDir returns ~/.safe-chain/scripts by default", async () => {
|
||||||
|
const { getScriptsDir } = await import("../config/safeChainDir.js");
|
||||||
|
assert.strictEqual(getScriptsDir(), path.join(homedir(), ".safe-chain", "scripts"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getCertsDir returns ~/.safe-chain/certs by default", async () => {
|
||||||
|
const { getCertsDir } = await import("../config/safeChainDir.js");
|
||||||
|
assert.strictEqual(getCertsDir(), path.join(homedir(), ".safe-chain", "certs"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,28 @@
|
||||||
|
|
||||||
# Function to remove shim from PATH (POSIX-compliant)
|
# Function to remove shim from PATH (POSIX-compliant)
|
||||||
remove_shim_from_path() {
|
remove_shim_from_path() {
|
||||||
echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
|
_safe_chain_phys=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P)
|
||||||
|
if [ -z "$_safe_chain_phys" ]; then
|
||||||
|
echo "$PATH"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
_path=$(echo "$PATH" | sed "s|${_safe_chain_phys}:||g")
|
||||||
|
# Also remove via dirname of $0 directly — on macOS /tmp is a symlink to /private/tmp,
|
||||||
|
# so pwd -P resolves to /private/tmp/… but PATH may still contain /tmp/….
|
||||||
|
_dir=$(dirname -- "$0")
|
||||||
|
case "$_dir" in
|
||||||
|
/*) [ "$_dir" != "$_safe_chain_phys" ] && _path=$(echo "$_path" | sed "s|${_dir}:||g") ;;
|
||||||
|
esac
|
||||||
|
echo "$_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
if command -v safe-chain >/dev/null 2>&1; then
|
if command -v safe-chain >/dev/null 2>&1; then
|
||||||
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
|
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
|
||||||
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
|
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
|
||||||
else
|
else
|
||||||
|
# safe-chain is not reachable — warn the user so they know protection is inactive
|
||||||
|
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)
|
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
|
||||||
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
|
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
|
||||||
if [ -n "$original_cmd" ]; then
|
if [ -n "$original_cmd" ]; then
|
||||||
|
|
|
||||||
|
|
@ -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 This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
|
||||||
|
|
||||||
REM Remove shim directory from PATH to prevent infinite loops
|
REM Remove shim directory from PATH to prevent infinite loops
|
||||||
set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
|
set "SHIM_DIR=%~dp0"
|
||||||
|
if "%SHIM_DIR:~-1%"=="\" set "SHIM_DIR=%SHIM_DIR:~0,-1%"
|
||||||
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
||||||
|
|
||||||
REM Check if aikido command is available with clean PATH
|
REM Check if aikido command is available with clean PATH
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,14 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js";
|
import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
|
||||||
|
import {
|
||||||
|
getShimsDir,
|
||||||
|
getBinDir,
|
||||||
|
getPathWrapperTemplatePath,
|
||||||
|
} from "../config/safeChainDir.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
// This checks the current file's dirname in a way that's compatible with:
|
|
||||||
// - Modulejs (import.meta.url)
|
|
||||||
// - ES modules (__dirname)
|
|
||||||
// This is needed because safe-chain's npm package is built using ES modules,
|
|
||||||
// but building the binaries requires commonjs.
|
|
||||||
let dirname;
|
|
||||||
if (import.meta.url) {
|
|
||||||
const filename = fileURLToPath(import.meta.url);
|
|
||||||
dirname = path.dirname(filename);
|
|
||||||
} else {
|
|
||||||
dirname = __dirname;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loops over the detected shells and calls the setup function for each.
|
* Loops over the detected shells and calls the setup function for each.
|
||||||
|
|
@ -31,7 +21,7 @@ export async function setupCi() {
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
||||||
const shimsDir = getShimsDir();
|
const shimsDir = getShimsDir();
|
||||||
const binDir = path.join(os.homedir(), ".safe-chain", "bin");
|
const binDir = getBinDir();
|
||||||
// Create the shims directory if it doesn't exist
|
// Create the shims directory if it doesn't exist
|
||||||
if (!fs.existsSync(shimsDir)) {
|
if (!fs.existsSync(shimsDir)) {
|
||||||
fs.mkdirSync(shimsDir, { recursive: true });
|
fs.mkdirSync(shimsDir, { recursive: true });
|
||||||
|
|
@ -50,12 +40,7 @@ export async function setupCi() {
|
||||||
*/
|
*/
|
||||||
function createUnixShims(shimsDir) {
|
function createUnixShims(shimsDir) {
|
||||||
// Read the template file
|
// Read the template file
|
||||||
const templatePath = path.resolve(
|
const templatePath = getPathWrapperTemplatePath(import.meta.url, "unix-wrapper.template.sh");
|
||||||
dirname,
|
|
||||||
"path-wrappers",
|
|
||||||
"templates",
|
|
||||||
"unix-wrapper.template.sh"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(templatePath)) {
|
if (!fs.existsSync(templatePath)) {
|
||||||
ui.writeError(`Template file not found: ${templatePath}`);
|
ui.writeError(`Template file not found: ${templatePath}`);
|
||||||
|
|
@ -89,12 +74,7 @@ function createUnixShims(shimsDir) {
|
||||||
*/
|
*/
|
||||||
function createWindowsShims(shimsDir) {
|
function createWindowsShims(shimsDir) {
|
||||||
// Read the template file
|
// Read the template file
|
||||||
const templatePath = path.resolve(
|
const templatePath = getPathWrapperTemplatePath(import.meta.url, "windows-wrapper.template.cmd");
|
||||||
dirname,
|
|
||||||
"path-wrappers",
|
|
||||||
"templates",
|
|
||||||
"windows-wrapper.template.cmd"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(templatePath)) {
|
if (!fs.existsSync(templatePath)) {
|
||||||
ui.writeError(`Windows template file not found: ${templatePath}`);
|
ui.writeError(`Windows template file not found: ${templatePath}`);
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@ describe("Setup CI shell integration", () => {
|
||||||
fs.mkdirSync(path.join(mockTemplateDir, "path-wrappers", "templates"), { recursive: true });
|
fs.mkdirSync(path.join(mockTemplateDir, "path-wrappers", "templates"), { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(mockTemplateDir, "path-wrappers", "templates", "unix-wrapper.template.sh"),
|
path.join(mockTemplateDir, "path-wrappers", "templates", "unix-wrapper.template.sh"),
|
||||||
"#!/bin/bash\n# Template for {{PACKAGE_MANAGER}}\nexec {{AIKIDO_COMMAND}} \"$@\"\n",
|
"#!/bin/bash\n# Template for {{PACKAGE_MANAGER}}\n_safe_chain_shims=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" 2>/dev/null && pwd -P)\nexec {{AIKIDO_COMMAND}} \"$@\"\n",
|
||||||
"utf-8"
|
"utf-8"
|
||||||
);
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(mockTemplateDir, "path-wrappers", "templates", "windows-wrapper.template.cmd"),
|
path.join(mockTemplateDir, "path-wrappers", "templates", "windows-wrapper.template.cmd"),
|
||||||
"@echo off\nREM Template for {{PACKAGE_MANAGER}}\n{{AIKIDO_COMMAND}} %*\n",
|
"@echo off\nset \"SHIM_DIR=%~dp0\"\n{{AIKIDO_COMMAND}} %*\n",
|
||||||
"utf-8"
|
"utf-8"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -50,7 +50,15 @@ describe("Setup CI shell integration", () => {
|
||||||
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||||
],
|
],
|
||||||
getPackageManagerList: () => "npm, yarn",
|
getPackageManagerList: () => "npm, yarn",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
getShimsDir: () => mockShimsDir,
|
getShimsDir: () => mockShimsDir,
|
||||||
|
getBinDir: () => path.join(mockHomeDir, ".safe-chain", "bin"),
|
||||||
|
getPathWrapperTemplatePath: (_moduleUrl, fileName) =>
|
||||||
|
path.join(mockTemplateDir, "path-wrappers", "templates", fileName),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -63,22 +71,6 @@ describe("Setup CI shell integration", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock path module to resolve templates correctly
|
|
||||||
mock.module("path", {
|
|
||||||
namedExports: {
|
|
||||||
join: path.join,
|
|
||||||
dirname: () => mockTemplateDir,
|
|
||||||
resolve: (...args) => path.resolve(mockTemplateDir, ...args.slice(1)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock fileURLToPath
|
|
||||||
mock.module("url", {
|
|
||||||
namedExports: {
|
|
||||||
fileURLToPath: () => path.join(mockTemplateDir, "setup-ci.js"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Import setupCi module after mocking
|
// Import setupCi module after mocking
|
||||||
setupCi = (await import("./setup-ci.js")).setupCi;
|
setupCi = (await import("./setup-ci.js")).setupCi;
|
||||||
});
|
});
|
||||||
|
|
@ -119,6 +111,10 @@ describe("Setup CI shell integration", () => {
|
||||||
const npmShimContent = fs.readFileSync(npmShimPath, "utf-8");
|
const npmShimContent = fs.readFileSync(npmShimPath, "utf-8");
|
||||||
assert.ok(npmShimContent.includes("aikido-npm"), "npm shim should contain aikido-npm");
|
assert.ok(npmShimContent.includes("aikido-npm"), "npm shim should contain aikido-npm");
|
||||||
assert.ok(npmShimContent.includes("#!/bin/bash"), "npm shim should have bash shebang");
|
assert.ok(npmShimContent.includes("#!/bin/bash"), "npm shim should have bash shebang");
|
||||||
|
assert.ok(
|
||||||
|
npmShimContent.includes("_safe_chain_shims=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" 2>/dev/null && pwd -P)"),
|
||||||
|
"npm shim should derive the shims directory from its own location",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create Windows .cmd shims on win32 platform", async () => {
|
it("should create Windows .cmd shims on win32 platform", async () => {
|
||||||
|
|
@ -142,6 +138,10 @@ describe("Setup CI shell integration", () => {
|
||||||
assert.ok(npmShimContent.includes("aikido-npm"), "npm.cmd should contain aikido-npm");
|
assert.ok(npmShimContent.includes("aikido-npm"), "npm.cmd should contain aikido-npm");
|
||||||
assert.ok(npmShimContent.includes("@echo off"), "npm.cmd should have Windows batch header");
|
assert.ok(npmShimContent.includes("@echo off"), "npm.cmd should have Windows batch header");
|
||||||
assert.ok(npmShimContent.includes("%*"), "npm.cmd should use Windows argument passing");
|
assert.ok(npmShimContent.includes("%*"), "npm.cmd should use Windows argument passing");
|
||||||
|
assert.ok(
|
||||||
|
npmShimContent.includes('set "SHIM_DIR=%~dp0"'),
|
||||||
|
"npm.cmd should derive the shims directory from its own location",
|
||||||
|
);
|
||||||
|
|
||||||
// Verify Unix shims were NOT created
|
// Verify Unix shims were NOT created
|
||||||
const unixNpmShim = path.join(mockShimsDir, "npm");
|
const unixNpmShim = path.join(mockShimsDir, "npm");
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,10 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { detectShells } from "./shellDetection.js";
|
import { detectShells } from "./shellDetection.js";
|
||||||
import {
|
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
||||||
knownAikidoTools,
|
import { getScriptsDir, getStartupScriptSourcePath } from "../config/safeChainDir.js";
|
||||||
getPackageManagerList,
|
|
||||||
getScriptsDir,
|
|
||||||
} from "./helpers.js";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
// This checks the current file's dirname in a way that's compatible with:
|
|
||||||
// - Modulejs (import.meta.url)
|
|
||||||
// - ES modules (__dirname)
|
|
||||||
// This is needed because safe-chain's npm package is built using ES modules,
|
|
||||||
// but building the binaries requires commonjs.
|
|
||||||
let dirname;
|
|
||||||
if (import.meta.url) {
|
|
||||||
const filename = fileURLToPath(import.meta.url);
|
|
||||||
dirname = path.dirname(filename);
|
|
||||||
} else {
|
|
||||||
dirname = __dirname;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loops over the detected shells and calls the setup function for each.
|
* Loops over the detected shells and calls the setup function for each.
|
||||||
|
|
@ -122,8 +104,7 @@ function copyStartupFiles() {
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use absolute path for source
|
const sourcePath = getStartupScriptSourcePath(import.meta.url, file);
|
||||||
const sourcePath = path.join(dirname, "startup-scripts", file);
|
|
||||||
fs.copyFileSync(sourcePath, targetPath);
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
set -gx PATH $PATH $HOME/.safe-chain/bin
|
set -l safe_chain_script (status filename)
|
||||||
|
set -l safe_chain_scripts_dir (dirname $safe_chain_script)
|
||||||
|
set -l safe_chain_base (dirname $safe_chain_scripts_dir)
|
||||||
|
set -gx PATH $PATH $safe_chain_base/bin
|
||||||
|
|
||||||
function npx
|
function npx
|
||||||
wrapSafeChainCommand "npx" $argv
|
wrapSafeChainCommand "npx" $argv
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
export PATH="$PATH:$HOME/.safe-chain/bin"
|
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||||
|
_sc_script_path="${BASH_SOURCE[0]}"
|
||||||
|
elif [ -n "${ZSH_VERSION:-}" ]; then
|
||||||
|
# ${(%):-%x} uses Zsh prompt expansion to get the sourced file's path.
|
||||||
|
# eval is required so other shells don't try to parse the Zsh-specific syntax.
|
||||||
|
eval '_sc_script_path="${(%):-%x}"'
|
||||||
|
else
|
||||||
|
_sc_script_path="$0"
|
||||||
|
fi
|
||||||
|
_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 _sc_script_path _sc_scripts_dir
|
||||||
|
|
||||||
function npx() {
|
function npx() {
|
||||||
wrapSafeChainCommand "npx" "$@"
|
wrapSafeChainCommand "npx" "$@"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
# $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell
|
# $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 }
|
$isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true }
|
||||||
$pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' }
|
$pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' }
|
||||||
$safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
|
$safeChainBase = Split-Path -Parent $PSScriptRoot
|
||||||
|
$safeChainBin = Join-Path $safeChainBase 'bin'
|
||||||
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
|
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
|
||||||
|
|
||||||
function npx {
|
function npx {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ import {
|
||||||
doesExecutableExistOnSystem,
|
doesExecutableExistOnSystem,
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
|
import { getScriptsDir } from "../../config/safeChainDir.js";
|
||||||
import { execSync, spawnSync } from "child_process";
|
import { execSync, spawnSync } from "child_process";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const shellName = "Bash";
|
const shellName = "Bash";
|
||||||
const executableName = "bash";
|
const executableName = "bash";
|
||||||
|
|
@ -32,10 +34,10 @@ function teardown(tools) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh)
|
// Remove sourcing line to disable safe-chain shell integration
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
|
/^source\s+.*init-posix\.sh.*#\s*Safe-chain/,
|
||||||
eol
|
eol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -44,10 +46,11 @@ function teardown(tools) {
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
const startupFile = getStartupFile();
|
const startupFile = getStartupFile();
|
||||||
|
const scriptsDir = getShellScriptsDir();
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
startupFile,
|
||||||
`source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`,
|
`source ${path.posix.join(scriptsDir, "init-posix.sh")} # Safe-chain bash initialization script`,
|
||||||
eol
|
eol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -94,6 +97,51 @@ function windowsFixPath(path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getShellScriptsDir() {
|
||||||
|
return toBashPath(getScriptsDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function toBashPath(path) {
|
||||||
|
try {
|
||||||
|
if (os.platform() !== "win32") {
|
||||||
|
return path.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const directWindowsPath = windowsPathToBashPath(path);
|
||||||
|
if (directWindowsPath) {
|
||||||
|
return directWindowsPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCygpath()) {
|
||||||
|
return convertCygwinPathToUnix(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.replace(/\\/g, "/");
|
||||||
|
} catch {
|
||||||
|
return path.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
*
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
function windowsPathToBashPath(path) {
|
||||||
|
const match = /^([A-Za-z]):[\\/](.*)$/.exec(path);
|
||||||
|
if (!match) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, driveLetter, rest] = match;
|
||||||
|
return `/${driveLetter.toLowerCase()}/${rest.replace(/\\/g, "/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
function hasCygpath() {
|
function hasCygpath() {
|
||||||
try {
|
try {
|
||||||
var result = spawnSync("where", ["cygpath"], { shell: executableName });
|
var result = spawnSync("where", ["cygpath"], { shell: executableName });
|
||||||
|
|
@ -123,18 +171,40 @@ function cygpathw(path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function convertCygwinPathToUnix(path) {
|
||||||
|
try {
|
||||||
|
var result = spawnSync("cygpath", ["-u", path], {
|
||||||
|
encoding: "utf8",
|
||||||
|
shell: executableName,
|
||||||
|
});
|
||||||
|
if (result.status === 0) {
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
return path.replace(/\\/g, "/");
|
||||||
|
} catch {
|
||||||
|
return path.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getManualTeardownInstructions() {
|
function getManualTeardownInstructions() {
|
||||||
|
const scriptsDir = getShellScriptsDir();
|
||||||
return [
|
return [
|
||||||
`Remove the following line from your ~/.bashrc file:`,
|
`Remove the following line from your ~/.bashrc file:`,
|
||||||
` source ~/.safe-chain/scripts/init-posix.sh`,
|
` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
|
||||||
`Then restart your terminal or run: source ~/.bashrc`,
|
`Then restart your terminal or run: source ~/.bashrc`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getManualSetupInstructions() {
|
function getManualSetupInstructions() {
|
||||||
|
const scriptsDir = getShellScriptsDir();
|
||||||
return [
|
return [
|
||||||
`Add the following line to your ~/.bashrc file:`,
|
`Add the following line to your ~/.bashrc file:`,
|
||||||
` source ~/.safe-chain/scripts/init-posix.sh`,
|
` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
|
||||||
`Then restart your terminal or run: source ~/.bashrc`,
|
`Then restart your terminal or run: source ~/.bashrc`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ describe("Bash shell integration", () => {
|
||||||
let mockStartupFile;
|
let mockStartupFile;
|
||||||
let bash;
|
let bash;
|
||||||
let windowsCygwinPath = "";
|
let windowsCygwinPath = "";
|
||||||
|
let mockScriptsDir = "/test-home/.safe-chain/scripts";
|
||||||
let platform = "linux";
|
let platform = "linux";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
@ -35,6 +36,12 @@ describe("Bash shell integration", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("../../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
|
getScriptsDir: () => mockScriptsDir,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Mock child_process execSync
|
// Mock child_process execSync
|
||||||
mock.module("child_process", {
|
mock.module("child_process", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -61,6 +68,17 @@ describe("Bash shell integration", () => {
|
||||||
stdout: windowsCygwinPath + "\n",
|
stdout: windowsCygwinPath + "\n",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
command === "cygpath" &&
|
||||||
|
args[0] === "-u" &&
|
||||||
|
args[1] === mockScriptsDir
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
stdout: "/c/test-home/.safe-chain/scripts\n",
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -87,6 +105,7 @@ describe("Bash shell integration", () => {
|
||||||
|
|
||||||
// Reset mocks
|
// Reset mocks
|
||||||
mock.reset();
|
mock.reset();
|
||||||
|
mockScriptsDir = "/test-home/.safe-chain/scripts";
|
||||||
platform = "linux";
|
platform = "linux";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -109,7 +128,7 @@ describe("Bash shell integration", () => {
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
content.includes(
|
||||||
"source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
|
"source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -129,7 +148,24 @@ describe("Bash shell integration", () => {
|
||||||
const content = fs.readFileSync(windowsCygwinPath, "utf-8");
|
const content = fs.readFileSync(windowsCygwinPath, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
content.includes(
|
||||||
"source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
|
"source /c/test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should write a bash-compatible scripts path on Windows", () => {
|
||||||
|
platform = "win32";
|
||||||
|
windowsCygwinPath = mockStartupFile;
|
||||||
|
mockScriptsDir = "C:\\test-home\\.safe-chain\\scripts";
|
||||||
|
mockStartupFile = "DUMMY";
|
||||||
|
|
||||||
|
const result = bash.setup();
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(windowsCygwinPath, "utf-8");
|
||||||
|
assert.ok(
|
||||||
|
content.includes(
|
||||||
|
"source /c/test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -209,13 +245,13 @@ describe("Bash shell integration", () => {
|
||||||
// Setup
|
// Setup
|
||||||
bash.setup(tools);
|
bash.setup(tools);
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
|
assert.ok(content.includes("source /test-home/.safe-chain/scripts/init-posix.sh"));
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
bash.teardown(tools);
|
bash.teardown(tools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
!content.includes("source /test-home/.safe-chain/scripts/init-posix.sh")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -236,7 +272,7 @@ describe("Bash shell integration", () => {
|
||||||
const initialContent = [
|
const initialContent = [
|
||||||
"#!/bin/bash",
|
"#!/bin/bash",
|
||||||
"alias npm='old-npm'",
|
"alias npm='old-npm'",
|
||||||
"source ~/.safe-chain/scripts/init-posix.sh",
|
"source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script",
|
||||||
"alias ls='ls --color=auto'",
|
"alias ls='ls --color=auto'",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
|
@ -247,7 +283,7 @@ describe("Bash shell integration", () => {
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(!content.includes("alias npm="));
|
assert.ok(!content.includes("alias npm="));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
!content.includes("source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script")
|
||||||
);
|
);
|
||||||
assert.ok(content.includes("alias ls="));
|
assert.ok(content.includes("alias ls="));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import {
|
||||||
doesExecutableExistOnSystem,
|
doesExecutableExistOnSystem,
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
|
import { getScriptsDir } from "../../config/safeChainDir.js";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const shellName = "Fish";
|
const shellName = "Fish";
|
||||||
const executableName = "fish";
|
const executableName = "fish";
|
||||||
|
|
@ -31,10 +33,10 @@ function teardown(tools) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish)
|
// Remove sourcing line to prevent safe-chain initialization in future shell sessions
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/,
|
/^source\s+.*init-fish\.fish.*#\s*Safe-chain/,
|
||||||
eol
|
eol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -46,7 +48,7 @@ function setup() {
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
startupFile,
|
||||||
`source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script`,
|
`source ${path.join(getScriptsDir(), "init-fish.fish")} # Safe-chain Fish initialization script`,
|
||||||
eol
|
eol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -69,7 +71,7 @@ function getStartupFile() {
|
||||||
function getManualTeardownInstructions() {
|
function getManualTeardownInstructions() {
|
||||||
return [
|
return [
|
||||||
`Remove the following line from your ~/.config/fish/config.fish file:`,
|
`Remove the following line from your ~/.config/fish/config.fish file:`,
|
||||||
` source ~/.safe-chain/scripts/init-fish.fish`,
|
` source ${path.join(getScriptsDir(), "init-fish.fish")}`,
|
||||||
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +79,7 @@ function getManualTeardownInstructions() {
|
||||||
function getManualSetupInstructions() {
|
function getManualSetupInstructions() {
|
||||||
return [
|
return [
|
||||||
`Add the following line to your ~/.config/fish/config.fish file:`,
|
`Add the following line to your ~/.config/fish/config.fish file:`,
|
||||||
` source ~/.safe-chain/scripts/init-fish.fish`,
|
` source ${path.join(getScriptsDir(), "init-fish.fish")}`,
|
||||||
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ describe("Fish shell integration", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("../../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
|
getScriptsDir: () => "/test-home/.safe-chain/scripts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Mock child_process execSync
|
// Mock child_process execSync
|
||||||
mock.module("child_process", {
|
mock.module("child_process", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -72,7 +78,7 @@ describe("Fish shell integration", () => {
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes('source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script')
|
content.includes('source /test-home/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -81,7 +87,7 @@ describe("Fish shell integration", () => {
|
||||||
fish.setup();
|
fish.setup();
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length;
|
const sourceMatches = (content.match(/source \/test-home\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length;
|
||||||
assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)");
|
assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -93,7 +99,7 @@ describe("Fish shell integration", () => {
|
||||||
"alias npm 'aikido-npm'",
|
"alias npm 'aikido-npm'",
|
||||||
"alias npx 'aikido-npx'",
|
"alias npx 'aikido-npx'",
|
||||||
"alias yarn 'aikido-yarn'",
|
"alias yarn 'aikido-yarn'",
|
||||||
"source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script",
|
"source /test-home/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script",
|
||||||
"alias ls 'ls --color=auto'",
|
"alias ls 'ls --color=auto'",
|
||||||
"alias grep 'grep --color=auto'",
|
"alias grep 'grep --color=auto'",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
@ -107,7 +113,7 @@ describe("Fish shell integration", () => {
|
||||||
assert.ok(!content.includes("alias npm "));
|
assert.ok(!content.includes("alias npm "));
|
||||||
assert.ok(!content.includes("alias npx "));
|
assert.ok(!content.includes("alias npx "));
|
||||||
assert.ok(!content.includes("alias yarn "));
|
assert.ok(!content.includes("alias yarn "));
|
||||||
assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish"));
|
assert.ok(!content.includes("source /test-home/.safe-chain/scripts/init-fish.fish"));
|
||||||
assert.ok(content.includes("alias ls "));
|
assert.ok(content.includes("alias ls "));
|
||||||
assert.ok(content.includes("alias grep "));
|
assert.ok(content.includes("alias grep "));
|
||||||
});
|
});
|
||||||
|
|
@ -162,12 +168,12 @@ describe("Fish shell integration", () => {
|
||||||
// Setup
|
// Setup
|
||||||
fish.setup();
|
fish.setup();
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(content.includes('source ~/.safe-chain/scripts/init-fish.fish'));
|
assert.ok(content.includes('source /test-home/.safe-chain/scripts/init-fish.fish'));
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
fish.teardown(tools);
|
fish.teardown(tools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish"));
|
assert.ok(!content.includes("source /test-home/.safe-chain/scripts/init-fish.fish"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle multiple setup calls", () => {
|
it("should handle multiple setup calls", () => {
|
||||||
|
|
@ -176,7 +182,7 @@ describe("Fish shell integration", () => {
|
||||||
fish.setup();
|
fish.setup();
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length;
|
const sourceMatches = (content.match(/source \/test-home\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length;
|
||||||
assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle");
|
assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import {
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
validatePowerShellExecutionPolicy,
|
validatePowerShellExecutionPolicy,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
|
import { getScriptsDir } from "../../config/safeChainDir.js";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const shellName = "PowerShell Core";
|
const shellName = "PowerShell Core";
|
||||||
const executableName = "pwsh";
|
const executableName = "pwsh";
|
||||||
|
|
@ -30,10 +32,10 @@ function teardown(tools) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the line that sources the safe-chain PowerShell initialization script
|
// Remove sourcing line to prevent shell from loading safe-chain after uninstallation
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/,
|
/^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -52,7 +54,7 @@ async function setup() {
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
startupFile,
|
||||||
`. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`,
|
`. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -74,7 +76,7 @@ function getStartupFile() {
|
||||||
function getManualTeardownInstructions() {
|
function getManualTeardownInstructions() {
|
||||||
return [
|
return [
|
||||||
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
|
||||||
`Then restart your terminal or run: . $PROFILE`,
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +84,7 @@ function getManualTeardownInstructions() {
|
||||||
function getManualSetupInstructions() {
|
function getManualSetupInstructions() {
|
||||||
return [
|
return [
|
||||||
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
|
||||||
`Then restart your terminal or run: . $PROFILE`,
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ describe("PowerShell Core shell integration", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("../../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
|
getScriptsDir: () => "/test-home/.safe-chain/scripts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Mock child_process execSync
|
// Mock child_process execSync
|
||||||
mock.module("child_process", {
|
mock.module("child_process", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -83,7 +89,7 @@ describe("PowerShell Core shell integration", () => {
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
content.includes(
|
||||||
'. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
'. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -93,7 +99,7 @@ describe("PowerShell Core shell integration", () => {
|
||||||
it("should remove init-pwsh.ps1 source line", () => {
|
it("should remove init-pwsh.ps1 source line", () => {
|
||||||
const initialContent = [
|
const initialContent = [
|
||||||
"# PowerShell profile",
|
"# PowerShell profile",
|
||||||
'. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
'. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
||||||
"Set-Alias ls Get-ChildItem",
|
"Set-Alias ls Get-ChildItem",
|
||||||
"Set-Alias grep Select-String",
|
"Set-Alias grep Select-String",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
@ -105,7 +111,7 @@ describe("PowerShell Core shell integration", () => {
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
!content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
assert.ok(content.includes("Set-Alias ls "));
|
assert.ok(content.includes("Set-Alias ls "));
|
||||||
assert.ok(content.includes("Set-Alias grep "));
|
assert.ok(content.includes("Set-Alias grep "));
|
||||||
|
|
@ -180,14 +186,14 @@ describe("PowerShell Core shell integration", () => {
|
||||||
await powershell.setup();
|
await powershell.setup();
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
powershell.teardown(knownAikidoTools);
|
powershell.teardown(knownAikidoTools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
!content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -198,7 +204,7 @@ describe("PowerShell Core shell integration", () => {
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
const sourceMatches = (
|
const sourceMatches = (
|
||||||
content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) ||
|
content.match(/\. "\/test-home\/\.safe-chain\/scripts\/init-pwsh\.ps1"/g) ||
|
||||||
[]
|
[]
|
||||||
).length;
|
).length;
|
||||||
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import {
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
validatePowerShellExecutionPolicy,
|
validatePowerShellExecutionPolicy,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
|
import { getScriptsDir } from "../../config/safeChainDir.js";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const shellName = "Windows PowerShell";
|
const shellName = "Windows PowerShell";
|
||||||
const executableName = "powershell";
|
const executableName = "powershell";
|
||||||
|
|
@ -30,10 +32,10 @@ function teardown(tools) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the line that sources the safe-chain PowerShell initialization script
|
// Remove sourcing line to clean up safe-chain integration from the shell profile
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/,
|
/^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -52,7 +54,7 @@ async function setup() {
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
startupFile,
|
||||||
`. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`,
|
`. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -74,7 +76,7 @@ function getStartupFile() {
|
||||||
function getManualTeardownInstructions() {
|
function getManualTeardownInstructions() {
|
||||||
return [
|
return [
|
||||||
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
|
||||||
`Then restart your terminal or run: . $PROFILE`,
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +84,7 @@ function getManualTeardownInstructions() {
|
||||||
function getManualSetupInstructions() {
|
function getManualSetupInstructions() {
|
||||||
return [
|
return [
|
||||||
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
|
||||||
`Then restart your terminal or run: . $PROFILE`,
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("../../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
|
getScriptsDir: () => "/test-home/.safe-chain/scripts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Mock child_process execSync
|
// Mock child_process execSync
|
||||||
mock.module("child_process", {
|
mock.module("child_process", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -83,7 +89,7 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
content.includes(
|
||||||
'. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
'. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -93,7 +99,7 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
it("should remove init-pwsh.ps1 source line", () => {
|
it("should remove init-pwsh.ps1 source line", () => {
|
||||||
const initialContent = [
|
const initialContent = [
|
||||||
"# Windows PowerShell profile",
|
"# Windows PowerShell profile",
|
||||||
'. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
'. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script',
|
||||||
"Set-Alias ls Get-ChildItem",
|
"Set-Alias ls Get-ChildItem",
|
||||||
"Set-Alias grep Select-String",
|
"Set-Alias grep Select-String",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
@ -105,7 +111,7 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
!content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
assert.ok(content.includes("Set-Alias ls "));
|
assert.ok(content.includes("Set-Alias ls "));
|
||||||
assert.ok(content.includes("Set-Alias grep "));
|
assert.ok(content.includes("Set-Alias grep "));
|
||||||
|
|
@ -180,14 +186,14 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
await windowsPowershell.setup();
|
await windowsPowershell.setup();
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
windowsPowershell.teardown(knownAikidoTools);
|
windowsPowershell.teardown(knownAikidoTools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
!content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -198,7 +204,7 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
const sourceMatches = (
|
const sourceMatches = (
|
||||||
content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) ||
|
content.match(/\. "\/test-home\/\.safe-chain\/scripts\/init-pwsh\.ps1"/g) ||
|
||||||
[]
|
[]
|
||||||
).length;
|
).length;
|
||||||
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import {
|
||||||
doesExecutableExistOnSystem,
|
doesExecutableExistOnSystem,
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
|
import { getScriptsDir } from "../../config/safeChainDir.js";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const shellName = "Zsh";
|
const shellName = "Zsh";
|
||||||
const executableName = "zsh";
|
const executableName = "zsh";
|
||||||
|
|
@ -31,10 +33,10 @@ function teardown(tools) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes the line that sources the safe-chain zsh initialization script (~/.safe-chain/scripts/init-posix.sh)
|
// Remove sourcing line to complete shell integration cleanup
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
|
/^source\s+.*init-posix\.sh.*#\s*Safe-chain/,
|
||||||
eol
|
eol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -46,7 +48,7 @@ function setup() {
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
startupFile,
|
||||||
`source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script`,
|
`source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain Zsh initialization script`,
|
||||||
eol
|
eol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -69,7 +71,7 @@ function getStartupFile() {
|
||||||
function getManualTeardownInstructions() {
|
function getManualTeardownInstructions() {
|
||||||
return [
|
return [
|
||||||
`Remove the following line from your ~/.zshrc file:`,
|
`Remove the following line from your ~/.zshrc file:`,
|
||||||
` source ~/.safe-chain/scripts/init-posix.sh`,
|
` source ${path.join(getScriptsDir(), "init-posix.sh")}`,
|
||||||
`Then restart your terminal or run: source ~/.zshrc`,
|
`Then restart your terminal or run: source ~/.zshrc`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +79,7 @@ function getManualTeardownInstructions() {
|
||||||
function getManualSetupInstructions() {
|
function getManualSetupInstructions() {
|
||||||
return [
|
return [
|
||||||
`Add the following line to your ~/.zshrc file:`,
|
`Add the following line to your ~/.zshrc file:`,
|
||||||
` source ~/.safe-chain/scripts/init-posix.sh`,
|
` source ${path.join(getScriptsDir(), "init-posix.sh")}`,
|
||||||
`Then restart your terminal or run: source ~/.zshrc`,
|
`Then restart your terminal or run: source ~/.zshrc`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ describe("Zsh shell integration", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("../../config/safeChainDir.js", {
|
||||||
|
namedExports: {
|
||||||
|
getScriptsDir: () => "/test-home/.safe-chain/scripts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Mock child_process execSync
|
// Mock child_process execSync
|
||||||
mock.module("child_process", {
|
mock.module("child_process", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -73,7 +79,7 @@ describe("Zsh shell integration", () => {
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
content.includes(
|
||||||
"source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script"
|
"source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -83,7 +89,7 @@ describe("Zsh shell integration", () => {
|
||||||
assert.strictEqual(result, true);
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
|
assert.ok(content.includes("source /test-home/.safe-chain/scripts/init-posix.sh"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -114,7 +120,7 @@ describe("Zsh shell integration", () => {
|
||||||
it("should remove zsh initialization script source line", () => {
|
it("should remove zsh initialization script source line", () => {
|
||||||
const initialContent = [
|
const initialContent = [
|
||||||
"#!/bin/zsh",
|
"#!/bin/zsh",
|
||||||
"source ~/.safe-chain/scripts/init-posix.sh",
|
"source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script",
|
||||||
"alias ls='ls --color=auto'",
|
"alias ls='ls --color=auto'",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
|
@ -125,7 +131,7 @@ describe("Zsh shell integration", () => {
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
!content.includes("source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script")
|
||||||
);
|
);
|
||||||
assert.ok(content.includes("alias ls="));
|
assert.ok(content.includes("alias ls="));
|
||||||
});
|
});
|
||||||
|
|
@ -180,13 +186,13 @@ describe("Zsh shell integration", () => {
|
||||||
// Setup
|
// Setup
|
||||||
zsh.setup();
|
zsh.setup();
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
|
assert.ok(content.includes("source /test-home/.safe-chain/scripts/init-posix.sh"));
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
zsh.teardown(tools);
|
zsh.teardown(tools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
!content.includes("source /test-home/.safe-chain/scripts/init-posix.sh")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -207,7 +213,7 @@ describe("Zsh shell integration", () => {
|
||||||
const initialContent = [
|
const initialContent = [
|
||||||
"#!/bin/zsh",
|
"#!/bin/zsh",
|
||||||
"alias npm='old-npm'",
|
"alias npm='old-npm'",
|
||||||
"source ~/.safe-chain/scripts/init-posix.sh",
|
"source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script",
|
||||||
"alias ls='ls --color=auto'",
|
"alias ls='ls --color=auto'",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
|
@ -218,7 +224,7 @@ describe("Zsh shell integration", () => {
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(!content.includes("alias npm="));
|
assert.ok(!content.includes("alias npm="));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
!content.includes("source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script")
|
||||||
);
|
);
|
||||||
assert.ok(content.includes("alias ls="));
|
assert.ok(content.includes("alias ls="));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { detectShells } from "./shellDetection.js";
|
import { detectShells } from "./shellDetection.js";
|
||||||
import { knownAikidoTools, getPackageManagerList, getShimsDir, getScriptsDir } from "./helpers.js";
|
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
||||||
|
import { getShimsDir, getScriptsDir } from "../config/safeChainDir.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -109,4 +110,5 @@ export async function teardownDirectories() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,14 @@ export class DockerTestContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openShell(shell) {
|
async openShell(shell, { user } = {}) {
|
||||||
|
const execArgs = user
|
||||||
|
? ["exec", "-it", "-u", user, this.containerName, shell]
|
||||||
|
: ["exec", "-it", this.containerName, shell];
|
||||||
|
|
||||||
let ptyProcess = pty.spawn(
|
let ptyProcess = pty.spawn(
|
||||||
"docker",
|
"docker",
|
||||||
["exec", "-it", this.containerName, shell],
|
execArgs,
|
||||||
{
|
{
|
||||||
name: "xterm-color",
|
name: "xterm-color",
|
||||||
cols: 80,
|
cols: 80,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue