Some cleanup

This commit is contained in:
Reinier Criel 2026-04-10 14:08:59 -07:00
parent 24af6f21eb
commit b0f392522b
19 changed files with 286 additions and 8 deletions

View file

@ -316,6 +316,19 @@ The base URL should point to a server that mirrors the structure of `https://mal
- `/releases/npm.json` (JavaScript new packages list) - `/releases/npm.json` (JavaScript new packages list)
- `/releases/pypi.json` (Python new packages list) - `/releases/pypi.json` (Python new packages list)
## Custom Install Directory
By default, Safe Chain installs itself into `~/.safe-chain`. You can change this by setting `SAFE_CHAIN_DIR` before running the installer. This is useful for system-wide installations (e.g. inside a Docker image) or when you need to avoid conflicts with other tools.
When set, all Safe Chain data (binary, shims, scripts) is placed under the custom directory instead of `~/.safe-chain`.
```shell
export SAFE_CHAIN_DIR=/usr/local/.safe-chain
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh
```
> **Note:** CLI argument and config file options are not supported for `SAFE_CHAIN_DIR`. The config file lives inside the Safe Chain directory itself, creating a chicken-and-egg problem, and passing a directory path as a flag to package manager commands (e.g. `npm install express --safe-chain-dir=...`) does not make sense.
# Usage in CI/CD # Usage in CI/CD
You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
@ -406,6 +419,7 @@ pipeline {
environment { environment {
// Jenkins does not automatically persist PATH updates from setup-ci, // Jenkins does not automatically persist PATH updates from setup-ci,
// so add the shims + binary directory explicitly for all stages. // so add the shims + binary directory explicitly for all stages.
// If you set SAFE_CHAIN_DIR, replace ~/.safe-chain with that path here.
PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}" PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}"
} }
@ -461,7 +475,7 @@ To add safe-chain in GitLab pipelines, you need to install it in the image runni
# Install safe-chain # Install safe-chain
RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
# Add safe-chain to PATH # Add safe-chain to PATH (update paths if you set SAFE_CHAIN_DIR during install)
ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}" ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}"
``` ```

View file

@ -8,7 +8,8 @@ param(
) )
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set $Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
$InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $SafeChainBase = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $env:USERPROFILE ".safe-chain" }
$InstallDir = Join-Path $SafeChainBase "bin"
$RepoUrl = "https://github.com/AikidoSec/safe-chain" $RepoUrl = "https://github.com/AikidoSec/safe-chain"
# Ensure TLS 1.2 is enabled for downloads # Ensure TLS 1.2 is enabled for downloads

View file

@ -8,7 +8,8 @@ set -e # Exit on error
# Configuration # Configuration
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
INSTALL_DIR="${HOME}/.safe-chain/bin" SAFE_CHAIN_BASE="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}"
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
REPO_URL="https://github.com/AikidoSec/safe-chain" REPO_URL="https://github.com/AikidoSec/safe-chain"
# Colors for output # Colors for output

View file

@ -4,7 +4,7 @@
# Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform) # Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform)
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
$DotSafeChain = Join-Path $HomeDir ".safe-chain" $DotSafeChain = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $HomeDir ".safe-chain" }
$InstallDir = Join-Path $DotSafeChain "bin" $InstallDir = Join-Path $DotSafeChain "bin"
# Helper functions # Helper functions

View file

@ -7,7 +7,7 @@
set -e # Exit on error set -e # Exit on error
# Configuration # Configuration
DOT_SAFE_CHAIN="${HOME}/.safe-chain" DOT_SAFE_CHAIN="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
@ -163,6 +163,7 @@ main() {
else else
info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove." info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove."
fi fi
} }
main "$@" main "$@"

View file

@ -3,6 +3,7 @@ import path from "path";
import os from "os"; import os from "os";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import { getEcoSystem } from "./settings.js"; import { getEcoSystem } from "./settings.js";
import { getSafeChainDir } from "./environmentVariables.js";
/** /**
* @typedef {Object} SafeChainConfig * @typedef {Object} SafeChainConfig
@ -304,8 +305,7 @@ function getConfigFilePath() {
* @returns {string} * @returns {string}
*/ */
export function getSafeChainDirectory() { export function getSafeChainDirectory() {
const homeDir = os.homedir(); const safeChainDir = getSafeChainDir() ?? path.join(os.homedir(), ".safe-chain");
const safeChainDir = path.join(homeDir, ".safe-chain");
if (!fs.existsSync(safeChainDir)) { if (!fs.existsSync(safeChainDir)) {
fs.mkdirSync(safeChainDir, { recursive: true }); fs.mkdirSync(safeChainDir, { recursive: true });

View file

@ -4,6 +4,7 @@ import fs from "fs";
import path from "path"; import path from "path";
import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
import { getSafeChainDir } from "../config/environmentVariables.js"; import { getSafeChainDir } from "../config/environmentVariables.js";
export { getSafeChainDir };
import { safeSpawn } from "../utils/safeSpawn.js"; import { safeSpawn } from "../utils/safeSpawn.js";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";

View file

@ -122,7 +122,6 @@ function copyStartupFiles() {
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
} }
// Use absolute path for source
const sourcePath = path.join(dirname, "startup-scripts", file); const sourcePath = path.join(dirname, "startup-scripts", file);
fs.copyFileSync(sourcePath, targetPath); fs.copyFileSync(sourcePath, targetPath);
} }

View file

@ -3,6 +3,7 @@ import {
doesExecutableExistOnSystem, doesExecutableExistOnSystem,
removeLinesMatchingPattern, removeLinesMatchingPattern,
getScriptsDir, getScriptsDir,
getSafeChainDir,
} from "../helpers.js"; } from "../helpers.js";
import { execSync, spawnSync } from "child_process"; import { execSync, spawnSync } from "child_process";
import * as os from "os"; import * as os from "os";
@ -41,12 +42,27 @@ function teardown(tools) {
eol eol
); );
removeLinesMatchingPattern(
startupFile,
/^export\s+SAFE_CHAIN_DIR=.*#\s*Safe-chain/,
eol
);
return true; return true;
} }
function setup() { function setup() {
const startupFile = getStartupFile(); const startupFile = getStartupFile();
const customDir = getSafeChainDir();
if (customDir) {
addLineToFile(
startupFile,
`export SAFE_CHAIN_DIR="${customDir}" # Safe-chain installation directory`,
eol
);
}
addLineToFile( addLineToFile(
startupFile, startupFile,
`source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain bash initialization script`, `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain bash initialization script`,

View file

@ -10,6 +10,7 @@ describe("Bash shell integration", () => {
let bash; let bash;
let windowsCygwinPath = ""; let windowsCygwinPath = "";
let platform = "linux"; let platform = "linux";
let getSafeChainDirResult = undefined;
beforeEach(async () => { beforeEach(async () => {
// Create temporary startup file for testing // Create temporary startup file for testing
@ -20,6 +21,7 @@ describe("Bash shell integration", () => {
namedExports: { namedExports: {
doesExecutableExistOnSystem: () => true, doesExecutableExistOnSystem: () => true,
getScriptsDir: () => "/test-home/.safe-chain/scripts", getScriptsDir: () => "/test-home/.safe-chain/scripts",
getSafeChainDir: () => getSafeChainDirResult,
addLineToFile: (filePath, line) => { addLineToFile: (filePath, line) => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, "", "utf-8"); fs.writeFileSync(filePath, "", "utf-8");
@ -89,6 +91,7 @@ describe("Bash shell integration", () => {
// Reset mocks // Reset mocks
mock.reset(); mock.reset();
platform = "linux"; platform = "linux";
getSafeChainDirResult = undefined;
}); });
describe("isInstalled", () => { describe("isInstalled", () => {
@ -200,6 +203,40 @@ describe("Bash shell integration", () => {
}); });
}); });
describe("SAFE_CHAIN_DIR", () => {
it("should write export line to rc file when custom dir is set", () => {
getSafeChainDirResult = "/custom/safe-chain";
bash.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(
content.includes('export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory')
);
});
it("should not write export line when no custom dir is set", () => {
getSafeChainDirResult = undefined;
bash.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
it("should remove export line on teardown", () => {
const initialContent = [
'#!/bin/bash',
'export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory',
'source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script',
].join("\n");
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
bash.teardown(knownAikidoTools);
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
});
describe("integration tests", () => { describe("integration tests", () => {
it("should handle complete setup and teardown cycle", () => { it("should handle complete setup and teardown cycle", () => {
const tools = [ const tools = [

View file

@ -3,6 +3,7 @@ import {
doesExecutableExistOnSystem, doesExecutableExistOnSystem,
removeLinesMatchingPattern, removeLinesMatchingPattern,
getScriptsDir, getScriptsDir,
getSafeChainDir,
} from "../helpers.js"; } from "../helpers.js";
import { execSync } from "child_process"; import { execSync } from "child_process";
import path from "path"; import path from "path";
@ -40,12 +41,27 @@ function teardown(tools) {
eol eol
); );
removeLinesMatchingPattern(
startupFile,
/^set\s+-gx\s+SAFE_CHAIN_DIR\s+.*#\s*Safe-chain/,
eol
);
return true; return true;
} }
function setup() { function setup() {
const startupFile = getStartupFile(); const startupFile = getStartupFile();
const customDir = getSafeChainDir();
if (customDir) {
addLineToFile(
startupFile,
`set -gx SAFE_CHAIN_DIR "${customDir}" # Safe-chain installation directory`,
eol
);
}
addLineToFile( addLineToFile(
startupFile, startupFile,
`source ${path.join(getScriptsDir(), "init-fish.fish")} # Safe-chain Fish initialization script`, `source ${path.join(getScriptsDir(), "init-fish.fish")} # Safe-chain Fish initialization script`,

View file

@ -8,6 +8,7 @@ import { knownAikidoTools } from "../helpers.js";
describe("Fish shell integration", () => { describe("Fish shell integration", () => {
let mockStartupFile; let mockStartupFile;
let fish; let fish;
let getSafeChainDirResult = undefined;
beforeEach(async () => { beforeEach(async () => {
// Create temporary startup file for testing // Create temporary startup file for testing
@ -18,6 +19,7 @@ describe("Fish shell integration", () => {
namedExports: { namedExports: {
doesExecutableExistOnSystem: () => true, doesExecutableExistOnSystem: () => true,
getScriptsDir: () => "/test-home/.safe-chain/scripts", getScriptsDir: () => "/test-home/.safe-chain/scripts",
getSafeChainDir: () => getSafeChainDirResult,
addLineToFile: (filePath, line) => { addLineToFile: (filePath, line) => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, "", "utf-8"); fs.writeFileSync(filePath, "", "utf-8");
@ -53,6 +55,7 @@ describe("Fish shell integration", () => {
// Reset mocks // Reset mocks
mock.reset(); mock.reset();
getSafeChainDirResult = undefined;
}); });
describe("isInstalled", () => { describe("isInstalled", () => {
@ -153,6 +156,39 @@ describe("Fish shell integration", () => {
}); });
}); });
describe("SAFE_CHAIN_DIR", () => {
it("should write set line to config file when custom dir is set", () => {
getSafeChainDirResult = "/custom/safe-chain";
fish.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(
content.includes('set -gx SAFE_CHAIN_DIR "/custom/safe-chain" # Safe-chain installation directory')
);
});
it("should not write set line when no custom dir is set", () => {
getSafeChainDirResult = undefined;
fish.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
it("should remove set line on teardown", () => {
const initialContent = [
'set -gx SAFE_CHAIN_DIR "/custom/safe-chain" # Safe-chain installation directory',
"source /test-home/.safe-chain/scripts/init-fish.fish # Safe-chain Fish initialization script",
].join("\n");
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
fish.teardown(knownAikidoTools);
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
});
describe("integration tests", () => { describe("integration tests", () => {
it("should handle complete setup and teardown cycle", () => { it("should handle complete setup and teardown cycle", () => {
const tools = [ const tools = [

View file

@ -4,6 +4,7 @@ import {
removeLinesMatchingPattern, removeLinesMatchingPattern,
validatePowerShellExecutionPolicy, validatePowerShellExecutionPolicy,
getScriptsDir, getScriptsDir,
getSafeChainDir,
} from "../helpers.js"; } from "../helpers.js";
import { execSync } from "child_process"; import { execSync } from "child_process";
import path from "path"; import path from "path";
@ -38,6 +39,11 @@ function teardown(tools) {
/^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/, /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/,
); );
removeLinesMatchingPattern(
startupFile,
/^\$env:SAFE_CHAIN_DIR\s*=.*#\s*Safe-chain/,
);
return true; return true;
} }
@ -52,6 +58,14 @@ async function setup() {
const startupFile = getStartupFile(); const startupFile = getStartupFile();
const customDir = getSafeChainDir();
if (customDir) {
addLineToFile(
startupFile,
`$env:SAFE_CHAIN_DIR = '${customDir}' # Safe-chain installation directory`,
);
}
addLineToFile( addLineToFile(
startupFile, startupFile,
`. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`, `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`,

View file

@ -9,6 +9,7 @@ describe("PowerShell Core shell integration", () => {
let mockStartupFile; let mockStartupFile;
let powershell; let powershell;
let executionPolicyResult; let executionPolicyResult;
let getSafeChainDirResult = undefined;
beforeEach(async () => { beforeEach(async () => {
// Create temporary startup file for testing // Create temporary startup file for testing
@ -26,6 +27,7 @@ describe("PowerShell Core shell integration", () => {
mock.module("../helpers.js", { mock.module("../helpers.js", {
namedExports: { namedExports: {
doesExecutableExistOnSystem: () => true, doesExecutableExistOnSystem: () => true,
getSafeChainDir: () => getSafeChainDirResult,
addLineToFile: (filePath, line) => { addLineToFile: (filePath, line) => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, "", "utf-8"); fs.writeFileSync(filePath, "", "utf-8");
@ -63,6 +65,7 @@ describe("PowerShell Core shell integration", () => {
// Reset mocks // Reset mocks
mock.reset(); mock.reset();
getSafeChainDirResult = undefined;
}); });
describe("isInstalled", () => { describe("isInstalled", () => {
@ -206,6 +209,40 @@ describe("PowerShell Core shell integration", () => {
}); });
}); });
describe("SAFE_CHAIN_DIR", () => {
it("should write $env:SAFE_CHAIN_DIR line to profile when custom dir is set", async () => {
getSafeChainDirResult = "C:\\custom\\safe-chain";
await powershell.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(
content.includes("$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory")
);
});
it("should not write $env:SAFE_CHAIN_DIR line when no custom dir is set", async () => {
getSafeChainDirResult = undefined;
await powershell.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
it("should remove $env:SAFE_CHAIN_DIR line on teardown", () => {
const initialContent = [
"# PowerShell profile",
"$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory",
'. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script',
].join("\n");
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
powershell.teardown(knownAikidoTools);
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
});
describe("execution policy", () => { describe("execution policy", () => {
it(`should throw for restricted policies`, async () => { it(`should throw for restricted policies`, async () => {
executionPolicyResult = { executionPolicyResult = {

View file

@ -4,6 +4,7 @@ import {
removeLinesMatchingPattern, removeLinesMatchingPattern,
validatePowerShellExecutionPolicy, validatePowerShellExecutionPolicy,
getScriptsDir, getScriptsDir,
getSafeChainDir,
} from "../helpers.js"; } from "../helpers.js";
import { execSync } from "child_process"; import { execSync } from "child_process";
import path from "path"; import path from "path";
@ -38,6 +39,11 @@ function teardown(tools) {
/^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/, /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/,
); );
removeLinesMatchingPattern(
startupFile,
/^\$env:SAFE_CHAIN_DIR\s*=.*#\s*Safe-chain/,
);
return true; return true;
} }
@ -52,6 +58,14 @@ async function setup() {
const startupFile = getStartupFile(); const startupFile = getStartupFile();
const customDir = getSafeChainDir();
if (customDir) {
addLineToFile(
startupFile,
`$env:SAFE_CHAIN_DIR = '${customDir}' # Safe-chain installation directory`,
);
}
addLineToFile( addLineToFile(
startupFile, startupFile,
`. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`, `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`,

View file

@ -9,6 +9,7 @@ describe("Windows PowerShell shell integration", () => {
let mockStartupFile; let mockStartupFile;
let windowsPowershell; let windowsPowershell;
let executionPolicyResult; let executionPolicyResult;
let getSafeChainDirResult = undefined;
beforeEach(async () => { beforeEach(async () => {
// Create temporary startup file for testing // Create temporary startup file for testing
@ -26,6 +27,7 @@ describe("Windows PowerShell shell integration", () => {
mock.module("../helpers.js", { mock.module("../helpers.js", {
namedExports: { namedExports: {
doesExecutableExistOnSystem: () => true, doesExecutableExistOnSystem: () => true,
getSafeChainDir: () => getSafeChainDirResult,
addLineToFile: (filePath, line) => { addLineToFile: (filePath, line) => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, "", "utf-8"); fs.writeFileSync(filePath, "", "utf-8");
@ -63,6 +65,7 @@ describe("Windows PowerShell shell integration", () => {
// Reset mocks // Reset mocks
mock.reset(); mock.reset();
getSafeChainDirResult = undefined;
}); });
describe("isInstalled", () => { describe("isInstalled", () => {
@ -206,6 +209,40 @@ describe("Windows PowerShell shell integration", () => {
}); });
}); });
describe("SAFE_CHAIN_DIR", () => {
it("should write $env:SAFE_CHAIN_DIR line to profile when custom dir is set", async () => {
getSafeChainDirResult = "C:\\custom\\safe-chain";
await windowsPowershell.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(
content.includes("$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory")
);
});
it("should not write $env:SAFE_CHAIN_DIR line when no custom dir is set", async () => {
getSafeChainDirResult = undefined;
await windowsPowershell.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
it("should remove $env:SAFE_CHAIN_DIR line on teardown", () => {
const initialContent = [
"# Windows PowerShell profile",
"$env:SAFE_CHAIN_DIR = 'C:\\custom\\safe-chain' # Safe-chain installation directory",
'. "/test-home/.safe-chain/scripts/init-pwsh.ps1" # Safe-chain PowerShell initialization script',
].join("\n");
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
windowsPowershell.teardown(knownAikidoTools);
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
});
describe("execution policy", () => { describe("execution policy", () => {
it(`should throw for restricted policies`, async () => { it(`should throw for restricted policies`, async () => {
executionPolicyResult = { executionPolicyResult = {

View file

@ -3,6 +3,7 @@ import {
doesExecutableExistOnSystem, doesExecutableExistOnSystem,
removeLinesMatchingPattern, removeLinesMatchingPattern,
getScriptsDir, getScriptsDir,
getSafeChainDir,
} from "../helpers.js"; } from "../helpers.js";
import { execSync } from "child_process"; import { execSync } from "child_process";
import path from "path"; import path from "path";
@ -40,12 +41,27 @@ function teardown(tools) {
eol eol
); );
removeLinesMatchingPattern(
startupFile,
/^export\s+SAFE_CHAIN_DIR=.*#\s*Safe-chain/,
eol
);
return true; return true;
} }
function setup() { function setup() {
const startupFile = getStartupFile(); const startupFile = getStartupFile();
const customDir = getSafeChainDir();
if (customDir) {
addLineToFile(
startupFile,
`export SAFE_CHAIN_DIR="${customDir}" # Safe-chain installation directory`,
eol
);
}
addLineToFile( addLineToFile(
startupFile, startupFile,
`source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain Zsh initialization script`, `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain Zsh initialization script`,

View file

@ -8,6 +8,7 @@ import { knownAikidoTools } from "../helpers.js";
describe("Zsh shell integration", () => { describe("Zsh shell integration", () => {
let mockStartupFile; let mockStartupFile;
let zsh; let zsh;
let getSafeChainDirResult = undefined;
beforeEach(async () => { beforeEach(async () => {
// Create temporary startup file for testing // Create temporary startup file for testing
@ -18,6 +19,7 @@ describe("Zsh shell integration", () => {
namedExports: { namedExports: {
doesExecutableExistOnSystem: () => true, doesExecutableExistOnSystem: () => true,
getScriptsDir: () => "/test-home/.safe-chain/scripts", getScriptsDir: () => "/test-home/.safe-chain/scripts",
getSafeChainDir: () => getSafeChainDirResult,
addLineToFile: (filePath, line) => { addLineToFile: (filePath, line) => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, "", "utf-8"); fs.writeFileSync(filePath, "", "utf-8");
@ -53,6 +55,7 @@ describe("Zsh shell integration", () => {
// Reset mocks // Reset mocks
mock.reset(); mock.reset();
getSafeChainDirResult = undefined;
}); });
describe("isInstalled", () => { describe("isInstalled", () => {
@ -171,6 +174,40 @@ describe("Zsh shell integration", () => {
}); });
}); });
describe("SAFE_CHAIN_DIR", () => {
it("should write export line to rc file when custom dir is set", () => {
getSafeChainDirResult = "/custom/safe-chain";
zsh.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(
content.includes('export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory')
);
});
it("should not write export line when no custom dir is set", () => {
getSafeChainDirResult = undefined;
zsh.setup();
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
it("should remove export line on teardown", () => {
const initialContent = [
"#!/bin/zsh",
'export SAFE_CHAIN_DIR="/custom/safe-chain" # Safe-chain installation directory',
"source /test-home/.safe-chain/scripts/init-posix.sh # Safe-chain Zsh initialization script",
].join("\n");
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
zsh.teardown(knownAikidoTools);
const content = fs.readFileSync(mockStartupFile, "utf-8");
assert.ok(!content.includes("SAFE_CHAIN_DIR"));
});
});
describe("integration tests", () => { describe("integration tests", () => {
it("should handle complete setup and teardown cycle", () => { it("should handle complete setup and teardown cycle", () => {
const tools = [ const tools = [

View file

@ -109,4 +109,5 @@ export async function teardownDirectories() {
); );
} }
} }
} }