import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js"; import { detectShells } from "./shellDetection.js"; import fs from "fs"; import os from "os"; import path from "path"; import { fileURLToPath } from "url"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: // - Modulejs (import.meta.url) // - ES modules (__dirname) // This is needed because safe-chain's npm package is built using ES modules, // but building the binaries requires commonjs. let dirname; if (import.meta.url) { const filename = fileURLToPath(import.meta.url); dirname = path.dirname(filename); } else { dirname = __dirname; } /** * Loops over the detected shells and calls the setup function for each. */ export async function setupCi() { ui.writeInformation( chalk.bold("Setting up shell aliases.") + ` This will wrap safe-chain around ${getPackageManagerList()}.` ); ui.emptyLine(); const shimsDir = getShimsDir(); const binDir = path.join(os.homedir(), ".safe-chain", "bin"); // Create the shims directory if it doesn't exist if (!fs.existsSync(shimsDir)) { fs.mkdirSync(shimsDir, { recursive: true }); } cleanupLegacyShellInit(); createShims(shimsDir); ui.writeInformation(`Created shims in ${shimsDir}`); modifyPathForCi(shimsDir, binDir); ui.writeInformation(`Added shims directory to PATH for CI environments.`); } /** * Removes shell-based initialization from RC files when switching to --ci install mode. */ function cleanupLegacyShellInit() { let shells; try { shells = detectShells(); } catch { return; } for (const shell of shells) { try { shell.teardown(knownAikidoTools); } catch { // Best-effort cleanup — don't fail the install if teardown errors } } } /** * @param {string} shimsDir * * @returns {void} */ function createUnixShims(shimsDir) { // Read the template file const templatePath = path.resolve( dirname, "path-wrappers", "templates", "unix-wrapper.template.sh" ); if (!fs.existsSync(templatePath)) { ui.writeError(`Template file not found: ${templatePath}`); return; } const template = fs.readFileSync(templatePath, "utf-8"); // Create a shim for each tool let created = 0; for (const toolInfo of getToolsToSetup()) { const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); const shimPath = path.join(shimsDir, toolInfo.tool); fs.writeFileSync(shimPath, shimContent, "utf-8"); // Make the shim executable on Unix systems fs.chmodSync(shimPath, 0o755); created++; } ui.writeInformation(`Created ${created} Unix shim(s) in ${shimsDir}`); } /** * @param {string} shimsDir * * @returns {void} */ function createWindowsShims(shimsDir) { // Read the template file const templatePath = path.resolve( dirname, "path-wrappers", "templates", "windows-wrapper.template.cmd" ); if (!fs.existsSync(templatePath)) { ui.writeError(`Windows template file not found: ${templatePath}`); return; } const template = fs.readFileSync(templatePath, "utf-8"); // Create a shim for each tool let created = 0; for (const toolInfo of getToolsToSetup()) { const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`; fs.writeFileSync(shimPath, shimContent, "utf-8"); created++; } ui.writeInformation(`Created ${created} Windows shim(s) in ${shimsDir}`); } /** * @param {string} shimsDir * * @returns {void} */ function createShims(shimsDir) { if (os.platform() === "win32") { createWindowsShims(shimsDir); } else { createUnixShims(shimsDir); } } /** * @param {string} shimsDir * @param {string} binDir * * @returns {void} */ function modifyPathForCi(shimsDir, binDir) { if (process.env.GITHUB_PATH) { // In GitHub Actions, append the shims directory to GITHUB_PATH fs.appendFileSync( process.env.GITHUB_PATH, shimsDir + os.EOL + binDir + os.EOL, "utf-8" ); ui.writeInformation( `Added shims directory to GITHUB_PATH for GitHub Actions.` ); } if (process.env.TF_BUILD) { // In Azure Pipelines, prepending the path is done via a logging command: // ##vso[task.prependpath]/path/to/add // Logging this to stdout will cause the Azure Pipelines agent to pick it up ui.writeInformation("##vso[task.prependpath]" + shimsDir); ui.writeInformation("##vso[task.prependpath]" + binDir); } if (process.env.BASH_ENV) { // In CircleCI, persisting PATH across steps is done by appending shell exports // to the file referenced by BASH_ENV. CircleCI sources this file for 'run' each step. const exportLine = `export PATH="${shimsDir}:${binDir}:$PATH"` + os.EOL; fs.appendFileSync(process.env.BASH_ENV, exportLine, "utf-8"); ui.writeInformation(`Added shims directory to BASH_ENV for CircleCI.`); } } function getToolsToSetup() { return knownAikidoTools; }