Merge pull request #185 from AikidoSec/safe-chain-binaries

Safe-chain: create standalone binaries
This commit is contained in:
Sander Declerck 2025-12-03 13:27:45 +01:00 committed by GitHub
commit 3595e87cd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 3398 additions and 453 deletions

View file

@ -7,10 +7,28 @@ on:
permissions:
id-token: write
contents: read
contents: write
jobs:
set-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.tag }}
steps:
- name: Set version number
id: get_version
run: |
version="${{ github.ref_name }}"
echo "tag=$version" >> $GITHUB_OUTPUT
create-binaries:
needs: set-version
uses: ./.github/workflows/create-artifact.yml
with:
version: ${{ needs.set-version.outputs.version }}
build:
needs: [set-version, create-binaries]
runs-on: ubuntu-latest
steps:
@ -30,14 +48,8 @@ jobs:
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
- name: Set version number
id: get_version
run: |
version="${{ github.ref_name }}"
echo "tag=$version" >> $GITHUB_OUTPUT
- name: Set the version in safe-chain package
run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }} --workspace=packages/safe-chain
run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain
- name: Install dependencies
run: npm ci
@ -55,3 +67,31 @@ jobs:
run: |
echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM"
npm publish --workspace=packages/safe-chain --access public --provenance
- name: Download all binary artifacts
uses: actions/download-artifact@v4
with:
path: binaries/
pattern: safe-chain-*
merge-multiple: false
- name: Rename binaries to include platform and architecture
run: |
mv binaries/safe-chain-macos-x64/safe-chain binaries/safe-chain-macos-x64/safe-chain-macos-x64
mv binaries/safe-chain-macos-arm64/safe-chain binaries/safe-chain-macos-arm64/safe-chain-macos-arm64
mv binaries/safe-chain-linux-x64/safe-chain binaries/safe-chain-linux-x64/safe-chain-linux-x64
mv binaries/safe-chain-linux-arm64/safe-chain binaries/safe-chain-linux-arm64/safe-chain-linux-arm64
mv binaries/safe-chain-win-x64/safe-chain.exe binaries/safe-chain-win-x64/safe-chain-win-x64.exe
mv binaries/safe-chain-win-arm64/safe-chain.exe binaries/safe-chain-win-arm64/safe-chain-win-arm64.exe
- name: Upload binaries to existing GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ needs.set-version.outputs.version }} \
binaries/safe-chain-macos-x64/* \
binaries/safe-chain-macos-arm64/* \
binaries/safe-chain-linux-x64/* \
binaries/safe-chain-linux-arm64/* \
binaries/safe-chain-win-x64/* \
binaries/safe-chain-win-arm64/*

82
.github/workflows/create-artifact.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Create binaries
on:
pull_request:
workflow_call:
inputs:
version:
description: 'Version to set in package.json'
required: false
type: string
jobs:
create-binaries:
name: Create binary for ${{ matrix.os }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: macos
arch: x64
runner: macos-15-intel
target: node20-macos-x64
extension: ""
- os: macos
arch: arm64
runner: macos-latest
target: node20-macos-arm64
extension: ""
- os: linux
arch: x64
runner: ubuntu-latest
target: node20-linux-x64
extension: ""
- os: linux
arch: arm64
runner: ubuntu-24.04-arm
target: node20-linux-arm64
extension: ""
- os: win
arch: x64
runner: windows-latest
target: node20-win-x64
extension: ".exe"
- os: win
arch: arm64
runner: windows-11-arm
target: node20-win-arm64
extension: ".exe"
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Setup safe-chain
run: |
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
- name: Set the version in safe-chain package
if: inputs.version != ''
run: npm --no-git-tag-version version ${{ inputs.version }} --workspace=packages/safe-chain
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Create binary
run: |
node build.js ${{ matrix.target }}
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: safe-chain-${{ matrix.os }}-${{ matrix.arch }}
path: dist/*

6
.gitignore vendored
View file

@ -143,4 +143,8 @@ vite.config.ts.timestamp-*
# AI
Claude.md
.claude
.reference
.reference
# Build files
build/
dist/

100
README.md
View file

@ -27,29 +27,45 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi
## Installation
Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
Installing the Aikido Safe Chain is easy with our one-line installer.
1. **Install the Aikido Safe Chain package globally** using npm:
```shell
npm install -g @aikidosec/safe-chain
```
2. **Setup the shell integration** by running:
> ⚠️ **Already installed via npm?** See the [migration guide](docs/npm-to-binary-migration.md) to switch to the binary version.
```shell
safe-chain setup
```
### Unix/Linux/macOS
To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag:
**Default installation (JavaScript packages only):**
```shell
safe-chain setup --include-python
```
```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh
```
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
**Include Python support (pip/pip3/uv):**
```shell
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python
```
### Windows (PowerShell)
**Default installation (JavaScript packages only):**
```powershell
iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing)
```
**Include Python support (pip/pip3/uv):**
```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -includepython"
```
### Verify the installation
1. **❗Restart your terminal** to start using the Aikido Safe Chain.
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
4. **Verify the installation** by running one of the following commands:
2. **Verify the installation** by running one of the following commands:
For JavaScript/Node.js:
@ -57,7 +73,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
npm install safe-chain-test
```
For Python (beta):
For Python (if you enabled Python support):
```shell
pip3 install safe-chain-pi-test
@ -168,21 +184,37 @@ You can protect your CI/CD pipelines from malicious packages by integrating Aiki
For optimal protection in CI/CD environments, we recommend using **npm >= 10.4.0** as it provides full dependency tree scanning. Other package managers currently offer limited scanning of install command arguments only.
## Setup
## Installation for CI/CD
To use Aikido Safe Chain in CI/CD environments, run the following command after installing the package:
Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases.
### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.)
**JavaScript only:**
```shell
safe-chain setup-ci
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
```
To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag:
**With Python support:**
```shell
safe-chain setup-ci --include-python
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
```
This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands.
### Windows (Azure Pipelines, etc.)
**JavaScript only:**
```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci"
```
**With Python support:**
```powershell
iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython"
```
## Supported Platforms
@ -198,16 +230,15 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
node-version: "22"
cache: "npm"
- name: Setup safe-chain
run: |
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
- name: Install safe-chain
run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
- name: Install dependencies
run: |
npm ci
run: npm ci
```
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
## Azure DevOps Example
```yaml
@ -216,14 +247,13 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
versionSpec: "22.x"
displayName: "Install Node.js"
- script: |
npm i -g @aikidosec/safe-chain
safe-chain setup-ci
displayName: "Install safe chain"
- script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python
displayName: "Install safe-chain"
- script: |
npm ci
displayName: "npm install and build"
- script: npm ci
displayName: "Install dependencies"
```
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.

125
build.js Normal file
View file

@ -0,0 +1,125 @@
import { build } from "esbuild";
import { mkdir, cp, rm, readFile, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
import { resolve } from "node:path";
const target = process.argv[2];
if (!target) {
// eslint-disable-next-line no-console
console.error("Usage: node build.js <target>");
// eslint-disable-next-line no-console
console.error("Example: node build.js node22-macos-arm64");
process.exit(1);
}
(async function main() {
await clearOutputFolder();
// Esbuild creates a single safe-chain.cjs with all dependencies included
await bundleSafeChain();
// Copy assets that need to be included in the binary
// - All shell scripts that are used to setup safe-chain
// - Certifi because it contains static root certs for Python
// - Package.json for its metadata (package name, version, ...)
await copyShellScripts();
await copyCertifi();
await copyAndModifyPackageJson();
// Creates a single binary with safe-chain.cjs and the copied assets
await buildSafeChainBinary(target);
})();
async function clearOutputFolder() {
await rm("./build", { recursive: true, force: true });
await mkdir("./build");
}
async function bundleSafeChain() {
await build({
entryPoints: ["./packages/safe-chain/bin/safe-chain.js"],
bundle: true,
platform: "node",
target: "node24",
outfile: "./build/bin/safe-chain.cjs",
external: ["certifi"],
});
let bundledContent = await readFile("./build/bin/safe-chain.cjs", "utf-8");
await writeFile("./build/bin/safe-chain.cjs", bundledContent);
}
async function copyShellScripts() {
await mkdir("./build/bin/startup-scripts", { recursive: true });
await cp(
"./packages/safe-chain/src/shell-integration/startup-scripts/",
"./build/bin/startup-scripts",
{ recursive: true }
);
await mkdir("./build/bin/path-wrappers", { recursive: true });
await cp(
"./packages/safe-chain/src/shell-integration/path-wrappers/",
"./build/bin/path-wrappers",
{ recursive: true }
);
}
async function copyCertifi() {
await mkdir("./build/node_modules/certifi", { recursive: true });
await cp("./node_modules/certifi/", "./build/node_modules/certifi", {
recursive: true,
});
}
async function copyAndModifyPackageJson() {
const packageJsonContent = await readFile(
"./packages/safe-chain/package.json",
"utf-8"
);
const packageJson = JSON.parse(packageJsonContent);
delete packageJson.main;
delete packageJson.scripts;
delete packageJson.exports;
delete packageJson.dependencies;
delete packageJson.devDependencies;
packageJson.bin = {
"safe-chain": "bin/safe-chain.cjs",
};
packageJson.type = "commonjs";
packageJson.pkg = {
outputPath: "dist",
assets: [
"node_modules/certifi/**/*",
"bin/startup-scripts/**/*",
"bin/path-wrappers/**/*",
],
};
await writeFile("./build/package.json", JSON.stringify(packageJson, null, 2));
return packageJson;
}
function buildSafeChainBinary(target) {
return new Promise((promiseResolve, reject) => {
// Use .cmd on Windows, resolve to absolute path for cross-platform compatibility
const pkgBin = process.platform === "win32"
? resolve("node_modules/.bin/pkg.cmd")
: resolve("node_modules/.bin/pkg");
const pkg = spawn(pkgBin, ["./build/package.json", "-t", target], {
stdio: "inherit",
shell: true,
});
pkg.on("close", (code) => {
if (code !== 0) {
reject(new Error(`pkg process exited with code ${code}`));
} else {
promiseResolve();
}
});
});
}

View file

@ -0,0 +1,89 @@
# Migrating from npm global tool to binary installation
If you previously installed safe-chain as an npm global package, you need to migrate to the binary installation.
Depending on the version manager you're using, the uninstall process differs:
### Standard npm (no version manager)
1. **Clean up shell aliases:**
```bash
safe-chain teardown
```
2. **Restart your terminal**
3. **Uninstall the npm package:**
```bash
npm uninstall -g @aikidosec/safe-chain
```
4. **Install the binary version** (see [Installation](../README.md#installation))
### nvm (Node Version Manager)
**Important:** nvm installs global packages separately for each Node version, so safe-chain must be uninstalled from each version where it was installed.
1. **Clean up shell aliases:**
```bash
safe-chain teardown
```
2. **Restart your terminal**
3. **Uninstall from all Node versions:**
**Option A** - Automated script (recommended):
```bash
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do nvm use $version && npm uninstall -g @aikidosec/safe-chain; done
```
**Option B** - Manual per version:
```bash
nvm use <version>
npm uninstall -g @aikidosec/safe-chain
```
Repeat for each Node version where safe-chain was installed.
4. **Install the binary version** (see [Installation](../README.md#installation))
### Volta
1. **Clean up shell aliases:**
```bash
safe-chain teardown
```
2. **Restart your terminal**
3. **Uninstall the Volta package:**
```bash
volta uninstall @aikidosec/safe-chain
```
4. **Install the binary version** (see [Installation](../README.md#installation))
## Troubleshooting
### Shell aliases still present after migration
1. Run `safe-chain teardown` (if the binary is installed)
2. Manually remove any safe-chain entries from your shell config files:
- Bash: `~/.bashrc`
- Zsh: `~/.zshrc`
- Fish: `~/.config/fish/config.fish`
- PowerShell: `$PROFILE`
3. Restart your terminal
4. Re-run the install script
### "command not found: safe-chain" after migration
The binary installation directory (`~/.safe-chain/bin`) may not be in your PATH. Restart your terminal. If the problem persists: re-run the installation of safe-chain.

View file

@ -0,0 +1,213 @@
# Downloads and installs safe-chain for Windows
#
# Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md
param(
[switch]$ci,
[switch]$includepython
)
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
$InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin"
$RepoUrl = "https://github.com/AikidoSec/safe-chain"
# Ensure TLS 1.2 is enabled for downloads
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Helper functions
function Write-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Green
}
function Write-Warn {
param([string]$Message)
Write-Host "[WARN] $Message" -ForegroundColor Yellow
}
function Write-Error-Custom {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
exit 1
}
# Fetch latest release version tag from GitHub
function Get-LatestVersion {
try {
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" -UseBasicParsing
$latestVersion = $response.tag_name
if ([string]::IsNullOrWhiteSpace($latestVersion)) {
Write-Error-Custom "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable."
}
return $latestVersion
}
catch {
Write-Error-Custom "Failed to fetch latest version from GitHub API: $($_.Exception.Message). Please set SAFE_CHAIN_VERSION environment variable."
}
}
# Detect architecture
function Get-Architecture {
$arch = $env:PROCESSOR_ARCHITECTURE
switch ($arch) {
"AMD64" { return "x64" }
"ARM64" { return "arm64" }
default { Write-Error-Custom "Unsupported architecture: $arch" }
}
}
# Check and uninstall npm global package if present
function Remove-NpmInstallation {
# Check if npm is available
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
return
}
# Check if safe-chain is installed as an npm global package
npm list -g @aikidosec/safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Detected npm global installation of @aikidosec/safe-chain"
Write-Info "Uninstalling npm version before installing binary version..."
npm uninstall -g @aikidosec/safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Successfully uninstalled npm version"
}
else {
Write-Warn "Failed to uninstall npm version automatically"
Write-Warn "Please run: npm uninstall -g @aikidosec/safe-chain"
}
}
}
# Check and uninstall Volta-managed package if present
function Remove-VoltaInstallation {
# Check if Volta is available
if (-not (Get-Command volta -ErrorAction SilentlyContinue)) {
return
}
# Volta manages global packages in its own directory
# Check if safe-chain is installed via Volta
volta list safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Detected Volta installation of @aikidosec/safe-chain"
Write-Info "Uninstalling Volta version before installing binary version..."
volta uninstall @aikidosec/safe-chain 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Info "Successfully uninstalled Volta version"
}
else {
Write-Warn "Failed to uninstall Volta version automatically"
Write-Warn "Please run: volta uninstall @aikidosec/safe-chain"
}
}
}
# Main installation
function Install-SafeChain {
# Fetch latest version if VERSION is not set
if ([string]::IsNullOrWhiteSpace($script:Version)) {
Write-Info "Fetching latest release version..."
$script:Version = Get-LatestVersion
}
# Build installation message
$installMsg = "Installing safe-chain $Version"
if ($includepython) {
$installMsg += " with python"
}
if ($ci) {
$installMsg += " in ci"
}
Write-Info $installMsg
# Check for existing safe-chain installation through npm or volta
Remove-NpmInstallation
Remove-VoltaInstallation
# Detect platform
$arch = Get-Architecture
$binaryName = "safe-chain-win-$arch.exe"
Write-Info "Detected architecture: $arch"
# Create installation directory
if (-not (Test-Path $InstallDir)) {
Write-Info "Creating installation directory: $InstallDir"
try {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
catch {
Write-Error-Custom "Failed to create directory $InstallDir : $_"
}
}
# Download binary
$downloadUrl = "$RepoUrl/releases/download/$Version/$binaryName"
$tempFile = Join-Path $InstallDir $binaryName
Write-Info "Downloading from: $downloadUrl"
try {
# Download with progress suppressed for cleaner output
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing
$ProgressPreference = 'Continue'
}
catch {
Write-Error-Custom "Failed to download from $downloadUrl : $_"
}
# Rename to final location
$finalFile = Join-Path $InstallDir "safe-chain.exe"
try {
Move-Item -Path $tempFile -Destination $finalFile -Force
}
catch {
Write-Error-Custom "Failed to move binary to $finalFile : $_"
}
Write-Info "Binary installed to: $finalFile"
# Build setup command based on parameters
$setupCmd = if ($ci) { "setup-ci" } else { "setup" }
$setupArgs = @()
if ($includepython) {
$setupArgs += "--include-python"
}
# 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
try {
Install-SafeChain
}
catch {
Write-Error-Custom "Installation failed: $_"
}

View file

@ -0,0 +1,224 @@
#!/bin/sh
# Downloads and installs safe-chain, depending on the operating system and architecture
#
# Usage with "curl -fsSL {url} | sh" --> See README.md
set -e # Exit on error
# Configuration
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
INSTALL_DIR="${HOME}/.safe-chain/bin"
REPO_URL="https://github.com/AikidoSec/safe-chain"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Helper functions
info() {
printf "${GREEN}[INFO]${NC} %s\n" "$1"
}
warn() {
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
}
error() {
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
exit 1
}
# Detect OS
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
*) error "Unsupported operating system: $(uname -s)" ;;
esac
}
# Detect architecture
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo "x64" ;;
aarch64|arm64) echo "arm64" ;;
*) error "Unsupported architecture: $(uname -m)" ;;
esac
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Fetch latest release version tag from GitHub
fetch_latest_version() {
# Try using GitHub API to get the latest release tag
if command_exists curl; then
latest_version=$(curl -fsSL "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
elif command_exists wget; then
latest_version=$(wget -qO- "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
else
error "Neither curl nor wget found. Please install one of them or set SAFE_CHAIN_VERSION environment variable."
fi
if [ -z "$latest_version" ]; then
error "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable."
fi
echo "$latest_version"
}
# Download file
download() {
url="$1"
dest="$2"
if command_exists curl; then
curl -fsSL "$url" -o "$dest" || error "Failed to download from $url"
elif command_exists wget; then
wget -q "$url" -O "$dest" || error "Failed to download from $url"
else
error "Neither curl nor wget found. Please install one of them."
fi
}
# Check and uninstall npm global package if present
remove_npm_installation() {
if ! command_exists npm; then
return
fi
# Check if safe-chain is installed as an npm global package
if npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
info "Detected npm global installation of @aikidosec/safe-chain"
info "Uninstalling npm version before installing binary version..."
if npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
info "Successfully uninstalled npm version"
else
warn "Failed to uninstall npm version automatically"
warn "Please run: npm uninstall -g @aikidosec/safe-chain"
fi
fi
}
# Check and uninstall Volta-managed package if present
remove_volta_installation() {
if ! command_exists volta; then
return
fi
# Volta manages global packages in its own directory
# Check if safe-chain is installed via Volta
if volta list safe-chain >/dev/null 2>&1; then
info "Detected Volta installation of @aikidosec/safe-chain"
info "Uninstalling Volta version before installing binary version..."
if volta uninstall @aikidosec/safe-chain >/dev/null 2>&1; then
info "Successfully uninstalled Volta version"
else
warn "Failed to uninstall Volta version automatically"
warn "Please run: volta uninstall @aikidosec/safe-chain"
fi
fi
}
# Parse command-line arguments
parse_arguments() {
for arg in "$@"; do
case "$arg" in
--ci)
USE_CI_SETUP=true
;;
--include-python)
INCLUDE_PYTHON=true
;;
*)
error "Unknown argument: $arg"
;;
esac
done
}
# Main installation
main() {
# Initialize argument flags
USE_CI_SETUP=false
INCLUDE_PYTHON=false
# Parse command-line arguments
parse_arguments "$@"
# Fetch latest version if VERSION is not set
if [ -z "$VERSION" ]; then
info "Fetching latest release version..."
VERSION=$(fetch_latest_version)
fi
# Build installation message
INSTALL_MSG="Installing safe-chain ${VERSION}"
if [ "$INCLUDE_PYTHON" = "true" ]; then
INSTALL_MSG="${INSTALL_MSG} with python"
fi
if [ "$USE_CI_SETUP" = "true" ]; then
INSTALL_MSG="${INSTALL_MSG} in ci"
fi
info "$INSTALL_MSG"
# Check for existing safe-chain installation through npm or volta
remove_npm_installation
remove_volta_installation
# Detect platform
OS=$(detect_os)
ARCH=$(detect_arch)
BINARY_NAME="safe-chain-${OS}-${ARCH}"
info "Detected platform: ${OS}-${ARCH}"
# Create installation directory
if [ ! -d "$INSTALL_DIR" ]; then
info "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR" || error "Failed to create directory $INSTALL_DIR"
fi
# Download binary
DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}/${BINARY_NAME}"
TEMP_FILE="${INSTALL_DIR}/${BINARY_NAME}"
info "Downloading from: $DOWNLOAD_URL"
download "$DOWNLOAD_URL" "$TEMP_FILE"
# Rename and make executable
FINAL_FILE="${INSTALL_DIR}/safe-chain"
mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE"
chmod +x "$FINAL_FILE" || error "Failed to make binary executable"
info "Binary installed to: $FINAL_FILE"
# Build setup command based on arguments
SETUP_CMD="setup"
SETUP_ARGS=""
if [ "$USE_CI_SETUP" = "true" ]; then
SETUP_CMD="setup-ci"
fi
if [ "$INCLUDE_PYTHON" = "true" ]; then
SETUP_ARGS="--include-python"
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 "$@"

1921
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,8 @@
"author": "Aikido Security",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"oxlint": "^1.22.0"
"oxlint": "^1.22.0",
"esbuild": "^0.27.0",
"@yao-pkg/pkg": "6.10.1"
}
}

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "bun";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "bunx";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "npm";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "npx";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -13,6 +13,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -14,6 +14,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
// Create package manager
initializePackageManager(PIP_PACKAGE_MANAGER);
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
// Pass through only user-supplied pip args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "pnpm";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "pnpx";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -11,18 +11,20 @@ setEcoSystem(ECOSYSTEM_PY);
// Strip nodejs and wrapper script from args
let argv = process.argv.slice(2);
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
setEcoSystem(ECOSYSTEM_PY);
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
(async () => {
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
setEcoSystem(ECOSYSTEM_PY);
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
// Strip off the '-m pip' or '-m pip3' from the args
argv = argv.slice(2);
// Strip off the '-m pip' or '-m pip3' from the args
argv = argv.slice(2);
var exitCode = await main(argv);
process.exit(exitCode);
} else {
// Forward to real python binary for non-pip flows
const { spawn } = await import('child_process');
spawn('python', argv, { stdio: 'inherit' });
}
var exitCode = await main(argv);
process.exit(exitCode);
} else {
// Forward to real python binary for non-pip flows
const { spawn } = await import('child_process');
spawn('python', argv, { stdio: 'inherit' });
}
})();

View file

@ -11,18 +11,20 @@ setEcoSystem(ECOSYSTEM_PY);
// Strip nodejs and wrapper script from args
let argv = process.argv.slice(2);
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
setEcoSystem(ECOSYSTEM_PY);
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
(async () => {
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
setEcoSystem(ECOSYSTEM_PY);
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
initializePackageManager(PIP_PACKAGE_MANAGER);
// Strip off the '-m pip' or '-m pip3' from the args
argv = argv.slice(2);
// Strip off the '-m pip' or '-m pip3' from the args
argv = argv.slice(2);
var exitCode = await main(argv);
process.exit(exitCode);
} else {
// Forward to real python3 binary for non-pip flows
const { spawn } = await import('child_process');
spawn('python3', argv, { stdio: 'inherit' });
}
var exitCode = await main(argv);
process.exit(exitCode);
} else {
// Forward to real python3 binary for non-pip flows
const { spawn } = await import('child_process');
spawn('python3', argv, { stdio: 'inherit' });
}
})();

View file

@ -9,6 +9,8 @@ setEcoSystem(ECOSYSTEM_PY);
initializePackageManager("uv");
// Pass through only user-supplied uv args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
// Pass through only user-supplied uv args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -7,6 +7,8 @@ import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "yarn";
initializePackageManager(packageManagerName);
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -1,12 +1,37 @@
#!/usr/bin/env node
import chalk from "chalk";
import { createRequire } from "module";
import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js";
import { teardown } from "../src/shell-integration/teardown.js";
import { setupCi } from "../src/shell-integration/setup-ci.js";
import { initializeCliArguments } from "../src/config/cliArguments.js";
import { setEcoSystem } from "../src/config/settings.js";
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
import { main } from "../src/main.js";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
import {
PIP_INVOCATIONS,
PIP_PACKAGE_MANAGER,
setCurrentPipInvocation,
} from "../src/packagemanager/pip/pipSettings.js";
/** @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;
}
if (process.argv.length < 3) {
ui.writeError("No command provided. Please provide a command to execute.");
@ -19,19 +44,35 @@ initializeCliArguments(process.argv);
const command = process.argv[2];
if (command === "help" || command === "--help" || command === "-h") {
const tool = knownAikidoTools.find((tool) => tool.tool === command);
if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) {
(async function () {
await executePip(tool);
})();
} else if (tool) {
const args = process.argv.slice(3);
setEcoSystem(tool.ecoSystem);
initializePackageManager(tool.internalPackageManagerName);
(async () => {
var exitCode = await main(args);
process.exit(exitCode);
})();
} else if (command === "help" || command === "--help" || command === "-h") {
writeHelp();
process.exit(0);
}
if (command === "setup") {
} else if (command === "setup") {
setup();
} else if (command === "teardown") {
teardown();
} else if (command === "setup-ci") {
setupCi();
} else if (command === "--version" || command === "-v" || command === "-v") {
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
(async () => {
ui.writeInformation(`Current safe-chain version: ${await getVersion()}`);
})();
} else {
ui.writeError(`Unknown command: ${command}.`);
ui.emptyLine();
@ -87,8 +128,63 @@ function writeHelp() {
ui.emptyLine();
}
function getVersion() {
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
return packageJson.version;
async function getVersion() {
const packageJsonPath = path.join(dirname, "..", "package.json");
const data = await fs.promises.readFile(packageJsonPath);
const json = JSON.parse(data.toString("utf8"));
if (json && json.version) {
return json.version;
}
return "0.0.0";
}
/**
* @param {import("../src/shell-integration/helpers.js").AikidoTool} tool
*/
async function executePip(tool) {
// Scanners for pip / pip3 / python / python3 use a slightly different approach:
// - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so
// - It needs to set which tool to run (pip / pip3 / python / python3)
// - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager
// - Python / python3 skips safe-chain if not being run with -m pip or -m pip3
let args = process.argv.slice(3);
setEcoSystem(tool.ecoSystem);
initializePackageManager(PIP_PACKAGE_MANAGER);
let shouldSkip = false;
if (tool.tool === "pip") {
setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
} else if (tool.tool === "pip3") {
setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
} else if (tool.tool === "python") {
if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
setCurrentPipInvocation(
args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP
);
args = args.slice(2);
} else {
shouldSkip = true;
}
} else if (tool.tool === "python3") {
if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
setCurrentPipInvocation(
args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP
);
args = args.slice(2);
} else {
shouldSkip = true;
}
}
if (shouldSkip) {
const { spawn } = await import("child_process");
spawn(tool.tool, args, { stdio: "inherit" });
} else {
var exitCode = await main(args);
process.exit(exitCode);
}
}

View file

@ -52,6 +52,7 @@
"@types/node-forge": "^1.3.14",
"@types/npm-registry-fetch": "^8.0.9",
"@types/semver": "^7.7.1",
"esbuild": "^0.27.0",
"typescript": "^5.9.3"
},
"main": "src/main.js",

View file

@ -9,24 +9,85 @@ import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
* @property {string} tool
* @property {string} aikidoCommand
* @property {string} ecoSystem
* @property {string} internalPackageManagerName
*/
/**
* @type {AikidoTool[]}
*/
export const knownAikidoTools = [
{ tool: "npm", aikidoCommand: "aikido-npm", ecoSystem: ECOSYSTEM_JS },
{ tool: "npx", aikidoCommand: "aikido-npx", ecoSystem: ECOSYSTEM_JS },
{ tool: "yarn", aikidoCommand: "aikido-yarn", ecoSystem: ECOSYSTEM_JS },
{ tool: "pnpm", aikidoCommand: "aikido-pnpm", ecoSystem: ECOSYSTEM_JS },
{ tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
{ tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS },
{ tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS },
{ tool: "uv", aikidoCommand: "aikido-uv", ecoSystem: ECOSYSTEM_PY },
{ tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY },
{ tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
{ tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },
{ tool: "python3", aikidoCommand: "aikido-python3", ecoSystem: ECOSYSTEM_PY },
{
tool: "npm",
aikidoCommand: "aikido-npm",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "npm",
},
{
tool: "npx",
aikidoCommand: "aikido-npx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "npx",
},
{
tool: "yarn",
aikidoCommand: "aikido-yarn",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "yarn",
},
{
tool: "pnpm",
aikidoCommand: "aikido-pnpm",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "pnpm",
},
{
tool: "pnpx",
aikidoCommand: "aikido-pnpx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "pnpx",
},
{
tool: "bun",
aikidoCommand: "aikido-bun",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "bun",
},
{
tool: "bunx",
aikidoCommand: "aikido-bunx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "bunx",
},
{
tool: "uv",
aikidoCommand: "aikido-uv",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "uv",
},
{
tool: "pip",
aikidoCommand: "aikido-pip",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
{
tool: "pip3",
aikidoCommand: "aikido-pip3",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
{
tool: "python",
aikidoCommand: "aikido-python",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
{
tool: "python3",
aikidoCommand: "aikido-python3",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
},
// When adding a new tool here, also update the documentation for the new tool in the README.md
];

View file

@ -7,9 +7,9 @@ remove_shim_from_path() {
echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
}
if command -v {{AIKIDO_COMMAND}} >/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
PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@"
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
else
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})

View file

@ -7,10 +7,10 @@ set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
REM Check if aikido command is available with clean PATH
set "PATH=%CLEAN_PATH%" & where {{AIKIDO_COMMAND}} >nul 2>&1
set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1
if %errorlevel%==0 (
REM Call aikido command with clean PATH
set "PATH=%CLEAN_PATH%" & {{AIKIDO_COMMAND}} %*
set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %*
) else (
REM Find the original command with clean PATH
for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (

View file

@ -8,6 +8,20 @@ import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
import { ECOSYSTEM_PY } from "../config/settings.js";
/** @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.
*/
@ -19,6 +33,7 @@ export async function setupCi() {
ui.emptyLine();
const shimsDir = path.join(os.homedir(), ".safe-chain", "shims");
const binDir = path.join(os.homedir(), ".safe-chain", "bin");
// Create the shims directory if it doesn't exist
if (!fs.existsSync(shimsDir)) {
fs.mkdirSync(shimsDir, { recursive: true });
@ -26,7 +41,7 @@ export async function setupCi() {
createShims(shimsDir);
ui.writeInformation(`Created shims in ${shimsDir}`);
modifyPathForCi(shimsDir);
modifyPathForCi(shimsDir, binDir);
ui.writeInformation(`Added shims directory to PATH for CI environments.`);
}
@ -37,10 +52,8 @@ export async function setupCi() {
*/
function createUnixShims(shimsDir) {
// Read the template file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const templatePath = path.resolve(
__dirname,
dirname,
"path-wrappers",
"templates",
"unix-wrapper.template.sh"
@ -78,10 +91,8 @@ function createUnixShims(shimsDir) {
*/
function createWindowsShims(shimsDir) {
// Read the template file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const templatePath = path.resolve(
__dirname,
dirname,
"path-wrappers",
"templates",
"windows-wrapper.template.cmd"
@ -124,13 +135,18 @@ function createShims(shimsDir) {
/**
* @param {string} shimsDir
* @param {string} binDir
*
* @returns {void}
*/
function modifyPathForCi(shimsDir) {
function modifyPathForCi(shimsDir, binDir) {
if (process.env.GITHUB_PATH) {
// In GitHub Actions, append the shims directory to GITHUB_PATH
fs.appendFileSync(process.env.GITHUB_PATH, shimsDir + os.EOL, "utf-8");
fs.appendFileSync(
process.env.GITHUB_PATH,
shimsDir + os.EOL + binDir + os.EOL,
"utf-8"
);
ui.writeInformation(
`Added shims directory to GITHUB_PATH for GitHub Actions.`
);
@ -141,6 +157,7 @@ function modifyPathForCi(shimsDir) {
// ##vso[task.prependpath]/path/to/add
// Logging this to stdout will cause the Azure Pipelines agent to pick it up
ui.writeInformation("##vso[task.prependpath]" + shimsDir);
ui.writeInformation("##vso[task.prependpath]" + binDir);
}
}

View file

@ -5,8 +5,22 @@ import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
import fs from "fs";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
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.
@ -103,10 +117,8 @@ function copyStartupFiles() {
}
// Use absolute path for source
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sourcePath = path.resolve(
__dirname,
const sourcePath = path.join(
dirname,
includePython() ? "startup-scripts/include-python" : "startup-scripts",
file
);

View file

@ -1,3 +1,67 @@
set -gx PATH $PATH $HOME/.safe-chain/bin
function npx
wrapSafeChainCommand "npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" $argv
end
function pip
wrapSafeChainCommand "pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" $argv
end
function uv
wrapSafeChainCommand "uv" $argv
end
# `python -m pip`, `python -m pip3`.
function python
wrapSafeChainCommand "python" $argv
end
# `python3 -m pip`, `python3 -m pip3'.
function python3
wrapSafeChainCommand "python3" $argv
end
function printSafeChainWarning
set original_cmd $argv[1]
@ -17,76 +81,14 @@ end
function wrapSafeChainCommand
set original_cmd $argv[1]
set aikido_cmd $argv[2]
set cmd_args $argv[3..-1]
if type -q $aikido_cmd
# If the aikido command is available, just run it with the provided arguments
$aikido_cmd $cmd_args
set cmd_args $argv[2..-1]
if type -q safe-chain
# If the safe-chain command is available, just run it with the provided arguments
safe-chain $original_cmd $cmd_args
else
# If the aikido command is not available, print a warning and run the original command
# If the safe-chain command is not available, print a warning and run the original command
printSafeChainWarning $original_cmd
command $original_cmd $cmd_args
end
end
function npx
wrapSafeChainCommand "npx" "aikido-npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" "aikido-yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" "aikido-bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" "aikido-bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" "aikido-npm" $argv
end
function pip
wrapSafeChainCommand "pip" "aikido-pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
end
function uv
wrapSafeChainCommand "uv" "aikido-uv" $argv
end
# `python -m pip`, `python -m pip3`.
function python
wrapSafeChainCommand "python" "aikido-python" $argv
end
# `python3 -m pip`, `python3 -m pip3'.
function python3
wrapSafeChainCommand "python3" "aikido-python3" $argv
end

View file

@ -1,3 +1,62 @@
export PATH="$PATH:$HOME/.safe-chain/bin"
function npx() {
wrapSafeChainCommand "npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "$@"
}
function pip() {
wrapSafeChainCommand "pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "$@"
}
function uv() {
wrapSafeChainCommand "uv" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "$@"
}
function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black
@ -9,15 +68,10 @@ function printSafeChainWarning() {
function wrapSafeChainCommand() {
local original_cmd="$1"
local aikido_cmd="$2"
# Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
# so that "$@" now contains only the arguments passed to the original command
shift 2
if command -v "$aikido_cmd" > /dev/null 2>&1; then
if command -v safe-chain > /dev/null 2>&1; then
# If the aikido command is available, just run it with the provided arguments
"$aikido_cmd" "$@"
safe-chain "$@"
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
@ -25,60 +79,3 @@ function wrapSafeChainCommand() {
command "$original_cmd" "$@"
fi
}
function npx() {
wrapSafeChainCommand "npx" "aikido-npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "aikido-bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "aikido-bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "aikido-npm" "$@"
}
function pip() {
wrapSafeChainCommand "pip" "aikido-pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
}
function uv() {
wrapSafeChainCommand "uv" "aikido-uv" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "aikido-python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "aikido-python3" "$@"
}

View file

@ -1,3 +1,66 @@
# Use cross-platform path separator (: on Unix, ; on Windows)
$pathSeparator = if ($IsWindows) { ';' } else { ':' }
$safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
function npx {
Invoke-WrappedCommand "npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" $args
}
function pip {
Invoke-WrappedCommand "pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" $args
}
function uv {
Invoke-WrappedCommand "uv" $args
}
# `python -m pip`, `python -m pip3`.
function python {
Invoke-WrappedCommand 'python' $args
}
# `python3 -m pip`, `python3 -m pip3'.
function python3 {
Invoke-WrappedCommand 'python3' $args
}
function Write-SafeChainWarning {
param([string]$Command)
@ -39,73 +102,14 @@ function Invoke-RealCommand {
function Invoke-WrappedCommand {
param(
[string]$OriginalCmd,
[string]$AikidoCmd,
[string[]]$Arguments
)
if (Test-CommandAvailable $AikidoCmd) {
& $AikidoCmd @Arguments
if (Test-CommandAvailable "safe-chain") {
& safe-chain $OriginalCmd @Arguments
}
else {
Write-SafeChainWarning $OriginalCmd
Invoke-RealCommand $OriginalCmd $Arguments
}
}
function npx {
Invoke-WrappedCommand "npx" "aikido-npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" "aikido-yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" "aikido-bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" "aikido-bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" "aikido-npm" $args
}
function pip {
Invoke-WrappedCommand "pip" "aikido-pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
}
function uv {
Invoke-WrappedCommand "uv" "aikido-uv" $args
}
# `python -m pip`, `python -m pip3`.
function python {
Invoke-WrappedCommand 'python' 'aikido-python' $args
}
# `python3 -m pip`, `python3 -m pip3'.
function python3 {
Invoke-WrappedCommand 'python3' 'aikido-python3' $args
}

View file

@ -1,3 +1,44 @@
set -gx PATH $PATH $HOME/.safe-chain/bin
function npx
wrapSafeChainCommand "npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" $argv
end
function printSafeChainWarning
set original_cmd $argv[1]
@ -17,54 +58,14 @@ end
function wrapSafeChainCommand
set original_cmd $argv[1]
set aikido_cmd $argv[2]
set cmd_args $argv[3..-1]
if type -q $aikido_cmd
# If the aikido command is available, just run it with the provided arguments
$aikido_cmd $cmd_args
set cmd_args $argv[2..-1]
if type -q safe-chain
# If the safe-chain command is available, just run it with the provided arguments
safe-chain $original_cmd $cmd_args
else
# If the aikido command is not available, print a warning and run the original command
# If the safe-chain command is not available, print a warning and run the original command
printSafeChainWarning $original_cmd
command $original_cmd $cmd_args
end
end
function npx
wrapSafeChainCommand "npx" "aikido-npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" "aikido-yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" "aikido-bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" "aikido-bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" "aikido-npm" $argv
end

View file

@ -1,3 +1,39 @@
export PATH="$PATH:$HOME/.safe-chain/bin"
function npx() {
wrapSafeChainCommand "npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "$@"
}
function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black
@ -9,15 +45,10 @@ function printSafeChainWarning() {
function wrapSafeChainCommand() {
local original_cmd="$1"
local aikido_cmd="$2"
# Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
# so that "$@" now contains only the arguments passed to the original command
shift 2
if command -v "$aikido_cmd" > /dev/null 2>&1; then
if command -v safe-chain > /dev/null 2>&1; then
# If the aikido command is available, just run it with the provided arguments
"$aikido_cmd" "$@"
safe-chain "$@"
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
@ -25,38 +56,3 @@ function wrapSafeChainCommand() {
command "$original_cmd" "$@"
fi
}
function npx() {
wrapSafeChainCommand "npx" "aikido-npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "aikido-bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "aikido-bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "aikido-npm" "$@"
}

View file

@ -1,3 +1,43 @@
# Use cross-platform path separator (: on Unix, ; on Windows)
$pathSeparator = if ($IsWindows) { ';' } else { ':' }
$safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
function npx {
Invoke-WrappedCommand "npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" $args
}
function Write-SafeChainWarning {
param([string]$Command)
@ -39,50 +79,14 @@ function Invoke-RealCommand {
function Invoke-WrappedCommand {
param(
[string]$OriginalCmd,
[string]$AikidoCmd,
[string[]]$Arguments
)
if (Test-CommandAvailable $AikidoCmd) {
& $AikidoCmd @Arguments
if (Test-CommandAvailable "safe-chain") {
& safe-chain $OriginalCmd @Arguments
}
else {
Write-SafeChainWarning $OriginalCmd
Invoke-RealCommand $OriginalCmd $Arguments
}
}
function npx {
Invoke-WrappedCommand "npx" "aikido-npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" "aikido-yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" "aikido-bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" "aikido-bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" "aikido-npm" $args
}