diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index 932eff7..b76a413 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -55,3 +55,14 @@ export function getMinimumPackageAgeExclusions() { export function getMalwareListBaseUrl() { return process.env.SAFE_CHAIN_MALWARE_LIST_BASE_URL; } + +/** + * Gets the safe-chain base directory from environment variable. + * When set, all safe-chain data (bin, shims, scripts) will be placed under this directory + * instead of the default ~/.safe-chain, enabling system-wide installations. + * Example: "/usr/local/.safe-chain" + * @returns {string | undefined} + */ +export function getSafeChainDir() { + return process.env.SAFE_CHAIN_DIR; +} diff --git a/packages/safe-chain/src/config/environmentVariables.spec.js b/packages/safe-chain/src/config/environmentVariables.spec.js new file mode 100644 index 0000000..2cbdd0f --- /dev/null +++ b/packages/safe-chain/src/config/environmentVariables.spec.js @@ -0,0 +1,30 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert"; + +const { getSafeChainDir } = await import("./environmentVariables.js"); + +describe("getSafeChainDir", () => { + let original; + + beforeEach(() => { + original = process.env.SAFE_CHAIN_DIR; + }); + + afterEach(() => { + if (original !== undefined) { + process.env.SAFE_CHAIN_DIR = original; + } else { + delete process.env.SAFE_CHAIN_DIR; + } + }); + + it("returns undefined when SAFE_CHAIN_DIR is not set", () => { + delete process.env.SAFE_CHAIN_DIR; + assert.strictEqual(getSafeChainDir(), undefined); + }); + + it("returns the value of SAFE_CHAIN_DIR when set", () => { + process.env.SAFE_CHAIN_DIR = "/usr/local/.safe-chain"; + assert.strictEqual(getSafeChainDir(), "/usr/local/.safe-chain"); + }); +}); diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 18ba52e..7ccfd99 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,7 @@ import * as os from "os"; import fs from "fs"; import path from "path"; import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { getSafeChainDir } from "../config/environmentVariables.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { ui } from "../environment/userInteraction.js"; @@ -121,18 +122,34 @@ export function getPackageManagerList() { return `${tools.join(", ")}, and ${lastTool} commands`; } +/** + * Returns the safe-chain base directory. + * Uses SAFE_CHAIN_DIR environment variable when set, otherwise defaults to ~/.safe-chain. + * @returns {string} + */ +export function getSafeChainBaseDir() { + return getSafeChainDir() ?? path.join(os.homedir(), ".safe-chain"); +} + +/** + * @returns {string} + */ +export function getBinDir() { + return path.join(getSafeChainBaseDir(), "bin"); +} + /** * @returns {string} */ export function getShimsDir() { - return path.join(os.homedir(), ".safe-chain", "shims"); + return path.join(getSafeChainBaseDir(), "shims"); } /** * @returns {string} */ export function getScriptsDir() { - return path.join(os.homedir(), ".safe-chain", "scripts"); + return path.join(getSafeChainBaseDir(), "scripts"); } /** diff --git a/packages/safe-chain/src/shell-integration/helpers.spec.js b/packages/safe-chain/src/shell-integration/helpers.spec.js index 4f18c36..8fd172b 100644 --- a/packages/safe-chain/src/shell-integration/helpers.spec.js +++ b/packages/safe-chain/src/shell-integration/helpers.spec.js @@ -1,6 +1,6 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; -import { tmpdir } from "node:os"; +import { tmpdir, homedir } from "node:os"; import fs from "node:fs"; import path from "path"; @@ -15,6 +15,7 @@ describe("removeLinesMatchingPatternTests", () => { mock.module("node:os", { namedExports: { EOL: "\r\n", // Simulate Windows line endings + homedir, tmpdir: tmpdir, platform: () => "linux", }, @@ -182,3 +183,66 @@ describe("removeLinesMatchingPatternTests", () => { assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines"); }); }); + +describe("getSafeChainBaseDir / getBinDir / getShimsDir / getScriptsDir", () => { + const customDir = "/usr/local/.safe-chain"; + + let originalSafeChainDir; + + beforeEach(() => { + originalSafeChainDir = process.env.SAFE_CHAIN_DIR; + delete process.env.SAFE_CHAIN_DIR; + }); + + afterEach(() => { + if (originalSafeChainDir !== undefined) { + process.env.SAFE_CHAIN_DIR = originalSafeChainDir; + } else { + delete process.env.SAFE_CHAIN_DIR; + } + }); + + it("defaults base dir to ~/.safe-chain when SAFE_CHAIN_DIR is not set", async () => { + const { getSafeChainBaseDir } = await import("./helpers.js"); + assert.strictEqual(getSafeChainBaseDir(), path.join(homedir(), ".safe-chain")); + }); + + it("uses SAFE_CHAIN_DIR as base dir when set", async () => { + process.env.SAFE_CHAIN_DIR = customDir; + const { getSafeChainBaseDir } = await import("./helpers.js"); + assert.strictEqual(getSafeChainBaseDir(), customDir); + }); + + it("getBinDir returns ~/.safe-chain/bin by default", async () => { + const { getBinDir } = await import("./helpers.js"); + assert.strictEqual(getBinDir(), path.join(homedir(), ".safe-chain", "bin")); + }); + + it("getBinDir returns custom dir + /bin when SAFE_CHAIN_DIR is set", async () => { + process.env.SAFE_CHAIN_DIR = customDir; + const { getBinDir } = await import("./helpers.js"); + assert.strictEqual(getBinDir(), `${customDir}/bin`); + }); + + it("getShimsDir returns ~/.safe-chain/shims by default", async () => { + const { getShimsDir } = await import("./helpers.js"); + assert.strictEqual(getShimsDir(), path.join(homedir(), ".safe-chain", "shims")); + }); + + it("getShimsDir returns custom dir + /shims when SAFE_CHAIN_DIR is set", async () => { + process.env.SAFE_CHAIN_DIR = customDir; + const { getShimsDir } = await import("./helpers.js"); + assert.strictEqual(getShimsDir(), `${customDir}/shims`); + }); + + it("getScriptsDir returns ~/.safe-chain/scripts by default", async () => { + const { getScriptsDir } = await import("./helpers.js"); + assert.strictEqual(getScriptsDir(), path.join(homedir(), ".safe-chain", "scripts")); + }); + + it("getScriptsDir returns custom dir + /scripts when SAFE_CHAIN_DIR is set", async () => { + process.env.SAFE_CHAIN_DIR = customDir; + const { getScriptsDir } = await import("./helpers.js"); + assert.strictEqual(getScriptsDir(), `${customDir}/scripts`); + }); +});