From 24af6f21eb4d05b84331dcb75d88d9c4a0db732c Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 10 Apr 2026 12:09:40 -0700 Subject: [PATCH] Add regular setup support --- .../supported-shells/bash.js | 8 +-- .../supported-shells/bash.spec.js | 13 ++--- .../supported-shells/fish.js | 8 +-- .../supported-shells/fish.spec.js | 15 +++--- .../supported-shells/powershell.js | 8 +-- .../supported-shells/powershell.spec.js | 13 ++--- .../supported-shells/windowsPowershell.js | 8 +-- .../windowsPowershell.spec.js | 13 ++--- .../shell-integration/supported-shells/zsh.js | 8 +-- .../supported-shells/zsh.spec.js | 17 ++++--- test/e2e/bun.e2e.spec.js | 35 +++++++++++++ test/e2e/npm-ci.e2e.spec.js | 43 ++++++++++++++++ test/e2e/npm.e2e.spec.js | 35 +++++++++++++ test/e2e/pip-ci.e2e.spec.js | 40 +++++++++++++++ test/e2e/pip.e2e.spec.js | 36 +++++++++++++ test/e2e/pipx.e2e.spec.js | 35 +++++++++++++ test/e2e/pnpm-ci.e2e.spec.js | 41 +++++++++++++++ test/e2e/pnpm.e2e.spec.js | 35 +++++++++++++ test/e2e/poetry.e2e.spec.js | 42 +++++++++++++++ test/e2e/safe-chain-dir.e2e.spec.js | 51 +++++++++++++++++++ test/e2e/uv.e2e.spec.js | 39 ++++++++++++++ test/e2e/yarn-ci.e2e.spec.js | 41 +++++++++++++++ test/e2e/yarn.e2e.spec.js | 39 ++++++++++++++ 23 files changed, 575 insertions(+), 48 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.js index cc50223..364323e 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -2,9 +2,11 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, + getScriptsDir, } from "../helpers.js"; import { execSync, spawnSync } from "child_process"; import * as os from "os"; +import path from "path"; const shellName = "Bash"; const executableName = "bash"; @@ -32,10 +34,10 @@ function teardown(tools) { ); } - // Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh) + // Removes the line that sources the safe-chain bash initialization script (any path, requires safe-chain comment) removeLinesMatchingPattern( startupFile, - /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/, + /^source\s+.*init-posix\.sh.*#\s*Safe-chain/, eol ); @@ -47,7 +49,7 @@ function setup() { addLineToFile( startupFile, - `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`, + `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain bash initialization script`, eol ); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js index aa7159f..f0a56d2 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.spec.js @@ -19,6 +19,7 @@ describe("Bash shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, + getScriptsDir: () => "/test-home/.safe-chain/scripts", addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -109,7 +110,7 @@ describe("Bash shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script" + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script" ) ); }); @@ -129,7 +130,7 @@ describe("Bash shell integration", () => { const content = fs.readFileSync(windowsCygwinPath, "utf-8"); assert.ok( content.includes( - "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script" + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script" ) ); }); @@ -209,13 +210,13 @@ describe("Bash shell integration", () => { // Setup bash.setup(tools); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); + assert.ok(content.includes("source /test-home/.safe-chain/scripts/init-posix.sh")); // Teardown bash.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + !content.includes("source /test-home/.safe-chain/scripts/init-posix.sh") ); }); @@ -236,7 +237,7 @@ describe("Bash shell integration", () => { const initialContent = [ "#!/bin/bash", "alias npm='old-npm'", - "source ~/.safe-chain/scripts/init-posix.sh", + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script", "alias ls='ls --color=auto'", ].join("\n"); @@ -247,7 +248,7 @@ describe("Bash shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm=")); assert.ok( - !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + !content.includes("source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script") ); assert.ok(content.includes("alias ls=")); }); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.js index a623d0b..5f59826 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.js @@ -2,8 +2,10 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, + getScriptsDir, } from "../helpers.js"; import { execSync } from "child_process"; +import path from "path"; const shellName = "Fish"; const executableName = "fish"; @@ -31,10 +33,10 @@ function teardown(tools) { ); } - // Removes the line that sources the safe-chain fish initialization script (~/.safe-chain/scripts/init-fish.fish) + // Removes the line that sources the safe-chain fish initialization script (any path, requires safe-chain comment) removeLinesMatchingPattern( startupFile, - /^source\s+~\/\.safe-chain\/scripts\/init-fish\.fish/, + /^source\s+.*init-fish\.fish.*#\s*Safe-chain/, eol ); @@ -46,7 +48,7 @@ function setup() { addLineToFile( startupFile, - `source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script`, + `source ${path.join(getScriptsDir(), "init-fish.fish")} # Safe-chain Fish initialization script`, eol ); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js index e138957..0933b6e 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.spec.js @@ -17,6 +17,7 @@ describe("Fish shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, + getScriptsDir: () => "/test-home/.safe-chain/scripts", addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -72,7 +73,7 @@ describe("Fish shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script') + content.includes('source /test-home/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script') ); }); @@ -81,7 +82,7 @@ describe("Fish shell integration", () => { fish.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + const sourceMatches = (content.match(/source \/test-home\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; assert.strictEqual(sourceMatches, 2, "Should allow multiple source lines (helper doesn't dedupe)"); }); }); @@ -93,7 +94,7 @@ describe("Fish shell integration", () => { "alias npm 'aikido-npm'", "alias npx 'aikido-npx'", "alias yarn 'aikido-yarn'", - "source ~/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script", + "source /test-home/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script", "alias ls 'ls --color=auto'", "alias grep 'grep --color=auto'", ].join("\n"); @@ -107,7 +108,7 @@ describe("Fish shell integration", () => { assert.ok(!content.includes("alias npm ")); assert.ok(!content.includes("alias npx ")); assert.ok(!content.includes("alias yarn ")); - assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); + assert.ok(!content.includes("source /test-home/.safe-chain/scripts/init-fish.fish")); assert.ok(content.includes("alias ls ")); assert.ok(content.includes("alias grep ")); }); @@ -162,12 +163,12 @@ describe("Fish shell integration", () => { // Setup fish.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes('source ~/.safe-chain/scripts/init-fish.fish')); + assert.ok(content.includes('source /test-home/.safe-chain/scripts/init-fish.fish')); // Teardown fish.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(!content.includes("source ~/.safe-chain/scripts/init-fish.fish")); + assert.ok(!content.includes("source /test-home/.safe-chain/scripts/init-fish.fish")); }); it("should handle multiple setup calls", () => { @@ -176,7 +177,7 @@ describe("Fish shell integration", () => { fish.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); - const sourceMatches = (content.match(/source ~\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; + const sourceMatches = (content.match(/source \/test-home\/\.safe-chain\/scripts\/init-fish\.fish/g) || []).length; assert.strictEqual(sourceMatches, 1, "Should have exactly one source line after setup-teardown-setup cycle"); }); }); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js index 4bbc332..59aee41 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -3,8 +3,10 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, validatePowerShellExecutionPolicy, + getScriptsDir, } from "../helpers.js"; import { execSync } from "child_process"; +import path from "path"; const shellName = "PowerShell Core"; const executableName = "pwsh"; @@ -30,10 +32,10 @@ function teardown(tools) { ); } - // Remove the line that sources the safe-chain PowerShell initialization script + // Remove the line that sources the safe-chain PowerShell initialization script (any path, requires safe-chain comment) removeLinesMatchingPattern( startupFile, - /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/, + /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/, ); return true; @@ -52,7 +54,7 @@ async function setup() { addLineToFile( startupFile, - `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`, + `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`, ); return true; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js index de2c14b..1d9f65c 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js @@ -40,6 +40,7 @@ describe("PowerShell Core shell integration", () => { fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); }, validatePowerShellExecutionPolicy: () => executionPolicyResult, + getScriptsDir: () => "/test-home/.safe-chain/scripts", }, }); @@ -83,7 +84,7 @@ describe("PowerShell Core shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + '. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script', ), ); }); @@ -93,7 +94,7 @@ describe("PowerShell Core shell integration", () => { it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# PowerShell profile", - '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + '. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script', "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -105,7 +106,7 @@ describe("PowerShell Core shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), + !content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'), ); assert.ok(content.includes("Set-Alias ls ")); assert.ok(content.includes("Set-Alias grep ")); @@ -180,14 +181,14 @@ describe("PowerShell Core shell integration", () => { await powershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), + content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'), ); // Teardown powershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), + !content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'), ); }); @@ -198,7 +199,7 @@ describe("PowerShell Core shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); const sourceMatches = ( - content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) || + content.match(/\. "\/test-home\/\.safe-chain\/scripts\/init-pwsh\.ps1"/g) || [] ).length; assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js index 3e81da7..36ab114 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -3,8 +3,10 @@ import { doesExecutableExistOnSystem, removeLinesMatchingPattern, validatePowerShellExecutionPolicy, + getScriptsDir, } from "../helpers.js"; import { execSync } from "child_process"; +import path from "path"; const shellName = "Windows PowerShell"; const executableName = "powershell"; @@ -30,10 +32,10 @@ function teardown(tools) { ); } - // Remove the line that sources the safe-chain PowerShell initialization script + // Remove the line that sources the safe-chain PowerShell initialization script (any path, requires safe-chain comment) removeLinesMatchingPattern( startupFile, - /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/, + /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/, ); return true; @@ -52,7 +54,7 @@ async function setup() { addLineToFile( startupFile, - `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`, + `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`, ); return true; diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js index 561d0d4..621b380 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -40,6 +40,7 @@ describe("Windows PowerShell shell integration", () => { fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); }, validatePowerShellExecutionPolicy: () => executionPolicyResult, + getScriptsDir: () => "/test-home/.safe-chain/scripts", }, }); @@ -83,7 +84,7 @@ describe("Windows PowerShell shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + '. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script', ), ); }); @@ -93,7 +94,7 @@ describe("Windows PowerShell shell integration", () => { it("should remove init-pwsh.ps1 source line", () => { const initialContent = [ "# Windows PowerShell profile", - '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + '. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script', "Set-Alias ls Get-ChildItem", "Set-Alias grep Select-String", ].join("\n"); @@ -105,7 +106,7 @@ describe("Windows PowerShell shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), + !content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'), ); assert.ok(content.includes("Set-Alias ls ")); assert.ok(content.includes("Set-Alias grep ")); @@ -180,14 +181,14 @@ describe("Windows PowerShell shell integration", () => { await windowsPowershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), + content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'), ); // Teardown windowsPowershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), + !content.includes('. "/test-home/.safe-chain/scripts/init-pwsh.ps1"'), ); }); @@ -198,7 +199,7 @@ describe("Windows PowerShell shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); const sourceMatches = ( - content.match(/\. "\$HOME\\.safe-chain\\scripts\\init-pwsh\.ps1"/g) || + content.match(/\. "\/test-home\/\.safe-chain\/scripts\/init-pwsh\.ps1"/g) || [] ).length; assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js index f187af3..369b445 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -2,8 +2,10 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, + getScriptsDir, } from "../helpers.js"; import { execSync } from "child_process"; +import path from "path"; const shellName = "Zsh"; const executableName = "zsh"; @@ -31,10 +33,10 @@ function teardown(tools) { ); } - // Removes the line that sources the safe-chain zsh initialization script (~/.safe-chain/scripts/init-posix.sh) + // Removes the line that sources the safe-chain zsh initialization script (any path, requires safe-chain comment) removeLinesMatchingPattern( startupFile, - /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/, + /^source\s+.*init-posix\.sh.*#\s*Safe-chain/, eol ); @@ -46,7 +48,7 @@ function setup() { addLineToFile( startupFile, - `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script`, + `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain Zsh initialization script`, eol ); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js index 99106ec..41e1bd1 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.spec.js @@ -17,6 +17,7 @@ describe("Zsh shell integration", () => { mock.module("../helpers.js", { namedExports: { doesExecutableExistOnSystem: () => true, + getScriptsDir: () => "/test-home/.safe-chain/scripts", addLineToFile: (filePath, line) => { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, "", "utf-8"); @@ -73,7 +74,7 @@ describe("Zsh shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - "source ~/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script" + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script" ) ); }); @@ -83,7 +84,7 @@ describe("Zsh shell integration", () => { assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); + assert.ok(content.includes("source /test-home/.safe-chain/scripts/init-posix.sh")); }); }); @@ -114,7 +115,7 @@ describe("Zsh shell integration", () => { it("should remove zsh initialization script source line", () => { const initialContent = [ "#!/bin/zsh", - "source ~/.safe-chain/scripts/init-posix.sh", + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script", "alias ls='ls --color=auto'", ].join("\n"); @@ -125,7 +126,7 @@ describe("Zsh shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + !content.includes("source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script") ); assert.ok(content.includes("alias ls=")); }); @@ -180,13 +181,13 @@ describe("Zsh shell integration", () => { // Setup zsh.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); - assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh")); + assert.ok(content.includes("source /test-home/.safe-chain/scripts/init-posix.sh")); // Teardown zsh.teardown(tools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + !content.includes("source /test-home/.safe-chain/scripts/init-posix.sh") ); }); @@ -207,7 +208,7 @@ describe("Zsh shell integration", () => { const initialContent = [ "#!/bin/zsh", "alias npm='old-npm'", - "source ~/.safe-chain/scripts/init-posix.sh", + "source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script", "alias ls='ls --color=auto'", ].join("\n"); @@ -218,7 +219,7 @@ describe("Zsh shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok(!content.includes("alias npm=")); assert.ok( - !content.includes("source ~/.safe-chain/scripts/init-posix.sh") + !content.includes("source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script") ); assert.ok(content.includes("alias ls=")); }); diff --git a/test/e2e/bun.e2e.spec.js b/test/e2e/bun.e2e.spec.js index fb6e99a..1de6100 100644 --- a/test/e2e/bun.e2e.spec.js +++ b/test/e2e/bun.e2e.spec.js @@ -78,4 +78,39 @@ describe("E2E: bun coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("bash"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious bun packages when scripts are in a custom directory", async () => { + const shell = await customContainer.openShell("bash"); + const result = await shell.runCommand("bunx safe-chain-test"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/npm-ci.e2e.spec.js b/test/e2e/npm-ci.e2e.spec.js index 1698759..cc3349b 100644 --- a/test/e2e/npm-ci.e2e.spec.js +++ b/test/e2e/npm-ci.e2e.spec.js @@ -102,4 +102,47 @@ describe("E2E: npm coverage using PATH", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup-ci"); + // Persist SAFE_CHAIN_DIR and the custom shims dir in .zshrc so new shells + // inherit both (shims need SAFE_CHAIN_DIR to strip themselves from PATH) + await setupShell.runCommand( + `echo 'export SAFE_CHAIN_DIR=${CUSTOM_DIR}' >> ~/.zshrc` + ); + await setupShell.runCommand( + `echo 'export PATH="${CUSTOM_DIR}/shims:$PATH"' >> ~/.zshrc` + ); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious npm packages when shims are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("npm i safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index e8ba7c8..d86af3c 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -119,4 +119,39 @@ describe("E2E: npm coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious npm packages when scripts are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("npm i safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index 49db6ce..e1a7aed 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -204,4 +204,44 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); }); } + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand("pip3 cache purge"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup-ci"); + await setupShell.runCommand( + `echo 'export SAFE_CHAIN_DIR=${CUSTOM_DIR}' >> ~/.zshrc` + ); + await setupShell.runCommand( + `echo 'export PATH="${CUSTOM_DIR}/shims:$PATH"' >> ~/.zshrc` + ); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("intercepts pip3 install when shims are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand( + "pip3 install --break-system-packages certifi --safe-chain-logging=verbose" + ); + + assert.ok( + result.output.includes("no malware found."), + `Expected pip3 to be protected with SAFE_CHAIN_DIR. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index b06978f..684ee4f 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -844,4 +844,40 @@ describe("E2E: pip coverage", () => { `python -m pip SHOULD go through safe-chain. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + await setupShell.runCommand("pip3 cache purge"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("intercepts pip3 install when scripts are in a custom directory", async () => { + // New shell sources ~/.zshrc → sources init-posix.sh from custom dir + // → defines pip3() shell function that routes through safe-chain + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand( + "pip3 install --break-system-packages requests --safe-chain-logging=verbose" + ); + + assert.ok( + result.output.includes("no malware found."), + `Expected pip3 to be protected with SAFE_CHAIN_DIR. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/pipx.e2e.spec.js b/test/e2e/pipx.e2e.spec.js index a554aa6..489d8c6 100644 --- a/test/e2e/pipx.e2e.spec.js +++ b/test/e2e/pipx.e2e.spec.js @@ -197,4 +197,39 @@ describe("E2E: pipx coverage", () => { `Expected exit message. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious pipx packages when scripts are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("pipx install safe-chain-pi-test"); + + assert.ok( + result.output.includes("blocked by safe-chain"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/pnpm-ci.e2e.spec.js b/test/e2e/pnpm-ci.e2e.spec.js index a56bb77..391001e 100644 --- a/test/e2e/pnpm-ci.e2e.spec.js +++ b/test/e2e/pnpm-ci.e2e.spec.js @@ -122,4 +122,45 @@ describe("E2E: pnpm coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup-ci"); + await setupShell.runCommand( + `echo 'export SAFE_CHAIN_DIR=${CUSTOM_DIR}' >> ~/.zshrc` + ); + await setupShell.runCommand( + `echo 'export PATH="${CUSTOM_DIR}/shims:$PATH"' >> ~/.zshrc` + ); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious pnpm packages when shims are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("pnpm add safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/pnpm.e2e.spec.js b/test/e2e/pnpm.e2e.spec.js index a15250a..90ef57c 100644 --- a/test/e2e/pnpm.e2e.spec.js +++ b/test/e2e/pnpm.e2e.spec.js @@ -139,4 +139,39 @@ describe("E2E: pnpm coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious pnpm packages when scripts are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("pnpm add safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/poetry.e2e.spec.js b/test/e2e/poetry.e2e.spec.js index 58b74fd..072d1b6 100644 --- a/test/e2e/poetry.e2e.spec.js +++ b/test/e2e/poetry.e2e.spec.js @@ -422,4 +422,46 @@ describe("E2E: poetry coverage", () => { `Expected env list output. Output was:\n${envListResult.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + await setupShell.runCommand("command poetry cache clear pypi --all -n"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious poetry packages when scripts are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + await shell.runCommand("mkdir /tmp/test-poetry-custom-dir"); + await shell.runCommand( + "cd /tmp/test-poetry-custom-dir && poetry init --no-interaction" + ); + const result = await shell.runCommand( + "cd /tmp/test-poetry-custom-dir && poetry add safe-chain-pi-test" + ); + + assert.ok( + result.output.includes("blocked by safe-chain"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/safe-chain-dir.e2e.spec.js b/test/e2e/safe-chain-dir.e2e.spec.js index e28bd72..e738949 100644 --- a/test/e2e/safe-chain-dir.e2e.spec.js +++ b/test/e2e/safe-chain-dir.e2e.spec.js @@ -64,6 +64,57 @@ describe("E2E: SAFE_CHAIN_DIR support", () => { ); }); + it("setup writes the custom path to ~/.bashrc when SAFE_CHAIN_DIR is set", async () => { + const shell = await container.openShell("bash"); + await shell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await shell.runCommand("safe-chain setup"); + + const result = await shell.runCommand("cat ~/.bashrc"); + + assert.ok( + result.output.includes(`source ${CUSTOM_DIR}/scripts/init-posix.sh`), + `Expected ~/.bashrc to contain custom scripts path. Output:\n${result.output}` + ); + assert.ok( + !result.output.includes("source ~/.safe-chain/scripts/init-posix.sh"), + `Expected ~/.bashrc to NOT contain default path. Output:\n${result.output}` + ); + }); + + it("setup with SAFE_CHAIN_DIR still protects npm in a new shell session", async () => { + // Run setup with the custom dir + const setupShell = await container.openShell("bash"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + + // Open a fresh shell — it will source ~/.bashrc which sources init-posix.sh + // from the custom dir, defining the npm wrapper function + const projectShell = await container.openShell("bash"); + await projectShell.runCommand("cd /testapp"); + const result = await projectShell.runCommand( + "npm i axios@1.13.0 --safe-chain-logging=verbose" + ); + + // "Safe-chain: Package" appears before npm downloads — confirms interception happened + assert.ok( + result.output.includes("Safe-chain: Package"), + `Expected npm to be protected after setup with SAFE_CHAIN_DIR. Output:\n${result.output}` + ); + }); + + it("teardown removes the custom SAFE_CHAIN_DIR source line from ~/.bashrc", async () => { + const shell = await container.openShell("bash"); + await shell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await shell.runCommand("safe-chain setup"); + await shell.runCommand("safe-chain teardown"); + + const result = await shell.runCommand("cat ~/.bashrc"); + assert.ok( + !result.output.includes(`source ${CUSTOM_DIR}/scripts/init-posix.sh`), + `Expected custom source line to be removed from ~/.bashrc. Output:\n${result.output}` + ); + }); + it("safe-chain protects a non-root user when installed to a shared dir with SAFE_CHAIN_DIR", async () => { // Step 1: create a non-root user inside the container container.dockerExec("useradd -m safeuser"); diff --git a/test/e2e/uv.e2e.spec.js b/test/e2e/uv.e2e.spec.js index 9d5f3b9..ad24f6e 100644 --- a/test/e2e/uv.e2e.spec.js +++ b/test/e2e/uv.e2e.spec.js @@ -569,4 +569,43 @@ describe("E2E: uv coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + await setupShell.runCommand("uv cache clean"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious uv packages when scripts are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + await shell.runCommand("uv init test-project-custom-dir"); + const result = await shell.runCommand( + "cd test-project-custom-dir && uv add safe-chain-pi-test" + ); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/yarn-ci.e2e.spec.js b/test/e2e/yarn-ci.e2e.spec.js index 47e2120..35047c1 100644 --- a/test/e2e/yarn-ci.e2e.spec.js +++ b/test/e2e/yarn-ci.e2e.spec.js @@ -84,4 +84,45 @@ describe("E2E: yarn coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup-ci"); + await setupShell.runCommand( + `echo 'export SAFE_CHAIN_DIR=${CUSTOM_DIR}' >> ~/.zshrc` + ); + await setupShell.runCommand( + `echo 'export PATH="${CUSTOM_DIR}/shims:$PATH"' >> ~/.zshrc` + ); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious yarn packages when shims are in a custom directory", async () => { + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("yarn add safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); }); diff --git a/test/e2e/yarn.e2e.spec.js b/test/e2e/yarn.e2e.spec.js index 5e56d12..5b677d6 100644 --- a/test/e2e/yarn.e2e.spec.js +++ b/test/e2e/yarn.e2e.spec.js @@ -125,4 +125,43 @@ describe("E2E: yarn coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + describe("with SAFE_CHAIN_DIR (custom install directory)", () => { + const CUSTOM_DIR = "/usr/local/.safe-chain"; + let customContainer; + + beforeEach(async () => { + customContainer = new DockerTestContainer(); + await customContainer.start(); + + // Run setup with the custom dir — init-posix.sh is copied to the custom + // scripts dir, and ~/.zshrc gets a source line pointing there + const setupShell = await customContainer.openShell("zsh"); + await setupShell.runCommand(`export SAFE_CHAIN_DIR=${CUSTOM_DIR}`); + await setupShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + if (customContainer) { + await customContainer.stop(); + customContainer = null; + } + }); + + it("blocks malicious yarn packages when scripts are in a custom directory", async () => { + // New shell sources ~/.zshrc → sources init-posix.sh from custom dir + // → defines yarn() shell function that routes through safe-chain + const shell = await customContainer.openShell("zsh"); + const result = await shell.runCommand("yarn add safe-chain-test"); + + assert.ok( + result.output.includes("Malicious changes detected:"), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Expected malicious package to be blocked. Output:\n${result.output}` + ); + }); + }); });