diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index ad43a9d..5aa6422 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -5,7 +5,7 @@ on: workflow_call: inputs: version: - description: 'Version to set in package.json' + description: "Version to set in package.json" required: false type: string @@ -64,13 +64,13 @@ jobs: 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: Set the version in safe-chain package + if: inputs.version != '' + run: npm --no-git-tag-version version ${{ inputs.version }} --workspace=packages/safe-chain --ignore-scripts + - name: Create binary run: | node build.js ${{ matrix.target }} diff --git a/README.md b/README.md index 28b94cf..9047def 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Aikido Safe Chain supports the following package managers: - πŸ“¦ **pnpx** - πŸ“¦ **bun** - πŸ“¦ **bunx** -- πŸ“¦ **pip** (beta) -- πŸ“¦ **pip3** (beta) -- πŸ“¦ **uv** (beta) -- πŸ“¦ **poetry** (beta) +- πŸ“¦ **pip** +- πŸ“¦ **pip3** +- πŸ“¦ **uv** +- πŸ“¦ **poetry** # Usage @@ -34,32 +34,16 @@ Installing the Aikido Safe Chain is easy with our one-line installer. ### Unix/Linux/macOS -**Default installation (JavaScript packages only):** - ```shell curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh ``` -**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. @@ -74,7 +58,7 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst npm install safe-chain-test ``` - For Python (if you enabled Python support): + For Python: ```shell pip3 install safe-chain-pi-test @@ -193,32 +177,16 @@ Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD envir ### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.) -**JavaScript only:** - ```shell curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci ``` -**With Python support:** - -```shell -curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python -``` - ### 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 - βœ… **GitHub Actions** @@ -234,14 +202,12 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst cache: "npm" - 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 + run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci - name: Install dependencies run: npm ci ``` -> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv/poetry) support. - ## Azure DevOps Example ```yaml @@ -250,13 +216,11 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst versionSpec: "22.x" displayName: "Install Node.js" -- script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python +- script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci displayName: "Install safe-chain" - script: npm ci displayName: "Install dependencies" ``` -> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv/poetry) support. - After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 081d232..9c0dcf7 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -3,8 +3,7 @@ # Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md param( - [switch]$ci, - [switch]$includepython + [switch]$ci ) $Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set @@ -117,9 +116,6 @@ function Install-SafeChain { # Build installation message $installMsg = "Installing safe-chain $Version" - if ($includepython) { - $installMsg += " with python" - } if ($ci) { $installMsg += " in ci" } @@ -181,9 +177,6 @@ function Install-SafeChain { # 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 ' ' })..." diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 2afb583..37d1710 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -134,9 +134,6 @@ parse_arguments() { --ci) USE_CI_SETUP=true ;; - --include-python) - INCLUDE_PYTHON=true - ;; *) error "Unknown argument: $arg" ;; @@ -148,7 +145,6 @@ parse_arguments() { main() { # Initialize argument flags USE_CI_SETUP=false - INCLUDE_PYTHON=false # Parse command-line arguments parse_arguments "$@" @@ -161,9 +157,6 @@ main() { # 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 @@ -209,10 +202,6 @@ main() { 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 diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 2793987..aed77f0 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -3,7 +3,7 @@ import chalk from "chalk"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; -import { teardown } from "../src/shell-integration/teardown.js"; +import { teardown, teardownDirectories } 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"; @@ -60,6 +60,7 @@ if (tool) { } else if (command === "setup") { setup(); } else if (command === "teardown") { + teardownDirectories(); teardown(); } else if (command === "setup-ci") { setupCi(); @@ -94,11 +95,6 @@ function writeHelp() { "safe-chain setup" )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` ); - ui.writeInformation( - ` ${chalk.yellow( - "--include-python" - )}: Experimental: include Python package managers (pip, pip3) in the setup.` - ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown" @@ -109,11 +105,6 @@ function writeHelp() { "safe-chain setup-ci" )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` ); - ui.writeInformation( - ` ${chalk.yellow( - "--include-python" - )}: Experimental: include Python package managers (pip, pip3) in the setup.` - ); ui.writeInformation( `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan( "-v" diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index ddcd8b9..4dd9336 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,11 +1,10 @@ /** - * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, includePython: boolean}} + * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined}} */ const state = { loggingLevel: undefined, skipMinimumPackageAge: undefined, minimumPackageAgeHours: undefined, - includePython: false, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; @@ -34,7 +33,6 @@ export function initializeCliArguments(args) { setLoggingLevel(safeChainArgs); setSkipMinimumPackageAge(safeChainArgs); setMinimumPackageAgeHours(safeChainArgs); - setIncludePython(args); return remainingArgs; } @@ -109,20 +107,6 @@ export function getMinimumPackageAgeHours() { return state.minimumPackageAgeHours; } -/** - * @param {string[]} args - */ -function setIncludePython(args) { - // This flag doesn't have the --safe-chain- prefix because - // it is only used for the safe-chain command itself and - // not when wrapped around package manager commands. - state.includePython = hasFlagArg(args, "--include-python"); -} - -export function includePython() { - return state.includePython; -} - /** * @param {string[]} args * @param {string} flagName diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index ae25a1d..23387f5 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -67,7 +67,7 @@ function validateMinimumPackageAgeHours(value) { */ export function getMinimumPackageAgeHours() { const config = readConfigFile(); - if (config.minimumPackageAgeHours) { + if (config.minimumPackageAgeHours !== undefined) { const validated = validateMinimumPackageAgeHours( config.minimumPackageAgeHours ); diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index 18415bc..7da7e8d 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -1,32 +1,24 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; -describe("getScanTimeout", () => { +let configFileContent = undefined; +mock.module("fs", { + namedExports: { + existsSync: () => configFileContent !== undefined, + readFileSync: () => configFileContent, + writeFileSync: (content) => (configFileContent = content), + mkdirSync: () => {}, + }, +}); + +describe("getScanTimeout", async () => { let originalEnv; - let fsMock; - let getScanTimeout; + + const { getScanTimeout } = await import("./configFile.js"); beforeEach(async () => { // Save original environment originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS; - - // Mock fs module - fsMock = { - existsSync: mock.fn(() => false), - readFileSync: mock.fn(() => "{}"), - writeFileSync: mock.fn(), - mkdirSync: mock.fn(), - }; - - mock.module("fs", { - namedExports: fsMock, - }); - - // Re-import the module to get the mocked version - const configFileModule = await import( - `./configFile.js?update=${Date.now()}` - ); - getScanTimeout = configFileModule.getScanTimeout; }); afterEach(() => { @@ -37,14 +29,12 @@ describe("getScanTimeout", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; } - // Reset all mocks - mock.restoreAll(); + configFileContent = undefined; }); it("should return default timeout of 10000ms when no config or env var is set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - // Mock: config file doesn't exist - fsMock.existsSync.mock.mockImplementation(() => false); + configFileContent = undefined; const timeout = getScanTimeout(); @@ -53,11 +43,7 @@ describe("getScanTimeout", () => { it("should return timeout from config file when set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - // Mock: config file exists with scanTimeout: 5000 - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: 5000 }) - ); + configFileContent = JSON.stringify({ scanTimeout: 5000 }); const timeout = getScanTimeout(); @@ -66,11 +52,7 @@ describe("getScanTimeout", () => { it("should prioritize environment variable over config file", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; - // Mock: config file exists with scanTimeout: 5000 - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: 5000 }) - ); + configFileContent = JSON.stringify({ scanTimeout: 5000 }); const timeout = getScanTimeout(); @@ -79,11 +61,7 @@ describe("getScanTimeout", () => { it("should handle invalid environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; - // Mock: config file exists with scanTimeout: 7000 - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: 7000 }) - ); + configFileContent = JSON.stringify({ scanTimeout: 7000 }); const timeout = getScanTimeout(); @@ -91,8 +69,7 @@ describe("getScanTimeout", () => { }); it("should ignore zero and negative values and fall back to default", () => { - // Mock: config file doesn't exist - fsMock.existsSync.mock.mockImplementation(() => false); + configFileContent = undefined; process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; @@ -107,11 +84,7 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; - // Mock: config file exists with scanTimeout: 8000 - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: 8000 }) - ); + configFileContent = JSON.stringify({ scanTimeout: 8000 }); const timeout = getScanTimeout(); @@ -120,11 +93,7 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in config file and fall back to default", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - // Mock: config file exists with scanTimeout: "slow" - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: "slow" }) - ); + configFileContent = JSON.stringify({ scanTimeout: "slow" }); const timeout = getScanTimeout(); @@ -133,11 +102,7 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in both env and config, fall back to default", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; - // Mock: config file exists with scanTimeout: "medium" - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: "medium" }) - ); + configFileContent = JSON.stringify({ scanTimeout: "medium" }); const timeout = getScanTimeout(); @@ -146,11 +111,7 @@ describe("getScanTimeout", () => { it("should ignore mixed alphanumeric strings in environment variable", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; - // Mock: config file exists with scanTimeout: 6000 - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: 6000 }) - ); + configFileContent = JSON.stringify({ scanTimeout: 6000 }); const timeout = getScanTimeout(); @@ -159,11 +120,7 @@ describe("getScanTimeout", () => { it("should ignore mixed alphanumeric strings in config file", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - // Mock: config file exists with scanTimeout: "3000ms" - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: "3000ms" }) - ); + configFileContent = JSON.stringify({ scanTimeout: "3000ms" }); const timeout = getScanTimeout(); @@ -171,37 +128,15 @@ describe("getScanTimeout", () => { }); }); -describe("getMinimumPackageAgeHours", () => { - let fsMock; - let getMinimumPackageAgeHours; - - beforeEach(async () => { - // Mock fs module - fsMock = { - existsSync: mock.fn(() => false), - readFileSync: mock.fn(() => "{}"), - writeFileSync: mock.fn(), - mkdirSync: mock.fn(), - }; - - mock.module("fs", { - namedExports: fsMock, - }); - - // Re-import the module to get the mocked version - const configFileModule = await import( - `./configFile.js?update=${Date.now()}` - ); - getMinimumPackageAgeHours = configFileModule.getMinimumPackageAgeHours; - }); +describe("getMinimumPackageAgeHours", async () => { + const { getMinimumPackageAgeHours } = await import("./configFile.js"); afterEach(() => { - // Reset all mocks - mock.restoreAll(); + configFileContent = undefined; }); it("should return null when config file doesn't exist", () => { - fsMock.existsSync.mock.mockImplementation(() => false); + configFileContent = undefined; const hours = getMinimumPackageAgeHours(); @@ -209,10 +144,7 @@ describe("getMinimumPackageAgeHours", () => { }); it("should return null when config file exists but minimumPackageAgeHours is not set", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ scanTimeout: 5000 }) - ); + configFileContent = JSON.stringify({ scanTimeout: 5000 }); const hours = getMinimumPackageAgeHours(); @@ -220,10 +152,7 @@ describe("getMinimumPackageAgeHours", () => { }); it("should return value from config file when set to valid number", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ minimumPackageAgeHours: 48 }) - ); + configFileContent = JSON.stringify({ minimumPackageAgeHours: 48 }); const hours = getMinimumPackageAgeHours(); @@ -231,10 +160,7 @@ describe("getMinimumPackageAgeHours", () => { }); it("should handle string numbers in config file", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ minimumPackageAgeHours: "72" }) - ); + configFileContent = JSON.stringify({ minimumPackageAgeHours: "72" }); const hours = getMinimumPackageAgeHours(); @@ -242,10 +168,7 @@ describe("getMinimumPackageAgeHours", () => { }); it("should handle decimal values", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ minimumPackageAgeHours: 1.5 }) - ); + configFileContent = JSON.stringify({ minimumPackageAgeHours: 1.5 }); const hours = getMinimumPackageAgeHours(); @@ -253,21 +176,15 @@ describe("getMinimumPackageAgeHours", () => { }); it("should return null for non-numeric strings", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ minimumPackageAgeHours: "invalid" }) - ); + configFileContent = JSON.stringify({ minimumPackageAgeHours: "invalid" }); const hours = getMinimumPackageAgeHours(); assert.strictEqual(hours, undefined); }); - it("should return null for values with units suffix", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => - JSON.stringify({ minimumPackageAgeHours: "48h" }) - ); + it("should return undefined for values with units suffix", () => { + configFileContent = JSON.stringify({ minimumPackageAgeHours: "48h" }); const hours = getMinimumPackageAgeHours(); @@ -275,11 +192,42 @@ describe("getMinimumPackageAgeHours", () => { }); it("should handle malformed JSON and return null", () => { - fsMock.existsSync.mock.mockImplementation(() => true); - fsMock.readFileSync.mock.mockImplementation(() => "{ invalid json"); + configFileContent = "{ invalid json"; const hours = getMinimumPackageAgeHours(); assert.strictEqual(hours, undefined); }); + + it("should return 0 when minimumPackageAgeHours is set to 0", () => { + configFileContent = JSON.stringify({ minimumPackageAgeHours: 0 }); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, 0); + }); + + it("should return 0 when minimumPackageAgeHours is set to string '0'", () => { + configFileContent = JSON.stringify({ minimumPackageAgeHours: "0" }); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, 0); + }); + + it("should handle negative numeric values", () => { + configFileContent = JSON.stringify({ minimumPackageAgeHours: -24 }); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, -24); + }); + + it("should handle negative string values", () => { + configFileContent = JSON.stringify({ minimumPackageAgeHours: "-48" }); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, -48); + }); }); diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 7c20358..e1cec34 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -81,7 +81,7 @@ function validateMinimumPackageAgeHours(value) { return undefined; } - if (numericValue > 0) { + if (numericValue >= 0) { return numericValue; } diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 38bb8ff..0e895b3 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -23,6 +23,7 @@ export async function main(args) { process.on("uncaughtException", (error) => { ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); ui.writeVerbose(`Stack trace: ${error.stack}`); + ui.writeBufferedLogsAndStopBuffering(); process.exit(1); }); @@ -31,6 +32,7 @@ export async function main(args) { if (reason instanceof Error) { ui.writeVerbose(`Stack trace: ${reason.stack}`); } + ui.writeBufferedLogsAndStopBuffering(); process.exit(1); }); @@ -89,6 +91,7 @@ export async function main(args) { return packageManagerResult.status; } catch (/** @type any */ error) { ui.writeError("Failed to check for malicious packages:", error.message); + ui.writeBufferedLogsAndStopBuffering(); // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index e9f05c7..83bc03e 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -8,6 +8,7 @@ import fsSync from "node:fs"; import os from "node:os"; import path from "node:path"; import ini from "ini"; +import { spawn } from "child_process"; /** * Checks if this pip invocation should bypass safe-chain and spawn directly. @@ -16,7 +17,7 @@ import ini from "ini"; * @param {string[]} args - The arguments * @returns {boolean} */ -function shouldBypassSafeChain(command, args) { +export function shouldBypassSafeChain(command, args) { if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) { // Check if args start with -m pip if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) { @@ -77,14 +78,16 @@ export async function runPip(command, args) { if (shouldBypassSafeChain(command, args)) { ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`); // Spawn the ORIGINAL command with ORIGINAL args - const { spawn } = await import("child_process"); return new Promise((_resolve) => { const proc = spawn(command, args, { stdio: "inherit" }); proc.on("exit", (/** @type {number | null} */ code) => { + ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`); + ui.writeBufferedLogsAndStopBuffering(); process.exit(code ?? 0); }); proc.on("error", (/** @type {Error} */ err) => { ui.writeError(`Error executing command: ${err.message}`); + ui.writeBufferedLogsAndStopBuffering(); process.exit(1); }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index cf121f6..0707333 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -7,6 +7,7 @@ import ini from "ini"; describe("runPipCommand environment variable handling", () => { let runPip; + let shouldBypassSafeChain; let capturedArgs = null; let customEnv = null; let capturedConfigContent = null; // Capture config file content before cleanup @@ -56,6 +57,7 @@ describe("runPipCommand environment variable handling", () => { const mod = await import("./runPipCommand.js"); runPip = mod.runPip; + shouldBypassSafeChain = mod.shouldBypassSafeChain; }); afterEach(() => { @@ -66,14 +68,14 @@ describe("runPipCommand environment variable handling", () => { 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, @@ -96,7 +98,7 @@ describe("runPipCommand environment variable handling", () => { 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, @@ -108,7 +110,7 @@ describe("runPipCommand environment variable handling", () => { 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, @@ -120,13 +122,13 @@ describe("runPipCommand environment variable handling", () => { 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, @@ -139,7 +141,7 @@ describe("runPipCommand environment variable handling", () => { 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, @@ -151,7 +153,7 @@ describe("runPipCommand environment variable handling", () => { 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, @@ -181,7 +183,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(res.status, 0); assert.ok(capturedArgs, "safeSpawn should have been called"); - + // Check environment variables are set assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, @@ -218,7 +220,7 @@ describe("runPipCommand environment variable handling", () => { // For default PyPI, we still set env vars; pip CLI --cert takes precedence const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - + // Environment variables still set (pip CLI --cert takes precedence) assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, @@ -233,7 +235,7 @@ describe("runPipCommand environment variable handling", () => { it("should preserve HTTPS_PROXY from proxy merge", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - + assert.strictEqual( capturedArgs.options.env.HTTPS_PROXY, "http://localhost:8080", @@ -380,7 +382,7 @@ describe("runPipCommand environment variable handling", () => { await fs.writeFile(cfgPath, initialIni, "utf-8"); customEnv = { PIP_CONFIG_FILE: cfgPath }; - + // Capture stdout/stderr let output = ""; const originalWrite = process.stdout.write; @@ -397,4 +399,21 @@ describe("runPipCommand environment variable handling", () => { assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output"); customEnv = null; }); + + it("should bypass safe-chain for python correctly", async () => { + assert.strictEqual(shouldBypassSafeChain("python", []), true); + assert.strictEqual(shouldBypassSafeChain("python3", []), true); + + assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true); + assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true); + + assert.strictEqual(shouldBypassSafeChain("python", ["-m", "http.server"]), true); + assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "http.server"]), true); + + assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false); + assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false); + assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false); + assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false); + }); + }); diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 50cea5d..3b08bf2 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -113,6 +113,20 @@ export function getPackageManagerList() { return `${tools.join(", ")}, and ${lastTool} commands`; } +/** + * @returns {string} + */ +export function getShimsDir() { + return path.join(os.homedir(), ".safe-chain", "shims"); +} + +/** + * @returns {string} + */ +export function getScriptsDir() { + return path.join(os.homedir(), ".safe-chain", "scripts"); +} + /** * @param {string} executableName * diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index bc5c5e6..14510f9 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -1,12 +1,10 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; -import { getPackageManagerList, knownAikidoTools } from "./helpers.js"; +import { getPackageManagerList, knownAikidoTools, getShimsDir } 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 { ECOSYSTEM_PY } from "../config/settings.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -32,7 +30,7 @@ export async function setupCi() { ); ui.emptyLine(); - const shimsDir = path.join(os.homedir(), ".safe-chain", "shims"); + const shimsDir = getShimsDir(); const binDir = path.join(os.homedir(), ".safe-chain", "bin"); // Create the shims directory if it doesn't exist if (!fs.existsSync(shimsDir)) { @@ -162,9 +160,5 @@ function modifyPathForCi(shimsDir, binDir) { } function getToolsToSetup() { - if (includePython()) { - return knownAikidoTools; - } else { - return knownAikidoTools.filter((tool) => tool.ecoSystem !== ECOSYSTEM_PY); - } + return knownAikidoTools; } diff --git a/packages/safe-chain/src/shell-integration/setup-ci.spec.js b/packages/safe-chain/src/shell-integration/setup-ci.spec.js index 92ef82e..b437157 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.spec.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.spec.js @@ -50,6 +50,7 @@ describe("Setup CI shell integration", () => { { tool: "yarn", aikidoCommand: "aikido-yarn" }, ], getPackageManagerList: () => "npm, yarn", + getShimsDir: () => mockShimsDir, }, }); diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index d5c4be9..7e64c0b 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -1,11 +1,9 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +import { knownAikidoTools, getPackageManagerList, getScriptsDir } from "./helpers.js"; import fs from "fs"; -import os from "os"; import path from "path"; -import { includePython } from "../config/cliArguments.js"; import { fileURLToPath } from "url"; /** @type {string} */ @@ -107,10 +105,10 @@ function setupShell(shell) { function copyStartupFiles() { const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"]; + const targetDir = getScriptsDir(); for (const file of startupFiles) { - const targetDir = path.join(os.homedir(), ".safe-chain", "scripts"); - const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file); + const targetPath = path.join(targetDir, file); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); @@ -119,7 +117,7 @@ function copyStartupFiles() { // Use absolute path for source const sourcePath = path.join( dirname, - includePython() ? "startup-scripts/include-python" : "startup-scripts", + "startup-scripts", file ); fs.copyFileSync(sourcePath, targetPath); diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish deleted file mode 100644 index 386144c..0000000 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish +++ /dev/null @@ -1,98 +0,0 @@ -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] - - # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" - set_color -b yellow black - printf "Warning:" - set_color normal - printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd - - # Cyan text for the install command - printf "Install safe-chain by using " - set_color cyan - printf "npm install -g @aikidosec/safe-chain" - set_color normal - printf ".\n" -end - -function wrapSafeChainCommand - set original_cmd $argv[1] - set cmd_args $argv[2..-1] - - if type -q safe-chain - # If the safe-chain command is available, just run it with the provided arguments - safe-chain $original_cmd $cmd_args - else - # If the safe-chain command is not available, print a warning and run the original command - printSafeChainWarning $original_cmd - command $original_cmd $cmd_args - end -end diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh deleted file mode 100644 index c71c741..0000000 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh +++ /dev/null @@ -1,85 +0,0 @@ -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 - # \033[0m is used to reset the text formatting - printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" - # \033[36m is used to set the text color to cyan - printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" -} - -function wrapSafeChainCommand() { - local original_cmd="$1" - - if command -v safe-chain > /dev/null 2>&1; then - # If the aikido command is available, just run it with the provided arguments - safe-chain "$@" - else - # If the aikido command is not available, print a warning and run the original command - printSafeChainWarning "$original_cmd" - - command "$original_cmd" "$@" - fi -} diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 deleted file mode 100644 index c3d21c4..0000000 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -# Use cross-platform path separator (: on Unix, ; on Windows) -# $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell -$isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true } -$pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' } -$safeChainBin = Join-Path (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) - - # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" - Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline - Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." - - # Cyan text for the install command - Write-Host "Install safe-chain by using " -NoNewline - Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline - Write-Host "." -} - -function Test-CommandAvailable { - param([string]$Command) - - try { - Get-Command $Command -ErrorAction Stop | Out-Null - return $true - } - catch { - return $false - } -} - -function Invoke-RealCommand { - param( - [string]$Command, - [string[]]$Arguments - ) - - # Find the real executable to avoid calling our wrapped functions - $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 - if ($realCommand) { - & $realCommand.Source @Arguments - } -} - -function Invoke-WrappedCommand { - param( - [string]$OriginalCmd, - [string[]]$Arguments - ) - - if (Test-CommandAvailable "safe-chain") { - & safe-chain $OriginalCmd @Arguments - } - else { - Write-SafeChainWarning $OriginalCmd - Invoke-RealCommand $OriginalCmd $Arguments - } -} diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index b18ff96..386144c 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -39,6 +39,33 @@ function npm 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] diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index 5c32143..c71c741 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -35,6 +35,33 @@ function npm() { 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 # \033[0m is used to reset the text formatting diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index 0fc3385..c3d21c4 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -40,6 +40,33 @@ function npm { 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) diff --git a/packages/safe-chain/src/shell-integration/teardown.js b/packages/safe-chain/src/shell-integration/teardown.js index bc83b48..de3fbd7 100644 --- a/packages/safe-chain/src/shell-integration/teardown.js +++ b/packages/safe-chain/src/shell-integration/teardown.js @@ -1,7 +1,8 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +import { knownAikidoTools, getPackageManagerList, getShimsDir, getScriptsDir } from "./helpers.js"; +import fs from "fs"; /** * @returns {Promise} @@ -62,3 +63,44 @@ export async function teardown() { return; } } + +/** + * Removes directories created by setup-ci and setup commands + * @returns {Promise} + */ +export async function teardownDirectories() { + const shimsDir = getShimsDir(); + const scriptsDir = getScriptsDir(); + + // Remove CI shims directory + if (fs.existsSync(shimsDir)) { + try { + fs.rmSync(shimsDir, { recursive: true, force: true }); + ui.writeInformation( + `${chalk.bold("- CI Shims:")} ${chalk.green("Removed successfully")}` + ); + } catch (/** @type {any} */ error) { + ui.writeError( + `${chalk.bold("- CI Shims:")} ${chalk.red( + "Failed to remove" + )}. Error: ${error.message}` + ); + } + } + + // Remove scripts directory + if (fs.existsSync(scriptsDir)) { + try { + fs.rmSync(scriptsDir, { recursive: true, force: true }); + ui.writeInformation( + `${chalk.bold("- Scripts:")} ${chalk.green("Removed successfully")}` + ); + } catch (/** @type {any} */ error) { + ui.writeError( + `${chalk.bold("- Scripts:")} ${chalk.red( + "Failed to remove" + )}. Error: ${error.message}` + ); + } + } +} diff --git a/test/e2e/certbundle.e2e.spec.js b/test/e2e/certbundle.e2e.spec.js index caf4102..4b4ad84 100644 --- a/test/e2e/certbundle.e2e.spec.js +++ b/test/e2e/certbundle.e2e.spec.js @@ -231,7 +231,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => { it(`pip install works without NODE_EXTRA_CA_CERTS set`, async () => { const shell = await container.openShell("zsh"); - await shell.runCommand("safe-chain setup --include-python"); + await shell.runCommand("safe-chain setup"); await shell.runCommand("unset NODE_EXTRA_CA_CERTS"); const result = await shell.runCommand( @@ -247,7 +247,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => { it(`pip install works with valid NODE_EXTRA_CA_CERTS set`, async () => { const shell = await container.openShell("zsh"); - await shell.runCommand("safe-chain setup --include-python"); + await shell.runCommand("safe-chain setup"); // Create a temporary valid certificate await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/pip-valid-certs.pem"); @@ -265,7 +265,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => { it(`pip install handles non-existent NODE_EXTRA_CA_CERTS gracefully`, async () => { const shell = await container.openShell("zsh"); - await shell.runCommand("safe-chain setup --include-python"); + await shell.runCommand("safe-chain setup"); const result = await shell.runCommand( 'export NODE_EXTRA_CA_CERTS="/tmp/nonexistent-pip-certs.pem" && pip3 install --break-system-packages requests' @@ -281,7 +281,7 @@ describe("E2E: NODE_EXTRA_CA_CERTS merging", () => { it(`pip install handles invalid NODE_EXTRA_CA_CERTS gracefully`, async () => { const shell = await container.openShell("zsh"); - await shell.runCommand("safe-chain setup --include-python"); + await shell.runCommand("safe-chain setup"); // Create invalid cert await shell.runCommand( diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index 85a4a46..49db6ce 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -86,7 +86,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { // Setup safe-chain CI shims const installationShell = await container.openShell(shell); await installationShell.runCommand( - "safe-chain setup-ci --include-python" + "safe-chain setup-ci" ); // Add $HOME/.safe-chain/shims to PATH for subsequent shells @@ -115,7 +115,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes python -m pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand( - "safe-chain setup-ci --include-python" + "safe-chain setup-ci" ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" @@ -138,7 +138,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand( - "safe-chain setup-ci --include-python" + "safe-chain setup-ci" ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" @@ -161,7 +161,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand( - "safe-chain setup-ci --include-python" + "safe-chain setup-ci" ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" @@ -184,7 +184,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand( - "safe-chain setup-ci --include-python" + "safe-chain setup-ci" ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index e02d1b3..b06978f 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -15,7 +15,7 @@ describe("E2E: pip coverage", () => { await container.start(); const installationShell = await container.openShell("zsh"); - await installationShell.runCommand("safe-chain setup --include-python"); + await installationShell.runCommand("safe-chain setup"); // Clear pip cache before each test to ensure fresh downloads through proxy await installationShell.runCommand("pip3 cache purge"); diff --git a/test/e2e/poetry.e2e.spec.js b/test/e2e/poetry.e2e.spec.js index 3d19783..58b74fd 100644 --- a/test/e2e/poetry.e2e.spec.js +++ b/test/e2e/poetry.e2e.spec.js @@ -15,7 +15,7 @@ describe("E2E: poetry coverage", () => { await container.start(); const installationShell = await container.openShell("zsh"); - await installationShell.runCommand("safe-chain setup --include-python"); + await installationShell.runCommand("safe-chain setup"); // Clear poetry cache await installationShell.runCommand("command poetry cache clear pypi --all -n"); diff --git a/test/e2e/teardown-dirs.e2e.spec.js b/test/e2e/teardown-dirs.e2e.spec.js new file mode 100644 index 0000000..853c503 --- /dev/null +++ b/test/e2e/teardown-dirs.e2e.spec.js @@ -0,0 +1,96 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: safe-chain teardown command", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + container = new DockerTestContainer(); + await container.start(); + }); + + afterEach(async () => { + if (container) { + await container.stop(); + container = null; + } + }); + + it("safe-chain teardown removes shims directory created by setup-ci", async () => { + const shell = await container.openShell("bash"); + + // Run setup-ci + await shell.runCommand("safe-chain setup-ci"); + + // Verify shims directory exists + const checkShimsExist = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'"); + assert.ok(checkShimsExist.output.includes("exists"), "Shims directory should exist after setup-ci"); + + // Run teardown + await shell.runCommand("safe-chain teardown"); + + // Verify shims directory is gone + const checkShimsGone = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'"); + assert.ok(checkShimsGone.output.includes("missing"), "Shims directory should be removed after teardown"); + }); + + it("safe-chain teardown removes scripts directory created by setup", async () => { + const shell = await container.openShell("bash"); + + // Run setup + await shell.runCommand("safe-chain setup"); + + // Verify scripts directory exists + const checkScriptsExist = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'"); + assert.ok(checkScriptsExist.output.includes("exists"), "Scripts directory should exist after setup"); + + // Run teardown + await shell.runCommand("safe-chain teardown"); + + // Verify scripts directory is gone + const checkScriptsGone = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'"); + assert.ok(checkScriptsGone.output.includes("missing"), "Scripts directory should be removed after teardown"); + }); + + it("safe-chain teardown removes shims directory created by setup-ci", async () => { + const shell = await container.openShell("bash"); + + // Run setup-ci + await shell.runCommand("safe-chain setup-ci"); + // Verify shims directory exists + const checkShimsExist = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'"); + assert.ok(checkShimsExist.output.includes("exists"), "Shims directory should exist after setup-ci"); + + // Verify Python shims were created + const checkPythonShims = await shell.runCommand("test -f ~/.safe-chain/shims/pip && echo 'exists' || echo 'missing'"); + assert.ok(checkPythonShims.output.includes("exists"), "Python shims should exist after setup-ci"); + // Run teardown + await shell.runCommand("safe-chain teardown"); + + // Verify shims directory is gone + const checkShimsGone = await shell.runCommand("test -d ~/.safe-chain/shims && echo 'exists' || echo 'missing'"); + assert.ok(checkShimsGone.output.includes("missing"), "Shims directory should be removed after teardown"); + }); + + it("safe-chain teardown removes scripts directory created by setup", async () => { + const shell = await container.openShell("bash"); + + // Run setup + await shell.runCommand("safe-chain setup"); + // Verify scripts directory exists + const checkScriptsExist = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'"); + assert.ok(checkScriptsExist.output.includes("exists"), "Scripts directory should exist after setup"); + + // Run teardown + await shell.runCommand("safe-chain teardown"); + + // Verify scripts directory is gone + const checkScriptsGone = await shell.runCommand("test -d ~/.safe-chain/scripts && echo 'exists' || echo 'missing'"); + assert.ok(checkScriptsGone.output.includes("missing"), "Scripts directory should be removed after teardown"); + }); +}); diff --git a/test/e2e/uv.e2e.spec.js b/test/e2e/uv.e2e.spec.js index 7e9daac..9d5f3b9 100644 --- a/test/e2e/uv.e2e.spec.js +++ b/test/e2e/uv.e2e.spec.js @@ -15,7 +15,7 @@ describe("E2E: uv coverage", () => { await container.start(); const installationShell = await container.openShell("zsh"); - await installationShell.runCommand("safe-chain setup --include-python"); + await installationShell.runCommand("safe-chain setup"); // Clear uv cache await installationShell.runCommand("uv cache clean");