From 9494b5aae8d822add1d38a97f8fdc6c132e1c8ee Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 19 Mar 2026 09:13:45 +0100 Subject: [PATCH 1/3] Remove the .aikido directory when uninstalling --- install-scripts/uninstall-safe-chain.ps1 | 40 +++++++++++++----------- install-scripts/uninstall-safe-chain.sh | 21 +++++++++---- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/install-scripts/uninstall-safe-chain.ps1 b/install-scripts/uninstall-safe-chain.ps1 index f1e1ff7..5fdae1c 100644 --- a/install-scripts/uninstall-safe-chain.ps1 +++ b/install-scripts/uninstall-safe-chain.ps1 @@ -4,7 +4,9 @@ # Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform) $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } -$InstallDir = Join-Path $HomeDir ".safe-chain/bin" +$DotSafeChain = Join-Path $HomeDir ".safe-chain" +$DotAikido = Join-Path $HomeDir ".aikido" +$InstallDir = Join-Path $DotSafeChain "bin" # Helper functions function Write-Info { @@ -123,34 +125,34 @@ function Uninstall-SafeChain { Remove-NpmInstallation Remove-VoltaInstallation - # Remove installation directory - if (Test-Path $InstallDir) { - Write-Info "Removing installation directory: $InstallDir" + # Remove .safe-chain directory + if (Test-Path $DotSafeChain) { + Write-Info "Removing installation directory: $DotSafeChain" try { - Remove-Item -Path $InstallDir -Recurse -Force + Remove-Item -Path $DotSafeChain -Recurse -Force Write-Info "Successfully removed installation directory" } catch { - Write-Error-Custom "Failed to remove $InstallDir : $_" + Write-Error-Custom "Failed to remove $DotSafeChain : $_" } } else { - Write-Info "Installation directory $InstallDir does not exist. Nothing to remove." + Write-Info "Installation directory $DotSafeChain does not exist. Nothing to remove." } - # Also try to remove the parent .safe-chain directory if it's empty - $parentDir = Split-Path $InstallDir -Parent - if (Test-Path $parentDir) { - $items = Get-ChildItem -Path $parentDir -Force - if ($items.Count -eq 0) { - Write-Info "Removing empty parent directory: $parentDir" - try { - Remove-Item -Path $parentDir -Force - } - catch { - Write-Warn "Could not remove empty parent directory: $_" - } + # Remove .aikido directory + if (Test-Path $DotAikido) { + Write-Info "Removing installation directory: $DotAikido" + try { + Remove-Item -Path $DotAikido -Recurse -Force + Write-Info "Successfully removed installation directory" } + catch { + Write-Error-Custom "Failed to remove $DotAikido : $_" + } + } + else { + Write-Info "Installation directory $DotAikido does not exist. Nothing to remove." } Write-Info "safe-chain has been uninstalled successfully!" diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index e208319..0d04128 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -7,7 +7,8 @@ set -e # Exit on error # Configuration -INSTALL_DIR="${HOME}/.safe-chain/bin" +DOT_SAFE_CHAIN="${HOME}/.safe-chain" +DOT_AIKIDO="${HOME}/.aikido" # Colors for output RED='\033[0;31m' @@ -139,7 +140,7 @@ remove_nvm_installation() { # Main uninstallation main() { - SAFE_CHAIN_LOCATION="$INSTALL_DIR/safe-chain" + SAFE_CHAIN_LOCATION="$DOT_SAFE_CHAIN/bin/safe-chain" if [ -x "$SAFE_CHAIN_LOCATION" ]; then info "Running safe-chain teardown..." @@ -157,11 +158,19 @@ main() { remove_nvm_installation # Remove install dir recursively if it exists - if [ -d "$INSTALL_DIR" ]; then - info "Removing installation directory $INSTALL_DIR" - rm -rf "$INSTALL_DIR" || error "Failed to remove $INSTALL_DIR" + if [ -d "$DOT_SAFE_CHAIN" ]; then + info "Removing installation directory $DOT_SAFE_CHAIN" + rm -rf "$DOT_SAFE_CHAIN" || error "Failed to remove $DOT_SAFE_CHAIN" else - info "Installation directory $INSTALL_DIR does not exist. Nothing to remove." + info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove." + fi + + # Remove install dir recursively if it exists + if [ -d "$DOT_AIKIDO" ]; then + info "Removing installation directory $DOT_AIKIDO" + rm -rf "$DOT_AIKIDO" || error "Failed to remove $DOT_AIKIDO" + else + info "Installation directory $DOT_AIKIDO does not exist. Nothing to remove." fi } From ffbdedc7cdbf39ef42e362754112b661fdb68422 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 19 Mar 2026 15:51:20 +0100 Subject: [PATCH 2/3] Don't delete .aikido folder --- install-scripts/uninstall-safe-chain.ps1 | 16 ---------------- install-scripts/uninstall-safe-chain.sh | 9 --------- 2 files changed, 25 deletions(-) diff --git a/install-scripts/uninstall-safe-chain.ps1 b/install-scripts/uninstall-safe-chain.ps1 index 5fdae1c..3292cdd 100644 --- a/install-scripts/uninstall-safe-chain.ps1 +++ b/install-scripts/uninstall-safe-chain.ps1 @@ -5,7 +5,6 @@ # Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform) $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } $DotSafeChain = Join-Path $HomeDir ".safe-chain" -$DotAikido = Join-Path $HomeDir ".aikido" $InstallDir = Join-Path $DotSafeChain "bin" # Helper functions @@ -140,21 +139,6 @@ function Uninstall-SafeChain { Write-Info "Installation directory $DotSafeChain does not exist. Nothing to remove." } - # Remove .aikido directory - if (Test-Path $DotAikido) { - Write-Info "Removing installation directory: $DotAikido" - try { - Remove-Item -Path $DotAikido -Recurse -Force - Write-Info "Successfully removed installation directory" - } - catch { - Write-Error-Custom "Failed to remove $DotAikido : $_" - } - } - else { - Write-Info "Installation directory $DotAikido does not exist. Nothing to remove." - } - Write-Info "safe-chain has been uninstalled successfully!" } diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index 0d04128..dff6f31 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -8,7 +8,6 @@ set -e # Exit on error # Configuration DOT_SAFE_CHAIN="${HOME}/.safe-chain" -DOT_AIKIDO="${HOME}/.aikido" # Colors for output RED='\033[0;31m' @@ -164,14 +163,6 @@ main() { else info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove." fi - - # Remove install dir recursively if it exists - if [ -d "$DOT_AIKIDO" ]; then - info "Removing installation directory $DOT_AIKIDO" - rm -rf "$DOT_AIKIDO" || error "Failed to remove $DOT_AIKIDO" - else - info "Installation directory $DOT_AIKIDO does not exist. Nothing to remove." - fi } main "$@" From cfaa8e45ad4a0bb502da23f54293a1a825fafdf5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 19 Mar 2026 16:10:32 +0100 Subject: [PATCH 3/3] Move config file to .safe-chain path. --- README.md | 4 +- packages/safe-chain/src/config/configFile.js | 25 +++- .../safe-chain/src/config/configFile.spec.js | 130 ++++++++++++------ .../supported-shells/bash.js | 2 +- .../shell-integration/supported-shells/zsh.js | 2 +- 5 files changed, 115 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index d5270e5..4daf1d2 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ You can set the minimum package age through multiple sources (in order of priori npm install express ``` -3. **Config File** (`~/.aikido/config.json`): +3. **Config File** (`~/.safe-chain/config.json`): ```json { @@ -246,7 +246,7 @@ You can set custom registries through environment variable or config file. Both export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net" ``` -2. **Config File** (`~/.aikido/config.json`): +2. **Config File** (`~/.safe-chain/config.json`): ```json { diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index fd6ac26..bc4dc94 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -252,7 +252,30 @@ function getDatabaseVersionPath() { * @returns {string} */ function getConfigFilePath() { - return path.join(getAikidoDirectory(), "config.json"); + const primaryPath = path.join(getSafeChainDirectory(), "config.json"); + if (fs.existsSync(primaryPath)) { + return primaryPath; + } + + const legacyPath = path.join(getAikidoDirectory(), "config.json"); + if (fs.existsSync(legacyPath)) { + return legacyPath; + } + + return primaryPath; +} + +/** + * @returns {string} + */ +function getSafeChainDirectory() { + const homeDir = os.homedir(); + const safeChainDir = path.join(homeDir, ".safe-chain"); + + if (!fs.existsSync(safeChainDir)) { + fs.mkdirSync(safeChainDir, { recursive: true }); + } + return safeChainDir; } /** diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index eff4048..8b36ff2 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -1,16 +1,35 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; +import os from "os"; +import path from "path"; -let configFileContent = undefined; +const safeChainConfigPath = path.join(os.homedir(), ".safe-chain", "config.json"); +const aikidoConfigPath = path.join(os.homedir(), ".aikido", "config.json"); + +/** @type {Map} */ +let mockFiles = new Map(); mock.module("fs", { namedExports: { - existsSync: () => configFileContent !== undefined, - readFileSync: () => configFileContent, - writeFileSync: (content) => (configFileContent = content), + existsSync: (filePath) => mockFiles.has(filePath), + readFileSync: (filePath) => { + if (!mockFiles.has(filePath)) { + throw new Error(`ENOENT: no such file: ${filePath}`); + } + return mockFiles.get(filePath); + }, + writeFileSync: (filePath, content) => mockFiles.set(filePath, content), mkdirSync: () => {}, }, }); +/** + * Helper to set config content at the primary (~/.safe-chain/) location. + * @param {string} content + */ +function setConfigContent(content) { + mockFiles.set(safeChainConfigPath, content); +} + describe("getScanTimeout", async () => { let originalEnv; @@ -29,12 +48,11 @@ describe("getScanTimeout", async () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; } - configFileContent = undefined; + mockFiles.clear(); }); it("should return default timeout of 10000ms when no config or env var is set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - configFileContent = undefined; const timeout = getScanTimeout(); @@ -43,7 +61,7 @@ describe("getScanTimeout", async () => { it("should return timeout from config file when set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - configFileContent = JSON.stringify({ scanTimeout: 5000 }); + setConfigContent(JSON.stringify({ scanTimeout: 5000 })); const timeout = getScanTimeout(); @@ -52,7 +70,7 @@ describe("getScanTimeout", async () => { it("should prioritize environment variable over config file", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; - configFileContent = JSON.stringify({ scanTimeout: 5000 }); + setConfigContent(JSON.stringify({ scanTimeout: 5000 })); const timeout = getScanTimeout(); @@ -61,7 +79,7 @@ describe("getScanTimeout", async () => { it("should handle invalid environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; - configFileContent = JSON.stringify({ scanTimeout: 7000 }); + setConfigContent(JSON.stringify({ scanTimeout: 7000 })); const timeout = getScanTimeout(); @@ -69,8 +87,6 @@ describe("getScanTimeout", async () => { }); it("should ignore zero and negative values and fall back to default", () => { - configFileContent = undefined; - process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; let timeout = getScanTimeout(); @@ -84,7 +100,7 @@ describe("getScanTimeout", async () => { it("should ignore textual non-numeric values in environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; - configFileContent = JSON.stringify({ scanTimeout: 8000 }); + setConfigContent(JSON.stringify({ scanTimeout: 8000 })); const timeout = getScanTimeout(); @@ -93,7 +109,7 @@ describe("getScanTimeout", async () => { it("should ignore textual non-numeric values in config file and fall back to default", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - configFileContent = JSON.stringify({ scanTimeout: "slow" }); + setConfigContent(JSON.stringify({ scanTimeout: "slow" })); const timeout = getScanTimeout(); @@ -102,7 +118,7 @@ describe("getScanTimeout", async () => { it("should ignore textual non-numeric values in both env and config, fall back to default", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; - configFileContent = JSON.stringify({ scanTimeout: "medium" }); + setConfigContent(JSON.stringify({ scanTimeout: "medium" })); const timeout = getScanTimeout(); @@ -111,7 +127,7 @@ describe("getScanTimeout", async () => { it("should ignore mixed alphanumeric strings in environment variable", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; - configFileContent = JSON.stringify({ scanTimeout: 6000 }); + setConfigContent(JSON.stringify({ scanTimeout: 6000 })); const timeout = getScanTimeout(); @@ -120,7 +136,7 @@ describe("getScanTimeout", async () => { it("should ignore mixed alphanumeric strings in config file", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - configFileContent = JSON.stringify({ scanTimeout: "3000ms" }); + setConfigContent(JSON.stringify({ scanTimeout: "3000ms" })); const timeout = getScanTimeout(); @@ -132,19 +148,17 @@ describe("getMinimumPackageAgeHours", async () => { const { getMinimumPackageAgeHours } = await import("./configFile.js"); afterEach(() => { - configFileContent = undefined; + mockFiles.clear(); }); it("should return null when config file doesn't exist", () => { - configFileContent = undefined; - const hours = getMinimumPackageAgeHours(); assert.strictEqual(hours, undefined); }); it("should return null when config file exists but minimumPackageAgeHours is not set", () => { - configFileContent = JSON.stringify({ scanTimeout: 5000 }); + setConfigContent(JSON.stringify({ scanTimeout: 5000 })); const hours = getMinimumPackageAgeHours(); @@ -152,7 +166,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should return value from config file when set to valid number", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: 48 }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: 48 })); const hours = getMinimumPackageAgeHours(); @@ -160,7 +174,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should handle string numbers in config file", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: "72" }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: "72" })); const hours = getMinimumPackageAgeHours(); @@ -168,7 +182,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should handle decimal values", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: 1.5 }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: 1.5 })); const hours = getMinimumPackageAgeHours(); @@ -176,7 +190,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should return null for non-numeric strings", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: "invalid" }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: "invalid" })); const hours = getMinimumPackageAgeHours(); @@ -184,7 +198,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should return undefined for values with units suffix", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: "48h" }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: "48h" })); const hours = getMinimumPackageAgeHours(); @@ -192,7 +206,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should handle malformed JSON and return null", () => { - configFileContent = "{ invalid json"; + setConfigContent("{ invalid json"); const hours = getMinimumPackageAgeHours(); @@ -200,7 +214,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should return 0 when minimumPackageAgeHours is set to 0", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: 0 }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: 0 })); const hours = getMinimumPackageAgeHours(); @@ -208,7 +222,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should return 0 when minimumPackageAgeHours is set to string '0'", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: "0" }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: "0" })); const hours = getMinimumPackageAgeHours(); @@ -216,7 +230,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should handle negative numeric values", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: -24 }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: -24 })); const hours = getMinimumPackageAgeHours(); @@ -224,7 +238,7 @@ describe("getMinimumPackageAgeHours", async () => { }); it("should handle negative string values", () => { - configFileContent = JSON.stringify({ minimumPackageAgeHours: "-48" }); + setConfigContent(JSON.stringify({ minimumPackageAgeHours: "-48" })); const hours = getMinimumPackageAgeHours(); @@ -249,19 +263,17 @@ for (const { packageManager, getCustomRegistries } of [ { describe(getCustomRegistries.name, async () => { afterEach(() => { - configFileContent = undefined; + mockFiles.clear(); }); it("should return empty array when config file doesn't exist", () => { - configFileContent = undefined; - const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); it(`should return empty array when ${packageManager} config is not set`, () => { - configFileContent = JSON.stringify({ scanTimeout: 5000 }); + setConfigContent(JSON.stringify({ scanTimeout: 5000 })); const registries = getCustomRegistries(); @@ -269,9 +281,9 @@ for (const { packageManager, getCustomRegistries } of [ }); it("should return empty array when customRegistries is not an array", () => { - configFileContent = JSON.stringify({ + setConfigContent(JSON.stringify({ [packageManager]: { customRegistries: "not-an-array" }, - }); + })); const registries = getCustomRegistries(); @@ -279,11 +291,11 @@ for (const { packageManager, getCustomRegistries } of [ }); it("should return array of custom registries when set", () => { - configFileContent = JSON.stringify({ + setConfigContent(JSON.stringify({ [packageManager]: { customRegistries: [`${packageManager}.company.com`, "registry.internal.net"], }, - }); + })); const registries = getCustomRegistries(); @@ -294,7 +306,7 @@ for (const { packageManager, getCustomRegistries } of [ }); it("should filter out non-string values", () => { - configFileContent = JSON.stringify({ + setConfigContent(JSON.stringify({ [packageManager]: { customRegistries: [ `${packageManager}.company.com`, @@ -305,7 +317,7 @@ for (const { packageManager, getCustomRegistries } of [ {}, ], }, - }); + })); const registries = getCustomRegistries(); @@ -316,9 +328,9 @@ for (const { packageManager, getCustomRegistries } of [ }); it("should return empty array for empty customRegistries array", () => { - configFileContent = JSON.stringify({ + setConfigContent(JSON.stringify({ [packageManager]: { customRegistries: [] }, - }); + })); const registries = getCustomRegistries(); @@ -326,7 +338,7 @@ for (const { packageManager, getCustomRegistries } of [ }); it("should handle malformed JSON and return empty array", () => { - configFileContent = "{ invalid json"; + setConfigContent("{ invalid json"); const registries = getCustomRegistries(); @@ -334,3 +346,35 @@ for (const { packageManager, getCustomRegistries } of [ }); }); } + +describe("config file location fallback", async () => { + const { getScanTimeout } = await import("./configFile.js"); + + afterEach(() => { + mockFiles.clear(); + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + }); + + it("should read config from ~/.safe-chain/config.json when it exists", () => { + mockFiles.set(safeChainConfigPath, JSON.stringify({ scanTimeout: 3000 })); + + assert.strictEqual(getScanTimeout(), 3000); + }); + + it("should fall back to ~/.aikido/config.json when primary does not exist", () => { + mockFiles.set(aikidoConfigPath, JSON.stringify({ scanTimeout: 4000 })); + + assert.strictEqual(getScanTimeout(), 4000); + }); + + it("should prefer ~/.safe-chain/config.json when both exist", () => { + mockFiles.set(safeChainConfigPath, JSON.stringify({ scanTimeout: 3000 })); + mockFiles.set(aikidoConfigPath, JSON.stringify({ scanTimeout: 4000 })); + + assert.strictEqual(getScanTimeout(), 3000); + }); + + it("should return default when neither config file exists", () => { + assert.strictEqual(getScanTimeout(), 10000); + }); +}); 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 a2a3739..07d89cb 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -32,7 +32,7 @@ function teardown(tools) { ); } - // Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh) + // Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh) removeLinesMatchingPattern( startupFile, /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/, 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 fc2b807..6086095 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -31,7 +31,7 @@ function teardown(tools) { ); } - // Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-posix.sh) + // Removes the line that sources the safe-chain zsh initialization script (~/.safe-chain/scripts/init-posix.sh) removeLinesMatchingPattern( startupFile, /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,