mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge branch 'main' into feature/poetry-2
This commit is contained in:
commit
c1a12c9573
39 changed files with 3771 additions and 474 deletions
56
.github/workflows/build-and-release.yml
vendored
56
.github/workflows/build-and-release.yml
vendored
|
|
@ -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
82
.github/workflows/create-artifact.yml
vendored
Normal 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/*
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -144,3 +144,10 @@ vite.config.ts.timestamp-*
|
|||
Claude.md
|
||||
.claude
|
||||
.reference
|
||||
|
||||
# Build files
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Jetbrains IDEs
|
||||
.idea/**
|
||||
|
|
|
|||
109
README.md
109
README.md
|
|
@ -1,13 +1,16 @@
|
|||

|
||||

|
||||
|
||||
# Aikido Safe Chain
|
||||
|
||||
[](https://www.npmjs.com/package/@aikidosec/safe-chain)
|
||||
[](https://www.npmjs.com/package/@aikidosec/safe-chain)
|
||||
|
||||
- ✅ **Block malware on developer laptops and CI/CD**
|
||||
- ✅ **Supports npm and PyPI** more package managers coming
|
||||
- ✅ **Blocks packages newer than 24 hours** without breaking your build
|
||||
- ✅ **Tokenless, free, no build data shared**
|
||||
|
||||
Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers:
|
||||
Aikido Safe Chain supports the following package managers:
|
||||
|
||||
- 📦 **npm**
|
||||
- 📦 **npx**
|
||||
|
|
@ -24,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](https://github.com/AikidoSec/safe-chain/blob/main/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:
|
||||
|
||||
|
|
@ -54,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
|
||||
|
|
@ -92,7 +111,7 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc
|
|||
- ✅ **PowerShell**
|
||||
- ✅ **PowerShell Core**
|
||||
|
||||
More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md).
|
||||
More information about the shell integration can be found in the [shell integration documentation](https://github.com/AikidoSec/safe-chain/blob/main/docs/shell-integration.md).
|
||||
|
||||
## Uninstallation
|
||||
|
||||
|
|
@ -163,23 +182,37 @@ You can set the minimum package age through multiple sources (in order of priori
|
|||
|
||||
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.
|
||||
|
||||
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.
|
||||
## Installation for CI/CD
|
||||
|
||||
## Setup
|
||||
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.
|
||||
|
||||
To use Aikido Safe Chain in CI/CD environments, run the following command after installing the package:
|
||||
### 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
|
||||
|
||||
|
|
@ -195,16 +228,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
|
||||
|
|
@ -213,14 +245,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.
|
||||
|
|
|
|||
135
build.js
Normal file
135
build.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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() {
|
||||
const startBuildTime = performance.now();
|
||||
|
||||
await clearOutputFolder();
|
||||
console.log("- Cleared output folder ✅")
|
||||
|
||||
// Esbuild creates a single safe-chain.cjs with all dependencies included
|
||||
await bundleSafeChain();
|
||||
console.log("- Bundled safe-chain into safe-chain.cjs (es-build) ✅")
|
||||
|
||||
// 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();
|
||||
console.log("- Copied auxiliary resources (shell, package.json,...) ✅")
|
||||
|
||||
// Creates a single binary with safe-chain.cjs and the copied assets
|
||||
await buildSafeChainBinary(target);
|
||||
console.log(`- Built safe-chain binary for ${target} (pkg) ✅`)
|
||||
|
||||
|
||||
const endBuildTime = performance.now();
|
||||
console.log(`🏁 Finished build in ${((endBuildTime - startBuildTime)/1000).toFixed(2)}s`);
|
||||
})();
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
89
docs/npm-to-binary-migration.md
Normal file
89
docs/npm-to-binary-migration.md
Normal 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](https://github.com/AikidoSec/safe-chain/blob/main/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](https://github.com/AikidoSec/safe-chain/blob/main/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](https://github.com/AikidoSec/safe-chain/blob/main/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.
|
||||
217
install-scripts/install-safe-chain.ps1
Normal file
217
install-scripts/install-safe-chain.ps1
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# 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($Version)) {
|
||||
Write-Info "Fetching latest release version..."
|
||||
$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 {
|
||||
# Remove existing file if present (Move-Item -Force doesn't overwrite)
|
||||
if (Test-Path $finalFile) {
|
||||
Remove-Item -Path $finalFile -Force
|
||||
}
|
||||
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: $_"
|
||||
}
|
||||
224
install-scripts/install-safe-chain.sh
Executable file
224
install-scripts/install-safe-chain.sh
Executable 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
1921
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,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",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
|
|||
* If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
|
||||
* their settings with safe-chain's, leaving the original file unchanged.
|
||||
*
|
||||
* Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
|
||||
* users to read/write persistent config. Only CA environment variables are set for these commands.
|
||||
*
|
||||
* @param {string} command - The pip command to execute (e.g., 'pip3')
|
||||
* @param {string[]} args - Command line arguments to pass to pip
|
||||
* @returns {Promise<{status: number}>} Exit status of the pip command
|
||||
|
|
@ -59,6 +62,12 @@ export async function runPip(command, args) {
|
|||
// validates correctly under both MITM'd and tunneled HTTPS.
|
||||
const combinedCaPath = getCombinedCaBundlePath();
|
||||
|
||||
// Commands that need access to persistent config/cache/state files
|
||||
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
|
||||
// reading/writing to the user's actual pip configuration and cache directories
|
||||
const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
|
||||
const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
|
||||
|
||||
// https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
|
||||
// will tell pip to use the provided CA bundle for HTTPS verification.
|
||||
|
||||
|
|
@ -70,6 +79,22 @@ export async function runPip(command, args) {
|
|||
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
|
||||
let cleanupConfigPath = null; // Track temp file for cleanup
|
||||
|
||||
if (isConfigRelatedCommand) {
|
||||
ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
|
||||
|
||||
// Still set the fallback CA bundle environment variables to avoid edge cases where a
|
||||
// plugin or extension triggers a network call during config introspection
|
||||
// This can do no harm
|
||||
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||
|
||||
const result = await safeSpawn(command, args, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
|
||||
return { status: result.status };
|
||||
}
|
||||
|
||||
// Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
|
||||
if (!env.PIP_CONFIG_FILE) {
|
||||
/** @type {{ global: { cert: string, proxy?: string } }} */
|
||||
|
|
|
|||
|
|
@ -62,6 +62,103 @@ describe("runPipCommand environment variable handling", () => {
|
|||
mock.reset();
|
||||
});
|
||||
|
||||
it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => {
|
||||
const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||
|
||||
// PIP_CONFIG_FILE should NOT be set for config commands
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||
undefined,
|
||||
"PIP_CONFIG_FILE should NOT be set for pip config commands"
|
||||
);
|
||||
|
||||
// But CA environment variables should still be set
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.REQUESTS_CA_BUNDLE,
|
||||
"/tmp/test-combined-ca.pem",
|
||||
"REQUESTS_CA_BUNDLE should still be set"
|
||||
);
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.SSL_CERT_FILE,
|
||||
"/tmp/test-combined-ca.pem",
|
||||
"SSL_CERT_FILE should still be set"
|
||||
);
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CERT,
|
||||
"/tmp/test-combined-ca.pem",
|
||||
"PIP_CERT should still be set"
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT set PIP_CONFIG_FILE for 'pip config get' commands", async () => {
|
||||
const res = await runPip("pip3", ["config", "get", "global.index-url"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||
undefined,
|
||||
"PIP_CONFIG_FILE should NOT be set for pip config get"
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT set PIP_CONFIG_FILE for 'pip config list' commands", async () => {
|
||||
const res = await runPip("pip3", ["config", "list"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||
undefined,
|
||||
"PIP_CONFIG_FILE should NOT be set for pip config list"
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT set PIP_CONFIG_FILE for 'pip cache' commands", async () => {
|
||||
const res = await runPip("pip3", ["cache", "dir"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||
undefined,
|
||||
"PIP_CONFIG_FILE should NOT be set for pip cache commands"
|
||||
);
|
||||
|
||||
// CA env vars should still be set
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.SSL_CERT_FILE,
|
||||
"/tmp/test-combined-ca.pem",
|
||||
"SSL_CERT_FILE should still be set"
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT set PIP_CONFIG_FILE for 'pip debug' commands", async () => {
|
||||
const res = await runPip("pip3", ["debug"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||
undefined,
|
||||
"PIP_CONFIG_FILE should NOT be set for pip debug"
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT set PIP_CONFIG_FILE for 'pip completion' commands", async () => {
|
||||
const res = await runPip("pip3", ["completion", "--bash"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||
|
||||
assert.strictEqual(
|
||||
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||
undefined,
|
||||
"PIP_CONFIG_FILE should NOT be set for pip completion"
|
||||
);
|
||||
});
|
||||
|
||||
it("should set PIP_CERT env var and create config file", async () => {
|
||||
const res = await runPip("pip3", ["install", "requests"]);
|
||||
assert.strictEqual(res.status, 0);
|
||||
|
|
|
|||
|
|
@ -117,14 +117,16 @@ function forwardRequest(req, hostname, res, requestHandler) {
|
|||
|
||||
proxyReq.on("error", (err) => {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: Error occurred while proxying request: ${err.message}`
|
||||
`Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`
|
||||
);
|
||||
res.writeHead(502);
|
||||
res.end("Bad Gateway");
|
||||
});
|
||||
|
||||
req.on("error", (err) => {
|
||||
ui.writeError(`Safe-chain: Error reading client request: ${err.message}`);
|
||||
ui.writeError(
|
||||
`Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`
|
||||
);
|
||||
proxyReq.destroy();
|
||||
});
|
||||
|
||||
|
|
@ -175,7 +177,7 @@ function createProxyRequest(hostname, req, res, requestHandler) {
|
|||
const proxyReq = https.request(options, (proxyRes) => {
|
||||
proxyRes.on("error", (err) => {
|
||||
ui.writeError(
|
||||
`Safe-chain: Error reading upstream response: ${err.message}`
|
||||
`Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`
|
||||
);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502);
|
||||
|
|
@ -184,7 +186,9 @@ function createProxyRequest(hostname, req, res, requestHandler) {
|
|||
});
|
||||
|
||||
if (!proxyRes.statusCode) {
|
||||
ui.writeError("Safe-chain: Proxy response missing status code");
|
||||
ui.writeError(
|
||||
`Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`
|
||||
);
|
||||
res.writeHead(500);
|
||||
res.end("Internal Server Error");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,25 +9,91 @@ 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: "poetry", aikidoCommand: "aikido-poetry", 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: "poetry",
|
||||
aikidoCommand: "aikido-poetry",
|
||||
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
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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}})
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,71 @@
|
|||
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
|
||||
|
||||
function poetry
|
||||
wrapSafeChainCommand "poetry" $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,80 +85,14 @@ end
|
|||
|
||||
function wrapSafeChainCommand
|
||||
set original_cmd $argv[1]
|
||||
set aikido_cmd $argv[2]
|
||||
set cmd_args $argv[3..-1]
|
||||
set cmd_args $argv[2..-1]
|
||||
|
||||
if type -q $aikido_cmd
|
||||
# If the aikido command is available, just run it with the provided arguments
|
||||
$aikido_cmd $cmd_args
|
||||
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
|
||||
|
||||
function poetry
|
||||
wrapSafeChainCommand "poetry" "aikido-poetry" $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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,66 @@
|
|||
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" "$@"
|
||||
}
|
||||
|
||||
function poetry() {
|
||||
wrapSafeChainCommand "poetry" "$@"
|
||||
}
|
||||
|
||||
# `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 +72,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,64 +83,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" "$@"
|
||||
}
|
||||
|
||||
function poetry() {
|
||||
wrapSafeChainCommand "poetry" "aikido-poetry" "$@"
|
||||
}
|
||||
|
||||
# `python -m pip`, `python -m pip3`.
|
||||
function python() {
|
||||
wrapSafeChainCommand "python" "aikido-python" "$@"
|
||||
}
|
||||
|
||||
# `python3 -m pip`, `python3 -m pip3'.
|
||||
function python3() {
|
||||
wrapSafeChainCommand "python3" "aikido-python3" "$@"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,70 @@
|
|||
# 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
|
||||
}
|
||||
|
||||
function poetry {
|
||||
Invoke-WrappedCommand "poetry" $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,77 +106,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
|
||||
}
|
||||
|
||||
function poetry {
|
||||
Invoke-WrappedCommand "poetry" "aikido-poetry" $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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
set cmd_args $argv[2..-1]
|
||||
|
||||
if type -q $aikido_cmd
|
||||
# If the aikido command is available, just run it with the provided arguments
|
||||
$aikido_cmd $cmd_args
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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" "$@"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,4 +333,207 @@ describe("E2E: pip coverage", () => {
|
|||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 config set should work and persist configuration`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Set a config value
|
||||
const setResult = await shell.runCommand(
|
||||
"pip3 config set global.timeout 60"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
setResult.output.includes("Writing to"),
|
||||
`pip3 config set should write config. Output was:\n${setResult.output}`
|
||||
);
|
||||
|
||||
// Verify it was persisted by reading it back
|
||||
const getResult = await shell.runCommand(
|
||||
"pip3 config get global.timeout"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
getResult.output.includes("60"),
|
||||
`Config value should be 60. Output was:\n${getResult.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 config list should show user configuration`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Set a value first
|
||||
await shell.runCommand("pip3 config set global.timeout 90");
|
||||
|
||||
// List config
|
||||
const listResult = await shell.runCommand("pip3 config list");
|
||||
|
||||
assert.ok(
|
||||
listResult.output.includes("timeout") && listResult.output.includes("90"),
|
||||
`Config list should show timeout=90. Output was:\n${listResult.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 config unset should remove configuration`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Set a value
|
||||
await shell.runCommand("pip3 config set global.timeout 120");
|
||||
|
||||
// Verify it exists
|
||||
const getResult = await shell.runCommand("pip3 config get global.timeout");
|
||||
assert.ok(getResult.output.includes("120"));
|
||||
|
||||
// Unset it
|
||||
const unsetResult = await shell.runCommand("pip3 config unset global.timeout");
|
||||
assert.ok(
|
||||
unsetResult.output.includes("Writing to"),
|
||||
`pip3 config unset should write config. Output was:\n${unsetResult.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 cache dir should return cache directory path`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand("pip3 cache dir");
|
||||
|
||||
// Should output a directory path
|
||||
assert.ok(
|
||||
result.output.includes("/") && result.output.includes("cache"),
|
||||
`Should output a cache directory path. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 cache info should show cache information`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Install something first to populate cache
|
||||
await shell.runCommand("pip3 install --break-system-packages certifi");
|
||||
|
||||
const result = await shell.runCommand("pip3 cache info");
|
||||
|
||||
// Output should contain cache-related information
|
||||
assert.ok(
|
||||
result.output.match(/cache|wheel|http/i),
|
||||
`Should output cache information. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 cache list should list cached packages`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Download a package to ensure something is in cache
|
||||
await shell.runCommand("pip3 download certifi");
|
||||
|
||||
const result = await shell.runCommand("pip3 cache list certifi");
|
||||
|
||||
// Should show either cached wheels or "No locally built wheels"
|
||||
assert.ok(
|
||||
result.output.includes("certifi") || result.output.includes("No locally built"),
|
||||
`Should output cache list information. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 debug should output debug information`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand("pip3 debug");
|
||||
|
||||
// Should contain debug information about pip environment
|
||||
assert.ok(
|
||||
result.output.match(/pip version|sys\.version|sys\.executable/i),
|
||||
`Should output debug information. Output was:\n${result.output}`
|
||||
);
|
||||
|
||||
// Should NOT show safe-chain's temporary config file in the debug output
|
||||
assert.ok(
|
||||
!result.output.includes("safe-chain-pip-"),
|
||||
`Debug output should not reference safe-chain temp config. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 completion should generate shell completion script`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
const result = await shell.runCommand("pip3 completion --zsh");
|
||||
|
||||
// Should output shell completion code
|
||||
assert.ok(
|
||||
result.output.includes("compdef") || result.output.includes("_pip") || result.output.includes("pip completion"),
|
||||
`Should output completion code. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 install still works after config operations`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Perform config operations
|
||||
await shell.runCommand("pip3 config set global.timeout 60");
|
||||
await shell.runCommand("pip3 cache dir");
|
||||
|
||||
// Now install should still work with malware protection
|
||||
const result = await shell.runCommand(
|
||||
"pip3 install --break-system-packages certifi"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("Successfully installed") ||
|
||||
result.output.includes("Requirement already satisfied"),
|
||||
`Install should succeed after config operations. Output was:\n${result.output}`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found."),
|
||||
`Should still scan for malware. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pip3 download works after configuring pip settings`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Configure pip with timeout and extra index URL
|
||||
const configTimeout = await shell.runCommand("pip3 config set global.timeout 60");
|
||||
assert.ok(
|
||||
configTimeout.output.includes("Writing to"),
|
||||
`Config set should succeed. Output was:\n${configTimeout.output}`
|
||||
);
|
||||
|
||||
const configIndex = await shell.runCommand(
|
||||
"pip3 config set global.extra-index-url https://pypi.org/simple"
|
||||
);
|
||||
assert.ok(
|
||||
configIndex.output.includes("Writing to"),
|
||||
`Config set should succeed. Output was:\n${configIndex.output}`
|
||||
);
|
||||
|
||||
// Verify config persisted
|
||||
const listConfig = await shell.runCommand("pip3 config list");
|
||||
assert.ok(
|
||||
listConfig.output.includes("timeout") && listConfig.output.includes("60"),
|
||||
`Config should show timeout=60. Output was:\n${listConfig.output}`
|
||||
);
|
||||
assert.ok(
|
||||
listConfig.output.includes("extra-index-url") && listConfig.output.includes("pypi.org"),
|
||||
`Config should show extra-index-url. Output was:\n${listConfig.output}`
|
||||
);
|
||||
|
||||
// Now download packages with the configured settings
|
||||
const downloadResult = await shell.runCommand(
|
||||
"pip3 download -d /tmp/packages requests certifi"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
downloadResult.output.includes("no malware found."),
|
||||
`Should scan for malware. Output was:\n${downloadResult.output}`
|
||||
);
|
||||
|
||||
// Verify downloads succeeded
|
||||
assert.ok(
|
||||
downloadResult.output.includes("Saved") || downloadResult.output.includes("requests"),
|
||||
`Download should succeed with configured settings. Output was:\n${downloadResult.output}`
|
||||
);
|
||||
assert.ok(
|
||||
downloadResult.output.includes("certifi"),
|
||||
`Should download certifi. Output was:\n${downloadResult.output}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue